careermate 0.1.0 โ 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -7
- package/apps/mcp/src/index.ts +3 -1
- package/apps/web/DESIGN_GUIDE.md +2 -2
- package/apps/web/public/icon-180.png +0 -0
- package/apps/web/public/index.html +4 -1
- package/apps/web/public/lib.js +1 -1
- package/apps/web/public/pages/applications.js +17 -1
- package/apps/web/public/pages/settings.js +6 -4
- package/apps/web/public/styles.css +49 -36
- package/apps/web/src/info.ts +9 -22
- package/apps/web/src/routes.ts +17 -13
- package/apps/web/src/server.ts +33 -25
- package/apps/web/src/settings.ts +35 -17
- package/bin/careermate.mjs +8 -5
- package/dist/{mcpb-stage/dist/web.mjs โ doctor.mjs} +1122 -1450
- package/dist/install-page/favicon.svg +1 -0
- package/dist/install-page/icon-180.png +0 -0
- package/dist/install-page/icon-512.png +0 -0
- package/dist/install-page/index.html +309 -167
- package/dist/install-page/llms-install.txt +202 -0
- package/dist/install-page/og.png +0 -0
- package/dist/install-page/shots/applications-dark.png +0 -0
- package/dist/install-page/shots/applications-light.png +0 -0
- package/dist/install-page/shots/documents-dark.png +0 -0
- package/dist/install-page/shots/documents-light.png +0 -0
- package/dist/install-page/shots/home-dark.png +0 -0
- package/dist/install-page/shots/home-light.png +0 -0
- package/dist/install-page/shots/interview-dark.png +0 -0
- package/dist/install-page/shots/interview-light.png +0 -0
- package/dist/install-page/shots/jobs-dark.png +0 -0
- package/dist/install-page/shots/jobs-light.png +0 -0
- package/dist/install-page/shots/settings-dark.png +0 -0
- package/dist/install-page/shots/settings-light.png +0 -0
- package/dist/install-page/start.html +346 -0
- package/dist/install-page/style.css +94 -3
- package/dist/mcp.mjs +326 -68
- package/dist/migrate.mjs +4715 -0
- package/dist/public/icon-180.png +0 -0
- package/dist/public/index.html +4 -1
- package/dist/public/lib.js +1 -1
- package/dist/public/pages/applications.js +17 -1
- package/dist/public/pages/settings.js +6 -4
- package/dist/public/styles.css +49 -36
- package/dist/web.mjs +323 -129
- package/docs/DEMO.md +3 -3
- package/docs/MCP_TOOLS.md +21 -2
- package/docs/ROADMAP.md +3 -3
- package/docs/RUNTIME_DISTRIBUTION_RESEARCH.md +135 -0
- package/docs/START_WORKFLOW.md +6 -0
- package/docs/SUPPORTED_AI_APPS.md +85 -33
- package/docs/WORKFLOWS.md +1 -1
- package/package.json +3 -2
- package/packages/core/src/services.ts +42 -1
- package/packages/db/src/connection.ts +30 -0
- package/packages/db/src/index.ts +1 -0
- package/packages/db/src/repositories.ts +109 -63
- package/packages/db/src/schema.ts +118 -0
- package/packages/exporters/src/html.ts +9 -12
- package/packages/mcp-tools/src/tools.ts +81 -0
- package/packages/shared/src/enums.ts +39 -0
- package/scripts/build-dist.mjs +27 -8
- package/scripts/build-mcpb.mjs +60 -4
- package/scripts/gen-og.mjs +86 -0
- package/scripts/init.ts +32 -3
- package/scripts/seed.ts +5 -5
- package/scripts/shots-seed.ts +192 -0
- package/scripts/shots.ts +89 -0
- package/tsconfig.json +3 -1
- package/dist/careermate.mcpb +0 -0
- package/dist/mcp-smoke.err +0 -3
- package/dist/mcpb-stage/README.md +0 -219
- package/dist/mcpb-stage/dist/install-page/index.html +0 -434
- package/dist/mcpb-stage/dist/install-page/style.css +0 -407
- package/dist/mcpb-stage/dist/install-page/vercel.json +0 -20
- package/dist/mcpb-stage/dist/mcp.mjs +0 -23704
- package/dist/mcpb-stage/dist/public/app.js +0 -118
- package/dist/mcpb-stage/dist/public/index.html +0 -41
- package/dist/mcpb-stage/dist/public/lib.js +0 -282
- package/dist/mcpb-stage/dist/public/pages/applications.js +0 -98
- package/dist/mcpb-stage/dist/public/pages/documents.js +0 -446
- package/dist/mcpb-stage/dist/public/pages/home.js +0 -263
- package/dist/mcpb-stage/dist/public/pages/interview.js +0 -230
- package/dist/mcpb-stage/dist/public/pages/jobs.js +0 -494
- package/dist/mcpb-stage/dist/public/pages/profile.js +0 -576
- package/dist/mcpb-stage/dist/public/pages/settings.js +0 -233
- package/dist/mcpb-stage/dist/public/styles.css +0 -420
- package/dist/mcpb-stage/manifest.json +0 -40
- package/docs/INSTALL.md +0 -148
- package/docs/INSTALL_AND_USAGE.md +0 -99
- package/install-page/index.html +0 -474
- package/install-page/style.css +0 -391
- package/install-page/vercel.json +0 -20
- /package/dist/{mcpb-stage/dist/public โ install-page}/fonts/PretendardVariable.woff2 +0 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import {
|
|
7
7
|
newId,
|
|
8
8
|
now,
|
|
9
|
+
assertStatusTransition,
|
|
9
10
|
type ProfileInput,
|
|
10
11
|
type ProfileRecord,
|
|
11
12
|
type ExperienceInput,
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
type ContentSource,
|
|
34
35
|
type DocumentKind,
|
|
35
36
|
} from '@careermate/shared';
|
|
36
|
-
import { getDb, toJson, fromJson, toBit, fromBit } from './connection.ts';
|
|
37
|
+
import { getDb, tx, toJson, fromJson, toBit, fromBit } from './connection.ts';
|
|
37
38
|
|
|
38
39
|
const PROFILE_ID = 'profile-singleton';
|
|
39
40
|
|
|
@@ -364,34 +365,40 @@ export const documentRepo = {
|
|
|
364
365
|
const ts = now();
|
|
365
366
|
const id = newId('doc_');
|
|
366
367
|
const db = getDb();
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
368
|
+
// Demoting the previous primary + inserting the new doc must be atomic.
|
|
369
|
+
return tx(() => {
|
|
370
|
+
if (input.is_primary) {
|
|
371
|
+
db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(input.kind);
|
|
372
|
+
}
|
|
373
|
+
db.prepare(
|
|
374
|
+
`INSERT INTO documents (id,kind,title,content,source,is_primary,tags,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?)`,
|
|
375
|
+
).run(
|
|
376
|
+
id,
|
|
377
|
+
input.kind,
|
|
378
|
+
input.title,
|
|
379
|
+
input.content ?? '',
|
|
380
|
+
(input.source ?? 'manual') as ContentSource,
|
|
381
|
+
toBit(input.is_primary),
|
|
382
|
+
toJson(input.tags ?? []),
|
|
383
|
+
ts,
|
|
384
|
+
ts,
|
|
385
|
+
);
|
|
386
|
+
return this.get(id)!;
|
|
387
|
+
});
|
|
384
388
|
},
|
|
385
389
|
update(id: string, input: Partial<DocumentInput>): DocumentRecord | null {
|
|
386
390
|
const cur = this.get(id);
|
|
387
391
|
if (!cur) return null;
|
|
388
392
|
const db = getDb();
|
|
389
393
|
const m = { ...cur, ...input } as DocumentRecord;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
`UPDATE documents SET
|
|
393
|
-
|
|
394
|
-
|
|
394
|
+
// Demoting the previous primary + updating this doc must be atomic.
|
|
395
|
+
return tx(() => {
|
|
396
|
+
if (input.is_primary) db.prepare(`UPDATE documents SET is_primary=0 WHERE kind=?`).run(m.kind);
|
|
397
|
+
db.prepare(
|
|
398
|
+
`UPDATE documents SET kind=?,title=?,content=?,source=?,is_primary=?,tags=?,updated_at=? WHERE id=?`,
|
|
399
|
+
).run(m.kind, m.title, m.content, m.source, toBit(m.is_primary), toJson(m.tags), now(), id);
|
|
400
|
+
return this.get(id);
|
|
401
|
+
});
|
|
395
402
|
},
|
|
396
403
|
remove(id: string): boolean {
|
|
397
404
|
return getDb().prepare(`DELETE FROM documents WHERE id=?`).run(id).changes > 0;
|
|
@@ -453,11 +460,13 @@ export const coverLetterRepo = {
|
|
|
453
460
|
const ts = now();
|
|
454
461
|
const id = newId('cl_');
|
|
455
462
|
const db = getDb();
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
463
|
+
return tx(() => {
|
|
464
|
+
if (opts.is_primary) db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
|
|
465
|
+
db.prepare(
|
|
466
|
+
`INSERT INTO cover_letters (id,title,job_id,is_primary,current_version_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?)`,
|
|
467
|
+
).run(id, opts.title, opts.job_id ?? null, toBit(opts.is_primary), null, ts, ts);
|
|
468
|
+
return this.get(id)!;
|
|
469
|
+
});
|
|
461
470
|
},
|
|
462
471
|
/** Append a new version. Creates the cover letter if cover_letter_id is omitted. */
|
|
463
472
|
addVersion(opts: {
|
|
@@ -470,32 +479,36 @@ export const coverLetterRepo = {
|
|
|
470
479
|
set_current?: boolean;
|
|
471
480
|
}): { coverLetter: CoverLetterRecord; version: CoverLetterVersionRecord } {
|
|
472
481
|
const db = getDb();
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
clId =
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
482
|
+
// Create-if-needed + version INSERT + current_version_id/title/job_id UPDATEs
|
|
483
|
+
// must all commit together, or mapCoverLetter's "current" lookup goes stale.
|
|
484
|
+
return tx(() => {
|
|
485
|
+
let clId = opts.cover_letter_id;
|
|
486
|
+
if (!clId) {
|
|
487
|
+
const cl = this.create({ title: opts.title ?? '์๊ธฐ์๊ฐ์', job_id: opts.job_id ?? null });
|
|
488
|
+
clId = cl.id;
|
|
489
|
+
}
|
|
490
|
+
const maxRow = db
|
|
491
|
+
.prepare(`SELECT MAX(version_no) AS m FROM cover_letter_versions WHERE cover_letter_id=?`)
|
|
492
|
+
.get(clId) as { m: number | null };
|
|
493
|
+
const versionNo = (maxRow.m ?? 0) + 1;
|
|
494
|
+
const vid = newId('clv_');
|
|
495
|
+
const ts = now();
|
|
496
|
+
db.prepare(
|
|
497
|
+
`INSERT INTO cover_letter_versions (id,cover_letter_id,version_no,content,note,source,created_at) VALUES (?,?,?,?,?,?,?)`,
|
|
498
|
+
).run(vid, clId, versionNo, opts.content, opts.note ?? null, opts.source ?? 'ai', ts);
|
|
499
|
+
|
|
500
|
+
const setCurrent = opts.set_current !== false;
|
|
501
|
+
if (setCurrent) {
|
|
502
|
+
db.prepare(`UPDATE cover_letters SET current_version_id=?, updated_at=? WHERE id=?`).run(vid, ts, clId);
|
|
503
|
+
} else {
|
|
504
|
+
db.prepare(`UPDATE cover_letters SET updated_at=? WHERE id=?`).run(ts, clId);
|
|
505
|
+
}
|
|
506
|
+
if (opts.title) db.prepare(`UPDATE cover_letters SET title=? WHERE id=?`).run(opts.title, clId);
|
|
507
|
+
if (opts.job_id !== undefined)
|
|
508
|
+
db.prepare(`UPDATE cover_letters SET job_id=? WHERE id=?`).run(opts.job_id, clId);
|
|
509
|
+
|
|
510
|
+
return { coverLetter: this.get(clId)!, version: mapVersion(db.prepare(`SELECT * FROM cover_letter_versions WHERE id=?`).get(vid)) };
|
|
511
|
+
});
|
|
499
512
|
},
|
|
500
513
|
setCurrentVersion(coverLetterId: string, versionId: string): CoverLetterRecord | null {
|
|
501
514
|
getDb()
|
|
@@ -505,14 +518,18 @@ export const coverLetterRepo = {
|
|
|
505
518
|
},
|
|
506
519
|
setPrimary(id: string): CoverLetterRecord | null {
|
|
507
520
|
const db = getDb();
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
521
|
+
return tx(() => {
|
|
522
|
+
db.prepare(`UPDATE cover_letters SET is_primary=0`).run();
|
|
523
|
+
db.prepare(`UPDATE cover_letters SET is_primary=1, updated_at=? WHERE id=?`).run(now(), id);
|
|
524
|
+
return this.get(id);
|
|
525
|
+
});
|
|
511
526
|
},
|
|
512
527
|
remove(id: string): boolean {
|
|
513
528
|
const db = getDb();
|
|
514
|
-
|
|
515
|
-
|
|
529
|
+
return tx(() => {
|
|
530
|
+
db.prepare(`DELETE FROM cover_letter_versions WHERE cover_letter_id=?`).run(id);
|
|
531
|
+
return db.prepare(`DELETE FROM cover_letters WHERE id=?`).run(id).changes > 0;
|
|
532
|
+
});
|
|
516
533
|
},
|
|
517
534
|
};
|
|
518
535
|
|
|
@@ -603,10 +620,13 @@ export const jobRepo = {
|
|
|
603
620
|
},
|
|
604
621
|
remove(id: string): boolean {
|
|
605
622
|
const db = getDb();
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
623
|
+
// Delete the job and all its dependent rows atomically (no orphans on failure).
|
|
624
|
+
return tx(() => {
|
|
625
|
+
db.prepare(`DELETE FROM fit_analyses WHERE job_id=?`).run(id);
|
|
626
|
+
db.prepare(`DELETE FROM applications WHERE job_id=?`).run(id);
|
|
627
|
+
db.prepare(`DELETE FROM interview_preps WHERE job_id=?`).run(id);
|
|
628
|
+
return db.prepare(`DELETE FROM jobs WHERE id=?`).run(id).changes > 0;
|
|
629
|
+
});
|
|
610
630
|
},
|
|
611
631
|
};
|
|
612
632
|
|
|
@@ -727,6 +747,8 @@ export const applicationRepo = {
|
|
|
727
747
|
},
|
|
728
748
|
upsert(input: ApplicationInput): ApplicationRecord {
|
|
729
749
|
const app = this.ensure(input.job_id, input.status ?? 'draft');
|
|
750
|
+
// A status change through upsert must respect the same lifecycle gate as setStatus.
|
|
751
|
+
if (input.status && input.status !== app.status) assertStatusTransition(app.status, input.status);
|
|
730
752
|
const m = { ...app, ...input } as ApplicationRecord;
|
|
731
753
|
getDb()
|
|
732
754
|
.prepare(
|
|
@@ -737,6 +759,7 @@ export const applicationRepo = {
|
|
|
737
759
|
},
|
|
738
760
|
setStatus(jobId: string, status: ApplicationStatus, note?: string): ApplicationRecord {
|
|
739
761
|
const app = this.ensure(jobId);
|
|
762
|
+
assertStatusTransition(app.status, status);
|
|
740
763
|
const appliedAt =
|
|
741
764
|
status === 'applied' && !app.applied_at ? now() : app.applied_at;
|
|
742
765
|
const notes = note ? [app.notes, note].filter(Boolean).join('\n') : app.notes;
|
|
@@ -826,3 +849,26 @@ export const activityRepo = {
|
|
|
826
849
|
.all(limit) as ActivityRecord[];
|
|
827
850
|
},
|
|
828
851
|
};
|
|
852
|
+
|
|
853
|
+
/* ------------------------------------------------------------------ Counts */
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Entity counts for the dashboard/Settings/health endpoint. Uses COUNT(*) per
|
|
857
|
+
* table instead of loading every row (the /api/health call hits this), and keeps
|
|
858
|
+
* the raw SQL in the db layer. Table names are hardcoded constants, not input.
|
|
859
|
+
*/
|
|
860
|
+
export function getEntityCounts(): Record<string, number> {
|
|
861
|
+
const db = getDb();
|
|
862
|
+
const c = (t: string): number => (db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get() as { n: number }).n;
|
|
863
|
+
return {
|
|
864
|
+
profile: c('profile'),
|
|
865
|
+
experiences: c('experiences'),
|
|
866
|
+
projects: c('projects'),
|
|
867
|
+
skills: c('skills'),
|
|
868
|
+
documents: c('documents'),
|
|
869
|
+
cover_letters: c('cover_letters'),
|
|
870
|
+
jobs: c('jobs'),
|
|
871
|
+
applications: c('applications'),
|
|
872
|
+
interview_preps: c('interview_preps'),
|
|
873
|
+
};
|
|
874
|
+
}
|
|
@@ -165,6 +165,124 @@ export const MIGRATIONS: string[] = [
|
|
|
165
165
|
CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
|
|
166
166
|
CREATE INDEX IF NOT EXISTS idx_activities_created ON activities(created_at DESC);
|
|
167
167
|
`,
|
|
168
|
+
|
|
169
|
+
// v2 โ add foreign keys so PRAGMA foreign_keys=ON is actually enforced.
|
|
170
|
+
//
|
|
171
|
+
// SQLite cannot ALTER a table to add constraints, so each table that needs an
|
|
172
|
+
// FK is recreated (create new โ copy โ drop old โ rename). The copy also
|
|
173
|
+
// repairs pre-existing orphans: child rows whose parent is gone are dropped
|
|
174
|
+
// (CASCADE intent) and dangling optional refs are nulled (SET NULL intent), so
|
|
175
|
+
// a subsequent PRAGMA foreign_key_check stays clean. FKs are disabled for the
|
|
176
|
+
// rebuild and the whole thing runs in one transaction (auto-rolled-back if the
|
|
177
|
+
// process dies mid-migration, since the version bump happens only afterwards).
|
|
178
|
+
`
|
|
179
|
+
PRAGMA foreign_keys=OFF;
|
|
180
|
+
BEGIN;
|
|
181
|
+
|
|
182
|
+
-- cover_letters.job_id -> jobs(id) ON DELETE SET NULL
|
|
183
|
+
CREATE TABLE cover_letters_new (
|
|
184
|
+
id TEXT PRIMARY KEY,
|
|
185
|
+
title TEXT NOT NULL,
|
|
186
|
+
job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
|
|
187
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
188
|
+
current_version_id TEXT,
|
|
189
|
+
created_at TEXT NOT NULL,
|
|
190
|
+
updated_at TEXT NOT NULL
|
|
191
|
+
);
|
|
192
|
+
INSERT INTO cover_letters_new (id,title,job_id,is_primary,current_version_id,created_at,updated_at)
|
|
193
|
+
SELECT id,title,
|
|
194
|
+
CASE WHEN job_id IN (SELECT id FROM jobs) THEN job_id ELSE NULL END,
|
|
195
|
+
is_primary,current_version_id,created_at,updated_at
|
|
196
|
+
FROM cover_letters;
|
|
197
|
+
DROP TABLE cover_letters;
|
|
198
|
+
ALTER TABLE cover_letters_new RENAME TO cover_letters;
|
|
199
|
+
|
|
200
|
+
-- cover_letter_versions.cover_letter_id -> cover_letters(id) ON DELETE CASCADE
|
|
201
|
+
CREATE TABLE cover_letter_versions_new (
|
|
202
|
+
id TEXT PRIMARY KEY,
|
|
203
|
+
cover_letter_id TEXT NOT NULL REFERENCES cover_letters(id) ON DELETE CASCADE,
|
|
204
|
+
version_no INTEGER NOT NULL,
|
|
205
|
+
content TEXT NOT NULL,
|
|
206
|
+
note TEXT,
|
|
207
|
+
source TEXT NOT NULL DEFAULT 'ai',
|
|
208
|
+
created_at TEXT NOT NULL
|
|
209
|
+
);
|
|
210
|
+
INSERT INTO cover_letter_versions_new (id,cover_letter_id,version_no,content,note,source,created_at)
|
|
211
|
+
SELECT id,cover_letter_id,version_no,content,note,source,created_at
|
|
212
|
+
FROM cover_letter_versions
|
|
213
|
+
WHERE cover_letter_id IN (SELECT id FROM cover_letters);
|
|
214
|
+
DROP TABLE cover_letter_versions;
|
|
215
|
+
ALTER TABLE cover_letter_versions_new RENAME TO cover_letter_versions;
|
|
216
|
+
|
|
217
|
+
-- fit_analyses.job_id -> jobs(id) ON DELETE CASCADE
|
|
218
|
+
CREATE TABLE fit_analyses_new (
|
|
219
|
+
id TEXT PRIMARY KEY,
|
|
220
|
+
job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
|
|
221
|
+
score REAL,
|
|
222
|
+
summary TEXT,
|
|
223
|
+
strengths TEXT NOT NULL DEFAULT '[]',
|
|
224
|
+
gaps TEXT NOT NULL DEFAULT '[]',
|
|
225
|
+
matched_keywords TEXT NOT NULL DEFAULT '[]',
|
|
226
|
+
missing_keywords TEXT NOT NULL DEFAULT '[]',
|
|
227
|
+
recommendations TEXT NOT NULL DEFAULT '[]',
|
|
228
|
+
created_at TEXT NOT NULL,
|
|
229
|
+
updated_at TEXT NOT NULL
|
|
230
|
+
);
|
|
231
|
+
INSERT INTO fit_analyses_new (id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at)
|
|
232
|
+
SELECT id,job_id,score,summary,strengths,gaps,matched_keywords,missing_keywords,recommendations,created_at,updated_at
|
|
233
|
+
FROM fit_analyses
|
|
234
|
+
WHERE job_id IN (SELECT id FROM jobs);
|
|
235
|
+
DROP TABLE fit_analyses;
|
|
236
|
+
ALTER TABLE fit_analyses_new RENAME TO fit_analyses;
|
|
237
|
+
|
|
238
|
+
-- applications: job_id CASCADE, resume_id/cover_letter_id SET NULL
|
|
239
|
+
CREATE TABLE applications_new (
|
|
240
|
+
id TEXT PRIMARY KEY,
|
|
241
|
+
job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
|
|
242
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
243
|
+
resume_id TEXT REFERENCES documents(id) ON DELETE SET NULL,
|
|
244
|
+
cover_letter_id TEXT REFERENCES cover_letters(id) ON DELETE SET NULL,
|
|
245
|
+
applied_at TEXT,
|
|
246
|
+
notes TEXT,
|
|
247
|
+
created_at TEXT NOT NULL,
|
|
248
|
+
updated_at TEXT NOT NULL
|
|
249
|
+
);
|
|
250
|
+
INSERT INTO applications_new (id,job_id,status,resume_id,cover_letter_id,applied_at,notes,created_at,updated_at)
|
|
251
|
+
SELECT id,job_id,status,
|
|
252
|
+
CASE WHEN resume_id IN (SELECT id FROM documents) THEN resume_id ELSE NULL END,
|
|
253
|
+
CASE WHEN cover_letter_id IN (SELECT id FROM cover_letters) THEN cover_letter_id ELSE NULL END,
|
|
254
|
+
applied_at,notes,created_at,updated_at
|
|
255
|
+
FROM applications
|
|
256
|
+
WHERE job_id IN (SELECT id FROM jobs);
|
|
257
|
+
DROP TABLE applications;
|
|
258
|
+
ALTER TABLE applications_new RENAME TO applications;
|
|
259
|
+
|
|
260
|
+
-- interview_preps.job_id -> jobs(id) ON DELETE CASCADE
|
|
261
|
+
CREATE TABLE interview_preps_new (
|
|
262
|
+
id TEXT PRIMARY KEY,
|
|
263
|
+
job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
|
|
264
|
+
questions TEXT NOT NULL DEFAULT '[]',
|
|
265
|
+
star_guides TEXT NOT NULL DEFAULT '[]',
|
|
266
|
+
self_introduction TEXT,
|
|
267
|
+
notes TEXT,
|
|
268
|
+
created_at TEXT NOT NULL,
|
|
269
|
+
updated_at TEXT NOT NULL
|
|
270
|
+
);
|
|
271
|
+
INSERT INTO interview_preps_new (id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at)
|
|
272
|
+
SELECT id,job_id,questions,star_guides,self_introduction,notes,created_at,updated_at
|
|
273
|
+
FROM interview_preps
|
|
274
|
+
WHERE job_id IN (SELECT id FROM jobs);
|
|
275
|
+
DROP TABLE interview_preps;
|
|
276
|
+
ALTER TABLE interview_preps_new RENAME TO interview_preps;
|
|
277
|
+
|
|
278
|
+
-- Recreate indexes dropped along with their tables.
|
|
279
|
+
CREATE INDEX IF NOT EXISTS idx_fit_job ON fit_analyses(job_id);
|
|
280
|
+
CREATE INDEX IF NOT EXISTS idx_clv_letter ON cover_letter_versions(cover_letter_id);
|
|
281
|
+
CREATE INDEX IF NOT EXISTS idx_cl_job ON cover_letters(job_id);
|
|
282
|
+
|
|
283
|
+
COMMIT;
|
|
284
|
+
PRAGMA foreign_keys=ON;
|
|
285
|
+
`,
|
|
168
286
|
];
|
|
169
287
|
|
|
170
288
|
export function migrate(db: DatabaseSync): { from: number; to: number } {
|
|
@@ -18,20 +18,17 @@ export function escapeHtml(input: string | null | undefined): string {
|
|
|
18
18
|
.replace(/'/g, ''');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/** Heuristic: does this string already look like an HTML fragment? */
|
|
22
|
-
function looksLikeHtml(s: string): boolean {
|
|
23
|
-
return /<\/?(p|h[1-6]|ul|ol|li|div|section|article|br|hr|strong|em|a|table)\b/i.test(s);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
21
|
/**
|
|
27
|
-
* Wrap a
|
|
28
|
-
*
|
|
29
|
-
* is rendered through
|
|
22
|
+
* Wrap a markdown body in a complete, print-optimized HTML document.
|
|
23
|
+
*
|
|
24
|
+
* The body is ALWAYS rendered through {@link markdownToHtml}, which HTML-escapes
|
|
25
|
+
* its input before re-introducing only its own safe tags. Every exporter feeds
|
|
26
|
+
* this markdown it built itself, so there is no need to accept raw HTML โ and
|
|
27
|
+
* accepting it would let user content (e.g. a cover-letter body containing
|
|
28
|
+
* `<img onerror=...>`) execute when the exported file is opened in a browser.
|
|
30
29
|
*/
|
|
31
|
-
export function toPrintableHtml(title: string,
|
|
32
|
-
const body =
|
|
33
|
-
? bodyMarkdownOrHtml
|
|
34
|
-
: markdownToHtml(bodyMarkdownOrHtml);
|
|
30
|
+
export function toPrintableHtml(title: string, bodyMarkdown: string): string {
|
|
31
|
+
const body = markdownToHtml(bodyMarkdown);
|
|
35
32
|
const safeTitle = escapeHtml(title);
|
|
36
33
|
|
|
37
34
|
return `<!doctype html>
|
|
@@ -15,6 +15,9 @@ import fs from 'node:fs';
|
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import {
|
|
17
17
|
ProfileInputSchema,
|
|
18
|
+
ExperienceInputSchema,
|
|
19
|
+
ProjectInputSchema,
|
|
20
|
+
SkillInputSchema,
|
|
18
21
|
JobInputSchema,
|
|
19
22
|
FitAnalysisInputSchema,
|
|
20
23
|
CoverLetterVersionInputSchema,
|
|
@@ -26,6 +29,9 @@ import {
|
|
|
26
29
|
} from '@careermate/shared';
|
|
27
30
|
import {
|
|
28
31
|
profileRepo,
|
|
32
|
+
experienceRepo,
|
|
33
|
+
projectRepo,
|
|
34
|
+
skillRepo,
|
|
29
35
|
documentRepo,
|
|
30
36
|
coverLetterRepo,
|
|
31
37
|
jobRepo,
|
|
@@ -40,6 +46,9 @@ import {
|
|
|
40
46
|
getApplicationContext,
|
|
41
47
|
saveProfile,
|
|
42
48
|
addResume,
|
|
49
|
+
addExperience,
|
|
50
|
+
addProject,
|
|
51
|
+
addSkill,
|
|
43
52
|
saveJobPosting,
|
|
44
53
|
saveFitAnalysis,
|
|
45
54
|
saveCoverLetterVersion,
|
|
@@ -156,6 +165,78 @@ export const TOOLS: ToolDef[] = [
|
|
|
156
165
|
},
|
|
157
166
|
},
|
|
158
167
|
|
|
168
|
+
/* ----------------------------------------------------------- experiences */
|
|
169
|
+
{
|
|
170
|
+
name: 'add_experience',
|
|
171
|
+
title: '๊ฒฝ๋ ฅ ์ถ๊ฐ',
|
|
172
|
+
description:
|
|
173
|
+
'๊ตฌ์กฐํ๋ ๊ฒฝ๋ ฅ(์ง์ฅ ๋จ์)์ ๋ณ๋ ํญ๋ชฉ์ผ๋ก ์ ์ฅํฉ๋๋ค. ์ด๋ ฅ์ ๋ณธ๋ฌธ(add_resume)๊ณผ ๋ฌ๋ฆฌ, ํ์ฌยท์ง๋ฌดยท๊ธฐ๊ฐยท์ฑ๊ณผ๋ฅผ ํ๋๋ก ๋๋ ์ ์ฅํด ์ ํฉ๋ ๋ถ์/์์์ ์์ฑ ์ ์ ๋ฐํ๊ฒ ํ์ฉ๋ฉ๋๋ค. company๋ ํ์์ด๋ฉฐ, achievements์๋ ์ ๋ ์งํ๊ฐ ๋ด๊ธด ์ฑ๊ณผ๋ฅผ, tech์๋ ์ฌ์ฉ ๊ธฐ์ ์ ๋ฐฐ์ด๋ก ๋ฃ์ผ์ธ์. ์จ๋ณด๋ฉ ์์ฑ๋(get_onboarding_status)์ has_experience๋ฅผ ์ฑ์ฐ๋ ค๋ฉด ์ด ๋๊ตฌ๋ก 1๊ฑด ์ด์ ์ถ๊ฐํด์ผ ํฉ๋๋ค.',
|
|
174
|
+
inputSchema: ExperienceInputSchema.shape,
|
|
175
|
+
handler: (args) => {
|
|
176
|
+
const exp = addExperience(args);
|
|
177
|
+
return ok(`๊ฒฝ๋ ฅ '${exp.company}${exp.role ? ` ยท ${exp.role}` : ''}'์(๋ฅผ) ์ถ๊ฐํ์ต๋๋ค.`, exp);
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'get_experiences',
|
|
182
|
+
title: '๊ฒฝ๋ ฅ ๋ชฉ๋ก ์กฐํ',
|
|
183
|
+
description: '์ ์ฅ๋ ๊ฒฝ๋ ฅ์ ์๊ฐ ์ญ์์ผ๋ก ์กฐํํฉ๋๋ค. (์ ์ฒด ๋งฅ๋ฝ์ด ํ์ํ๋ฉด get_application_context๋ฅผ ์ฐ์ธ์.)',
|
|
184
|
+
inputSchema: {},
|
|
185
|
+
readOnly: true,
|
|
186
|
+
handler: () => {
|
|
187
|
+
const list = experienceRepo.list();
|
|
188
|
+
return ok(`๊ฒฝ๋ ฅ ${list.length}๊ฑด`, list);
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/* -------------------------------------------------------------- projects */
|
|
193
|
+
{
|
|
194
|
+
name: 'add_project',
|
|
195
|
+
title: 'ํ๋ก์ ํธ ์ถ๊ฐ',
|
|
196
|
+
description:
|
|
197
|
+
'๋ํ ํ๋ก์ ํธ๋ฅผ ๋ณ๋ ํญ๋ชฉ์ผ๋ก ์ ์ฅํฉ๋๋ค. name์ ํ์์ด๋ฉฐ, highlights(ํต์ฌ ์ฑ๊ณผ)์ tech(์ฌ์ฉ ๊ธฐ์ )๋ฅผ ๋ฐฐ์ด๋ก ๋ฃ์ผ๋ฉด ์์์ยท๋ฉด์ ์ค๋น์์ ๊ทผ๊ฑฐ๋ก ํ์ฉ๋ฉ๋๋ค. url์ ๊ฒฐ๊ณผ๋ฌผ ๋งํฌ๋ฅผ ๋ฃ์ ์ ์์ต๋๋ค.',
|
|
198
|
+
inputSchema: ProjectInputSchema.shape,
|
|
199
|
+
handler: (args) => {
|
|
200
|
+
const prj = addProject(args);
|
|
201
|
+
return ok(`ํ๋ก์ ํธ '${prj.name}'์(๋ฅผ) ์ถ๊ฐํ์ต๋๋ค.`, prj);
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'get_projects',
|
|
206
|
+
title: 'ํ๋ก์ ํธ ๋ชฉ๋ก ์กฐํ',
|
|
207
|
+
description: '์ ์ฅ๋ ํ๋ก์ ํธ๋ฅผ ์กฐํํฉ๋๋ค. (์ ์ฒด ๋งฅ๋ฝ์ด ํ์ํ๋ฉด get_application_context๋ฅผ ์ฐ์ธ์.)',
|
|
208
|
+
inputSchema: {},
|
|
209
|
+
readOnly: true,
|
|
210
|
+
handler: () => {
|
|
211
|
+
const list = projectRepo.list();
|
|
212
|
+
return ok(`ํ๋ก์ ํธ ${list.length}๊ฑด`, list);
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
/* ---------------------------------------------------------------- skills */
|
|
217
|
+
{
|
|
218
|
+
name: 'add_skill',
|
|
219
|
+
title: '๊ธฐ์ ์คํ ์ถ๊ฐ',
|
|
220
|
+
description:
|
|
221
|
+
'๋ณด์ ๊ธฐ์ ์คํ์ ๋ณ๋ ํญ๋ชฉ์ผ๋ก ์ ์ฅํฉ๋๋ค. name์ ํ์์ด๋ฉฐ, category(์ธ์ด/ํ๋ ์์ํฌ/ํด/์ํํธ์คํฌ ๋ฑ)ยทlevelยทyears๋ก ์๋ จ๋๋ฅผ ํจ๊ป ๊ธฐ๋กํ๋ฉด ๊ณต๊ณ ํค์๋ ๋งค์นญ์ด ์ ํํด์ง๋๋ค. ์จ๋ณด๋ฉ ์์ฑ๋์ has_skills๋ฅผ ์ฑ์ฐ๋ ค๋ฉด ์ด ๋๊ตฌ๋ก 1๊ฑด ์ด์ ์ถ๊ฐํด์ผ ํฉ๋๋ค. ์ฌ๋ฌ ๊ฐ๋ฉด ๋๊ตฌ๋ฅผ ๋ฐ๋ณต ํธ์ถํ์ธ์.',
|
|
222
|
+
inputSchema: SkillInputSchema.shape,
|
|
223
|
+
handler: (args) => {
|
|
224
|
+
const skill = addSkill(args);
|
|
225
|
+
return ok(`๊ธฐ์ '${skill.name}'์(๋ฅผ) ์ถ๊ฐํ์ต๋๋ค.`, skill);
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'get_skills',
|
|
230
|
+
title: '๊ธฐ์ ์คํ ๋ชฉ๋ก ์กฐํ',
|
|
231
|
+
description: '์ ์ฅ๋ ๊ธฐ์ ์คํ์ ์กฐํํฉ๋๋ค. (์ ์ฒด ๋งฅ๋ฝ์ด ํ์ํ๋ฉด get_application_context๋ฅผ ์ฐ์ธ์.)',
|
|
232
|
+
inputSchema: {},
|
|
233
|
+
readOnly: true,
|
|
234
|
+
handler: () => {
|
|
235
|
+
const list = skillRepo.list();
|
|
236
|
+
return ok(`๊ธฐ์ ${list.length}๊ฑด`, list);
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
|
|
159
240
|
/* ---------------------------------------------------------- cover letters */
|
|
160
241
|
{
|
|
161
242
|
name: 'add_cover_letter',
|
|
@@ -51,6 +51,45 @@ export const INTERVIEW_UNLOCK_STATUSES: ApplicationStatus[] = [
|
|
|
51
51
|
'final_passed',
|
|
52
52
|
];
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Allowed application-status transitions (server-enforced lifecycle).
|
|
56
|
+
*
|
|
57
|
+
* Design note: CareerMate lets the user's AI record where an application *really*
|
|
58
|
+
* is, so `draft` (the initial, untracked state) may jump to ANY real-world stage.
|
|
59
|
+
* Once an application is being tracked, it may move forward, be corrected
|
|
60
|
+
* backward among active stages, be parked (`on_hold`) or closed (`rejected`) from
|
|
61
|
+
* anywhere, and be reopened/resumed from those terminal states. The one move we
|
|
62
|
+
* forbid is regressing an *active* application back to `draft` ("untracked"),
|
|
63
|
+
* which would silently drop its lifecycle history. The hard spec invariant
|
|
64
|
+
* (interview prep requires `document_passed`+) is enforced separately at the
|
|
65
|
+
* service layer via {@link INTERVIEW_UNLOCK_STATUSES}.
|
|
66
|
+
*/
|
|
67
|
+
export const ALLOWED_STATUS_TRANSITIONS: Record<ApplicationStatus, ApplicationStatus[]> = {
|
|
68
|
+
draft: ['planned', 'applied', 'document_passed', 'interview', 'final_passed', 'rejected', 'on_hold'],
|
|
69
|
+
planned: ['applied', 'document_passed', 'interview', 'final_passed', 'rejected', 'on_hold'],
|
|
70
|
+
applied: ['planned', 'document_passed', 'interview', 'final_passed', 'rejected', 'on_hold'],
|
|
71
|
+
document_passed: ['planned', 'applied', 'interview', 'final_passed', 'rejected', 'on_hold'],
|
|
72
|
+
interview: ['planned', 'applied', 'document_passed', 'final_passed', 'rejected', 'on_hold'],
|
|
73
|
+
final_passed: ['document_passed', 'interview', 'rejected', 'on_hold'],
|
|
74
|
+
rejected: ['draft', 'planned', 'applied', 'document_passed', 'interview', 'final_passed', 'on_hold'],
|
|
75
|
+
on_hold: ['draft', 'planned', 'applied', 'document_passed', 'interview', 'final_passed', 'rejected'],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Whether moving from `from` to `to` is a permitted status transition (sameโsame is always allowed). */
|
|
79
|
+
export function canTransitionStatus(from: ApplicationStatus, to: ApplicationStatus): boolean {
|
|
80
|
+
if (from === to) return true;
|
|
81
|
+
return ALLOWED_STATUS_TRANSITIONS[from]?.includes(to) ?? false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Throw a clear, user-facing (Korean) error when a status transition is not allowed. */
|
|
85
|
+
export function assertStatusTransition(from: ApplicationStatus, to: ApplicationStatus): void {
|
|
86
|
+
if (!canTransitionStatus(from, to)) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`'${APPLICATION_STATUS_LABELS[from]}'์์ '${APPLICATION_STATUS_LABELS[to]}'(์ผ)๋ก๋ ์ํ๋ฅผ ๋ฐ๊ฟ ์ ์์ต๋๋ค.`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
/** Stored document kinds (resumes and career descriptions). Cover letters live in their own tables. */
|
|
55
94
|
export const DOCUMENT_KINDS = ['resume', 'career_description', 'portfolio', 'other'] as const;
|
|
56
95
|
export type DocumentKind = (typeof DOCUMENT_KINDS)[number];
|
package/scripts/build-dist.mjs
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* scripts/build-dist.mjs โ ๋ฐฐํฌ์ฉ ํ๋ ์ธ JS ๋ฒ๋ค ๋น๋ (esbuild).
|
|
4
4
|
*
|
|
5
5
|
* ๊ฐ๋ฐ์ ๊ทธ๋๋ก tsx ๋ฌด๋น๋๋ก ๋๋ฆฌ๊ณ , ๋ฐฐํฌ(.mcpb / npm publish / ์ถํ ๋
๋ฆฝ .exe)์์๋
|
|
6
|
-
* tsxยทtsconfig ๊ฒฝ๋ก๋ณ์นญ ๋ฐํ์ ์์กด์ฑ์ ์์ ๊ธฐ ์ํด
|
|
6
|
+
* tsxยทtsconfig ๊ฒฝ๋ก๋ณ์นญ ๋ฐํ์ ์์กด์ฑ์ ์์ ๊ธฐ ์ํด ๋ค ์ง์
์ ์ ๊ฐ๊ฐ ๋จ์ผ ESM ํ์ผ๋ก ๋ฒ๋คํ๋ค.
|
|
7
7
|
*
|
|
8
|
-
* dist/mcp.mjs
|
|
9
|
-
* dist/web.mjs
|
|
8
|
+
* dist/mcp.mjs MCP stdio ์๋ฒ (Claude Desktop์ด ์คํ)
|
|
9
|
+
* dist/web.mjs ๋ก์ปฌ ๋์๋ณด๋ ์๋ฒ
|
|
10
|
+
* dist/doctor.mjs ์ค์น/ํ๊ฒฝ ์ ๊ฒ CLI
|
|
11
|
+
* dist/migrate.mjs DB ๋ง์ด๊ทธ๋ ์ด์
CLI
|
|
10
12
|
* dist/public/ ๋์๋ณด๋ ์ ์ ์์ฐ (apps/web/public ๋ณต์ฌ)
|
|
11
13
|
* dist/install-page/ ์ค์น ์๋ด ํ์ด์ง ๋ณต์ฌ
|
|
12
14
|
*
|
|
@@ -23,14 +25,25 @@ const DIST = path.join(ROOT, 'dist');
|
|
|
23
25
|
|
|
24
26
|
async function main() {
|
|
25
27
|
console.log('CareerMate dist ๋ฒ๋ค ๋น๋(esbuild)โฆ');
|
|
26
|
-
|
|
27
|
-
fs.
|
|
28
|
+
// package.json์ ๋ฒ์ ๋จ์ผ ์ถ์ฒ๋ก ์ผ์ ๋ฒ๋ค์ ์ฃผ์
(์ฑ๋ณ ํ๋์ฝ๋ฉ ๋ฒ์ ๋๋ฆฌํํธ ๋ฐฉ์ง).
|
|
29
|
+
const pkgVersion = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version;
|
|
30
|
+
// dist๋ฅผ ํต์งธ๋ก ๋น์ฐ๊ณ ๋ค์ ๋ง๋ ๋ค. build-dist๊ฐ ์์ ํ๋ ์ฐ์ถ๋ฌผ(mcp/web/doctor/migrate/
|
|
31
|
+
// public/install-page)๋ง ๋จ๊ธฐ๊ธฐ ์ํจ. ์ด๋ ๊ฒ ํด์ผ ์ง์ build:mcpb๊ฐ ๋จ๊ธด mcpb-stage/ยท
|
|
32
|
+
// careermate.mcpb(์ MB)๋ ํ
์คํธ ์์ฌ๋ฌผ(mcp-smoke.err)์ด npm tarball์ ์์ฌ ๋ค์ด๊ฐ์ง ์๋๋ค
|
|
33
|
+
// (npm publish์ prepack์ build-dist๋ง ์คํํ๋ฏ๋ก ์ฌ๊ธฐ์ ๋น์ฐ๋ฉด ๊ฒ์๋ณธ์ด ๊นจ๋ํ๋ค).
|
|
34
|
+
// build:mcpb๋ ์ด ํจ์๋ฅผ ๋จผ์ ํธ์ถํ ๋ค mcpb-stage/ยทcareermate.mcpb๋ฅผ ๋ค์ ์์ฑํ๋ฏ๋ก ์์ .
|
|
35
|
+
fs.rmSync(DIST, { recursive: true, force: true });
|
|
36
|
+
fs.mkdirSync(DIST, { recursive: true });
|
|
28
37
|
|
|
29
38
|
await esbuild.build({
|
|
30
39
|
absWorkingDir: ROOT,
|
|
31
40
|
entryPoints: {
|
|
32
41
|
mcp: 'apps/mcp/src/index.ts',
|
|
33
42
|
web: 'apps/web/src/index.ts',
|
|
43
|
+
// doctor/migrate๋ ๋ฒ๋คํด ๋๋ค. ์ด ๋์ @careermate/* ์ํฌ์คํ์ด์ค ํจํค์ง๋ฅผ importํ๋๋ฐ,
|
|
44
|
+
// ๊ฒ์ tarball์๋ ๊ทธ ํจํค์ง๋ค์ด resolvableํ์ง ์์ tsx ํด๋ฐฑ์ด ์คํจํ๋ค(๋ฒ๋ค์ด๋ฉด ์ธ๋ผ์ธ๋ผ ๋์).
|
|
45
|
+
doctor: 'scripts/doctor.ts',
|
|
46
|
+
migrate: 'scripts/migrate.ts',
|
|
34
47
|
},
|
|
35
48
|
outdir: 'dist',
|
|
36
49
|
outExtension: { '.js': '.mjs' },
|
|
@@ -39,7 +52,7 @@ async function main() {
|
|
|
39
52
|
format: 'esm',
|
|
40
53
|
target: 'node22',
|
|
41
54
|
tsconfig: 'tsconfig.json',
|
|
42
|
-
define: { __BUNDLED__: 'true' },
|
|
55
|
+
define: { __BUNDLED__: 'true', __APP_VERSION__: JSON.stringify(pkgVersion) },
|
|
43
56
|
// ESM ์ถ๋ ฅ์์ CJS ์์กด์ฑ์ด require/__dirname์ ์จ๋ ๋์ํ๋๋ก shim ์ฃผ์
.
|
|
44
57
|
banner: {
|
|
45
58
|
js: "import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);",
|
|
@@ -51,9 +64,15 @@ async function main() {
|
|
|
51
64
|
fs.rmSync(path.join(DIST, 'public'), { recursive: true, force: true });
|
|
52
65
|
fs.rmSync(path.join(DIST, 'install-page'), { recursive: true, force: true });
|
|
53
66
|
fs.cpSync(path.join(ROOT, 'apps', 'web', 'public'), path.join(DIST, 'public'), { recursive: true });
|
|
54
|
-
|
|
67
|
+
// install-page/careermate.mcpb ๋ ๊ณต๊ฐ ์ฌ์ดํธ(careermate.life)์ ๋ค์ด๋ก๋ ์ฐ์ถ๋ฌผ์ผ ๋ฟ,
|
|
68
|
+
// ๋ฒ๋ค ๋์๋ณด๋์ /install/ ๋ผ์ฐํธ์๋ ํ์ ์๋ค. ์ด๊ฑธ dist๋ก ๋ค์ด๋ฉด .mcpb ๋น๋๊ฐ ์ง์
|
|
69
|
+
// ๋ฒ๋ค(์๊ธฐ ์์ , ~6MB)์ ๋ค์ ์ ์ด ๋ฃ์ด ๋งค ๋น๋๋ง๋ค ์ฉ๋์ด ๋ถ์ด๋๋ค(์๊ธฐ์ฐธ์กฐ). ์ ์ธํ๋ค.
|
|
70
|
+
fs.cpSync(path.join(ROOT, 'install-page'), path.join(DIST, 'install-page'), {
|
|
71
|
+
recursive: true,
|
|
72
|
+
filter: (src) => path.basename(src) !== 'careermate.mcpb',
|
|
73
|
+
});
|
|
55
74
|
|
|
56
|
-
console.log('โ
dist ๋น๋ ์๋ฃ: dist/mcp.mjs, dist/web.mjs, dist/public, dist/install-page');
|
|
75
|
+
console.log('โ
dist ๋น๋ ์๋ฃ: dist/mcp.mjs, dist/web.mjs, dist/doctor.mjs, dist/migrate.mjs, dist/public, dist/install-page');
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
main().catch((err) => {
|