project-auto-wizard 0.13.3 → 0.15.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.
Files changed (115) hide show
  1. package/README.ja.md +2 -0
  2. package/README.ko.md +2 -0
  3. package/README.md +3 -1
  4. package/README.zh-CN.md +2 -0
  5. package/bin/project-auto-wizard.js +18 -4
  6. package/package.json +2 -2
  7. package/payload/config/breaking-changes.json +28 -14
  8. package/payload/config/wizard-prompts.yml +102 -46
  9. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  10. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  11. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  12. package/payload/scripts/changelog_manager.py +304 -265
  13. package/payload/scripts/issue_helper.py +54 -50
  14. package/payload/scripts/messages.py +2585 -0
  15. package/payload/scripts/truncate_release_notes.py +7 -6
  16. package/payload/scripts/version_manager.py +110 -107
  17. package/payload/version.yml.template +2 -1
  18. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +23 -16
  19. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +106 -63
  20. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  21. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +55 -45
  22. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +90 -72
  23. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +59 -45
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  29. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  30. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  31. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  32. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  33. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  34. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  35. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  36. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  37. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  38. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  39. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  40. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  41. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  42. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  44. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  45. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  46. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  47. package/src/cli/args.js +98 -52
  48. package/src/cli/help.js +8 -41
  49. package/src/commands/doctor.js +90 -89
  50. package/src/commands/dry-run.js +64 -43
  51. package/src/commands/full.js +79 -77
  52. package/src/commands/install-settings.js +16 -15
  53. package/src/commands/interactive-flutter.js +20 -19
  54. package/src/commands/interactive.js +135 -132
  55. package/src/commands/purge.js +47 -42
  56. package/src/commands/status.js +36 -36
  57. package/src/commands/uninstall.js +43 -41
  58. package/src/context.js +12 -11
  59. package/src/core/assets.js +12 -11
  60. package/src/core/baseline.js +27 -27
  61. package/src/core/branches.js +29 -27
  62. package/src/core/branding.js +7 -6
  63. package/src/core/breaking-check.js +22 -21
  64. package/src/core/breaking.js +15 -8
  65. package/src/core/copy/app-files.js +5 -4
  66. package/src/core/copy/flutter-app.js +8 -8
  67. package/src/core/copy/gitignore.js +58 -50
  68. package/src/core/copy/readme.js +102 -36
  69. package/src/core/copy/simple.js +16 -15
  70. package/src/core/copy/workflows.js +131 -124
  71. package/src/core/deploy-style.js +51 -48
  72. package/src/core/detect-fs.js +57 -57
  73. package/src/core/detect.js +67 -65
  74. package/src/core/errors.js +2 -2
  75. package/src/core/flutter-doctor.js +17 -16
  76. package/src/core/flutter-hooks.js +33 -32
  77. package/src/core/flutter-options.js +34 -33
  78. package/src/core/fsutil.js +12 -12
  79. package/src/core/installed-stores.js +4 -4
  80. package/src/core/logger.js +42 -40
  81. package/src/core/paths-resolve.js +88 -86
  82. package/src/core/paths.js +10 -10
  83. package/src/core/release-options.js +8 -8
  84. package/src/core/removal-exec.js +14 -13
  85. package/src/core/removal-plan.js +37 -37
  86. package/src/core/types.js +47 -47
  87. package/src/core/verify.js +44 -38
  88. package/src/core/version-yml.js +76 -68
  89. package/src/core/wizard-env.js +46 -46
  90. package/src/core/wizard-labels.js +63 -41
  91. package/src/i18n/catalog/en/cli.js +50 -0
  92. package/src/i18n/catalog/en/commands.js +188 -0
  93. package/src/i18n/catalog/en/copy.js +20 -0
  94. package/src/i18n/catalog/en/core.js +74 -0
  95. package/src/i18n/catalog/en/core2.js +15 -0
  96. package/src/i18n/catalog/en/ui.js +221 -0
  97. package/src/i18n/catalog/en.js +12 -0
  98. package/src/i18n/catalog/index.js +5 -0
  99. package/src/i18n/catalog/ko/cli.js +50 -0
  100. package/src/i18n/catalog/ko/commands.js +188 -0
  101. package/src/i18n/catalog/ko/copy.js +20 -0
  102. package/src/i18n/catalog/ko/core.js +74 -0
  103. package/src/i18n/catalog/ko/core2.js +15 -0
  104. package/src/i18n/catalog/ko/ui.js +221 -0
  105. package/src/i18n/catalog/ko.js +10 -0
  106. package/src/i18n/index.js +44 -0
  107. package/src/i18n/languages.js +9 -0
  108. package/src/index.js +132 -97
  109. package/src/ui/ansi.js +7 -7
  110. package/src/ui/banner.js +7 -6
  111. package/src/ui/env-plan.js +82 -81
  112. package/src/ui/prompts.js +97 -122
  113. package/src/ui/readline-engine.js +59 -58
  114. package/src/ui/status-cards.js +37 -31
  115. package/src/ui/summary.js +90 -89
