projectops 4.31.5 → 4.33.0
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 +11 -2
- package/src/commands/interactive.js +71 -49
- package/src/commands/issues.js +4 -0
- 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 +4 -0
- 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 +47 -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 +141 -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 +134 -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/ui/env-plan.js
CHANGED
|
@@ -11,6 +11,7 @@ import { exists, listYamlFiles } from "../core/fsutil.js";
|
|
|
11
11
|
import { parseWizardLine, resolveToken, resolveGlobalTokens } from "../core/wizard-env.js";
|
|
12
12
|
import { loadWizardPrompts, wfField, workflowDisplayName } from "../core/wizard-labels.js";
|
|
13
13
|
import * as engine from "./readline-engine.js";
|
|
14
|
+
import { t } from "../i18n/index.js";
|
|
14
15
|
|
|
15
16
|
const CANCEL = engine.CANCEL;
|
|
16
17
|
|
|
@@ -52,7 +53,7 @@ export function collectAsks(tempDir, types = [], opts = {}) {
|
|
|
52
53
|
// 복사 엔진과 동일한 폴더 구성: 타입 직하위 + (docker-ssh면) server-deploy + 선택된 publish/<target>
|
|
53
54
|
const dirs = [typeDir];
|
|
54
55
|
if ((deployTarget || "docker-ssh") === "docker-ssh") dirs.push(join(typeDir, "server-deploy"));
|
|
55
|
-
for (const
|
|
56
|
+
for (const pt of publishTargets) dirs.push(join(typeDir, "publish", pt));
|
|
56
57
|
|
|
57
58
|
for (const dir of dirs) {
|
|
58
59
|
if (!exists(dir)) continue;
|
|
@@ -91,18 +92,18 @@ function firstTypeFor(usages, key) {
|
|
|
91
92
|
// KEY 1개를 'label·사용처·설명·예시·기본값' 카드로 출력 (.sh _wf_print_field_card 등가).
|
|
92
93
|
// info: { default, usages } — idx/tot 있으면 "(i/t)" 진행 표시. log 주입 가능(테스트 무음화).
|
|
93
94
|
export function printFieldCard(prompts, key, info, idx = null, tot = null, log = defaultLog) {
|
|
94
|
-
const
|
|
95
|
-
const label = wfField(prompts,
|
|
96
|
-
const help = wfField(prompts,
|
|
97
|
-
const ex = wfField(prompts,
|
|
95
|
+
const ty = info.usages?.[0]?.type ?? "";
|
|
96
|
+
const label = wfField(prompts, ty, key, "label");
|
|
97
|
+
const help = wfField(prompts, ty, key, "help");
|
|
98
|
+
const ex = wfField(prompts, ty, key, "example");
|
|
98
99
|
const scope = scopeString(info.usages || []);
|
|
99
100
|
const head = idx != null && tot != null
|
|
100
101
|
? ` ▸ (${idx}/${tot}) ${label} [${scope}]`
|
|
101
102
|
: ` ▸ ${label} [${scope}]`;
|
|
102
103
|
log(head);
|
|
103
104
|
if (help) log(` ${help}`);
|
|
104
|
-
if (ex) log(
|
|
105
|
-
log(
|
|
105
|
+
if (ex) log(t("envPlan.example", { ex }));
|
|
106
|
+
log(t("envPlan.default", { value: info.default ?? "" }));
|
|
106
107
|
log("");
|
|
107
108
|
}
|
|
108
109
|
|
|
@@ -112,14 +113,14 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
|
|
|
112
113
|
const tot = todoKeys.length;
|
|
113
114
|
if (tot === 0) return;
|
|
114
115
|
log("");
|
|
115
|
-
log("
|
|
116
|
+
log(t("envPlan.enterValues"));
|
|
116
117
|
log("");
|
|
117
118
|
let i = 0;
|
|
118
119
|
for (const key of todoKeys) {
|
|
119
120
|
i++;
|
|
120
121
|
const def = asks.defaults.get(key) ?? "";
|
|
121
122
|
printFieldCard(prompts, key, { default: def, usages: asks.usages.get(key) || [] }, i, tot, log);
|
|
122
|
-
let input = await io.text({ message:
|
|
123
|
+
let input = await io.text({ message: t("envPlan.valuePrompt", { def }), defaultValue: def });
|
|
123
124
|
if (input === CANCEL || input == null || input === "") input = def;
|
|
124
125
|
values.set(key, input);
|
|
125
126
|
const label = wfField(prompts, firstTypeFor(asks.usages, key), key, "label");
|
|
@@ -159,10 +160,10 @@ export async function promptEnvPlan({
|
|
|
159
160
|
|
|
160
161
|
// 기본값 미리보기 카드 전체 출력 (.sh 3237~3251)
|
|
161
162
|
log("");
|
|
162
|
-
log("
|
|
163
|
+
log(t("envPlan.title"));
|
|
163
164
|
log("");
|
|
164
|
-
log("
|
|
165
|
-
log("
|
|
165
|
+
log(t("envPlan.intro1"));
|
|
166
|
+
log(t("envPlan.intro2"));
|
|
166
167
|
log("");
|
|
167
168
|
const tot = asks.keys.length;
|
|
168
169
|
asks.keys.forEach((key, i) => {
|
|
@@ -171,11 +172,11 @@ export async function promptEnvPlan({
|
|
|
171
172
|
log(" ─────────────────────────────────────────────");
|
|
172
173
|
|
|
173
174
|
const choice = await ui.select({
|
|
174
|
-
message: "
|
|
175
|
+
message: t("envPlan.how"),
|
|
175
176
|
options: [
|
|
176
|
-
{ value: "all", label: "
|
|
177
|
-
{ value: "each", label: "
|
|
178
|
-
{ value: "some", label: "
|
|
177
|
+
{ value: "all", label: t("envPlan.optAll") },
|
|
178
|
+
{ value: "each", label: t("envPlan.optEach") },
|
|
179
|
+
{ value: "some", label: t("envPlan.optSome") },
|
|
179
180
|
],
|
|
180
181
|
});
|
|
181
182
|
// ESC/취소 → 전부 기본값 (.sh `if [ "$_rc" -ne 0 ]` 등가)
|
|
@@ -194,10 +195,10 @@ export async function promptEnvPlan({
|
|
|
194
195
|
// some: 바꿀 항목만 멀티선택 → 고른 것만 입력 (.sh 3266~3277)
|
|
195
196
|
const options = asks.keys.map((key) => ({
|
|
196
197
|
value: key,
|
|
197
|
-
label: `${wfField(prompts, firstTypeFor(asks.usages, key), key, "label")} (
|
|
198
|
+
label: `${wfField(prompts, firstTypeFor(asks.usages, key), key, "label")} ${t("envPlan.defaultTag", { value: defaults.get(key) })}`,
|
|
198
199
|
}));
|
|
199
200
|
const selected = await ui.multiselect({
|
|
200
|
-
message: "
|
|
201
|
+
message: t("envPlan.pickPrompt"),
|
|
201
202
|
options,
|
|
202
203
|
initialValues: [],
|
|
203
204
|
});
|
package/src/ui/prompts.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// node:readline 기반 자체 엔진 사용 (@clack/prompts 는 Windows TTY에서 Enter가 멈추는 버그로 제거).
|
|
3
3
|
// 취소(ESC/Ctrl+C)는 각 함수가 CANCEL 심볼을 반환 → 호출부가 정상 종료(exit 0) 처리.
|
|
4
4
|
import * as engine from "./readline-engine.js";
|
|
5
|
+
import { t } from "../i18n/index.js";
|
|
5
6
|
|
|
6
7
|
export const CANCEL = engine.CANCEL;
|
|
7
8
|
|
|
@@ -12,26 +13,26 @@ export async function selectMode({ update = null } = {}) {
|
|
|
12
13
|
const options = [];
|
|
13
14
|
if (update) {
|
|
14
15
|
const range = update.from ? `v${update.from} → v${update.to}` : `v${update.to}`;
|
|
15
|
-
options.push({ value: "update", label:
|
|
16
|
+
options.push({ value: "update", label: t("mode.update", { range: `v${range}` }) });
|
|
16
17
|
}
|
|
17
18
|
options.push(
|
|
18
|
-
{ value: "full", label: "
|
|
19
|
-
{ value: "version", label: "
|
|
20
|
-
{ value: "workflows", label: "
|
|
21
|
-
{ value: "issues", label: "
|
|
22
|
-
{ value: "skills", label: "
|
|
19
|
+
{ value: "full", label: t("mode.full") },
|
|
20
|
+
{ value: "version", label: t("mode.version") },
|
|
21
|
+
{ value: "workflows", label: t("mode.workflows") },
|
|
22
|
+
{ value: "issues", label: t("mode.issues") },
|
|
23
|
+
{ value: "skills", label: t("mode.skills") },
|
|
23
24
|
);
|
|
24
|
-
return engine.select({ message: "
|
|
25
|
+
return engine.select({ message: t("mode.prompt"), options });
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
// 프로젝트 확인 화면 메뉴 (계속/수정/취소).
|
|
28
29
|
export async function confirmProjectMenu() {
|
|
29
30
|
return engine.select({
|
|
30
|
-
message: "
|
|
31
|
+
message: t("confirm.prompt"),
|
|
31
32
|
options: [
|
|
32
|
-
{ value: "continue", label: "
|
|
33
|
-
{ value: "edit", label: "
|
|
34
|
-
{ value: "cancel", label: "
|
|
33
|
+
{ value: "continue", label: t("confirm.continue") },
|
|
34
|
+
{ value: "edit", label: t("confirm.edit") },
|
|
35
|
+
{ value: "cancel", label: t("confirm.cancel") },
|
|
35
36
|
],
|
|
36
37
|
});
|
|
37
38
|
}
|
|
@@ -41,32 +42,32 @@ export async function confirmProjectMenu() {
|
|
|
41
42
|
// null이면 기존처럼 전부 노출 (테스트 스텁 하위호환).
|
|
42
43
|
export async function editMenu({ showOptional = false, axes = null } = {}) {
|
|
43
44
|
const options = [
|
|
44
|
-
{ value: "type", label: "
|
|
45
|
-
{ value: "version", label: "
|
|
46
|
-
{ value: "branch", label: "
|
|
45
|
+
{ value: "type", label: t("edit.type") },
|
|
46
|
+
{ value: "version", label: t("edit.version") },
|
|
47
|
+
{ value: "branch", label: t("edit.branch") },
|
|
47
48
|
];
|
|
48
49
|
if (showOptional) {
|
|
49
50
|
const hasDeploy = axes == null || axes.deploy.length > 0;
|
|
50
51
|
const hasPublish = axes == null || axes.publish.length > 0;
|
|
51
52
|
// #485 — 프로젝트 성격(intent): 재선택 시 배포/publish 축을 재유도한다. 축이 하나도 없으면 무의미 → 숨김.
|
|
52
|
-
if (hasDeploy || hasPublish) options.push({ value: "intent", label: "
|
|
53
|
+
if (hasDeploy || hasPublish) options.push({ value: "intent", label: t("edit.intent") });
|
|
53
54
|
// #483 — 항목별 격리: 한 축만 골라 그 축만 재질문한다 (통짜 "배포/Publish 방식" 분해)
|
|
54
|
-
if (hasDeploy) options.push({ value: "deploy", label: "
|
|
55
|
-
if (hasPublish) options.push({ value: "publish", label: "
|
|
56
|
-
options.push({ value: "code-review", label: "
|
|
57
|
-
options.push({ value: "changelog", label: "
|
|
58
|
-
options.push({ value: "release-branch", label: "
|
|
59
|
-
options.push({ value: "secret", label: "
|
|
55
|
+
if (hasDeploy) options.push({ value: "deploy", label: t("edit.deploy") });
|
|
56
|
+
if (hasPublish) options.push({ value: "publish", label: t("edit.publish") });
|
|
57
|
+
options.push({ value: "code-review", label: t("edit.codeReview") });
|
|
58
|
+
options.push({ value: "changelog", label: t("edit.changelog") });
|
|
59
|
+
options.push({ value: "release-branch", label: t("edit.releaseBranch") });
|
|
60
|
+
options.push({ value: "secret", label: t("edit.secret") });
|
|
60
61
|
}
|
|
61
|
-
options.push({ value: "done", label: "
|
|
62
|
-
return engine.select({ message: "
|
|
62
|
+
options.push({ value: "done", label: t("edit.done") });
|
|
63
|
+
return engine.select({ message: t("edit.prompt"), options });
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// 타입 멀티선택.
|
|
66
67
|
export async function selectTypes(current = []) {
|
|
67
68
|
const all = ["spring", "flutter", "react", "react-native", "react-native-expo", "node", "python", "basic"];
|
|
68
69
|
return engine.multiselect({
|
|
69
|
-
message: "
|
|
70
|
+
message: t("types.prompt"),
|
|
70
71
|
options: all.map((t) => ({ value: t, label: t })),
|
|
71
72
|
initialValues: current.length ? current : ["basic"],
|
|
72
73
|
required: true,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { emitKeypressEvents } from "node:readline";
|
|
8
8
|
import { stdin, stdout } from "node:process";
|
|
9
9
|
import { visualWidth } from "./ansi.js";
|
|
10
|
+
import { t } from "../i18n/index.js";
|
|
10
11
|
|
|
11
12
|
export const CANCEL = Symbol("cancel");
|
|
12
13
|
|
|
@@ -114,7 +115,7 @@ export async function select({ message, options, initialIndex = 0 }) {
|
|
|
114
115
|
const hint = o.hint && sel ? paint(` (${o.hint})`, c.dim) : "";
|
|
115
116
|
lines.push(`${S_BAR} ${marker} ${label}${hint}`);
|
|
116
117
|
});
|
|
117
|
-
lines.push(paint(`└
|
|
118
|
+
lines.push(paint(`└ ${t("engine.hint.select")}`, c.gray));
|
|
118
119
|
r.render(lines);
|
|
119
120
|
};
|
|
120
121
|
|
|
@@ -161,7 +162,7 @@ export async function multiselect({ message, options, initialValues = [], requir
|
|
|
161
162
|
lines.push(`${S_BAR} ${pointer} ${box} ${label}${hint}`);
|
|
162
163
|
});
|
|
163
164
|
if (warn) lines.push(paint(` ${warn}`, c.yellow));
|
|
164
|
-
lines.push(paint(`└
|
|
165
|
+
lines.push(paint(`└ ${t("engine.hint.multiselect")}`, c.gray));
|
|
165
166
|
r.render(lines);
|
|
166
167
|
};
|
|
167
168
|
|
|
@@ -171,19 +172,26 @@ export async function multiselect({ message, options, initialValues = [], requir
|
|
|
171
172
|
if (key.name === "down" || key.name === "j") { idx = (idx + 1) % options.length; return; }
|
|
172
173
|
if (key.name === "space" || str === " ") {
|
|
173
174
|
const o = options[idx];
|
|
174
|
-
if (o.disabled) { warn = "
|
|
175
|
+
if (o.disabled) { warn = t("engine.disabled"); return; }
|
|
175
176
|
if (chosen.has(o.value)) chosen.delete(o.value); else chosen.add(o.value);
|
|
176
177
|
return;
|
|
177
178
|
}
|
|
179
|
+
if (str === "a") {
|
|
180
|
+
// Select all; if everything is already selected, clear instead (like a toggle).
|
|
181
|
+
const selectable = options.filter((o) => !o.disabled).map((o) => o.value);
|
|
182
|
+
const allOn = selectable.every((v) => chosen.has(v));
|
|
183
|
+
for (const v of selectable) { if (allOn) chosen.delete(v); else chosen.add(v); }
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
178
186
|
if (key.name === "return" || key.name === "enter") {
|
|
179
|
-
if (required && chosen.size === 0) { warn = "
|
|
187
|
+
if (required && chosen.size === 0) { warn = t("engine.required"); return; }
|
|
180
188
|
return [...chosen];
|
|
181
189
|
}
|
|
182
190
|
return;
|
|
183
191
|
});
|
|
184
192
|
|
|
185
193
|
if (result !== CANCEL) {
|
|
186
|
-
const labels = options.filter((o) => chosen.has(o.value)).map((o) => o.label).join(", ") || "
|
|
194
|
+
const labels = options.filter((o) => chosen.has(o.value)).map((o) => o.label).join(", ") || t("engine.none");
|
|
187
195
|
r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(labels, c.dim)}`]);
|
|
188
196
|
}
|
|
189
197
|
return result;
|
|
@@ -238,10 +246,10 @@ export async function confirm({ message, initialValue = true }) {
|
|
|
238
246
|
let val = initialValue;
|
|
239
247
|
|
|
240
248
|
const draw = () => {
|
|
241
|
-
const yes = val ? paint(
|
|
242
|
-
const no = !val ? paint(
|
|
249
|
+
const yes = val ? paint(`● ${t("engine.yes")}`, c.green) : paint(`○ ${t("engine.yes")}`, c.dim);
|
|
250
|
+
const no = !val ? paint(`● ${t("engine.no")}`, c.green) : paint(`○ ${t("engine.no")}`, c.dim);
|
|
243
251
|
r.render([S_BAR, `${S_Q} ${paint(message, c.bold)}`, `${S_BAR} ${yes} ${no}`,
|
|
244
|
-
paint(`└
|
|
252
|
+
paint(`└ ${t("engine.hint.confirm")}`, c.gray)]);
|
|
245
253
|
};
|
|
246
254
|
|
|
247
255
|
const result = await keySession(draw, (str, key) => {
|
|
@@ -253,7 +261,7 @@ export async function confirm({ message, initialValue = true }) {
|
|
|
253
261
|
});
|
|
254
262
|
|
|
255
263
|
if (result !== CANCEL) {
|
|
256
|
-
r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(result ? "
|
|
264
|
+
r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(result ? t("engine.yes") : t("engine.no"), c.dim)}`]);
|
|
257
265
|
}
|
|
258
266
|
return result;
|
|
259
267
|
}
|
|
@@ -261,7 +269,7 @@ export async function confirm({ message, initialValue = true }) {
|
|
|
261
269
|
// ── 출력 헬퍼 (clack intro/outro/note/cancel 대체) ──────────────────
|
|
262
270
|
export function intro(text) { stdout.write(`\n${paint("┌", c.gray)} ${paint(text, c.bold)}\n`); }
|
|
263
271
|
export function outro(text) { stdout.write(`${paint("└", c.gray)} ${paint(text, c.green)}\n\n`); }
|
|
264
|
-
export function cancelMessage(text = "
|
|
272
|
+
export function cancelMessage(text = t("engine.cancelled")) { stdout.write(`${paint("■", c.yellow)} ${paint(text, c.yellow)}\n`); }
|
|
265
273
|
export function note(text, title = "") {
|
|
266
274
|
const lines = String(text).split("\n");
|
|
267
275
|
stdout.write(`${paint("○", c.cyan)} ${paint(title, c.bold)}\n`);
|
package/src/ui/skills-prompts.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
// IDE Skills 대화형 프롬프트 (.sh 라우터 choose_menu 등가).
|
|
2
2
|
// node:readline 기반 자체 엔진 사용 (@clack/prompts 는 Windows TTY Enter 버그로 제거).
|
|
3
3
|
import * as engine from "./readline-engine.js";
|
|
4
|
+
import { t } from "../i18n/index.js";
|
|
4
5
|
|
|
5
6
|
export const CANCEL = engine.CANCEL;
|
|
6
7
|
|
|
7
8
|
// 동작 선택: 설치/업데이트 · 제거 · 그대로. 취소=skip.
|
|
8
9
|
export async function selectAction() {
|
|
9
10
|
const v = await engine.select({
|
|
10
|
-
message: "
|
|
11
|
+
message: t("skills.action.prompt"),
|
|
11
12
|
options: [
|
|
12
|
-
{ value: "apply", label: "
|
|
13
|
-
{ value: "remove", label: "
|
|
14
|
-
{ value: "skip", label: "
|
|
13
|
+
{ value: "apply", label: t("skills.action.apply") },
|
|
14
|
+
{ value: "remove", label: t("skills.action.remove") },
|
|
15
|
+
{ value: "skip", label: t("skills.action.skip") },
|
|
15
16
|
],
|
|
16
17
|
});
|
|
17
18
|
return v === CANCEL ? "skip" : v;
|
|
@@ -21,12 +22,12 @@ export async function selectAction() {
|
|
|
21
22
|
export async function selectTargets(choices, preselect = [], action = "apply") {
|
|
22
23
|
const options = choices.map((c) => ({
|
|
23
24
|
value: c.id,
|
|
24
|
-
label: c.label + (c.disabled ? "
|
|
25
|
-
hint: c.disabled ? "
|
|
25
|
+
label: c.label + (c.disabled ? t("skills.target.undetected") : ""),
|
|
26
|
+
hint: c.disabled ? t("ide.cliMissing") : undefined,
|
|
26
27
|
disabled: c.disabled,
|
|
27
28
|
}));
|
|
28
29
|
return engine.multiselect({
|
|
29
|
-
message:
|
|
30
|
+
message: t(action === "apply" ? "skills.target.promptApply" : "skills.target.promptRemove"),
|
|
30
31
|
options,
|
|
31
32
|
initialValues: preselect,
|
|
32
33
|
required: false,
|
package/src/ui/status-cards.js
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
//
|
|
2
|
-
// (
|
|
1
|
+
// Status layers shown around the start screen: detection log, summary card, IDE status, install kind.
|
|
2
|
+
// (The Breaking Changes box lives in core/breaking-check.js.)
|
|
3
|
+
// All text goes through t() so it follows the display language; markers and ids stay as-is.
|
|
3
4
|
import { A, paint, padEndVisual } from "./ansi.js";
|
|
4
5
|
import { ADAPTERS } from "../core/ide/registry.js";
|
|
5
6
|
import { markerForType } from "../core/detect.js";
|
|
7
|
+
import { t } from "../i18n/index.js";
|
|
6
8
|
|
|
7
9
|
const GUT = paint("│", A.gray);
|
|
8
10
|
const HEAD = paint("◆", A.cyan);
|
|
9
11
|
const OK = paint("✓", A.green);
|
|
10
12
|
|
|
11
|
-
//
|
|
13
|
+
// Detection log: one line per detected type, then version and branch.
|
|
12
14
|
export function printDetectionLog({ types = [], version = "", branch = "" }, out = (s) => process.stdout.write(s)) {
|
|
13
|
-
out(`${paint("┌", A.gray)}
|
|
15
|
+
out(`${paint("┌", A.gray)} ${t("detect.title")}\n`);
|
|
14
16
|
if (types.length && !(types.length === 1 && types[0] === "basic")) {
|
|
15
|
-
for (const
|
|
16
|
-
const marker = markerForType(
|
|
17
|
-
|
|
17
|
+
for (const type of types) {
|
|
18
|
+
const marker = markerForType(type);
|
|
19
|
+
const name = paint(type, A.bold);
|
|
20
|
+
out(`${GUT} ${OK} ${marker ? t("detect.found", { marker, type: name }) : t("detect.foundNoMarker", { type: name })}\n`);
|
|
18
21
|
}
|
|
19
22
|
} else {
|
|
20
|
-
out(`${GUT} ${paint("
|
|
23
|
+
out(`${GUT} ${paint("-", A.dim)} ${t("detect.none")}\n`);
|
|
21
24
|
}
|
|
22
|
-
out(`${GUT} ${OK}
|
|
25
|
+
out(`${GUT} ${OK} ${t("detect.versionBranch", { version: paint(version, A.green), branch: paint(branch, A.green) })}\n`);
|
|
23
26
|
out(`${GUT}\n`);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
//
|
|
29
|
+
// Summary card shown before the "continue?" menu: what is about to be installed.
|
|
27
30
|
export function printAnalysisCard({ mode = "", modeLabel = "", types = [], version = "", branch = "",
|
|
28
31
|
deployTarget = null, publishTargets = null, includeSecretBackup = null, paths = new Map(), showOptional = false },
|
|
29
32
|
out = (s) => process.stdout.write(s)) {
|
|
30
|
-
out(`${HEAD} ${paint("
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
row("
|
|
35
|
-
row("
|
|
36
|
-
row("
|
|
37
|
-
if (modeLabel || mode) row("💫", "통합 모드", modeLabel || mode);
|
|
33
|
+
out(`${HEAD} ${paint(t("card.title"), A.bold)}\n`);
|
|
34
|
+
// Labels are padded by visual width (CJK counts as 2 columns) so values line up in both languages.
|
|
35
|
+
const row = (label, value) => out(`${GUT} ${padEndVisual(label, 14)} ${value}\n`);
|
|
36
|
+
row(types.length > 1 ? t("card.typeMulti") : t("card.type"), paint(types.join(", ") || "basic", A.bold));
|
|
37
|
+
row(t("card.version"), paint(`v${version}`, A.green));
|
|
38
|
+
row(t("card.branch"), branch);
|
|
39
|
+
if (modeLabel || mode) row(t("card.mode"), modeLabel || mode);
|
|
38
40
|
if (showOptional) {
|
|
39
|
-
row("
|
|
41
|
+
row(t("card.deploy"), paint(deployTarget || "docker-ssh", A.bold));
|
|
40
42
|
const pub = (publishTargets ?? []).join(",");
|
|
41
|
-
row("
|
|
42
|
-
row("
|
|
43
|
+
row(t("card.publish"), pub ? paint(pub, A.green) : paint(t("card.none"), A.dim));
|
|
44
|
+
row(t("card.secret"), includeSecretBackup === true ? paint(t("card.included"), A.green) : paint(t("card.excluded"), A.dim));
|
|
43
45
|
}
|
|
44
|
-
//
|
|
46
|
+
// Monorepo paths: shown only when at least one type is not at the repository root.
|
|
45
47
|
const nonRoot = [...paths.entries()].filter(([, p]) => p && p !== ".");
|
|
46
48
|
if (nonRoot.length) {
|
|
47
|
-
row("
|
|
49
|
+
row(t("card.paths"), [...paths.entries()].map(([type, p]) => `${type}→${p}`).join(", "));
|
|
48
50
|
}
|
|
49
51
|
out(`${GUT}\n`);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
//
|
|
54
|
+
// IDE skills: ask every adapter what it sees (adapters never throw by contract).
|
|
53
55
|
export function collectIdeStatuses(io) {
|
|
54
56
|
return ADAPTERS.map((a) => {
|
|
55
57
|
let st;
|
|
56
|
-
try { st = a.detect(io); } catch { st = { installed: false, version: null, cliMissing: true, note: "
|
|
58
|
+
try { st = a.detect(io); } catch { st = { installed: false, version: null, cliMissing: true, note: t("ide.detectFailed") }; }
|
|
57
59
|
return { id: a.id, label: a.label, ...st };
|
|
58
60
|
});
|
|
59
61
|
}
|
|
60
62
|
|
|
61
|
-
// 층4 — IDE Skills 현재 상태 표시
|
|
62
63
|
export function printIdeStatus(statuses, out = (s) => process.stdout.write(s)) {
|
|
63
64
|
if (!statuses?.length) return;
|
|
64
|
-
out(`${HEAD} ${paint("
|
|
65
|
+
out(`${HEAD} ${paint(t("ide.title"), A.bold)}\n`);
|
|
65
66
|
for (const s of statuses) {
|
|
66
67
|
let mark, detail;
|
|
67
68
|
if (s.installed) {
|
|
68
69
|
mark = OK;
|
|
69
|
-
detail = paint(
|
|
70
|
+
detail = paint(`${t("ide.installed")}${s.version ? ` v${s.version}` : ""}${s.scope ? ` (${s.scope})` : ""}`, A.green);
|
|
70
71
|
} else if (s.cliMissing) {
|
|
71
72
|
mark = paint("⚠", A.yellow);
|
|
72
|
-
detail = paint(s.note || "
|
|
73
|
+
detail = paint(s.note || t("ide.cliMissing"), A.yellow);
|
|
73
74
|
} else {
|
|
74
|
-
mark = paint("
|
|
75
|
-
detail = paint("
|
|
75
|
+
mark = paint("-", A.dim);
|
|
76
|
+
detail = paint(t("ide.notInstalled"), A.dim);
|
|
76
77
|
}
|
|
77
78
|
out(`${GUT} ${mark} ${s.label.padEnd(14)} ${detail}${s.note && !s.cliMissing ? paint(` ${s.note}`, A.dim) : ""}\n`);
|
|
78
79
|
}
|
|
79
80
|
out(`${GUT}\n`);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// 사용자가 "무슨 기준으로 신규/업데이트인지"를 바로 알도록 판정 근거를 화면에 밝힌다.
|
|
83
|
+
// New install vs update: one line plus the reason, so users know why the menu looks the way it does.
|
|
84
|
+
// Rule: version.yml carrying metadata.template.version means a previous install, so it is an update.
|
|
85
85
|
export function printInstallKind({ currentTemplateVersion = "", templateVersion = "" }, out = (s) => process.stdout.write(s)) {
|
|
86
86
|
if (currentTemplateVersion) {
|
|
87
|
-
out(`${GUT}
|
|
88
|
-
out(`${GUT}
|
|
87
|
+
out(`${GUT} ${paint(t("kind.update"), A.bold)}: ${t("kind.updateFrom", { from: paint(currentTemplateVersion, A.dim), to: paint(templateVersion, A.green) })}\n`);
|
|
88
|
+
out(`${GUT} ${paint(t("kind.updateWhy"), A.dim)}\n`);
|
|
89
89
|
} else {
|
|
90
|
-
out(`${GUT}
|
|
91
|
-
out(`${GUT}
|
|
90
|
+
out(`${GUT} ${paint(t("kind.new"), A.bold)}: ${t("kind.newTemplate", { to: paint(templateVersion, A.green) })}\n`);
|
|
91
|
+
out(`${GUT} ${paint(t("kind.newWhy"), A.dim)}\n`);
|
|
92
92
|
}
|
|
93
93
|
out(`${GUT}\n`);
|
|
94
94
|
}
|