careermate 0.0.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/AGENTS.md +33 -0
- package/INSTALL.md +230 -0
- package/LICENSE +21 -0
- package/README.md +532 -0
- package/THIRD_PARTY_NOTICES.md +57 -0
- package/bin/careermate.mjs +77 -0
- package/dist/career-os/README.md +78 -0
- package/dist/career-os/eop/cover-letter.md +46 -0
- package/dist/career-os/eop/fit-analysis.md +48 -0
- package/dist/career-os/eop/interview-prep.md +39 -0
- package/dist/career-os/eop/job-analysis.md +42 -0
- package/dist/career-os/eop/profile-extraction.md +37 -0
- package/dist/career-os/knowledge/ats.md +131 -0
- package/dist/career-os/knowledge/company-research.md +137 -0
- package/dist/career-os/knowledge/cover-letter.md +135 -0
- package/dist/career-os/knowledge/fit-matching.md +121 -0
- package/dist/career-os/knowledge/human-writing.md +118 -0
- package/dist/career-os/knowledge/interview-behavioral.md +132 -0
- package/dist/career-os/knowledge/interview-technical.md +126 -0
- package/dist/career-os/knowledge/linkedin-profile.md +152 -0
- package/dist/career-os/knowledge/networking-referrals.md +166 -0
- package/dist/career-os/knowledge/offer-evaluation-decision.md +121 -0
- package/dist/career-os/knowledge/onboarding-first-90-days.md +126 -0
- package/dist/career-os/knowledge/portfolio.md +121 -0
- package/dist/career-os/knowledge/recruiter-screen.md +105 -0
- package/dist/career-os/knowledge/rejection-triage-iteration.md +115 -0
- package/dist/career-os/knowledge/resume.md +123 -0
- package/dist/career-os/knowledge/salary-negotiation.md +123 -0
- package/dist/career-os/knowledge/verifiers/ats-compat.md +64 -0
- package/dist/career-os/knowledge/verifiers/consistency.md +61 -0
- package/dist/career-os/knowledge/verifiers/human-voice.md +82 -0
- package/dist/career-os/knowledge/verifiers/recency-staleness.md +72 -0
- package/dist/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
- package/dist/career-os/knowledge/verifiers/truthfulness.md +67 -0
- package/dist/doctor.mjs +79094 -0
- package/dist/mcp.mjs +96108 -0
- package/dist/migrate.mjs +4735 -0
- package/dist/pdf.worker.min.mjs +28 -0
- package/dist/public/app.js +150 -0
- package/dist/public/fonts/PretendardVariable.woff2 +0 -0
- package/dist/public/icon-180.png +0 -0
- package/dist/public/index.html +52 -0
- package/dist/public/lib.js +394 -0
- package/dist/public/pages/applications.js +107 -0
- package/dist/public/pages/documents.js +467 -0
- package/dist/public/pages/home.js +261 -0
- package/dist/public/pages/interview.js +227 -0
- package/dist/public/pages/jobs.js +493 -0
- package/dist/public/pages/profile.js +556 -0
- package/dist/public/pages/settings.js +240 -0
- package/dist/public/styles.css +498 -0
- package/dist/site/analytics.js +97 -0
- package/dist/site/favicon.svg +1 -0
- package/dist/site/fonts/PretendardVariable.woff2 +0 -0
- package/dist/site/icon-180.png +0 -0
- package/dist/site/icon-512.png +0 -0
- package/dist/site/index.html +598 -0
- package/dist/site/landing.js +54 -0
- package/dist/site/llms-install.txt +217 -0
- package/dist/site/robots.txt +4 -0
- package/dist/site/sitemap.xml +21 -0
- package/dist/site/start.html +326 -0
- package/dist/site/style.css +550 -0
- package/dist/site/vercel.json +20 -0
- package/dist/web.mjs +46852 -0
- package/docs/FAQ.md +101 -0
- package/docs/MCP_TOOLS.md +311 -0
- package/docs/README.md +28 -0
- package/docs/START_WORKFLOW.md +137 -0
- package/docs/SUPPORTED_AI_APPS.md +112 -0
- package/docs/WORKFLOWS.md +202 -0
- package/docs/career-os/README.md +78 -0
- package/docs/career-os/eop/cover-letter.md +46 -0
- package/docs/career-os/eop/fit-analysis.md +48 -0
- package/docs/career-os/eop/interview-prep.md +39 -0
- package/docs/career-os/eop/job-analysis.md +42 -0
- package/docs/career-os/eop/profile-extraction.md +37 -0
- package/docs/career-os/knowledge/ats.md +131 -0
- package/docs/career-os/knowledge/company-research.md +137 -0
- package/docs/career-os/knowledge/cover-letter.md +135 -0
- package/docs/career-os/knowledge/fit-matching.md +121 -0
- package/docs/career-os/knowledge/human-writing.md +118 -0
- package/docs/career-os/knowledge/interview-behavioral.md +132 -0
- package/docs/career-os/knowledge/interview-technical.md +126 -0
- package/docs/career-os/knowledge/linkedin-profile.md +152 -0
- package/docs/career-os/knowledge/networking-referrals.md +166 -0
- package/docs/career-os/knowledge/offer-evaluation-decision.md +121 -0
- package/docs/career-os/knowledge/onboarding-first-90-days.md +126 -0
- package/docs/career-os/knowledge/portfolio.md +121 -0
- package/docs/career-os/knowledge/recruiter-screen.md +105 -0
- package/docs/career-os/knowledge/rejection-triage-iteration.md +115 -0
- package/docs/career-os/knowledge/resume.md +123 -0
- package/docs/career-os/knowledge/salary-negotiation.md +123 -0
- package/docs/career-os/knowledge/verifiers/ats-compat.md +64 -0
- package/docs/career-os/knowledge/verifiers/consistency.md +61 -0
- package/docs/career-os/knowledge/verifiers/human-voice.md +82 -0
- package/docs/career-os/knowledge/verifiers/recency-staleness.md +72 -0
- package/docs/career-os/knowledge/verifiers/responsiveness-on-target.md +82 -0
- package/docs/career-os/knowledge/verifiers/truthfulness.md +67 -0
- package/package.json +89 -0
- package/packages/core/src/context.ts +74 -0
- package/packages/core/src/index.ts +8 -0
- package/packages/core/src/onboarding.ts +81 -0
- package/packages/core/src/services.ts +187 -0
- package/packages/core/src/summary.ts +104 -0
- package/packages/db/src/connection.ts +76 -0
- package/packages/db/src/index.ts +23 -0
- package/packages/db/src/paths.ts +52 -0
- package/packages/db/src/repositories.ts +891 -0
- package/packages/db/src/runtime.ts +58 -0
- package/packages/db/src/schema.ts +307 -0
- package/packages/exporters/src/html.ts +110 -0
- package/packages/exporters/src/index.ts +364 -0
- package/packages/exporters/src/markdown.ts +178 -0
- package/packages/knowledge/src/index.ts +168 -0
- package/packages/mcp-tools/src/bridge.ts +106 -0
- package/packages/mcp-tools/src/index.ts +17 -0
- package/packages/mcp-tools/src/result.ts +49 -0
- package/packages/mcp-tools/src/tools.ts +747 -0
- package/packages/mcp-tools/src/update.ts +330 -0
- package/packages/parsers/src/documents.ts +533 -0
- package/packages/parsers/src/html.ts +86 -0
- package/packages/parsers/src/index.ts +231 -0
- package/packages/parsers/src/keywords.ts +151 -0
- package/packages/prompts/src/humanize.ts +59 -0
- package/packages/prompts/src/index.ts +82 -0
- package/packages/prompts/src/install.ts +43 -0
- package/packages/prompts/src/onboarding.ts +35 -0
- package/packages/prompts/src/system.ts +57 -0
- package/packages/shared/src/build-info.ts +22 -0
- package/packages/shared/src/enums.ts +142 -0
- package/packages/shared/src/index.ts +19 -0
- package/packages/shared/src/schemas.ts +453 -0
- package/packages/workflows/src/definitions.ts +109 -0
- package/packages/workflows/src/index.ts +39 -0
- package/scripts/init.ts +371 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 업데이트 확인 + 자기 업데이트.
|
|
3
|
+
*
|
|
4
|
+
* CareerMate는 `npm install --prefix <설치폴더> careermate`로 한 곳에 **고정 설치**된다
|
|
5
|
+
* (npx@latest 매 실행 방식의 불안정함을 피하기 위함). 고정 설치라 자동 최신화가 안 되므로,
|
|
6
|
+
* 사용자가 새 버전을 모르고 지나가는 갭을 메우는 두 장치를 둔다:
|
|
7
|
+
*
|
|
8
|
+
* 1) getUpdateStatus() — 비차단 신호. 캐시(24h)를 동기로 읽고, 만료면 백그라운드로만
|
|
9
|
+
* 재확인한다. get_onboarding_status 응답에 실어 에이전트가 사용자에게 알리게 한다.
|
|
10
|
+
* 2) runSelfUpdate() — 사용자가 "업데이트해줘"라고 하면 에이전트가 호출. 설치 폴더에
|
|
11
|
+
* `npm install ... careermate@latest`를 실행하고 재시작을 안내한다(재시작 시 DB는
|
|
12
|
+
* connection.ts의 migrate()가 자동 적용).
|
|
13
|
+
*
|
|
14
|
+
* 네트워크: 공개 npm 레지스트리(registry.npmjs.org)에 버전만 GET한다. 사용자 데이터는
|
|
15
|
+
* 전송하지 않는다. `CAREERMATE_NO_UPDATE_CHECK=1`로 끌 수 있다.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
import { getDataDir } from '@careermate/db';
|
|
22
|
+
import { APP_VERSION, BUNDLED } from '@careermate/shared';
|
|
23
|
+
|
|
24
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/careermate/latest';
|
|
25
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 성공한 확인은 하루에 한 번만
|
|
26
|
+
const NULL_CACHE_TTL_MS = 30 * 60 * 1000; // 실패(오프라인 등)는 30분 뒤 재시도 — 하루 동안 고정되지 않게
|
|
27
|
+
const FETCH_TIMEOUT_MS = 1500;
|
|
28
|
+
const INSTALL_TIMEOUT_MS = 120_000;
|
|
29
|
+
|
|
30
|
+
const cacheFile = (): string => path.join(getDataDir(), '.update-check.json');
|
|
31
|
+
|
|
32
|
+
function checkDisabled(): boolean {
|
|
33
|
+
const v = process.env.CAREERMATE_NO_UPDATE_CHECK?.trim().toLowerCase();
|
|
34
|
+
return v === '1' || v === 'true';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** x.y.z[-pre][+build] 비교. a>b면 1, a<b면 -1, 같으면 0. 정식 릴리스가 프리릴리스보다 높다(semver). */
|
|
38
|
+
export function compareVersions(a: string, b: string): number {
|
|
39
|
+
const parse = (v: string) => {
|
|
40
|
+
const noBuild = (v.split('+', 1)[0] ?? '').trim(); // build metadata(+...)는 우선순위에 영향 없음
|
|
41
|
+
const dash = noBuild.indexOf('-');
|
|
42
|
+
const core = dash === -1 ? noBuild : noBuild.slice(0, dash);
|
|
43
|
+
const pre = dash === -1 ? '' : noBuild.slice(dash + 1);
|
|
44
|
+
const nums = core.split('.').map((n) => parseInt(n, 10) || 0);
|
|
45
|
+
return { nums, pre };
|
|
46
|
+
};
|
|
47
|
+
const pa = parse(a);
|
|
48
|
+
const pb = parse(b);
|
|
49
|
+
for (let i = 0; i < 3; i++) {
|
|
50
|
+
const d = (pa.nums[i] ?? 0) - (pb.nums[i] ?? 0);
|
|
51
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
52
|
+
}
|
|
53
|
+
// 코어 동일: 프리릴리스 비교. 프리릴리스 없음(정식)이 있음보다 높다.
|
|
54
|
+
if (pa.pre === '' && pb.pre === '') return 0;
|
|
55
|
+
if (pa.pre === '') return 1;
|
|
56
|
+
if (pb.pre === '') return -1;
|
|
57
|
+
return comparePrerelease(pa.pre, pb.pre);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* semver 프리릴리스 식별자 비교(점으로 분리). 규칙: 숫자vs숫자는 수치 비교,
|
|
62
|
+
* 숫자는 영숫자보다 낮음, 영숫자는 사전식, 모두 같으면 식별자가 적은 쪽이 낮음.
|
|
63
|
+
* 예: rc.2 < rc.10, alpha < beta, 1.0.0-rc < 1.0.0-rc.1
|
|
64
|
+
*/
|
|
65
|
+
function comparePrerelease(a: string, b: string): number {
|
|
66
|
+
const as = a.split('.');
|
|
67
|
+
const bs = b.split('.');
|
|
68
|
+
for (let i = 0; i < Math.max(as.length, bs.length); i++) {
|
|
69
|
+
const x = as[i];
|
|
70
|
+
const y = bs[i];
|
|
71
|
+
if (x === undefined) return -1; // 식별자가 적은 쪽이 낮음
|
|
72
|
+
if (y === undefined) return 1;
|
|
73
|
+
const xn = /^\d+$/.test(x);
|
|
74
|
+
const yn = /^\d+$/.test(y);
|
|
75
|
+
if (xn && yn) {
|
|
76
|
+
const d = parseInt(x, 10) - parseInt(y, 10);
|
|
77
|
+
if (d !== 0) return d > 0 ? 1 : -1;
|
|
78
|
+
} else if (xn !== yn) {
|
|
79
|
+
return xn ? -1 : 1; // 숫자 식별자는 영숫자보다 낮음
|
|
80
|
+
} else if (x !== y) {
|
|
81
|
+
return x < y ? -1 : 1; // 둘 다 영숫자 → 사전식
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface UpdateCache {
|
|
88
|
+
checkedAt: number;
|
|
89
|
+
latest: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readCache(): UpdateCache | null {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(fs.readFileSync(cacheFile(), 'utf8')) as UpdateCache;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeCache(c: UpdateCache): void {
|
|
101
|
+
try {
|
|
102
|
+
fs.writeFileSync(cacheFile(), JSON.stringify(c), 'utf8');
|
|
103
|
+
} catch {
|
|
104
|
+
/* 캐시는 best-effort. 못 써도 동작에 지장 없다. */
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 캐시가 만료됐는지. 실패(latest=null)는 짧은 TTL로 더 자주 재시도한다. */
|
|
109
|
+
function cacheExpired(cache: UpdateCache | null): boolean {
|
|
110
|
+
if (!cache) return true;
|
|
111
|
+
const ttl = cache.latest === null ? NULL_CACHE_TTL_MS : CACHE_TTL_MS;
|
|
112
|
+
return Date.now() - cache.checkedAt >= ttl;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 레지스트리에서 latest 버전만 가져온다. 오프라인/오류면 null(절대 throw 안 함). */
|
|
116
|
+
async function fetchLatest(): Promise<string | null> {
|
|
117
|
+
const ctrl = new AbortController();
|
|
118
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch(REGISTRY_URL, { signal: ctrl.signal, headers: { accept: 'application/json' } });
|
|
121
|
+
if (!res.ok) return null;
|
|
122
|
+
const json = (await res.json()) as { version?: unknown };
|
|
123
|
+
return typeof json.version === 'string' ? json.version : null;
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
} finally {
|
|
127
|
+
clearTimeout(timer);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let refreshing = false;
|
|
132
|
+
function refreshInBackground(): void {
|
|
133
|
+
if (refreshing) return;
|
|
134
|
+
refreshing = true;
|
|
135
|
+
void fetchLatest()
|
|
136
|
+
.then((latest) => writeCache({ checkedAt: Date.now(), latest }))
|
|
137
|
+
.finally(() => {
|
|
138
|
+
refreshing = false;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 이 설치를 담은 npm `--prefix`를 돌려준다(자동 업데이트 대상). 못 구하면 null.
|
|
146
|
+
* 번들 레이아웃: <prefix>/node_modules/careermate/dist/<entry>.mjs — __dirname은 .../dist.
|
|
147
|
+
* 개발(tsx)·전역·npx 캐시 설치는 표준 위치가 아니므로 null.
|
|
148
|
+
*/
|
|
149
|
+
export function installPrefix(): string | null {
|
|
150
|
+
if (!BUNDLED) return null;
|
|
151
|
+
const pkgRoot = path.resolve(__dirname, '..'); // .../node_modules/careermate
|
|
152
|
+
const parent = path.dirname(pkgRoot); // .../node_modules
|
|
153
|
+
if (path.basename(parent) !== 'node_modules') return null;
|
|
154
|
+
return path.dirname(parent); // .../ ← npm --prefix
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function manualCommand(): string {
|
|
158
|
+
const prefix = installPrefix();
|
|
159
|
+
return prefix
|
|
160
|
+
? `npm install --prefix "${prefix}" careermate@latest`
|
|
161
|
+
: 'npm install --prefix "<설치 폴더>" careermate@latest';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface UpdateStatus {
|
|
165
|
+
current: string;
|
|
166
|
+
latest: string | null;
|
|
167
|
+
update_available: boolean;
|
|
168
|
+
/** 사용자가 직접 칠 수 있는 업데이트 명령(자동 실행이 막힐 때 안내용). */
|
|
169
|
+
update_command: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 캐시 상태에서 UpdateStatus를 조립한다(동기/비동기 두 경로 공통). */
|
|
173
|
+
function buildStatus(cache: UpdateCache | null): UpdateStatus {
|
|
174
|
+
const current = APP_VERSION;
|
|
175
|
+
const latest = cache?.latest ?? null;
|
|
176
|
+
return {
|
|
177
|
+
current,
|
|
178
|
+
latest,
|
|
179
|
+
update_available: !!latest && compareVersions(latest, current) > 0,
|
|
180
|
+
update_command: manualCommand(),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 비차단 업데이트 신호. 캐시를 동기로 읽고, 만료됐으면 백그라운드 재확인만 건다(절대 await 안 함).
|
|
186
|
+
* get_onboarding_status 등 자주 불리는 도구에서 쓰기 위함 — 네트워크로 도구 호출을 막지 않는다.
|
|
187
|
+
*/
|
|
188
|
+
export function getUpdateStatus(): UpdateStatus {
|
|
189
|
+
if (checkDisabled()) return buildStatus(null);
|
|
190
|
+
const cache = readCache();
|
|
191
|
+
if (cacheExpired(cache)) refreshInBackground();
|
|
192
|
+
return buildStatus(cache);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 즉시(바운드) 업데이트 확인. 캐시가 만료면 레지스트리를 한 번 await하고 캐시를 갱신한다.
|
|
197
|
+
* doctor·check_for_update처럼 "지금 확인" 의미가 분명한 일회성 호출에서 사용.
|
|
198
|
+
*/
|
|
199
|
+
export async function getUpdateStatusAsync(): Promise<UpdateStatus> {
|
|
200
|
+
if (checkDisabled()) return buildStatus(null);
|
|
201
|
+
let cache = readCache();
|
|
202
|
+
if (cacheExpired(cache)) {
|
|
203
|
+
cache = { checkedAt: Date.now(), latest: await fetchLatest() };
|
|
204
|
+
writeCache(cache);
|
|
205
|
+
}
|
|
206
|
+
return buildStatus(cache);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface UpdateResult {
|
|
210
|
+
ok: boolean;
|
|
211
|
+
from: string;
|
|
212
|
+
to: string | null;
|
|
213
|
+
message: string;
|
|
214
|
+
/** 자동 업데이트가 막힌 경우 사용자가 직접 칠 명령. */
|
|
215
|
+
manual?: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 자식 프로세스 트리를 종료한다. Windows의 child.kill()은 `cmd` 래퍼만 죽이고 그 아래
|
|
220
|
+
* npm/node 압축해제 프로세스는 살아남아(트리킬 아님) 설치를 반쯤 덮어쓸 수 있다. taskkill로
|
|
221
|
+
* 트리 전체를 강제 종료한다. 그 외 OS는 child.kill()로 충분.
|
|
222
|
+
*/
|
|
223
|
+
function killTree(child: ReturnType<typeof spawn>): void {
|
|
224
|
+
if (process.platform === 'win32' && child.pid) {
|
|
225
|
+
try {
|
|
226
|
+
spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' }).on('error', () => {});
|
|
227
|
+
return;
|
|
228
|
+
} catch {
|
|
229
|
+
/* taskkill을 못 띄우면 아래 기본 kill로 폴백 */
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
child.kill();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function npmInstallLatest(prefix: string): Promise<{ ok: boolean; error?: string }> {
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
let settled = false;
|
|
238
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
239
|
+
const done = (r: { ok: boolean; error?: string }): void => {
|
|
240
|
+
if (settled) return; // 타임아웃과 close가 경쟁해도 한 번만 resolve
|
|
241
|
+
settled = true;
|
|
242
|
+
if (timer) clearTimeout(timer);
|
|
243
|
+
resolve(r);
|
|
244
|
+
};
|
|
245
|
+
// Windows에서 .cmd 직접 spawn은 Node 보안 변경으로 까다롭다 → cmd /c로 명령 문자열을 넘겨
|
|
246
|
+
// prefix 경로의 공백까지 안전히 인용한다. 그 외 OS는 배열 인자(셸 없음)로 실행.
|
|
247
|
+
const [cmd, args] =
|
|
248
|
+
process.platform === 'win32'
|
|
249
|
+
? ['cmd', ['/c', `npm install --prefix "${prefix}" careermate@latest`]]
|
|
250
|
+
: ['npm', ['install', '--prefix', prefix, 'careermate@latest']];
|
|
251
|
+
let child: ReturnType<typeof spawn>;
|
|
252
|
+
try {
|
|
253
|
+
// stdout은 반드시 'ignore'로 흘려보낸다 — 읽지 않은 채 pipe로 두면 npm의 많은 출력이
|
|
254
|
+
// 파이프 버퍼를 채워 설치가 멈추고(=120s 타임아웃) 반파 위험으로 이어진다. stderr만
|
|
255
|
+
// 캡처해 실패 메시지로 쓴다. cwd는 설치 대상 패키지 안이 아니라 prefix로 고정한다.
|
|
256
|
+
child = spawn(cmd as string, args as string[], { cwd: prefix, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
257
|
+
} catch (e) {
|
|
258
|
+
done({ ok: false, error: e instanceof Error ? e.message : String(e) });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
let stderr = '';
|
|
262
|
+
child.stderr?.on('data', (d) => {
|
|
263
|
+
if (stderr.length < 8192) stderr += d.toString(); // bounded capture
|
|
264
|
+
});
|
|
265
|
+
timer = setTimeout(() => {
|
|
266
|
+
killTree(child);
|
|
267
|
+
done({ ok: false, error: `시간 초과(${INSTALL_TIMEOUT_MS / 1000}s)` });
|
|
268
|
+
}, INSTALL_TIMEOUT_MS);
|
|
269
|
+
child.on('error', (e) => done({ ok: false, error: e.message }));
|
|
270
|
+
child.on('close', (code) => {
|
|
271
|
+
if (code === 0) done({ ok: true });
|
|
272
|
+
else done({ ok: false, error: stderr.trim().split('\n').slice(-3).join(' ') || `종료 코드 ${code}` });
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** careermate를 최신으로 설치한다. 성공해도 적용엔 AI 앱 재시작이 필요하다. */
|
|
278
|
+
export async function runSelfUpdate(): Promise<UpdateResult> {
|
|
279
|
+
const from = APP_VERSION;
|
|
280
|
+
const prefix = installPrefix();
|
|
281
|
+
if (!prefix) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
from,
|
|
285
|
+
to: null,
|
|
286
|
+
message:
|
|
287
|
+
'설치 위치를 자동으로 찾지 못했습니다(개발 모드이거나 비표준 설치). 아래 명령을 직접 실행해 업데이트해 주세요.',
|
|
288
|
+
manual: manualCommand(),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
// 최신 버전: 신선한 캐시가 있으면 재사용하고, 없으면 한 번만 조회한다(레지스트리 중복 호출 방지).
|
|
292
|
+
let cache = readCache();
|
|
293
|
+
if (cacheExpired(cache)) {
|
|
294
|
+
cache = { checkedAt: Date.now(), latest: await fetchLatest() };
|
|
295
|
+
writeCache(cache);
|
|
296
|
+
}
|
|
297
|
+
const latest = cache?.latest ?? null;
|
|
298
|
+
// 최신 버전을 확인하지 못함(오프라인/레지스트리 오류): 무엇을 설치할지 모른 채 실행 중인
|
|
299
|
+
// 설치를 덮어쓰면 반파 위험이 있으므로 시도하지 않고 안내한다.
|
|
300
|
+
if (latest === null) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
from,
|
|
304
|
+
to: null,
|
|
305
|
+
message:
|
|
306
|
+
'최신 버전을 확인하지 못했습니다(오프라인이거나 레지스트리 일시 오류). 잠시 후 다시 시도하거나 아래 명령을 직접 실행해 주세요.',
|
|
307
|
+
manual: manualCommand(),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (compareVersions(latest, from) <= 0) {
|
|
311
|
+
return { ok: true, from, to: from, message: `이미 최신 버전입니다 (v${from}).` };
|
|
312
|
+
}
|
|
313
|
+
const installed = await npmInstallLatest(prefix);
|
|
314
|
+
if (!installed.ok) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
from,
|
|
318
|
+
to: latest,
|
|
319
|
+
message: `업데이트 설치에 실패했습니다: ${installed.error}`,
|
|
320
|
+
manual: manualCommand(),
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
writeCache({ checkedAt: Date.now(), latest });
|
|
324
|
+
return {
|
|
325
|
+
ok: true,
|
|
326
|
+
from,
|
|
327
|
+
to: latest,
|
|
328
|
+
message: `careermate를 ${latest ? `v${latest}` : '최신 버전'}(으)로 업데이트했습니다. 적용하려면 AI 앱(또는 MCP 연결)을 재시작하세요. 재시작 시 데이터베이스는 자동으로 마이그레이션되며 기존 데이터는 보존됩니다.`,
|
|
329
|
+
};
|
|
330
|
+
}
|