@@ -1,33 +1,34 @@
1
- // node:readline 기반 대화형 프롬프트 엔진 (@clack/prompts 대체).
2
- // WHY: @clack/prompts 1.7.0 이 Windows TTY 콘솔에서 Enter(return) 키를 처리하지 못하고
3
- // 멈추는 버그가 있다. node:readline 의 keypress 이벤트는 Windows에서 정상 동작한다.
4
- // 메뉴를 직접 구현하면 외부 의존성 0 → 내부망에서도 안전.
1
+ // Interactive prompt engine built on node:readline (replaces @clack/prompts).
2
+ // WHY: @clack/prompts 1.7.0 has a bug on Windows TTY consoles where the Enter (return) key is not
3
+ // handled and the prompt hangs. node:readline keypress events work correctly on Windows.
4
+ // Implementing the menus ourselves means zero external dependencies -> safe on internal networks too.
5
5
  //
6
- // 계약: ESC는 CANCEL 심볼 반환(호출부가 "기본값/머무르기"로 해석). 각 함수 async.
7
- // Ctrl+C·Ctrl+D·stdin 종료는 중단 — PromptAbortError로 reject해 어느 질문에서든 즉시 빠져나간다.
6
+ // Contract: ESC returns the CANCEL symbol (callers interpret it as "default/stay"). Every function is async.
7
+ // Ctrl+C, Ctrl+D and stdin close abort - rejected as PromptAbortError so any question can be left immediately.
8
8
  import { emitKeypressEvents } from "node:readline";
9
9
  import { stdin, stdout } from "node:process";
10
10
  import { visualWidth } from "./ansi.js";
11
+ import { t } from "../i18n/index.js";
11
12
 
12
13
  export const CANCEL = Symbol("cancel");
13
14
 
14
- // 사용자 중단(Ctrl+C 등). ESC와 달리 기본값으로 진행하면 안 되므로 반환값이 아니라 예외로 전파한다 —
15
- // 호출부마다 CANCEL 해석이 달라 일부는 기본값으로 설치를 강행하거나 같은 화면을 무한히 다시 그렸다.
16
- // signal: 외부 신호로 중단된 경우 그 신호명("SIGINT"·"SIGTERM") — 종료 코드를 셸 관례(128+번호)에 맞춘다.
15
+ // User abort (Ctrl+C etc.). Unlike ESC it must not continue with defaults, so it propagates as an exception, not a return value -
16
+ // callers interpreted CANCEL differently, and some forced the install with defaults or redrew the same screen forever.
17
+ // signal: name of the external signal ("SIGINT"/"SIGTERM") when aborted by one - aligns the exit code with the shell convention (128+number).
17
18
  export class PromptAbortError extends Error {
18
19
  constructor(signal = null) {
19
- super("사용자가 중단했습니다.");
20
+ super(t("ui.readline-engine.aborted"));
20
21
  this.name = "PromptAbortError";
21
22
  this.signal = signal;
22
23
  }
23
24
  }
24
25
  export const isPromptAbort = (e) => e instanceof PromptAbortError;
25
26
 
26
- // raw mode에서는 Ctrl+C가 SIGINT가 아니라 keypress로 들어온다. Ctrl+D도 raw mode에선 EOF가 아니다.
27
+ // In raw mode Ctrl+C arrives as a keypress, not SIGINT. Ctrl+D is not EOF in raw mode either.
27
28
  const isAbortKey = (key) => key.ctrl && (key.name === "c" || key.name === "d");
28
29
 
29
- // 외부 신호(kill -INT·kill -TERM)는 keypress가 아니라 프로세스 신호로 온다. 리스너가 없으면 Node가 곧바로
30
- // 종료해 커서 숨김과 raw mode가 복구되지 않는다 — 입력 대기 중에는 키보드 Ctrl+C와 같은 중단 경로로 돌린다.
30
+ // External signals (kill -INT / kill -TERM) arrive as process signals, not keypresses. Without a listener Node exits
31
+ // immediately and the hidden cursor and raw mode are never restored - while waiting for input, route them through the same abort path as keyboard Ctrl+C.
31
32
  const TERM_SIGNALS = ["SIGINT", "SIGTERM"];
