From 9ba9ae675a17cae960d5e08cf28eecb373d9130a Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Sun, 16 Aug 2026 19:05:19 +0800 Subject: [PATCH] Job search tracker: CRUD, AI parse/tailor/cover via OmniRoute, legacy import --- .gitignore | 1 + package-lock.json | 1062 ++++++++++++++++- package.json | 6 + prisma/dev.db | Bin 0 -> 77824 bytes .../20260816110117_init/migration.sql | 91 ++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 97 ++ scripts/import-legacy.ts | 169 +++ src/app/api/ai/cover/route.ts | 26 + src/app/api/ai/parse/route.ts | 44 + src/app/api/ai/tailor/route.ts | 36 + src/app/api/applications/route.ts | 19 + src/app/api/jobs/[id]/route.ts | 27 + src/app/api/jobs/route.ts | 22 + src/app/api/profile/route.ts | 17 + src/app/api/resumes/[id]/route.ts | 24 + src/app/api/resumes/route.ts | 16 + src/app/applications/page.tsx | 27 + src/app/jobs/[id]/job-actions.tsx | 79 ++ src/app/jobs/[id]/page.tsx | 71 ++ src/app/jobs/new/page.tsx | 85 ++ src/app/jobs/page.tsx | 68 ++ src/app/layout.tsx | 47 +- src/app/page.tsx | 104 +- src/app/profile/page.tsx | 9 + src/app/profile/profile-editor.tsx | 62 + src/app/resumes/[id]/page.tsx | 12 + src/app/resumes/[id]/resume-editor.tsx | 59 + src/app/resumes/page.tsx | 26 + src/lib/ai.ts | 63 + src/lib/db.ts | 7 + 31 files changed, 2294 insertions(+), 85 deletions(-) create mode 100644 prisma/dev.db create mode 100644 prisma/migrations/20260816110117_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/schema.prisma create mode 100644 scripts/import-legacy.ts create mode 100644 src/app/api/ai/cover/route.ts create mode 100644 src/app/api/ai/parse/route.ts create mode 100644 src/app/api/ai/tailor/route.ts create mode 100644 src/app/api/applications/route.ts create mode 100644 src/app/api/jobs/[id]/route.ts create mode 100644 src/app/api/jobs/route.ts create mode 100644 src/app/api/profile/route.ts create mode 100644 src/app/api/resumes/[id]/route.ts create mode 100644 src/app/api/resumes/route.ts create mode 100644 src/app/applications/page.tsx create mode 100644 src/app/jobs/[id]/job-actions.tsx create mode 100644 src/app/jobs/[id]/page.tsx create mode 100644 src/app/jobs/new/page.tsx create mode 100644 src/app/jobs/page.tsx create mode 100644 src/app/profile/page.tsx create mode 100644 src/app/profile/profile-editor.tsx create mode 100644 src/app/resumes/[id]/page.tsx create mode 100644 src/app/resumes/[id]/resume-editor.tsx create mode 100644 src/app/resumes/page.tsx create mode 100644 src/lib/ai.ts create mode 100644 src/lib/db.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..6d805f8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.env diff --git a/package-lock.json b/package-lock.json index 8a89218..bb41e39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,12 @@ "name": "ai-job-search-app", "version": "0.1.0", "dependencies": { + "@prisma/client": "^6.19.3", + "gray-matter": "^4.0.3", + "marked": "^18.0.9", "next": "16.3.1", + "openai": "^7.4.0", + "prisma": "^6.19.3", "react": "19.2.8", "react-dom": "19.2.8" }, @@ -20,6 +25,7 @@ "eslint": "^9", "eslint-config-next": "16.3.1", "tailwindcss": "^4", + "tsx": "^4.23.12", "typescript": "^5" } }, @@ -309,6 +315,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -1377,6 +1825,85 @@ "node": ">=12.4.0" } }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1384,6 +1911,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.23", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", @@ -2750,6 +3283,34 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2847,6 +3408,30 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2880,6 +3465,21 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2995,6 +3595,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3031,6 +3640,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3054,6 +3675,18 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3069,6 +3702,16 @@ "node": ">= 0.4" } }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.407", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", @@ -3083,6 +3726,15 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -3296,6 +3948,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3679,6 +4373,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -3725,6 +4432,46 @@ "node": ">=0.10.0" } }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3866,6 +4613,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4000,6 +4762,23 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4063,6 +4842,43 @@ "dev": true, "license": "ISC" }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4400,6 +5216,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4693,7 +5518,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -4802,6 +5626,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -5165,6 +5998,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.9.tgz", + "integrity": "sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5370,6 +6215,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", @@ -5380,6 +6231,29 @@ "node": ">=18" } }, + "node_modules/nypm": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.9.tgz", + "integrity": "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.2.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5503,6 +6377,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "license": "MIT" + }, + "node_modules/openai": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.4.0.tgz", + "integrity": "sha512-+C9Muit5x8j9R8ej8ZzVgKcrVDtqFqTy9gxFdov0EItLgU68zrJtF9ZeT0cyqJQW9S3PCJkdFgADtRGquRBtew==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5612,6 +6525,18 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5631,6 +6556,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5680,6 +6616,31 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5702,6 +6663,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5723,6 +6700,16 @@ ], "license": "MIT" }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -5751,6 +6738,19 @@ "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5935,6 +6935,19 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -6165,6 +7178,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6310,6 +7329,15 @@ "node": ">=4" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6393,6 +7421,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6499,6 +7536,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6594,7 +7650,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6872,7 +7928,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index d33e5aa..632a7d0 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,12 @@ "lint": "eslint" }, "dependencies": { + "@prisma/client": "^6.19.3", + "gray-matter": "^4.0.3", + "marked": "^18.0.9", "next": "16.3.1", + "openai": "^7.4.0", + "prisma": "^6.19.3", "react": "19.2.8", "react-dom": "19.2.8" }, @@ -21,6 +26,7 @@ "eslint": "^9", "eslint-config-next": "16.3.1", "tailwindcss": "^4", + "tsx": "^4.23.12", "typescript": "^5" } } diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..0a579b202961c89fe677d7cd0472fe5977a36764 GIT binary patch literal 77824 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 f.endsWith(".md")); + for (const file of files) { + const raw = fs.readFileSync(path.join(dir, file), "utf8"); + const { data, content } = matter(raw); + const title = (data.title as string) ?? file.replace(/\.md$/, ""); + const company = (data.company as string) ?? "Unknown"; + const existing = await prisma.job.findFirst({ where: { title, company } }); + if (existing) { + console.log(`skip (exists): ${file}`); + continue; + } + const url = firstMatch(content, /\*\*Link:\*\*\s*(\S+)/); + const applyUrl = firstMatch(content, /\*\*Apply(?: link)?:\*\*\s*(\S+)/); + const details = section(content, "Details"); + const payLine = details ? firstMatch(details, /\*\*Pay(?:\s*Range)?:\*\*\s*(.+)/) : undefined; + const location = details ? firstMatch(details, /\*\*Location:\*\*\s*(.+)/) : undefined; + + // crude pay parse: "$15-20/hr", "₱150-170k/mo", "$65-120/hr" + let payMin: number | undefined, payMax: number | undefined, payCurrency: string | undefined, payPeriod: string | undefined; + if (payLine) { + const m = payLine.match(/([$₱P])\s*([\d,.]+)\s*[-–]\s*([\d,.]+)(k?)\s*\/?\s*(hr|hour|mo|month|yr|year)?/i); + if (m) { + const mult = m[4] === "k" ? 1000 : 1; + payMin = parseFloat(m[2].replace(/,/g, "")) * mult; + payMax = parseFloat(m[3].replace(/,/g, "")) * mult; + payCurrency = m[1] === "$" ? "USD" : "PHP"; + payPeriod = /hr|hour/i.test(m[5] ?? "") ? "hour" : /mo/i.test(m[5] ?? "") ? "month" : /yr|year/i.test(m[5] ?? "") ? "year" : undefined; + } + } + + const phEligibility = /philippines|ph eligible|170\+ countries|worldwide/i.test(content) ? "yes" : "unclear"; + const status = (data.status as string)?.replace(/\s+/g, "-") === "not-applied" ? "prospect" : "prospect"; + + await prisma.job.create({ + data: { + title, + company, + source: file.replace(/\.md$/, ""), + url, + applyUrl, + payMin, + payMax, + payCurrency, + payPeriod, + location, + remoteType: "remote", + phEligibility, + description: section(content, "Description"), + requirements: section(content, "Requirements"), + notes: section(content, "Notes") ?? section(content, "Tailoring notes"), + status, + }, + }); + console.log(`imported: ${file}`); + } +} + +async function importResumes() { + const base = path.join(LEGACY, "resume/resume.md"); + if (fs.existsSync(base)) { + const md = fs.readFileSync(base, "utf8"); + const exists = await prisma.resume.findFirst({ where: { kind: "base" } }); + if (!exists) { + await prisma.resume.create({ data: { name: "Base Resume", kind: "base", markdown: md } }); + console.log("imported: base resume"); + } + } + const tailoredDir = path.join(LEGACY, "resume/tailored"); + if (fs.existsSync(tailoredDir)) { + for (const f of fs.readdirSync(tailoredDir).filter((f) => f.endsWith(".md"))) { + const name = `Tailored: ${f.replace(/\.md$/, "")}`; + const exists = await prisma.resume.findFirst({ where: { name } }); + if (exists) continue; + await prisma.resume.create({ + data: { name, kind: "tailored", markdown: fs.readFileSync(path.join(tailoredDir, f), "utf8") }, + }); + console.log(`imported: tailored ${f}`); + } + } +} + +async function seedProfile() { + const exists = await prisma.profile.findUnique({ where: { id: "main" } }); + if (exists) return; + await prisma.profile.create({ + data: { + id: "main", + fullName: "Joshua C. Sarmiento", + email: "joshuacsarmiento@gmail.com", + location: "Philippines", + headline: "Software Developer / AI Engineer — Prompt Engineering, Conversational AI, Backend Systems", + summary: + "Software developer with 8+ years of experience spanning enterprise IT, full-stack web development, and applied AI: building LLM-powered APIs and RAG systems with FastAPI and Amazon Bedrock.", + skills: JSON.stringify([ + "Prompt engineering", "Amazon Bedrock", "RAG", "Conversational AI", "Python", "FastAPI", + "REST API design", "Active Directory", "Full-stack web development", "Bash/PowerShell automation", + ]), + workHistory: JSON.stringify([ + { + position: "Project Technical Specialist III", + company: "Philippine Institute of Volcanology and Seismology (PHIVOLCS)", + unit: "Geomatics and Hazard Assessment Services Section", + start: "2025-03", end: null, supervisor: "Eric Santos", + bullets: [ + "Contributed to CLUP and CDRA systems", + "Gates Handa: AI APIs with FastAPI + Amazon Bedrock, chatbot, RAG document/report analyzer", + "Design system diagrams and plans; support project management", + ], + }, + { + position: "Computer Programmer II", + company: "Presidential Commission for the Urban Poor (PCUP)", + unit: "IT Unit", + start: "2019-08", end: "2024-02", supervisor: "Jason Ronidel", + bullets: [ + "Developed major web apps: Attendance Monitoring, Service Workflow Management, UPO Accreditation, Reservation, Directory", + "Managed commission network infrastructure and devices", + "MS Access + VBA database applications", + ], + }, + { + position: "IT Associate", + company: "Manulife Business Process Services", + unit: "Global Access Management (GAM) Division", + start: "2016-11", end: "2017-10", supervisor: "Emory Lopez", + bullets: [ + "Automated Active Directory user management with scripts (.sh, .ps1, .bat)", + "Managed AD, mainframe, Oracle, FTP, and email accounts (Exchange, Lotus Notes, O365)", + "ServiceNow request handling; VPN token creation", + ], + }, + ]), + }, + }); + console.log("seeded: profile"); +} + +async function main() { + await importJobs(); + await importResumes(); + await seedProfile(); + console.log("done"); +} + +main().finally(() => prisma.$disconnect()); diff --git a/src/app/api/ai/cover/route.ts b/src/app/api/ai/cover/route.ts new file mode 100644 index 0000000..2cc12a7 --- /dev/null +++ b/src/app/api/ai/cover/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { aiComplete } from "@/lib/ai"; + +const SYSTEM = `You write concise, specific cover letters (250-350 words, markdown). +Truthful only — never invent experience. Reference the company and role by name. +Output ONLY the letter markdown.`; + +export async function POST(req: NextRequest) { + const { jobId } = await req.json(); + const job = await prisma.job.findUnique({ where: { id: jobId } }); + if (!job) return NextResponse.json({ error: "job not found" }, { status: 404 }); + const profile = await prisma.profile.findUnique({ where: { id: "main" } }); + const base = await prisma.resume.findFirst({ where: { kind: "base" } }); + + const markdown = await aiComplete({ + type: "cover", + system: SYSTEM, + user: `CANDIDATE PROFILE:\n${JSON.stringify(profile)}\n\nRESUME:\n${base?.markdown ?? ""}\n\nJOB: ${job.title} at ${job.company}\n${job.description ?? ""}\n${job.requirements ?? ""}`, + inputSummary: `${job.company} — ${job.title}`, + }); + + const version = (await prisma.coverLetter.count({ where: { jobId } })) + 1; + const letter = await prisma.coverLetter.create({ data: { jobId, markdown, version } }); + return NextResponse.json(letter, { status: 201 }); +} diff --git a/src/app/api/ai/parse/route.ts b/src/app/api/ai/parse/route.ts new file mode 100644 index 0000000..e8410d1 --- /dev/null +++ b/src/app/api/ai/parse/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; +import { aiComplete, extractJson } from "@/lib/ai"; + +const SYSTEM = `You are a job-posting parser. Extract structured data from the job posting text. +Reply with ONLY a JSON object with these keys (null when unknown): +{"title","company","url","applyUrl","payMin","payMax","payCurrency","payPeriod","location","remoteType","phEligibility","description","requirements","tags"} +- payCurrency: "USD" or "PHP". payPeriod: "hour"|"month"|"year". +- remoteType: "remote"|"hybrid"|"onsite". +- phEligibility: "yes" if Philippines applicants are explicitly allowed or the role is worldwide; "no" if excluded; otherwise "unclear". +- tags: comma-separated keywords. +- description: 2-4 sentence summary. requirements: bullet list as plain text.`; + +export async function POST(req: NextRequest) { + const { text, url } = await req.json(); + let input = text as string | undefined; + + if (!input && url) { + // Server-side fetch; many boards (JobStreet) will block — caller falls back to pasting text. + const res = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0" } }); + if (!res.ok) { + return NextResponse.json( + { error: `Fetch failed (${res.status}). Paste the job text instead.` }, + { status: 422 }, + ); + } + const html = await res.text(); + input = html.replace(//gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").slice(0, 20000); + } + if (!input) return NextResponse.json({ error: "Provide text or url" }, { status: 400 }); + + const reply = await aiComplete({ + type: "parse", + system: SYSTEM, + user: input, + inputSummary: url ?? input.slice(0, 120), + }); + try { + const parsed = extractJson>(reply); + if (url && !parsed.url) parsed.url = url; + return NextResponse.json(parsed); + } catch { + return NextResponse.json({ error: "AI reply was not valid JSON", raw: reply }, { status: 502 }); + } +} diff --git a/src/app/api/ai/tailor/route.ts b/src/app/api/ai/tailor/route.ts new file mode 100644 index 0000000..53c9026 --- /dev/null +++ b/src/app/api/ai/tailor/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { aiComplete } from "@/lib/ai"; + +const SYSTEM = `You are a resume-tailoring assistant. Given a base resume (markdown) and a job posting, produce a tailored resume in markdown. +Rules: +- Keep all facts truthful — never invent experience, employers, or dates. +- Reorder and reword to emphasize what the job asks for. +- Keep it one page worth of content. +- Keep the candidate's real contact lines exactly as in the base resume. +- Output ONLY the resume markdown, no commentary.`; + +export async function POST(req: NextRequest) { + const { jobId } = await req.json(); + const job = await prisma.job.findUnique({ where: { id: jobId } }); + if (!job) return NextResponse.json({ error: "job not found" }, { status: 404 }); + const base = await prisma.resume.findFirst({ where: { kind: "base" }, orderBy: { updatedAt: "desc" } }); + if (!base) return NextResponse.json({ error: "no base resume" }, { status: 400 }); + + const jobText = `# ${job.title} — ${job.company}\n${job.description ?? ""}\nRequirements:\n${job.requirements ?? ""}\nNotes:\n${job.notes ?? ""}`; + const markdown = await aiComplete({ + type: "tailor", + system: SYSTEM, + user: `BASE RESUME:\n${base.markdown}\n\nJOB POSTING:\n${jobText}`, + inputSummary: `${job.company} — ${job.title}`, + }); + + const version = (await prisma.resume.count({ where: { jobId } })) + 1; + const resume = await prisma.resume.create({ + data: { name: `Tailored: ${job.company} — ${job.title} v${version}`, kind: "tailored", markdown, jobId, version }, + }); + if (job.status === "prospect" || job.status === "reviewing") { + await prisma.job.update({ where: { id: jobId }, data: { status: "tailoring" } }); + } + return NextResponse.json(resume, { status: 201 }); +} diff --git a/src/app/api/applications/route.ts b/src/app/api/applications/route.ts new file mode 100644 index 0000000..c6f8a32 --- /dev/null +++ b/src/app/api/applications/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET() { + const apps = await prisma.application.findMany({ + orderBy: { updatedAt: "desc" }, + include: { job: { select: { title: true, company: true } } }, + }); + return NextResponse.json(apps); +} + +export async function POST(req: NextRequest) { + const data = await req.json(); + const app = await prisma.application.create({ data }); + if (data.submittedAt) { + await prisma.job.update({ where: { id: data.jobId }, data: { status: "applied" } }); + } + return NextResponse.json(app, { status: 201 }); +} diff --git a/src/app/api/jobs/[id]/route.ts b/src/app/api/jobs/[id]/route.ts new file mode 100644 index 0000000..029b1ad --- /dev/null +++ b/src/app/api/jobs/[id]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +type Params = { params: Promise<{ id: string }> }; + +export async function GET(_req: NextRequest, { params }: Params) { + const { id } = await params; + const job = await prisma.job.findUnique({ + where: { id }, + include: { resumes: true, coverLetters: true, applications: true }, + }); + if (!job) return NextResponse.json({ error: "not found" }, { status: 404 }); + return NextResponse.json(job); +} + +export async function PATCH(req: NextRequest, { params }: Params) { + const { id } = await params; + const data = await req.json(); + const job = await prisma.job.update({ where: { id }, data }); + return NextResponse.json(job); +} + +export async function DELETE(_req: NextRequest, { params }: Params) { + const { id } = await params; + await prisma.job.delete({ where: { id } }); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/jobs/route.ts b/src/app/api/jobs/route.ts new file mode 100644 index 0000000..4276c81 --- /dev/null +++ b/src/app/api/jobs/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET(req: NextRequest) { + const status = req.nextUrl.searchParams.get("status") ?? undefined; + const q = req.nextUrl.searchParams.get("q") ?? undefined; + const jobs = await prisma.job.findMany({ + where: { + ...(status ? { status } : {}), + ...(q ? { OR: [{ title: { contains: q } }, { company: { contains: q } }] } : {}), + }, + orderBy: { updatedAt: "desc" }, + include: { applications: true, resumes: { select: { id: true, name: true } } }, + }); + return NextResponse.json(jobs); +} + +export async function POST(req: NextRequest) { + const data = await req.json(); + const job = await prisma.job.create({ data }); + return NextResponse.json(job, { status: 201 }); +} diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts new file mode 100644 index 0000000..cf9a4b4 --- /dev/null +++ b/src/app/api/profile/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET() { + const profile = await prisma.profile.findUnique({ where: { id: "main" } }); + return NextResponse.json(profile); +} + +export async function PATCH(req: NextRequest) { + const data = await req.json(); + const profile = await prisma.profile.upsert({ + where: { id: "main" }, + update: data, + create: { id: "main", fullName: data.fullName ?? "Me", ...data }, + }); + return NextResponse.json(profile); +} diff --git a/src/app/api/resumes/[id]/route.ts b/src/app/api/resumes/[id]/route.ts new file mode 100644 index 0000000..235b108 --- /dev/null +++ b/src/app/api/resumes/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +type Params = { params: Promise<{ id: string }> }; + +export async function GET(_req: NextRequest, { params }: Params) { + const { id } = await params; + const resume = await prisma.resume.findUnique({ where: { id } }); + if (!resume) return NextResponse.json({ error: "not found" }, { status: 404 }); + return NextResponse.json(resume); +} + +export async function PATCH(req: NextRequest, { params }: Params) { + const { id } = await params; + const data = await req.json(); + const resume = await prisma.resume.update({ where: { id }, data }); + return NextResponse.json(resume); +} + +export async function DELETE(_req: NextRequest, { params }: Params) { + const { id } = await params; + await prisma.resume.delete({ where: { id } }); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/resumes/route.ts b/src/app/api/resumes/route.ts new file mode 100644 index 0000000..a493622 --- /dev/null +++ b/src/app/api/resumes/route.ts @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET() { + const resumes = await prisma.resume.findMany({ + orderBy: { updatedAt: "desc" }, + include: { job: { select: { title: true, company: true } } }, + }); + return NextResponse.json(resumes); +} + +export async function POST(req: NextRequest) { + const data = await req.json(); + const resume = await prisma.resume.create({ data }); + return NextResponse.json(resume, { status: 201 }); +} diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx new file mode 100644 index 0000000..c16b727 --- /dev/null +++ b/src/app/applications/page.tsx @@ -0,0 +1,27 @@ +import { prisma } from "@/lib/db"; +import Link from "next/link"; + +export const dynamic = "force-dynamic"; + +export default async function ApplicationsPage() { + const apps = await prisma.application.findMany({ + orderBy: { updatedAt: "desc" }, + include: { job: { select: { id: true, title: true, company: true, status: true } } }, + }); + return ( +
+

Applications ({apps.length})

+ {apps.length === 0 &&

No applications yet. Open a job and click “Mark Applied”.

} +
    + {apps.map((a) => ( +
  • + {a.job.title} — {a.job.company} +
    + {a.method} · {a.submittedAt ? `submitted ${a.submittedAt.toISOString().slice(0, 10)}` : "not submitted"} · job status: {a.job.status} +
    +
  • + ))} +
+
+ ); +} diff --git a/src/app/jobs/[id]/job-actions.tsx b/src/app/jobs/[id]/job-actions.tsx new file mode 100644 index 0000000..9d65600 --- /dev/null +++ b/src/app/jobs/[id]/job-actions.tsx @@ -0,0 +1,79 @@ +"use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"]; + +export default function JobActions({ job }: { job: { id: string; status: string } }) { + const router = useRouter(); + const [busy, setBusy] = useState(""); + const [msg, setMsg] = useState(""); + + async function setStatus(status: string) { + await fetch(`/api/jobs/${job.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status }), + }); + router.refresh(); + } + + async function run(kind: "tailor" | "cover") { + setBusy(kind); setMsg(""); + try { + const res = await fetch(`/api/ai/${kind}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jobId: job.id }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? "failed"); + setMsg(kind === "tailor" ? "Tailored resume created ✅" : "Cover letter created ✅"); + router.refresh(); + } catch (e) { setMsg(String(e)); } finally { setBusy(""); } + } + + async function markApplied() { + await fetch("/api/applications", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jobId: job.id, method: "manual", submittedAt: new Date().toISOString() }), + }); + router.refresh(); + } + + async function del() { + if (!confirm("Delete this job?")) return; + await fetch(`/api/jobs/${job.id}`, { method: "DELETE" }); + router.push("/jobs"); + } + + return ( +
+
+ Status: + +
+
+ + + + +
+ {msg &&
{msg}
} +
+ ); +} diff --git a/src/app/jobs/[id]/page.tsx b/src/app/jobs/[id]/page.tsx new file mode 100644 index 0000000..0ea01bd --- /dev/null +++ b/src/app/jobs/[id]/page.tsx @@ -0,0 +1,71 @@ +import { prisma } from "@/lib/db"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import JobActions from "./job-actions"; + +export const dynamic = "force-dynamic"; + +export default async function JobDetail({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const job = await prisma.job.findUnique({ + where: { id }, + include: { resumes: true, coverLetters: true, applications: true }, + }); + if (!job) notFound(); + + return ( +
+
+ ← Jobs +

{job.title}

+
{job.company} · {job.location ?? "Remote"} · PH: {job.phEligibility}
+
+ {job.url && Listing ↗} + {job.applyUrl && Apply page ↗} +
+
+ + + + {job.description && ( +
+

Description

+

{job.description}

+
+ )} + {job.requirements && ( +
+

Requirements

+

{job.requirements}

+
+ )} + {job.notes && ( +
+

Notes

+

{job.notes}

+
+ )} + + {job.resumes.length > 0 && ( +
+

Tailored resumes

+
    + {job.resumes.map((r) => ( +
  • {r.name}
  • + ))} +
+
+ )} + {job.coverLetters.length > 0 && ( +
+

Cover letters

+
    + {job.coverLetters.map((c) => ( +
  • {c.markdown}
  • + ))} +
+
+ )} +
+ ); +} diff --git a/src/app/jobs/new/page.tsx b/src/app/jobs/new/page.tsx new file mode 100644 index 0000000..518d983 --- /dev/null +++ b/src/app/jobs/new/page.tsx @@ -0,0 +1,85 @@ +"use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +type Parsed = Record; + +export default function NewJobPage() { + const router = useRouter(); + const [url, setUrl] = useState(""); + const [text, setText] = useState(""); + const [parsed, setParsed] = useState(null); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(""); + + async function parse() { + setBusy(true); setErr(""); + try { + const res = await fetch("/api/ai/parse", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: url || undefined, text: text || undefined }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? "parse failed"); + setParsed(data); + } catch (e) { setErr(String(e)); } finally { setBusy(false); } + } + + async function save() { + if (!parsed) return; + setBusy(true); + const res = await fetch("/api/jobs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(parsed), + }); + const job = await res.json(); + router.push(`/jobs/${job.id}`); + } + + function set(k: string, v: string) { setParsed((p) => ({ ...p!, [k]: v || null })); } + + return ( +
+

Add Job

+ {!parsed && ( + <> + setUrl(e.target.value)} placeholder="Job URL (may fail on Cloudflare sites)" + className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-2 text-sm" /> +
— or paste job text (more reliable) —
+