projectops 4.31.5 → 4.33.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 +144 -145
- package/package.json +2 -2
- package/src/cli/args.js +65 -29
- package/src/cli/errors.js +9 -7
- package/src/cli/help.js +51 -3
- package/src/commands/doctor.js +80 -78
- package/src/commands/full.js +17 -3
- package/src/commands/interactive.js +78 -50
- package/src/commands/issues.js +8 -1
- package/src/commands/skills.js +8 -7
- package/src/commands/version.js +4 -2
- package/src/commands/workflows.js +3 -1
- package/src/context.js +4 -0
- package/src/core/breaking-check.js +6 -5
- package/src/core/copy/simple.js +27 -1
- package/src/core/copy/workflows.js +10 -5
- package/src/core/exclusions.js +8 -0
- package/src/core/ide/adapters/claude.js +18 -17
- package/src/core/ide/adapters/codex.js +12 -11
- package/src/core/ide/adapters/cursor.js +11 -10
- package/src/core/ide/adapters/gemini.js +11 -10
- package/src/core/ide/adapters/pi-common.js +2 -1
- package/src/core/ide/adapters/pi-harness.js +13 -16
- package/src/core/ide/adapters/pi.js +12 -11
- package/src/core/label-style.js +78 -0
- package/src/core/migration-guide.js +38 -38
- package/src/core/migrations/index.js +19 -15
- package/src/core/migrations/registry.js +70 -55
- package/src/core/options-ask.js +65 -64
- package/src/core/orphan-workflows.js +2 -1
- package/src/core/paths-resolve.js +31 -30
- package/src/core/repo-language.js +57 -0
- package/src/core/run-trace.js +10 -0
- package/src/core/version-yml.js +68 -36
- package/src/i18n/core.en.js +31 -0
- package/src/i18n/core.ko.js +31 -0
- package/src/i18n/doctor.en.js +89 -0
- package/src/i18n/doctor.ko.js +89 -0
- package/src/i18n/en.js +142 -0
- package/src/i18n/ide.en.js +102 -0
- package/src/i18n/ide.ko.js +102 -0
- package/src/i18n/index.js +73 -0
- package/src/i18n/ko.js +135 -0
- package/src/i18n/migrate.en.js +66 -0
- package/src/i18n/migrate.ko.js +66 -0
- package/src/i18n/options.en.js +104 -0
- package/src/i18n/options.ko.js +104 -0
- package/src/i18n/summary.en.js +70 -0
- package/src/i18n/summary.ko.js +70 -0
- package/src/index.js +46 -28
- package/src/ui/banner.js +8 -6
- package/src/ui/env-plan.js +19 -18
- package/src/ui/prompts.js +25 -24
- package/src/ui/readline-engine.js +18 -10
- package/src/ui/skills-prompts.js +8 -7
- package/src/ui/status-cards.js +38 -38
- package/src/ui/summary.js +103 -111
package/src/core/options-ask.js
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { listYamlFiles } from "./fsutil.js";
|
|
12
12
|
import { PATHS } from "./paths.js";
|
|
13
|
+
import { t as msg } from "../i18n/index.js";
|
|
13
14
|
import { parseTemplateOptions, inferIntent } from "./version-yml.js";
|
|
14
15
|
import { branchStatus, createBranch, pushBranch } from "./git-branch.js";
|
|
15
16
|
|
|
@@ -26,28 +27,28 @@ export async function ensureDeployBranch({ targetRoot = ".", deployBranch = "",
|
|
|
26
27
|
if (!st.isRepo) return { created: false, pushed: false, ready: null };
|
|
27
28
|
if (st.local) return { created: false, pushed: false, ready: true };
|
|
28
29
|
if (st.remote === true) {
|
|
29
|
-
say(
|
|
30
|
+
say(msg("options.branch.remoteOnly", { branch: deployBranch }));
|
|
30
31
|
return { created: false, pushed: false, ready: true };
|
|
31
32
|
}
|
|
32
|
-
say(
|
|
33
|
-
const mk = await io.confirm({ message:
|
|
33
|
+
say(msg("options.branch.missing", { branch: deployBranch, base: defaultBranch || msg("options.branch.baseFallback") }));
|
|
34
|
+
const mk = await io.confirm({ message: msg("options.branch.createPrompt", { base: defaultBranch || msg("options.branch.currentFallback"), branch: deployBranch }), initialValue: true });
|
|
34
35
|
if (mk !== true) {
|
|
35
|
-
say(
|
|
36
|
+
say(msg("options.branch.skipped", { branch: deployBranch }));
|
|
36
37
|
return { created: false, pushed: false, ready: false };
|
|
37
38
|
}
|
|
38
39
|
if (!createBranch(targetRoot, deployBranch, defaultBranch)) {
|
|
39
|
-
say(
|
|
40
|
+
say(msg("options.branch.createFailed", { branch: deployBranch, from: defaultBranch ? ` ${defaultBranch}` : "" }));
|
|
40
41
|
return { created: false, pushed: false, ready: false };
|
|
41
42
|
}
|
|
42
|
-
say(
|
|
43
|
+
say(msg("options.branch.created", { branch: deployBranch }));
|
|
43
44
|
// #481 — push 질문에 브랜치명 명시 ("어느 브랜치를 push하는지" 불명확 방지)
|
|
44
|
-
const up = await io.confirm({ message:
|
|
45
|
+
const up = await io.confirm({ message: msg("options.branch.pushPrompt", { branch: deployBranch }), initialValue: true });
|
|
45
46
|
if (up !== true) return { created: true, pushed: false, ready: true };
|
|
46
47
|
if (pushBranch(targetRoot, deployBranch)) {
|
|
47
|
-
say(
|
|
48
|
+
say(msg("options.branch.pushed", { branch: deployBranch }));
|
|
48
49
|
return { created: true, pushed: true, ready: true };
|
|
49
50
|
}
|
|
50
|
-
say(
|
|
51
|
+
say(msg("options.branch.pushFailed", { branch: deployBranch }));
|
|
51
52
|
return { created: true, pushed: false, ready: true };
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -123,18 +124,18 @@ async function askOptionalWorkflow({ dir, icon, short, desc, current, force, tty
|
|
|
123
124
|
if (force || !tty) return false;
|
|
124
125
|
|
|
125
126
|
say("");
|
|
126
|
-
say(
|
|
127
|
+
say(msg("options.optional.found", { icon, name: short, count: files.length }));
|
|
127
128
|
say(` ${desc}`);
|
|
128
129
|
say("");
|
|
129
|
-
say("
|
|
130
|
+
say(msg("options.optional.included"));
|
|
130
131
|
for (const f of files) say(` • ${f}`);
|
|
131
132
|
say("");
|
|
132
133
|
|
|
133
|
-
const ans = await io.confirm({ message:
|
|
134
|
+
const ans = await io.confirm({ message: msg("options.optional.prompt", { name: short }), initialValue: false });
|
|
134
135
|
const include = ans === true && !isCancel(ans);
|
|
135
136
|
say(include
|
|
136
|
-
?
|
|
137
|
-
:
|
|
137
|
+
? msg("options.optional.yes", { name: short })
|
|
138
|
+
: msg("options.optional.no", { name: short }));
|
|
138
139
|
return include;
|
|
139
140
|
}
|
|
140
141
|
|
|
@@ -187,15 +188,15 @@ export async function askAllOptionalWorkflows({
|
|
|
187
188
|
const saved = parseTemplateOptions(readFileSync(vy, "utf8"));
|
|
188
189
|
if (deploy === null && saved.deploy !== null) {
|
|
189
190
|
deploy = saved.deploy;
|
|
190
|
-
say(
|
|
191
|
+
say(msg("options.saved.deploy", { value: deploy }));
|
|
191
192
|
}
|
|
192
193
|
if (publish === null && saved.publish !== null) {
|
|
193
194
|
publish = saved.publish;
|
|
194
|
-
say(
|
|
195
|
+
say(msg("options.saved.publish", { value: publish.join(",") || msg("options.saved.none") }));
|
|
195
196
|
}
|
|
196
197
|
if (secretBackup === null && saved.secretBackup !== null) {
|
|
197
198
|
secretBackup = saved.secretBackup;
|
|
198
|
-
say(
|
|
199
|
+
say(msg("options.saved.secret", { value: secretBackup }));
|
|
199
200
|
}
|
|
200
201
|
// #455 changelog/code_review 저장값 재사용
|
|
201
202
|
if (codeReviewCoderabbit === null && saved.codeReviewCoderabbit !== null) codeReviewCoderabbit = saved.codeReviewCoderabbit;
|
|
@@ -235,19 +236,19 @@ export async function askAllOptionalWorkflows({
|
|
|
235
236
|
intent = intent ?? inferIntent(deploy, publish) ?? "manual";
|
|
236
237
|
} else {
|
|
237
238
|
say("");
|
|
238
|
-
say(
|
|
239
|
+
say(msg("options.intent.intro"));
|
|
239
240
|
const ans = await io.select({
|
|
240
|
-
message: "
|
|
241
|
+
message: msg("options.intent.prompt"),
|
|
241
242
|
options: [
|
|
242
|
-
{ value: "app", label: "
|
|
243
|
-
{ value: "library", label: "
|
|
244
|
-
{ value: "both", label: "
|
|
245
|
-
{ value: "none", label: "
|
|
246
|
-
{ value: "manual", label: "
|
|
243
|
+
{ value: "app", label: msg("options.intent.app") },
|
|
244
|
+
{ value: "library", label: msg("options.intent.library") },
|
|
245
|
+
{ value: "both", label: msg("options.intent.both") },
|
|
246
|
+
{ value: "none", label: msg("options.intent.none") },
|
|
247
|
+
{ value: "manual", label: msg("options.intent.manual") },
|
|
247
248
|
],
|
|
248
249
|
});
|
|
249
250
|
intent = (!isCancel(ans) && INTENT_ASKS_DEPLOY[ans] !== undefined) ? ans : (intent ?? "manual");
|
|
250
|
-
say(
|
|
251
|
+
say(msg("options.intent.chosen", { value: intent }));
|
|
251
252
|
}
|
|
252
253
|
// intent 확정 후 유도: 안 묻는 축은 값 확정, 묻는 축은 재질문 대상으로 되돌린다.
|
|
253
254
|
if (!INTENT_ASKS_DEPLOY[intent]) deploy = "none";
|
|
@@ -268,9 +269,9 @@ export async function askAllOptionalWorkflows({
|
|
|
268
269
|
// manual(직접 고르기)에서 둘 다 물을 땐 두 축이 별개임을 한 번 안내 (#480 계승).
|
|
269
270
|
if (intent === "manual" && willAskDeploy && willAskPublish && tty && typeof io.select === "function") {
|
|
270
271
|
say("");
|
|
271
|
-
say("
|
|
272
|
-
say("
|
|
273
|
-
say("
|
|
272
|
+
say(msg("options.axes.intro"));
|
|
273
|
+
say(msg("options.axes.one"));
|
|
274
|
+
say(msg("options.axes.two"));
|
|
274
275
|
}
|
|
275
276
|
// 비대화형/폴백 기본값 (#498) — docker-ssh가 적용 가능할 때만 기본, 아니면 none.
|
|
276
277
|
const deployDefault = applicable.deploy.includes("docker-ssh") ? "docker-ssh" : "none";
|
|
@@ -279,22 +280,22 @@ export async function askAllOptionalWorkflows({
|
|
|
279
280
|
deploy = deploy ?? deployDefault;
|
|
280
281
|
} else {
|
|
281
282
|
say("");
|
|
282
|
-
say("
|
|
283
|
-
say("
|
|
283
|
+
say(msg("options.deploy.intro"));
|
|
284
|
+
say(msg("options.deploy.hint"));
|
|
284
285
|
// 선택지는 적용 가능 타겟으로 필터링 (#498) — 'none'은 항상 노출.
|
|
285
286
|
const deployLabels = [
|
|
286
|
-
{ value: "docker-ssh", label: "
|
|
287
|
+
{ value: "docker-ssh", label: msg("options.deploy.dockerSsh") },
|
|
287
288
|
{ value: "vercel", label: "Vercel" },
|
|
288
289
|
];
|
|
289
290
|
const ans = await io.select({
|
|
290
|
-
message: "
|
|
291
|
+
message: msg("options.deploy.prompt"),
|
|
291
292
|
options: [
|
|
292
293
|
...deployLabels.filter((o) => applicable.deploy.includes(o.value)),
|
|
293
|
-
{ value: "none", label: "
|
|
294
|
+
{ value: "none", label: msg("options.deploy.none") },
|
|
294
295
|
],
|
|
295
296
|
});
|
|
296
297
|
deploy = (!isCancel(ans) && (ans === "none" || applicable.deploy.includes(ans))) ? ans : (deploy ?? deployDefault);
|
|
297
|
-
say(
|
|
298
|
+
say(msg("options.deploy.chosen", { value: deploy }));
|
|
298
299
|
}
|
|
299
300
|
}
|
|
300
301
|
|
|
@@ -304,16 +305,16 @@ export async function askAllOptionalWorkflows({
|
|
|
304
305
|
publish = publish ?? [];
|
|
305
306
|
} else {
|
|
306
307
|
say("");
|
|
307
|
-
say("
|
|
308
|
-
say("
|
|
308
|
+
say(msg("options.publish.intro"));
|
|
309
|
+
say(msg("options.publish.hint"));
|
|
309
310
|
// 선택지는 적용 가능 타겟으로 필터링 (#498)
|
|
310
311
|
const publishLabels = [
|
|
311
|
-
{ value: "nexus", label: "
|
|
312
|
-
{ value: "npm", label: "
|
|
313
|
-
{ value: "github-packages", label: "
|
|
312
|
+
{ value: "nexus", label: msg("options.publish.nexus") },
|
|
313
|
+
{ value: "npm", label: msg("options.publish.npm") },
|
|
314
|
+
{ value: "github-packages", label: msg("options.publish.githubPackages") },
|
|
314
315
|
];
|
|
315
316
|
const ans = await io.multiselect({
|
|
316
|
-
message: "
|
|
317
|
+
message: msg("options.publish.prompt"),
|
|
317
318
|
options: publishLabels.filter((o) => applicable.publish.includes(o.value)),
|
|
318
319
|
initialValues: publish ?? [],
|
|
319
320
|
required: false,
|
|
@@ -321,7 +322,7 @@ export async function askAllOptionalWorkflows({
|
|
|
321
322
|
publish = (!isCancel(ans) && Array.isArray(ans))
|
|
322
323
|
? ans.filter((t) => applicable.publish.includes(t))
|
|
323
324
|
: (publish ?? []);
|
|
324
|
-
say(publish.length ?
|
|
325
|
+
say(publish.length ? msg("options.publish.chosen", { value: publish.join(", ") }) : msg("options.publish.chosenNone"));
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
}
|
|
@@ -338,17 +339,17 @@ export async function askAllOptionalWorkflows({
|
|
|
338
339
|
aiPrSummary = aiPrSummary ?? true;
|
|
339
340
|
} else {
|
|
340
341
|
say("");
|
|
341
|
-
say("
|
|
342
|
-
say("
|
|
343
|
-
say(
|
|
344
|
-
say(
|
|
342
|
+
say(msg("options.pr.intro"));
|
|
343
|
+
say(msg("options.pr.hint1"));
|
|
344
|
+
say(msg("options.pr.hint2"));
|
|
345
|
+
say(msg("options.pr.hint3"));
|
|
345
346
|
const ans = await io.select({
|
|
346
|
-
message: "
|
|
347
|
+
message: msg("options.pr.prompt"),
|
|
347
348
|
options: [
|
|
348
|
-
{ value: "both", label: "
|
|
349
|
-
{ value: "summary", label: "
|
|
350
|
-
{ value: "coderabbit", label: "
|
|
351
|
-
{ value: "none", label: "
|
|
349
|
+
{ value: "both", label: msg("options.pr.both") },
|
|
350
|
+
{ value: "summary", label: msg("options.pr.summary") },
|
|
351
|
+
{ value: "coderabbit", label: msg("options.pr.coderabbit") },
|
|
352
|
+
{ value: "none", label: msg("options.pr.none") },
|
|
352
353
|
],
|
|
353
354
|
});
|
|
354
355
|
// 취소·미응답은 추천값으로 — 다른 축과 같은 방어 패턴이다.
|
|
@@ -356,13 +357,13 @@ export async function askAllOptionalWorkflows({
|
|
|
356
357
|
const pick = (!isCancel(ans) && PR_COMMENT_CHOICES.includes(ans)) ? ans : "summary";
|
|
357
358
|
codeReviewCoderabbit = pick === "coderabbit" || pick === "both";
|
|
358
359
|
aiPrSummary = pick === "summary" || pick === "both";
|
|
359
|
-
say(
|
|
360
|
+
say(msg("options.pr.chosen", { value: msg(`options.pr.label.${pick}`) }));
|
|
360
361
|
// #481 — "사용"만으로는 안 붙는다. 앱 설치 + 레포 접근 권한이 있어야 실제로 리뷰가 달린다.
|
|
361
362
|
if (codeReviewCoderabbit) {
|
|
362
|
-
say("
|
|
363
|
-
say("
|
|
364
|
-
say("
|
|
365
|
-
say(
|
|
363
|
+
say(msg("options.pr.crWarn"));
|
|
364
|
+
say(msg("options.pr.crStep1"));
|
|
365
|
+
say(msg("options.pr.crStep2"));
|
|
366
|
+
say(msg("options.pr.crStep3"));
|
|
366
367
|
}
|
|
367
368
|
}
|
|
368
369
|
}
|
|
@@ -379,9 +380,9 @@ export async function askAllOptionalWorkflows({
|
|
|
379
380
|
if (force || !tty || typeof io.text !== "function") {
|
|
380
381
|
changelogBaseUrl = changelogBaseUrl ?? "";
|
|
381
382
|
} else {
|
|
382
|
-
const ans = await io.text({ message: "
|
|
383
|
+
const ans = await io.text({ message: msg("options.ollama.prompt") });
|
|
383
384
|
changelogBaseUrl = (typeof ans === "string" && !isCancel(ans)) ? ans.trim() : "";
|
|
384
|
-
say(
|
|
385
|
+
say(msg("options.ollama.chosen", { value: changelogBaseUrl || msg("options.ollama.unset") }));
|
|
385
386
|
}
|
|
386
387
|
} else if (changelogBaseUrl === null) {
|
|
387
388
|
changelogBaseUrl = "";
|
|
@@ -396,12 +397,12 @@ export async function askAllOptionalWorkflows({
|
|
|
396
397
|
} else {
|
|
397
398
|
const base = defaultBranch || "main"; // git으로 감지된 기본 브랜치 (#481 동적 안내)
|
|
398
399
|
say("");
|
|
399
|
-
say("
|
|
400
|
-
say(
|
|
401
|
-
say("
|
|
402
|
-
const ans = await io.text({ message: "
|
|
400
|
+
say(msg("options.devBranch.intro"));
|
|
401
|
+
say(msg("options.devBranch.detected", { base }));
|
|
402
|
+
say(msg("options.devBranch.hint"));
|
|
403
|
+
const ans = await io.text({ message: msg("options.devBranch.prompt"), initialValue: deployBranch ?? "develop" });
|
|
403
404
|
deployBranch = (typeof ans === "string" && !isCancel(ans) && ans.trim()) ? ans.trim() : (deployBranch ?? "develop");
|
|
404
|
-
say(
|
|
405
|
+
say(msg("options.devBranch.chosen", { value: deployBranch }));
|
|
405
406
|
// 브랜치 존재 확인 + 생성 제안 (#477) — 없으면 릴리스 파이프라인이 조용히 놀게 된다
|
|
406
407
|
// #490 — 결과(ready)를 완료 요약에 전달해 이미 생성/확인한 브랜치를 재지시하지 않는다
|
|
407
408
|
const br = await ensureDeployBranch({ targetRoot, deployBranch, defaultBranch, io, say });
|
|
@@ -414,8 +415,8 @@ export async function askAllOptionalWorkflows({
|
|
|
414
415
|
const real = join(tempDir, PATHS.workflowsDir, PATHS.projectTypesDir);
|
|
415
416
|
const ptDir = existsSync(real) ? real : join(tempDir, PATHS.projectTypesDir);
|
|
416
417
|
secretBackup = await askOptionalWorkflow({
|
|
417
|
-
dir: join(ptDir, "common", "secret-backup"), icon: "🔐", short: "
|
|
418
|
-
desc: "
|
|
418
|
+
dir: join(ptDir, "common", "secret-backup"), icon: "🔐", short: msg("options.secret.short"),
|
|
419
|
+
desc: msg("options.secret.desc"),
|
|
419
420
|
current: secretBackup, force, tty, io, forceAsk: ask("secret"), say,
|
|
420
421
|
});
|
|
421
422
|
|
|
@@ -30,12 +30,13 @@ function typeInventory(projectTypesDir, type) {
|
|
|
30
30
|
// 축을 끈 사용자에게는 그게 곧 "껐는데 왜 도나"가 된다 — 여기서 고아로 잡는다.
|
|
31
31
|
// deploy는 타겟별 폴더라 선택된 타겟만 살리고 나머지를 대상으로 삼는다.
|
|
32
32
|
function commonOptionalOrphans(commonDir, opts) {
|
|
33
|
-
const { includeSecretBackup = false, aiPrSummary = true, deployTarget = "docker-ssh" } = opts;
|
|
33
|
+
const { includeSecretBackup = false, aiPrSummary = true, projectsSync = true, deployTarget = "docker-ssh" } = opts;
|
|
34
34
|
const out = [];
|
|
35
35
|
|
|
36
36
|
const gated = [
|
|
37
37
|
["secret-backup", includeSecretBackup, "secret-backup"],
|
|
38
38
|
["pr-summary", aiPrSummary, "pr-summary"],
|
|
39
|
+
["projects-sync", projectsSync, "projects-sync"],
|
|
39
40
|
];
|
|
40
41
|
for (const [dir, enabled, label] of gated) {
|
|
41
42
|
if (enabled) continue;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// io.log(line) → 안내 출력 (없으면 stderr)
|
|
10
10
|
import { existsSync, readdirSync } from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
import { t as msg } from "../i18n/index.js";
|
|
12
13
|
import { markerForType as baseMarkerForType, extraMarkers } from "./detect.js";
|
|
13
14
|
import { normalizePath } from "../cli/args.js";
|
|
14
15
|
|
|
@@ -132,11 +133,11 @@ export async function resolveProjectPaths({
|
|
|
132
133
|
const total = targets.length;
|
|
133
134
|
// ── 도입부 안내 (.sh L1407~1434 — 감지 결과 + 무엇을 할지 설명) ──
|
|
134
135
|
say("");
|
|
135
|
-
if (total > 1) say(
|
|
136
|
-
else say(
|
|
136
|
+
if (total > 1) say(msg("paths.multi", { total }));
|
|
137
|
+
else say(msg("paths.single", { type: targets[0] }));
|
|
137
138
|
for (const t of targets) say(` • ${t.padEnd(8)} → ${existingMarkerInDir(t, root)}`);
|
|
138
139
|
say("");
|
|
139
|
-
say(
|
|
140
|
+
say(msg("paths.rootDef"));
|
|
140
141
|
say("");
|
|
141
142
|
|
|
142
143
|
let idx = 0;
|
|
@@ -146,7 +147,7 @@ export async function resolveProjectPaths({
|
|
|
146
147
|
|
|
147
148
|
// ① --paths 등으로 이미 지정됨 → 최우선 (.sh L1441~1446)
|
|
148
149
|
if (result.get(t)) {
|
|
149
|
-
say(
|
|
150
|
+
say(msg("paths.byFlag", { type: t, path: result.get(t) }));
|
|
150
151
|
continue;
|
|
151
152
|
}
|
|
152
153
|
|
|
@@ -154,7 +155,7 @@ export async function resolveProjectPaths({
|
|
|
154
155
|
const rootMarker = existingMarkerInDir(t, root);
|
|
155
156
|
if (rootMarker && existsSync(join(root, rootMarker))) {
|
|
156
157
|
result.set(t, ".");
|
|
157
|
-
say(
|
|
158
|
+
say(msg("paths.atRoot", { type: t, marker: rootMarker }));
|
|
158
159
|
continue;
|
|
159
160
|
}
|
|
160
161
|
|
|
@@ -169,13 +170,13 @@ export async function resolveProjectPaths({
|
|
|
169
170
|
if (force || !tty) {
|
|
170
171
|
if (existing) {
|
|
171
172
|
chosen = existing;
|
|
172
|
-
say(
|
|
173
|
+
say(msg("paths.keptSaved", { type: t, path: chosen }));
|
|
173
174
|
} else if (candidates.length === 1) {
|
|
174
175
|
chosen = candidates[0];
|
|
175
|
-
say(
|
|
176
|
+
say(msg("paths.autoDetected", { type: t, path: chosen }));
|
|
176
177
|
} else {
|
|
177
178
|
chosen = ".";
|
|
178
|
-
say(
|
|
179
|
+
say(msg("paths.ambiguous", { type: t, count: candidates.length }));
|
|
179
180
|
}
|
|
180
181
|
result.set(t, chosen);
|
|
181
182
|
continue;
|
|
@@ -189,14 +190,14 @@ export async function resolveProjectPaths({
|
|
|
189
190
|
const candMarker = existingMarkerInDir(t, cand === "." ? root : join(root, cand));
|
|
190
191
|
const candFull = cand === "." ? candMarker : `${cand}/${candMarker}`;
|
|
191
192
|
say("");
|
|
192
|
-
say(
|
|
193
|
-
say(
|
|
193
|
+
say(msg("paths.found", { prog, type: t, marker: candMarker }));
|
|
194
|
+
say(msg("paths.location", { file: candFull }));
|
|
194
195
|
const sel = await io.select({
|
|
195
|
-
message:
|
|
196
|
+
message: msg("paths.confirmPrompt", { type: t, cand }),
|
|
196
197
|
options: [
|
|
197
|
-
{ value: cand, label:
|
|
198
|
-
{ value: "직접 입력", label: "
|
|
199
|
-
{ value: EXCLUDE, label:
|
|
198
|
+
{ value: cand, label: msg("paths.confirmYes", { cand, file: candFull }) },
|
|
199
|
+
{ value: "직접 입력", label: msg("paths.confirmManual") },
|
|
200
|
+
{ value: EXCLUDE, label: msg("paths.exclude", { type: t }) },
|
|
200
201
|
],
|
|
201
202
|
});
|
|
202
203
|
if (sel === EXCLUDE) excluded = true;
|
|
@@ -204,28 +205,28 @@ export async function resolveProjectPaths({
|
|
|
204
205
|
// ESC/직접 입력 → 아래 직접입력 루프로
|
|
205
206
|
} else if (candidates.length > 1) {
|
|
206
207
|
say("");
|
|
207
|
-
say(
|
|
208
|
+
say(msg("paths.multiFound", { prog, type: t, count: candidates.length }));
|
|
208
209
|
// 후보들 + '직접 입력'/'이 타입 제외' 메뉴 — value 자체를 한국어로 (센티넬 노출 방지, .sh L1508~1521)
|
|
209
210
|
const options = candidates.map((c) => ({
|
|
210
211
|
value: c,
|
|
211
212
|
label: `${c} (${existingMarkerInDir(t, c === "." ? root : join(root, c))})`,
|
|
212
213
|
}));
|
|
213
|
-
options.push({ value: "직접 입력", label: "
|
|
214
|
-
options.push({ value: EXCLUDE, label:
|
|
215
|
-
const sel = await io.select({ message:
|
|
214
|
+
options.push({ value: "직접 입력", label: msg("paths.manual") });
|
|
215
|
+
options.push({ value: EXCLUDE, label: msg("paths.exclude", { type: t }) });
|
|
216
|
+
const sel = await io.select({ message: msg("paths.selectPrompt", { type: t }), options });
|
|
216
217
|
// ESC(취소)도 직접 입력으로 폴백 (.sh `|| _sel="직접 입력"`)
|
|
217
218
|
if (sel === EXCLUDE) excluded = true;
|
|
218
219
|
else if (!isCancel(sel) && sel != null && sel !== "직접 입력") chosen = sel;
|
|
219
220
|
} else {
|
|
220
221
|
say("");
|
|
221
|
-
say(
|
|
222
|
+
say(msg("paths.notFound", { prog, type: t }));
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
// ── 직접 입력 루프 (위에서 미확정 시, .sh L1528~1553) — 제외 탈출구 포함 (#487) ──
|
|
225
226
|
while (!chosen && !excluded) {
|
|
226
227
|
const hintMarker = existingMarkerInDir(t, root);
|
|
227
|
-
let prompt =
|
|
228
|
-
if (existing) prompt +=
|
|
228
|
+
let prompt = msg("paths.inputPrompt", { type: t, marker: hintMarker });
|
|
229
|
+
if (existing) prompt += msg("paths.inputCurrent", { value: existing });
|
|
229
230
|
prompt += "): ";
|
|
230
231
|
let input = await io.text({ message: prompt, defaultValue: "" });
|
|
231
232
|
if (isCancel(input) || input == null) input = ""; // ESC → 빈값 (아래 폴백)
|
|
@@ -237,13 +238,13 @@ export async function resolveProjectPaths({
|
|
|
237
238
|
if (m && existsSync(join(root, input === "." ? "" : input, m))) {
|
|
238
239
|
chosen = input;
|
|
239
240
|
} else {
|
|
240
|
-
say(
|
|
241
|
+
say(msg("paths.missingFile", { path: input, marker: m }));
|
|
241
242
|
const act = await io.select({
|
|
242
|
-
message: "
|
|
243
|
+
message: msg("paths.actionPrompt"),
|
|
243
244
|
options: [
|
|
244
|
-
{ value: "retry", label: "
|
|
245
|
-
{ value: "force", label:
|
|
246
|
-
{ value: EXCLUDE, label:
|
|
245
|
+
{ value: "retry", label: msg("paths.retry") },
|
|
246
|
+
{ value: "force", label: msg("paths.force", { path: input }) },
|
|
247
|
+
{ value: EXCLUDE, label: msg("paths.exclude", { type: t }) },
|
|
247
248
|
],
|
|
248
249
|
});
|
|
249
250
|
if (act === "force") chosen = input;
|
|
@@ -253,7 +254,7 @@ export async function resolveProjectPaths({
|
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
if (excluded) {
|
|
256
|
-
say(
|
|
257
|
+
say(msg("paths.excluded", { type: t }));
|
|
257
258
|
continue;
|
|
258
259
|
}
|
|
259
260
|
|
|
@@ -263,7 +264,7 @@ export async function resolveProjectPaths({
|
|
|
263
264
|
|
|
264
265
|
// ── 요약 + 같은 마커 파일 중복 경고 (.sh L1559~1587) ──
|
|
265
266
|
say("");
|
|
266
|
-
say("
|
|
267
|
+
say(msg("paths.summary"));
|
|
267
268
|
const fileToTypes = new Map(); // 마커 파일 상대경로 → 그 파일을 쓰는 타입들
|
|
268
269
|
for (const [pt, pp] of result) {
|
|
269
270
|
const m = existingMarkerInDir(pt, pp === "." ? root : join(root, pp));
|
|
@@ -275,8 +276,8 @@ export async function resolveProjectPaths({
|
|
|
275
276
|
for (const [file, ts] of fileToTypes) {
|
|
276
277
|
if (ts.length > 1) {
|
|
277
278
|
// 멱등 동작이라 막지는 않고 경고만 (.sh L1577~1586)
|
|
278
|
-
say(
|
|
279
|
-
say("
|
|
279
|
+
say(msg("paths.duplicate", { types: ts.join(" "), file }));
|
|
280
|
+
say(msg("paths.duplicateNote"));
|
|
280
281
|
}
|
|
281
282
|
}
|
|
282
283
|
say("");
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// 레포 문구 언어(#769): 영문("en")이 정본, 한국어("ko")는 오버레이다.
|
|
2
|
+
// 한글 사본을 파일마다 따로 두면 두 벌이 어긋나므로, 한국어판은 .github/i18n/ko/ 한 곳에만 두고
|
|
3
|
+
// language: ko 일 때 복사 직후 같은 상대경로로 덮는다.
|
|
4
|
+
import { join, relative, dirname } from "node:path";
|
|
5
|
+
import { existsSync, readdirSync, copyFileSync, mkdirSync, statSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
export const REPO_LANGUAGES = ["en", "ko"];
|
|
8
|
+
|
|
9
|
+
// version.yml 에 저장값이 없을 때의 결정. 신규 설치는 영문, 이미 설치된 레포는 한국어 유지.
|
|
10
|
+
// (label_style 과 같은 원칙 — 업데이트만으로 사용자 레포의 문구가 바뀌면 안 된다.)
|
|
11
|
+
export function resolveRepoLanguage({ flag, stored, existing }) {
|
|
12
|
+
if (REPO_LANGUAGES.includes(flag)) return flag;
|
|
13
|
+
if (REPO_LANGUAGES.includes(stored)) return stored;
|
|
14
|
+
return existing ? "ko" : "en";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function walk(dir) {
|
|
18
|
+
const out = [];
|
|
19
|
+
for (const name of readdirSync(dir)) {
|
|
20
|
+
const p = join(dir, name);
|
|
21
|
+
if (statSync(p).isDirectory()) out.push(...walk(p));
|
|
22
|
+
else out.push(p);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 한국어 모드: 오버레이의 모든 파일을 targetRoot/.github/ 아래 같은 상대경로로 복사한다.
|
|
28
|
+
// 반환: { changed, failed }. changed 는 복사한 파일의 레포 루트 기준 상대경로(.github/...),
|
|
29
|
+
// failed 는 복사하지 못한 { file, error }. en 이거나 오버레이가 없으면 둘 다 빈 배열이다.
|
|
30
|
+
// 파일 하나가 실패해도 나머지는 계속 복사한다 — 첫 실패에서 멈추면 영문과 한글이 섞이거나 전부 영문으로 남는다.
|
|
31
|
+
// 예외는 던지지 않는다: 영문 템플릿이 남을 뿐 설치를 죽일 이유가 아니다. 대신 호출부가 failed 를 알린다.
|
|
32
|
+
export function applyRepoLanguage(tempDir, targetRoot, language) {
|
|
33
|
+
const changed = [];
|
|
34
|
+
const failed = [];
|
|
35
|
+
if (language !== "ko") return { changed, failed };
|
|
36
|
+
const overlay = join(tempDir, ".github", "i18n", "ko");
|
|
37
|
+
if (!existsSync(overlay)) return { changed, failed };
|
|
38
|
+
let files = [];
|
|
39
|
+
try {
|
|
40
|
+
files = walk(overlay);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
failed.push({ file: ".github/i18n/ko", error: String(e?.message ?? e) });
|
|
43
|
+
return { changed, failed };
|
|
44
|
+
}
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const rel = relative(overlay, file).split("\\").join("/");
|
|
47
|
+
try {
|
|
48
|
+
const dst = join(targetRoot, ".github", rel);
|
|
49
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
50
|
+
copyFileSync(file, dst);
|
|
51
|
+
changed.push(`.github/${rel}`);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
failed.push({ file: `.github/${rel}`, error: String(e?.message ?? e) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { changed, failed };
|
|
57
|
+
}
|
package/src/core/run-trace.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Layer 1(큐레이션 가이드)은 migration-guide.js — 이 모듈의 events를 단일 소스로 소비한다.
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { writeText } from "./fsutil.js";
|
|
7
|
+
import { getLang } from "../i18n/index.js";
|
|
7
8
|
|
|
8
9
|
// 진단 로그 위치 (#561). docs/ 아래(추적 대상)에서 옮겼다 — 이 기록은 사람이 읽는 문서가
|
|
9
10
|
// 아니라 Agent가 "지난 실행에서 무슨 일이 있었나"를 확인하는 자료다.
|
|
@@ -124,7 +125,11 @@ export function createRunTrace({ clockIso = null } = {}) {
|
|
|
124
125
|
if (restore) return;
|
|
125
126
|
const so = process.stdout.write; // 원본 참조 보관 — 복원 시 identity 유지
|
|
126
127
|
const se = process.stderr.write;
|
|
128
|
+
// The run log is always English (agents read it later, and it must not change with the user's locale).
|
|
129
|
+
// When the screen is in another language the raw terminal text is not copied; the caller appends an
|
|
130
|
+
// English rendering of the completion screen with appendText() instead.
|
|
127
131
|
const capture = (chunk) => {
|
|
132
|
+
if (getLang() !== "en") return;
|
|
128
133
|
try {
|
|
129
134
|
const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
130
135
|
lines.push(stripAnsi(s));
|
|
@@ -135,6 +140,11 @@ export function createRunTrace({ clockIso = null } = {}) {
|
|
|
135
140
|
restore = () => { process.stdout.write = so; process.stderr.write = se; };
|
|
136
141
|
},
|
|
137
142
|
|
|
143
|
+
// Add already-rendered text (e.g. the English completion screen) to the log.
|
|
144
|
+
appendText(text) {
|
|
145
|
+
lines.push(stripAnsi(String(text)));
|
|
146
|
+
},
|
|
147
|
+
|
|
138
148
|
mirrorStop() {
|
|
139
149
|
if (restore) { restore(); restore = null; }
|
|
140
150
|
},
|