32
33
  function onTermSignal(fn) {
33
34
  const handlers = TERM_SIGNALS.map((sig) => [sig, () => fn(sig)]);
@@ -35,19 +36,19 @@ function onTermSignal(fn) {
35
36
  return () => { for (const [sig, h] of handlers) process.removeListener(sig, h); };
36
37
  }
37
38
 
38
- // ── ANSI 헬퍼 (picocolors 대체 — 의존성 0) ───────────────────────────
39
+ // ── ANSI helpers (replaces picocolors - zero dependencies) ───────────
39
40
  const ESC = "\x1b[";
40
41
  const c = {
41
42
  reset: `${ESC}0m`, dim: `${ESC}2m`, bold: `${ESC}1m`,
42
43
  cyan: `${ESC}36m`, green: `${ESC}32m`, gray: `${ESC}90m`, yellow: `${ESC}33m`,
43
44
  };
44
- // NO_COLOR(https://no-color.org)/비TTY 가드 — ansi.js와 동일한 규칙(존재 여부만 체크, 값 무관)이지만
45
- // 의존성 0 유지를 위해 자체 구현.
46
- // TERM=dumb은 색상뿐 아니라 커서 이동·지우기 시퀀스도 해석하지 못한다 — 그때는 다시 그리지 않고 이어서 출력한다.
45
+ // NO_COLOR (https://no-color.org) / non-TTY guard - same rule as ansi.js (presence only, value ignored) but
46
+ // implemented locally to stay dependency-free.
47
+ // TERM=dumb cannot interpret cursor-movement/erase sequences either, not just colors - then output continues instead of redrawing.
47
48
  const isDumb = () => process.env.TERM === "dumb";
48
49
  const colorEnabled = () => process.env.NO_COLOR === undefined && !!stdout.isTTY && !isDumb();
49
50
  const paint = (s, color, enabled = colorEnabled()) => (enabled ? `${color}${s}${c.reset}` : String(s));
50
- // 커서를 숨긴 채 프로세스가 끝나면(예외·process.exit 등) 터미널 커서가 사라진 채로 남는다 — 종료 시점에 한 번 더 복구한다.
51
+ // If the process ends with the cursor hidden (exception, process.exit, ...) the terminal cursor stays invisible - restore it once more on exit.
51
52
  let cursorHidden = false;
52
53
  let exitGuard = false;
53
54
  const hideCursor = () => {
@@ -65,7 +66,7 @@ const showCursor = () => {
65
66
  cursorHidden = false;
66
67
  };
67
68
 
68
- // 심볼 (clack 톤 유지)
69
+ // Symbols (keeps the clack look)
69
70
  const S_ACTIVE = paint("●", c.green);
70
71
  const S_INACTIVE = paint("○", c.dim);
71
72
  const S_CHECK_ON = paint("◼", c.green);
@@ -74,22 +75,22 @@ const S_BAR = paint("│", c.gray);
74
75
  const S_Q = paint("◆", c.cyan);
75
76
  const S_DONE = paint("◇", c.green);
76
77
 
77
- // 한 줄이 터미널에서 실제로 차지하는 행 수 — 폭을 넘는 줄은 터미널이 접어서 여러 행이 된다.
78
- // 논리 줄 수만큼만 올라가면 접힌 윗부분이 지워지지 않고 화면에 사본이 쌓인다.
78
+ // Number of rows a line actually occupies in the terminal - lines wider than the terminal wrap into several rows.
79
+ // Moving up only by the logical line count would leave the wrapped top part uncleared and stack copies on screen.
79
80
  export function physicalRows(line, columns = stdout.columns) {
80
81
  if (!columns) return 1;
81
82
  return Math.max(1, Math.ceil(visualWidth(line) / columns));
82
83
  }
83
84
 
84
- // 여러 줄 지운 뒤 커서를 블록 시작으로 되돌리는 렌더러.
85
- // 직전에 그린 물리 행 수만큼 위로 올라가 지우고 새로 그린다.
85
+ // Renderer that erases several lines and returns the cursor to the start of the block.
86
+ // Moves up by the number of physical rows drawn last time, erases, and redraws.
86
87
  function makeRenderer() {
87
88
  let prevLines = 0;
88
89
  return {
89
90
  render(lines) {
90
91
  if (isDumb()) { stdout.write(lines.join("\n") + "\n"); return; }
91
- if (prevLines > 0) stdout.write(`${ESC}${prevLines}A`); // 위로
92
- stdout.write(`${ESC}0J`); // 커서 아래 전부 지우기
92
+ if (prevLines > 0) stdout.write(`${ESC}${prevLines}A`); // up
93
+ stdout.write(`${ESC}0J`); // erase everything below the cursor
93
94
  stdout.write(lines.join("\n") + "\n");
94
95
  prevLines = lines.reduce((n, l) => n + physicalRows(l), 0);
95
96
  },
@@ -97,8 +98,8 @@ function makeRenderer() {
97
98
  };
98
99
  }
99
100
 
100
- // raw keypress 세션 공통 래퍼. onKey(str,key) → true 반환 시 종료.
101
- // 반환값은 finalize()가 만든다. 취소 시 CANCEL.
101
+ // Common wrapper for raw keypress sessions. Ends when onKey(str,key) returns a value.
102
+ // The result is produced by the caller's onKey; CANCEL on cancel.
102
103
  function keySession(renderFn, onKey) {
103
104
  return new Promise((resolve, reject) => {
104
105
  const wasRaw = stdin.isTTY ? stdin.isRaw : false;
@@ -121,8 +122,8 @@ function keySession(renderFn, onKey) {
121
122
  reject(new PromptAbortError(sig));
122
123
  });
123
124
 
124
- // stdin 종료(EOF, SSH 연결 끊김 등) — 더 이상 입력이 올 수 없으므로 중단한다.
125
- // CANCEL로 돌려주면 "머무르기"로 해석하는 화면에서 다시 묻다가 영원히 대기한다.
125
+ // stdin closed (EOF, dropped SSH connection, ...) - no more input can arrive, so abort.
126
+ // Returning CANCEL would make screens that read it as "stay" ask again and wait forever.
126
127
  const onEnd = () => {
127
128
  cleanup();
128
129
  reject(new PromptAbortError());
@@ -151,15 +152,15 @@ function keySession(renderFn, onKey) {
151
152
  };
152
153
  stdin.on("keypress", handler);
153
154
  stdin.on("end", onEnd);
154
- renderFn(); // 최초 렌더
155
+ renderFn(); // initial render
155
156
  });
156
157
  }
157
158
 
158
- // ── 단일 선택 (방향키 + Enter) ───────────────────────────────────────
159
- // options: [{value,label,hint?}]. 반환: 선택 value 또는 CANCEL.
159
+ // ── Single select (arrow keys + Enter) ───────────────────────────────
160
+ // options: [{value,label,hint?}]. Returns: the selected value or CANCEL.
160
161
  export async function select({ message, options, initialIndex = 0 }) {
161
162
  if (!stdin.isTTY) {
162
- // 비-TTY: 기본값(첫 항목) 반환 — 파이프 환경 방어
163
+ // Non-TTY: return the default (first item) - guards piped environments
163
164
  return options[initialIndex]?.value;
164
165
  }
165
166
  const r = makeRenderer();
@@ -174,24 +175,24 @@ export async function select({ message, options, initialIndex = 0 }) {
174
175
  const hint = o.hint && sel ? paint(` (${o.hint})`, c.dim) : "";
175
176
  lines.push(`${S_BAR} ${marker} ${label}${hint}`);
176
177
  });
177
- lines.push(paint(`└ ↑/↓ 이동 · Enter 확정 · ESC 취소`, c.gray));
178
+ lines.push(paint(t("ui.readline-engine.footer.select"), c.gray));
178
179
  r.render(lines);
179
180
  };
180
181
 
181
182
  const result = await keySession(draw, (str, key) => {
182
183
  if (key.name === "up" || key.name === "k") { idx = (idx - 1 + options.length) % options.length; return; }
183
184
  if (key.name === "down" || key.name === "j") { idx = (idx + 1) % options.length; return; }
184
- // 숫자 점프 (1-9)
185
+ // Number jump (1-9)
185
186
  if (/^[1-9]$/.test(str || "")) {
186
187
  const n = Number(str) - 1;
187
188
  if (n < options.length) { idx = n; return; }
188
189
  return;
189
190
  }
190
191
  if (key.name === "return" || key.name === "enter") return options[idx].value;
191
- return; // 그 외 키: 무시하고 계속
192
+ return; // any other key: ignore and continue
192
193
  });
193
194
 
194
- // 확정 화면 다시 그리기 (◇ 완료 심볼 + 선택값)
195
+ // Redraw the confirmed screen (◇ done symbol + chosen value)
195
196
  if (result !== CANCEL) {
196
197
  const chosen = options.find((o) => o.value === result);
197
198
  r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(chosen?.label ?? "", c.dim)}`]);
@@ -199,8 +200,8 @@ export async function select({ message, options, initialIndex = 0 }) {
199
200
  return result;
200
201
  }
201
202
 
202
- // ── 다중 선택 (Space 토글 + Enter) ──────────────────────────────────
203
- // options: [{value,label,hint?,disabled?}]. 반환: 선택 value 배열 또는 CANCEL.
203
+ // ── Multi select (Space toggle + Enter) ──────────────────────────────
204
+ // options: [{value,label,hint?,disabled?}]. Returns: array of selected values or CANCEL.
204
205
  export async function multiselect({ message, options, initialValues = [], required = false }) {
205
206
  if (!stdin.isTTY) {
206
207
  return initialValues.length ? [...initialValues] : (required ? [options[0]?.value].filter(Boolean) : []);
@@ -221,7 +222,7 @@ export async function multiselect({ message, options, initialValues = [], requir
221
222
  lines.push(`${S_BAR} ${pointer} ${box} ${label}${hint}`);
222
223
  });
223
224
  if (warn) lines.push(paint(` ${warn}`, c.yellow));
224
- lines.push(paint(`└ ↑/↓ 이동 · Space 토글 · Enter 확정 · ESC 취소`, c.gray));
225
+ lines.push(paint(t("ui.readline-engine.footer.multiselect"), c.gray));
225
226
  r.render(lines);
226
227
  };
227
228
 
@@ -231,26 +232,26 @@ export async function multiselect({ message, options, initialValues = [], requir
231
232
  if (key.name === "down" || key.name === "j") { idx = (idx + 1) % options.length; return; }
232
233
  if (key.name === "space" || str === " ") {
233
234
  const o = options[idx];
234
- if (o.disabled) { warn = "선택할 수 없는 항목입니다."; return; }
235
+ if (o.disabled) { warn = t("ui.readline-engine.warn.disabled"); return; }
235
236
  if (chosen.has(o.value)) chosen.delete(o.value); else chosen.add(o.value);
236
237
  return;
237
238
  }
238
239
  if (key.name === "return" || key.name === "enter") {
239
- if (required && chosen.size === 0) { warn = "최소 1개 이상 선택하세요."; return; }
240
+ if (required && chosen.size === 0) { warn = t("ui.readline-engine.warn.required"); return; }
240
241
  return [...chosen];
241
242
  }
242
243
  return;
243
244
  });
244
245
 
245
246
  if (result !== CANCEL) {
246
- const labels = options.filter((o) => chosen.has(o.value)).map((o) => o.label).join(", ") || "(없음)";
247
+ const labels = options.filter((o) => chosen.has(o.value)).map((o) => o.label).join(", ") || t("ui.readline-engine.none");
247
248
  r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(labels, c.dim)}`]);
248
249
  }
249
250
  return result;
250
251
  }
251
252
 
252
- // ── 텍스트 입력 (Enter 확정, 빈 입력=기본값) ─────────────────────────
253
- // 반환: 입력 문자열(빈 입력 시 defaultValue) 또는 CANCEL.
253
+ // ── Text input (Enter confirms, empty input = default) ────────────────
254
+ // Returns: the typed string (defaultValue on empty input) or CANCEL.
254
255
  export async function text({ message, defaultValue = "" }) {
255
256
  if (!stdin.isTTY) return defaultValue;
256
257
  return new Promise((resolve, reject) => {
@@ -260,15 +261,15 @@ export async function text({ message, defaultValue = "" }) {
260
261
  stdin.resume();
261
262
  let buf = "";
262
263
 
263
- // dumb 터미널은 줄을 지울 수 없으므로 프롬프트는 한 번만 쓰고 입력 글자만 이어서 출력한다.
264
+ // A dumb terminal cannot erase lines, so write the prompt once and just append typed characters.
264
265
  const dumb = isDumb();
265
- let prevRows = 0; // 직전 프롬프트가 차지한 물리 행 수 — 긴 프롬프트는 여러 행으로 접힌다
266
+ let prevRows = 0; // physical rows the previous prompt occupied - long prompts wrap into several rows
266
267
  const prompt = () => {
267
268
  if (dumb) {
268
269
  stdout.write(`${S_Q} ${message} ${defaultValue ? `[${defaultValue}] ` : ""}`);
269
270
  return;
270
271
  }
271
- // 커서는 접힌 마지막 행에 있으므로 첫 행까지 올라간 뒤 아래를 전부 지운다
272
+ // The cursor is on the last wrapped row, so move up to the first row and erase everything below
272
273
  stdout.write(prevRows > 1 ? `\r${ESC}${prevRows - 1}A${ESC}0J` : `\r${ESC}0J`);
273
274
  const shown = buf.length ? buf : paint(defaultValue || "", c.dim);
274
275
  const line = `${S_Q} ${paint(message, c.bold)} ${shown}`;
@@ -289,7 +290,7 @@ export async function text({ message, defaultValue = "" }) {
289
290
  reject(new PromptAbortError(sig));
290
291
  });
291
292
 
292
- // stdin 종료(EOF) — 더 이상 입력이 올 수 없으므로 중단한다.
293
+ // stdin closed (EOF) - no more input can arrive, so abort.
293
294
  const onEnd = () => {
294
295
  cleanup();
295
296
  reject(new PromptAbortError());
@@ -310,7 +311,7 @@ export async function text({ message, defaultValue = "" }) {
310
311
  if (!dumb) prompt();
311
312
  return;
312
313
  }
313
- // 일반 문자 (제어문자 제외)
314
+ // Regular character (control characters excluded)
314
315
  if (str && !key.ctrl && !key.meta && str.length === 1 && str >= " ") {
315
316
  buf += str;
316
317
  if (dumb) stdout.write(str); else prompt();
@@ -323,18 +324,18 @@ export async function text({ message, defaultValue = "" }) {
323
324
  });
324
325
  }
325
326
 
326
- // ── Y/N 확인 (←→ 또는 y/n, Enter 확정) ──────────────────────────────
327
- // 반환: true/false 또는 CANCEL.
327
+ // ── Y/N confirm (left/right or y/n, Enter confirms) ──────────────────
328
+ // Returns: true/false or CANCEL.
328
329
  export async function confirm({ message, initialValue = true }) {
329
330
  if (!stdin.isTTY) return initialValue;
330
331
  const r = makeRenderer();
331
332
  let val = initialValue;
332
333
 
333
334
  const draw = () => {
334
- const yes = val ? paint("● 예", c.green) : paint("○ 예", c.dim);
335
- const no = !val ? paint("● 아니오", c.green) : paint("○ 아니오", c.dim);
335
+ const yes = val ? paint(`● ${t("ui.readline-engine.yes")}`, c.green) : paint(`○ ${t("ui.readline-engine.yes")}`, c.dim);
336
+ const no = !val ? paint(`● ${t("ui.readline-engine.no")}`, c.green) : paint(`○ ${t("ui.readline-engine.no")}`, c.dim);
336
337
  r.render([S_BAR, `${S_Q} ${paint(message, c.bold)}`, `${S_BAR} ${yes} ${no}`,
337
- paint(`└ ←/→ 또는 y/n · Enter 확정 · ESC 취소`, c.gray)]);
338
+ paint(t("ui.readline-engine.footer.confirm"), c.gray)]);
338
339
  };
339
340
 
340
341
  const result = await keySession(draw, (str, key) => {
@@ -346,15 +347,15 @@ export async function confirm({ message, initialValue = true }) {
346
347
  });
347
348
 
348
349
  if (result !== CANCEL) {
349
- r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(result ? "예" : "아니오", c.dim)}`]);
350
+ r.render([S_BAR, `${S_DONE} ${paint(message, c.dim)}`, `${S_BAR} ${paint(result ? t("ui.readline-engine.yes") : t("ui.readline-engine.no"), c.dim)}`]);
350
351
  }
351
352
  return result;
352
353
  }
353
354
 
354
- // ── 출력 헬퍼 (clack intro/outro/note/cancel 대체) ──────────────────
355
+ // ── Output helpers (replace clack intro/outro/note/cancel) ────────────
355
356
  export function intro(text) { stdout.write(`\n${paint("┌", c.gray)} ${paint(text, c.bold)}\n`); }
356
357
  export function outro(text) { stdout.write(`${paint("└", c.gray)} ${paint(text, c.green)}\n\n`); }
357
- export function cancelMessage(text = "취소했습니다.") { stdout.write(`${paint("■", c.yellow)} ${paint(text, c.yellow)}\n`); }
358
+ export function cancelMessage(text = t("ui.readline-engine.cancelled")) { stdout.write(`${paint("■", c.yellow)} ${paint(text, c.yellow)}\n`); }
358
359
  export function note(text, title = "") {
359
360
  const lines = String(text).split("\n");
360
361
  stdout.write(`${paint("○", c.cyan)} ${paint(title, c.bold)}\n`);
@@ -1,75 +1,81 @@
1
- // 첫 화면 상태 표시 층 — 감지 로그 · 분석 카드 · 신규/업데이트 판별
2
- // (Breaking Changes 박스는 core/breaking-check.js가 담당)
1
+ // First-screen status layer - detection log, analysis card, new-install/update detection
2
+ // (the Breaking Changes box is handled by core/breaking-check.js)
3
3
  import { A, paint } from "./ansi.js";
4
+ import { t } from "../i18n/index.js";
4
5
  import { DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
5
6
 
6
7
  const GUT = paint("│", A.gray);
7
8
  const HEAD = paint("◆", A.cyan);
8
9
  const OK = paint("✓", A.green);
9
10
 
10
- // 감지 로그 — 무엇을 근거로 어떤 타입을 감지했는지
11
- // markers: Map<type, 실제 발견 파일>.
12
- // warnings: 감지 도중 나온 경고. 감지 함수를 먼저 호출한 뒤 박스를 그리는 구조라 경고가
13
- // 박스 위로 새어나가 앞선 질문에 대한 경고처럼 보였다 — 박스 안에서 출력한다.
11
+ // Detection log - which type was detected and on what evidence
12
+ // markers: Map<type, file actually found>.
13
+ // warnings: warnings raised during detection. Detection runs before the box is drawn, so warnings
14
+ // used to leak above the box and look like they belonged to the previous question - print them inside the box.
14
15
  export function printDetectionLog({ types = [], version = "", branch = "", markers = new Map(), warnings = [] },
15
16
  out = (s) => process.stderr.write(s)) {
16
- out(`${paint("┌", A.gray)} 🔍 프로젝트를 살펴보는 중...\n`);
17
+ out(`${paint("┌", A.gray)} ${t("ui.status-cards.detect.title")}\n`);
17
18
  if (types.length && !(types.length === 1 && types[0] === "basic")) {
18
- for (const t of types) {
19
- const marker = markers.get(t);
20
- out(`${GUT} ${OK} ${marker ? `${marker} 발견 → ` : ""}${paint(t, A.bold)} 감지\n`);
19
+ for (const type of types) {
20
+ const marker = markers.get(type);
21
+ const found = marker
22
+ ? t("ui.status-cards.detect.foundWithMarker", { marker, type: paint(type, A.bold) })
23
+ : t("ui.status-cards.detect.found", { type: paint(type, A.bold) });
24
+ out(`${GUT} ${OK} ${found}\n`);
21
25
  }
22
26
  } else {
23
- out(`${GUT} ${paint("─", A.dim)} 마커 파일 없음 → ${paint("basic", A.bold)} (직접 선택 가능)\n`);
27
+ out(`${GUT} ${paint("─", A.dim)} ${t("ui.status-cards.detect.none", { type: paint("basic", A.bold) })}\n`);
24
28
  }
25
- out(`${GUT} ${OK} 버전: ${paint(`v${version}`, A.green)} · 브랜치: ${paint(branch, A.green)}\n`);
29
+ out(`${GUT} ${OK} ${t("ui.status-cards.detect.versionBranch", { version: paint(`v${version}`, A.green), branch: paint(branch, A.green) })}\n`);
26
30
  for (const w of warnings) out(`${GUT} ${paint(w, A.yellow)}\n`);
27
31
  out(`${GUT}\n`);
28
32
  }
29
33
 
30
- // 프로젝트 분석 개요 카드
34
+ // Project analysis overview card
31
35
  export function printAnalysisCard({ mode = "", modeLabel = "", types = [], version = "", branch = "",
32
36
  paths = new Map(), showOptional = false,
33
37
  flutter = null, envModeDefault = "", options = null },
34
38
  out = (s) => process.stderr.write(s)) {
35
- out(`${HEAD} ${paint("프로젝트 분석 결과", A.bold)}\n`);
36
- const row = (icon, label, value) => out(`${GUT} ${icon} ${label.padEnd(10)} ${value}\n`);
37
- row("📂", types.length > 1 ? "타입(멀티)" : "타입", paint(types.join(", ") || "basic", A.bold));
38
- row("🌙", "버전", paint(`v${version}`, A.green));
39
- row("🌿", "브랜치", branch);
40
- if (modeLabel || mode) row("💫", "통합 모드", modeLabel || mode);
39
+ out(`${HEAD} ${paint(t("ui.status-cards.card.title"), A.bold)}\n`);
40
+ // Label column width differs per language (English labels are longer than the Korean ones)
41
+ const labelWidth = Number(t("ui.status-cards.card.labelWidth"));
42
+ const row = (icon, label, value) => out(`${GUT} ${icon} ${label.padEnd(labelWidth)} ${value}\n`);
43
+ row("📂", types.length > 1 ? t("ui.status-cards.card.typeMulti") : t("ui.status-cards.card.type"), paint(types.join(", ") || "basic", A.bold));
44
+ row("🌙", t("ui.status-cards.card.version"), paint(`v${version}`, A.green));
45
+ row("🌿", t("ui.status-cards.card.branch"), branch);
46
+ if (modeLabel || mode) row("💫", t("ui.status-cards.card.mode"), modeLabel || mode);
41
47
  if (showOptional) {
42
- // Flutter 옵션 — 확정 직전 화면에서도 선택값을 보여준다.
48
+ // Flutter options - show the chosen values on the pre-confirmation screen too.
43
49
  if (flutter && types.includes("flutter")) {
44
50
  const stores = flutter.stores ?? [];
45
51
  const modeParts = stores.map(
46
52
  (p) => `${p}=${(p === "android" ? flutter.androidDeployMode : flutter.iosDeployMode) || DEFAULT_DEPLOY_MODE}`,
47
53
  );
48
- row("⚙️", "환경변수", flutter.envMode || envModeDefault);
49
- row("🏬", "스토어", stores.length ? stores.join(", ") : "없음");
50
- row("🚀", "배포모드", modeParts.length ? modeParts.join(" ") : "없음");
54
+ row("⚙️", t("ui.status-cards.card.envMode"), flutter.envMode || envModeDefault);
55
+ row("🏬", t("ui.status-cards.card.stores"), stores.length ? stores.join(", ") : t("ui.status-cards.card.none"));
56
+ row("🚀", t("ui.status-cards.card.deployMode"), modeParts.length ? modeParts.join(" ") : t("ui.status-cards.card.none"));
51
57
  }
52
58
  }
53
- // 선택 워크플로우 — 저장값이 있으면 질문 없이 쓰이므로 확정 전에 현재값을 보여준다.
59
+ // Optional workflows - a saved value is used without asking, so show the current value before confirmation.
54
60
  if (options) {
55
- const onOff = (v) => (v ? paint("켜짐", A.green) : paint("꺼짐", A.dim));
56
- row("🔢", "자동승격", onOff(options.semverAuto));
61
+ const onOff = (v) => (v ? paint(t("ui.status-cards.card.on"), A.green) : paint(t("ui.status-cards.card.off"), A.dim));
62
+ row("🔢", t("ui.status-cards.card.autoBump"), onOff(options.semverAuto));
57
63
  row("🤖", "Copilot", onOff(options.copilotAi));
58
64
  }
59
- // 모노레포 경로 — 루트가 아닌 항목이 하나라도 있으면 표시
65
+ // Monorepo paths - shown when at least one entry is not the root
60
66
  const nonRoot = [...paths.entries()].filter(([, p]) => p && p !== ".");
61
67
  if (nonRoot.length) {
62
- row("📁", "경로", [...paths.entries()].map(([t, p]) => `${t}→${p}`).join(", "));
68
+ row("📁", t("ui.status-cards.card.paths"), [...paths.entries()].map(([type, p]) => `${type}→${p}`).join(", "));
63
69
  }
64
70
  out(`${GUT}\n`);
65
71
  }
66
72
 
67
- // 신규 통합 vs 업데이트 판별 라인 (Breaking 박스는 breaking-check.js)
73
+ // New-install vs update line (the Breaking box is in breaking-check.js)
68
74
  export function printInstallKind({ currentTemplateVersion = "", templateVersion = "" }, out = (s) => process.stderr.write(s)) {
69
75
  if (currentTemplateVersion) {
70
- out(`${GUT} ♻️ ${paint("업데이트", A.bold)} — 템플릿 ${paint(`v${currentTemplateVersion}`, A.dim)} → ${paint(`v${templateVersion}`, A.green)}\n`);
76
+ out(`${GUT} ♻️ ${t("ui.status-cards.kind.update", { title: paint(t("ui.status-cards.kind.updateTitle"), A.bold), from: paint(`v${currentTemplateVersion}`, A.dim), to: paint(`v${templateVersion}`, A.green) })}\n`);
71
77
  } else {
72
- out(`${GUT} 🆕 ${paint("신규 통합", A.bold)} — 이 프로젝트에 처음 설치합니다 (템플릿 ${paint(`v${templateVersion}`, A.green)})\n`);
78
+ out(`${GUT} 🆕 ${t("ui.status-cards.kind.fresh", { title: paint(t("ui.status-cards.kind.freshTitle"), A.bold), version: paint(`v${templateVersion}`, A.green) })}\n`);
73
79
  }
74
80
  out(`${GUT}\n`);
75
81
  }