project-auto-wizard 0.13.2 → 0.14.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 (112) hide show
  1. package/README.md +1 -1
  2. package/bin/project-auto-wizard.js +18 -4
  3. package/package.json +2 -2
  4. package/payload/config/breaking-changes.json +28 -14
  5. package/payload/config/wizard-prompts.yml +102 -46
  6. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  7. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  8. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  9. package/payload/scripts/changelog_manager.py +296 -257
  10. package/payload/scripts/issue_helper.py +54 -50
  11. package/payload/scripts/messages.py +2279 -0
  12. package/payload/scripts/truncate_release_notes.py +7 -6
  13. package/payload/scripts/version_manager.py +64 -61
  14. package/payload/version.yml.template +2 -1
  15. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
  16. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
  17. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  18. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
  19. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
  20. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  29. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  30. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  31. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  32. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  33. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  34. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  35. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  36. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  37. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  38. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  39. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  40. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  41. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  42. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  44. package/src/cli/args.js +98 -52
  45. package/src/cli/help.js +8 -41
  46. package/src/commands/doctor.js +85 -89
  47. package/src/commands/dry-run.js +63 -43
  48. package/src/commands/full.js +79 -77
  49. package/src/commands/install-settings.js +16 -15
  50. package/src/commands/interactive-flutter.js +20 -19
  51. package/src/commands/interactive.js +135 -132
  52. package/src/commands/purge.js +47 -42
  53. package/src/commands/status.js +36 -36
  54. package/src/commands/uninstall.js +43 -41
  55. package/src/context.js +12 -11
  56. package/src/core/assets.js +12 -11
  57. package/src/core/baseline.js +27 -27
  58. package/src/core/branches.js +29 -27
  59. package/src/core/branding.js +7 -6
  60. package/src/core/breaking-check.js +22 -21
  61. package/src/core/breaking.js +15 -8
  62. package/src/core/copy/app-files.js +5 -4
  63. package/src/core/copy/flutter-app.js +8 -8
  64. package/src/core/copy/gitignore.js +58 -50
  65. package/src/core/copy/readme.js +45 -35
  66. package/src/core/copy/simple.js +16 -15
  67. package/src/core/copy/workflows.js +131 -124
  68. package/src/core/deploy-style.js +51 -48
  69. package/src/core/detect-fs.js +57 -57
  70. package/src/core/detect.js +67 -65
  71. package/src/core/errors.js +2 -2
  72. package/src/core/flutter-doctor.js +17 -16
  73. package/src/core/flutter-hooks.js +33 -32
  74. package/src/core/flutter-options.js +34 -33
  75. package/src/core/fsutil.js +12 -12
  76. package/src/core/installed-stores.js +4 -4
  77. package/src/core/logger.js +42 -40
  78. package/src/core/paths-resolve.js +88 -86
  79. package/src/core/paths.js +10 -10
  80. package/src/core/release-options.js +8 -8
  81. package/src/core/removal-exec.js +14 -13
  82. package/src/core/removal-plan.js +37 -37
  83. package/src/core/types.js +47 -47
  84. package/src/core/verify.js +44 -38
  85. package/src/core/version-yml.js +76 -68
  86. package/src/core/wizard-env.js +46 -46
  87. package/src/core/wizard-labels.js +63 -41
  88. package/src/i18n/catalog/en/cli.js +50 -0
  89. package/src/i18n/catalog/en/commands.js +187 -0
  90. package/src/i18n/catalog/en/copy.js +19 -0
  91. package/src/i18n/catalog/en/core.js +74 -0
  92. package/src/i18n/catalog/en/core2.js +15 -0
  93. package/src/i18n/catalog/en/ui.js +221 -0
  94. package/src/i18n/catalog/en.js +12 -0
  95. package/src/i18n/catalog/index.js +5 -0
  96. package/src/i18n/catalog/ko/cli.js +50 -0
  97. package/src/i18n/catalog/ko/commands.js +187 -0
  98. package/src/i18n/catalog/ko/copy.js +19 -0
  99. package/src/i18n/catalog/ko/core.js +74 -0
  100. package/src/i18n/catalog/ko/core2.js +15 -0
  101. package/src/i18n/catalog/ko/ui.js +221 -0
  102. package/src/i18n/catalog/ko.js +10 -0
  103. package/src/i18n/index.js +44 -0
  104. package/src/i18n/languages.js +9 -0
  105. package/src/index.js +128 -94
  106. package/src/ui/ansi.js +7 -7
  107. package/src/ui/banner.js +7 -6
  108. package/src/ui/env-plan.js +82 -81
  109. package/src/ui/prompts.js +97 -122
  110. package/src/ui/readline-engine.js +59 -58
  111. package/src/ui/status-cards.js +37 -31
  112. package/src/ui/summary.js +89 -88
package/src/index.js CHANGED
@@ -1,13 +1,14 @@
1
- // project-auto-wizard CLI 진입 파이프라인.
2
- // 감지 → payload 해석 → 모드 라우팅 → 통합 실행. 비대화형(--force) 우선.
3
- // 설치 자산은 전부 npm 패키지 동봉 payload/ (단일 진실). 자체 네트워크 요청은 없고,
4
- // 기본 브랜치 감지용 git 명령만 사용자 레포의 origin에 접속할 수 있다.
1
+ // project-auto-wizard CLI entry pipeline.
2
+ // detect -> resolve payload -> route by mode -> run the install. Non-interactive (--force) first.
3
+ // All installed assets come from the payload/ bundled in the npm package (single source of truth). The tool
4
+ // makes no network requests of its own; only the git command that detects the default branch may contact the repo's origin.
5
5
  import { join, dirname } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { readFileSync, existsSync } from "node:fs";
8
8
  import { createInterface } from "node:readline/promises";
9
9
  import { parseArgs, parsePathsCsv, CliError, TYPE_CLI_FLAGS } from "./cli/args.js";
10
- import { HELP_TEXT } from "./cli/help.js";
10
+ import { helpText } from "./cli/help.js";
11
+ import { resolveLanguage, setLanguage, t, DEFAULT_LANGUAGE, LANG_ENV_VAR, normalizeLanguage } from "./i18n/index.js";
11
12
  import { fallbackStyleTypes } from "./core/deploy-style.js";
12
13
  import { hooksFor, mergeHookResults } from "./core/types.js";
13
14
  import { PATHS } from "./core/paths.js";
@@ -34,7 +35,7 @@ import { runDoctor, printDoctorReport } from "./commands/doctor.js";
34
35
  import { planDryRun, printDryRun } from "./commands/dry-run.js";
35
36
  import { planPurge, executePurge, printPurgePlan, printPurgeResult } from "./commands/purge.js";
36
37
 
37
- // 패키지 버전 읽기 (-v/--version 출력용). src/../package.json.
38
+ // Read the package version (for -v/--version output) from src/../package.json.
38
39
  function readPkgVersion() {
39
40
  try {
40
41
  const here = dirname(fileURLToPath(import.meta.url));
@@ -45,34 +46,47 @@ function readPkgVersion() {
45
46
  }
46
47
  }
47
48
 
48
- // 결정적 UTC 타임스탬프 (주입 가능 — 테스트/골든용)
49
+ // Deterministic UTC timestamp (injectable for tests/goldens)
49
50
  function utcNow(date = new Date()) {
50
51
  const p = (n) => String(n).padStart(2, "0");
51
52
  const d = `${date.getUTCFullYear()}-${p(date.getUTCMonth() + 1)}-${p(date.getUTCDate())}`;
52
53
  const t = `${p(date.getUTCHours())}:${p(date.getUTCMinutes())}:${p(date.getUTCSeconds())}`;
53
- // ms: 로그 파일명이 같은 초의 연속 실행끼리 겹치지 않도록 쓴다 (now와 같은 시각에서 뽑는다).
54
+ // ms: keeps log file names from colliding between runs in the same second (taken from the same instant as now).
54
55
  return { now: `${d} ${t}`, today: d, ms: date.getUTCMilliseconds() };
55
56
  }
56
57
 
57
- // purge TTY 확인 — 실제 stdin에서 한 줄 입력을 받는다 (테스트는 promptRepoName 주입으로 대체).
58
+ // purge TTY confirmation: reads one line from the real stdin (tests replace it by injecting promptRepoName).
58
59
  async function defaultPromptRepoName(repoName) {
59
60
  const rl = createInterface({ input: process.stdin, output: process.stdout });
60
61
  try {
61
- return await rl.question(`purge를 실행하려면 정확히 이 레포명을 입력하세요: ${repoName}\n> `);
62
+ return await rl.question(t("cli.index.purgePrompt", { repoName }));
62
63
  } finally {
63
64
  rl.close();
64
65
  }
65
66
  }
66
67
 
67
- // run(argv, opts) → exitCode. opts: { cwd, payloadRoot?, clock?, exec?, promptRepoName? }
68
- // payloadRoot: 테스트 픽스처 주입점 (기본: 패키지 동봉 payload/)
69
- // clock: {now, today} 주입 (기본 현재 UTC).
70
- // exec/promptRepoName: purge 모드 안전장치 게이트용 주입점 (기본 실제 구현, 테스트는 mock 주입).
68
+ // Provisional language from argv/env so argument errors are already printed in the requested language.
69
+ // Mirrors how parseArgs reads `--lang X` (last valid value wins); invalid values are ignored silently here
70
+ // because parseArgs / resolveLanguage report them. The saved version.yml language only applies later.
71
+ function presetLanguage(argv) {
72
+ setLanguage(DEFAULT_LANGUAGE);
73
+ setLanguage(normalizeLanguage(process.env[LANG_ENV_VAR]));
74
+ argv.forEach((a, i) => {
75
+ if (a === "--lang") setLanguage(normalizeLanguage(argv[i + 1]));
76
+ else if (a.startsWith("--lang=")) setLanguage(normalizeLanguage(a.slice("--lang=".length)));
77
+ });
78
+ }
79
+
80
+ // run(argv, opts) -> exitCode. opts: { cwd, payloadRoot?, clock?, exec?, promptRepoName? }
81
+ // payloadRoot: test fixture injection point (default: payload/ bundled in the package)
82
+ // clock: inject {now, today} (default: current UTC).
83
+ // exec/promptRepoName: injection points for the purge safety gates (real implementations by default, mocks in tests).
71
84
  async function runInner(argv, {
72
85
  cwd = process.cwd(), payloadRoot, clock,
73
86
  exec = defaultExec, promptRepoName = defaultPromptRepoName,
74
87
  } = {}) {
75
88
  let opts;
89
+ presetLanguage(argv);
76
90
  try {
77
91
  opts = parseArgs(argv);
78
92
  } catch (e) {
@@ -80,98 +94,118 @@ async function runInner(argv, {
80
94
  throw e;
81
95
  }
82
96
  if (opts.showVersion) { console.log(readPkgVersion()); return 0; }
83
- if (opts.help) { console.log(HELP_TEXT); return 0; }
97
+ if (opts.help) { console.log(helpText()); return 0; }
98
+
99
+ // Language: --lang -> env var -> saved version.yml value -> en. Resolved after --help/--version
100
+ // so an invalid env var cannot block the help output.
101
+ let language;
102
+ try {
103
+ const savedVy = join(cwd, "version.yml");
104
+ const saved = existsSync(savedVy) ? parseExisting(readFileSync(savedVy, "utf8")).language : null;
105
+ language = resolveLanguage({ flag: opts.lang, env: process.env[LANG_ENV_VAR], saved });
106
+ // An existing install without a saved language now falls back to English: say so once, since
107
+ // its workflow messages switch from Korean on the next update.
108
+ const existingWithoutLanguage = existsSync(savedVy) && !saved;
109
+ const unspecified = !normalizeLanguage(opts.lang) && !normalizeLanguage(process.env[LANG_ENV_VAR]);
110
+ if (existingWithoutLanguage && unspecified && ["full", "interactive"].includes(opts.mode)) {
111
+ console.error(t("cli.lang.defaultNotice", {}, DEFAULT_LANGUAGE));
112
+ }
113
+ } catch (e) {
114
+ if (e instanceof CliError) { console.error(e.message); return 1; }
115
+ throw e;
116
+ }
117
+ setLanguage(language);
84
118
 
85
119
  const payload = assertPayload(payloadRoot ?? resolvePayloadRoot());
86
120
 
87
- // 시각은 여기서 한 번만 계산한다 — 로그 파일명과 설치 기록이 같은 값을 쓰도록.
121
+ // Compute the time once here so the log file name and the install record use the same value.
88
122
  const { now, today, ms } = clock || utcNow();
89
123
 
90
- // 로그는 인자 검증과 모드별 게이트를 모두 통과한 "실제로 파일을 바꾸는" 실행만 남긴다.
91
- // status/doctor(읽기 전용)·dry-run·거부된 실행은 대상 레포에 아무 파일도 만들지 않아야 한다.
124
+ // Only runs that pass argument validation and every mode gate and actually change files leave a log.
125
+ // status/doctor (read-only), dry-run and rejected runs must not create any file in the target repo.
92
126
  const startLog = (action) => initLogger(cwd, { action, now, ms, argv, templateVersion: readTemplateVersion() });
93
127
 
94
- // 대화형 모드 — 인자 없이 실행 or --mode interactive
128
+ // Interactive mode: run without arguments or with --mode interactive
95
129
  if (opts.mode === "interactive") {
96
- // --dry-run은 대화형 모드에서 조용히 무시되면 안 됨(실제 설치가 진행돼버림) — 명시 에러로 차단.
130
+ // --dry-run must not be silently ignored in interactive mode (a real install would proceed), so fail explicitly.
97
131
  if (opts.dryRun) {
98
- console.error("--dry-run은 --mode <full|uninstall>와 함께 사용하세요 (대화형 모드에서는 지원하지 않습니다).");
132
+ console.error(t("cli.index.dryRunNeedsMode"));
99
133
  return 1;
100
134
  }
101
135
  if (!process.stdout.isTTY) {
102
- console.error("대화형 입력이 불가능한 환경입니다. --mode <full|uninstall> 와 --force 를 지정하세요.");
136
+ console.error(t("cli.index.noTty"));
103
137
  return 1;
104
138
  }
105
- // 켜고 끄기 옵션은 대화형에서도 반영한다(해당 질문 생략). 나머지 설치 플래그는 대화형 질문이 정하므로
106
- // 조용히 무시하지 않고 알린다.
139
+ // On/off options also apply in interactive mode (their question is skipped). The remaining install flags
140
+ // are decided by the interactive questions, so report them instead of ignoring them silently.
107
141
  const ignored = [
108
142
  [opts.types.length, "--type"], [opts.version, "--project-version"], [opts.pathsCsv, "--paths"],
109
143
  [opts.mainBranch, "--main-branch"], [opts.developBranch, "--develop-branch"], [opts.deployStyle, "--deploy-style"],
110
144
  ...TYPE_CLI_FLAGS.map((f) => [opts[f.field], f.flag]),
111
145
  ].filter(([v]) => v).map(([, flag]) => flag);
112
146
  if (ignored.length) {
113
- console.error(`⚠️ 대화형 모드에서는 ${ignored.join(", ")}를 사용하지 않습니다 — 질문에서 고르거나 --mode full --force와 함께 쓰세요.`);
147
+ console.error(`⚠️ ${t("cli.index.ignoredFlags", { flags: ignored.join(", ") })}`);
114
148
  }
115
- // 로그 파일은 첫 기록 때 생기므로 메뉴에서 status/doctor만 보고 나가면 아무것도 남지 않는다.
149
+ // The log file is created on the first write, so viewing only status/doctor from the menu leaves nothing behind.
116
150
  startLog("install");
117
151
  return await runInteractive(
118
- { includeSemverAuto: opts.includeSemverAuto, includeCopilotAi: opts.includeCopilotAi },
152
+ { includeSemverAuto: opts.includeSemverAuto, includeCopilotAi: opts.includeCopilotAi, language },
119
153
  { cwd, payloadRoot: payload, clock },
120
154
  );
121
155
  }
122
156
 
123
- // purge 모드 — 마법사가 만든 모든 산출물을 지워 설치 이전 상태로 완전히 되돌린다.
124
- // 개발·테스트 전용 숨김 모드 — --help/대화형 메뉴에 노출하지 않는다.
157
+ // purge mode: removes everything the wizard created and restores the pre-install state.
158
+ // Hidden development/testing mode; not exposed in --help or the interactive menu.
125
159
  if (opts.mode === "purge") {
126
160
  if (!existsSync(join(cwd, ".git"))) {
127
- console.error("git 레포가 아닙니다(.git 없음) — purge는 git 레포 안에서만 실행할 수 있습니다.");
161
+ console.error(t("cli.index.purgeNotGit"));
128
162
  return 1;
129
163
  }
130
164
  const keepFlags = {
131
165
  versionYml: opts.keepVersionYml, readme: opts.keepReadme, changelog: opts.keepChangelog,
132
166
  workflows: opts.keepWorkflows, scripts: opts.keepScripts,
133
167
  };
134
- // version.yml은 여기서 미리 읽어둔다 — (a) executePurge()가 version.yml 자체를 지울 수 있어
135
- // 실행 이후에는 읽을 수 없고, (b) 아래 dry-run 예고 문구도 trunk-based 여부(develop === main)를
136
- // 알아야 실제 실행 시 삭제를 건너뛸지 미리 알릴 수 있기 때문에, 두 지점보다 앞서 읽어야 한다.
168
+ // Read version.yml up front: (a) executePurge() may delete version.yml itself, so it cannot be read
169
+ // afterwards, and (b) the dry-run notice below must know whether the repo is trunk-based (develop === main)
170
+ // to tell in advance whether the real run will skip the deletion. Both need it before this point.
137
171
  const vyPath = join(cwd, "version.yml");
138
172
  const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
139
173
  if (opts.dryRun) {
140
174
  printPurgePlan(planPurge(payload, cwd, keepFlags), { dryRun: true });
141
- // develop 브랜치 삭제는 plan에 포함되지 않으므로(git 상태는 실행 시점에만
142
- // 판단 가능) 별도로 예고하지 않으면 dry-run 미리보기가 유일한 파괴적 동작을 사용자에게 숨기게 된다.
175
+ // Deleting the develop branch is not part of the plan (git state can only be judged at run time), so
176
+ // without a separate notice the dry-run preview would hide the only destructive action from the user.
143
177
  if (opts.deleteDevelopBranch) {
144
178
  const developBranch = existing?.branches?.develop || "develop";
145
179
  if (existing?.branches?.main && developBranch === existing.branches.main) {
146
- console.log("(--delete-develop-branch 지정됨: trunk-based 구성(develop === main)이라 실제 실행 시에도 삭제를 건너뜁니다)");
180
+ console.log(t("cli.index.deleteDevelopSkipDry"));
147
181
  } else {
148
- console.log("(--delete-develop-branch 지정됨: 실제 실행 시 로컬 develop 브랜치도 삭제를 시도합니다)");
182
+ console.log(t("cli.index.deleteDevelopTryDry"));
149
183
  }
150
184
  }
151
185
  return 0;
152
186
  }
153
187
  if (!opts.yes) {
154
- console.error("--yes 없이는 purge를 실행할 수 없습니다 (--force로 대체할 수 없습니다).");
188
+ console.error(t("cli.index.purgeNeedsYes"));
155
189
  return 1;
156
190
  }
157
191
  const st = await exec("git", ["status", "--porcelain"], { cwd });
158
192
  if (st.code !== 0) {
159
- console.error("git 상태를 확인할 수 없습니다 — 안전을 위해 purge를 중단합니다.");
193
+ console.error(t("cli.index.purgeGitStatusFailed"));
160
194
  return 1;
161
195
  }
162
196
  if (!opts.allowDirty && st.stdout.trim() !== "") {
163
- console.error("작업트리에 커밋되지 않은 변경 사항이 있습니다 — purge 후 복구할 수 없습니다. 커밋하거나 --allow-dirty를 사용하세요.");
197
+ console.error(t("cli.index.purgeDirty"));
164
198
  return 1;
165
199
  }
166
200
  if (!opts.force) {
167
201
  if (!process.stdout.isTTY) {
168
- console.error("비대화형 환경에서는 --force 옵션이 필요합니다.");
202
+ console.error(t("cli.index.forceRequiredNonTty"));
169
203
  return 1;
170
204
  }
171
205
  const repoName = detectRepoName(cwd);
172
206
  const typed = await promptRepoName(repoName);
173
207
  if (typed !== repoName) {
174
- console.error("입력한 레포명이 일치하지 않습니다 — purge를 중단합니다.");
208
+ console.error(t("cli.index.purgeRepoMismatch"));
175
209
  return 1;
176
210
  }
177
211
  }
@@ -183,20 +217,20 @@ async function runInner(argv, {
183
217
  if (opts.deleteDevelopBranch) {
184
218
  const developBranch = existing?.branches?.develop || "develop";
185
219
  if (existing?.branches?.main && developBranch === existing.branches.main) {
186
- console.error("trunk-based 구성(develop === main)입니다 — 릴리스 브랜치 삭제는 건너뜁니다.");
220
+ console.error(t("cli.index.deleteDevelopSkip"));
187
221
  } else {
188
222
  const br = await exec("git", ["branch", "-d", developBranch], { cwd });
189
223
  if (br.code !== 0) {
190
- console.error(`⚠️ 로컬 '${developBranch}' 브랜치 삭제 실패 (${(br.stderr || "").trim() || "이유 확인 불가"}) — 수동으로 확인하세요.`);
224
+ console.error(`⚠️ ${t("cli.index.deleteDevelopFailed", { branch: developBranch, reason: (br.stderr || "").trim() || t("cli.index.reasonUnknown") })}`);
191
225
  } else {
192
- console.error(`로컬 '${developBranch}' 브랜치를 삭제했습니다.`);
226
+ console.error(t("cli.index.deleteDevelopDone", { branch: developBranch }));
193
227
  }
194
228
  }
195
229
  }
196
230
  return 0;
197
231
  }
198
232
 
199
- // uninstall 모드 — 설치물에 더해 README·gitignore·version.yml까지 선택적으로 제거.
233
+ // uninstall mode: removes the installed files and, optionally, the README section, gitignore entries and version.yml.
200
234
  if (opts.mode === "uninstall") {
201
235
  const safeSelection = {
202
236
  workflows: true, scripts: true,
@@ -210,50 +244,50 @@ async function runInner(argv, {
210
244
  startLog("uninstall");
211
245
  const r = runUninstall({}, payload, cwd, safeSelection);
212
246
  const removed = [
213
- `워크플로우 ${r.workflows.length}개`, `스크립트 ${r.scripts.length}개`,
214
- r.appFiles.length > 0 && `Flutter 앱 파일 ${r.appFiles.length}개`,
215
- r.readme && "README 버전 섹션",
216
- r.gitignore && ".gitignore 자동 추가 항목", r.versionYml && "version.yml",
247
+ t("cli.index.removedWorkflows", { n: r.workflows.length }), t("cli.index.removedScripts", { n: r.scripts.length }),
248
+ r.appFiles.length > 0 && t("cli.index.removedAppFiles", { n: r.appFiles.length }),
249
+ r.readme && t("cli.index.removedReadme"),
250
+ r.gitignore && t("cli.index.removedGitignore"), r.versionYml && "version.yml",
217
251
  ].filter(Boolean).join(", ");
218
- console.error(`제거됨 — ${removed}`);
252
+ console.error(t("cli.index.removed", { removed }));
219
253
  return 0;
220
254
  }
221
255
  if (!process.stdout.isTTY) {
222
- console.error("비대화형 환경에서는 --force 옵션이 필요합니다.");
256
+ console.error(t("cli.index.forceRequiredNonTty"));
223
257
  return 1;
224
258
  }
225
259
  startLog("uninstall");
226
- // --purge-* 플래그는 체크리스트 초기 선택으로 반영한다(조용히 무시하지 않는다).
260
+ // --purge-* flags become the initial checklist selection (never silently ignored).
227
261
  await runUninstallFlow(payload, cwd, prompts, {
228
262
  readme: opts.purgeReadme, gitignore: opts.purgeGitignore, versionYml: opts.purgeVersion,
229
263
  });
230
264
  return 0;
231
265
  }
232
266
 
233
- // status 모드 — 읽기 전용, TTY/--force 무관하게 항상 동작
267
+ // status mode: read-only, always works regardless of TTY/--force
234
268
  if (opts.mode === "status") {
235
269
  printStatus(runStatus(payload, cwd));
236
270
  return 0;
237
271
  }
238
- // doctor 모드 — 읽기 전용, TTY/--force 무관하게 항상 동작
272
+ // doctor mode: read-only, always works regardless of TTY/--force
239
273
  if (opts.mode === "doctor") {
240
274
  printDoctorReport(runDoctor(cwd));
241
275
  return 0;
242
276
  }
243
- // 명시 모드(full)인데 --force 없으면 TTY 여부와 무관하게 즉시 거부한다
244
- // (TTY에서 확인 없이 즉시 설치되던 결함 수정).
245
- // --dry-run은 파일을 쓰지 않으므로 --force 게이트를 우회한다 (status/doctor와 동일한 안전성).
277
+ // An explicit mode (full) without --force is rejected immediately, TTY or not
278
+ // (fixes a defect where a TTY run installed at once without confirmation).
279
+ // --dry-run writes no files, so it bypasses the --force gate (same safety as status/doctor).
246
280
  if (!opts.force && !opts.dryRun) {
247
- console.error("--force 없이는 이 모드를 실행할 수 없습니다 (확인 절차가 없습니다).");
281
+ console.error(t("cli.index.forceRequired"));
248
282
  return 1;
249
283
  }
250
284
 
251
- // 기존 version.yml 로드 — version/version_code/project_paths 보존의 단일 진실
285
+ // Load the existing version.yml: the single source of truth for preserving version/version_code/project_paths
252
286
  const vyPath = join(cwd, "version.yml");
253
287
  const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
254
288
 
255
- // 감지 (CLI 인자 우선, 없으면 자동 감지 — version.yml 우선 규칙은 detectTypes/detectVersion 내부)
256
- // --paths만 주고 --type을 생략한 모노레포도 그 타입으로 설치되도록 감지에 넘긴다.
289
+ // Detection (CLI args first, otherwise auto-detect; the version.yml-first rule lives inside detectTypes/detectVersion).
290
+ // Pass --paths to detection so a monorepo that gives only --paths and omits --type is installed with those types.
257
291
  let cliPaths;
258
292
  try {
259
293
  cliPaths = parsePathsCsv(opts.pathsCsv);
@@ -266,10 +300,10 @@ async function runInner(argv, {
266
300
  const detectWarnings = [];
267
301
  const branch = detectDefaultBranch(cwd, {
268
302
  warn: (m) => { detectWarnings.push(m); console.error(m); },
269
- hint: "다르면 --main-branch로 지정하세요.",
303
+ hint: t("cli.index.mainBranchHint"),
270
304
  });
271
305
  const repoName = detectRepoName(cwd);
272
- // 경로 확정 (비대화형 — --paths 우선 → 저장값 → 후보 1개 자동 → 에러)
306
+ // Confirm paths (non-interactive: --paths first -> saved value -> single candidate auto-picked -> error)
273
307
  let paths;
274
308
  try {
275
309
  paths = await resolveProjectPaths({
@@ -281,25 +315,25 @@ async function runInner(argv, {
281
315
  throw e;
282
316
  }
283
317
 
284
- // version: 기존 version.yml 최우선(SSoT — 재실행 시 덮어쓰기 방지) → CLI 지정 → 파일 감지
285
- // 비대화형이므로 폴백 안내는 CLI 문구(--project-version)를 그대로 쓴다.
286
- // 경로 확정 뒤에 감지해야 모노레포 하위 폴더의 버전·빌드 번호를 읽는다.
318
+ // version: existing version.yml first (SSoT; prevents overwriting on re-run) -> CLI value -> file detection.
319
+ // Non-interactive, so the fallback notice uses the CLI wording (--project-version) as is.
320
+ // Detect after paths are confirmed so a monorepo subfolder's version and build number are read.
287
321
  const version = resolveVersion({
288
322
  cwd, existing, explicit: opts.version,
289
323
  types, paths, warn: (m) => { detectWarnings.push(m); console.error(m); },
290
324
  });
291
325
  const versionCode = resolveVersionCode({ cwd, existing, types, paths });
292
326
 
293
- // 브랜치 구성 (--main-branch/--develop-branch → version.yml 저장값 → 감지 default → main/develop)
294
- // 이전 버전이 저장한 감지 실패 값("(unknown)" 등)은 저장값으로 인정하지 않는다 — 그대로 두면 재실행해도 복구되지 않는다.
327
+ // Branch config (--main-branch/--develop-branch -> saved version.yml value -> detected default -> main/develop).
328
+ // Failed-detection values saved by an earlier version ("(unknown)" etc.) are not accepted as saved values, otherwise re-running would never recover.
295
329
  const savedBranch = (b) => (isValidBranchName(b) ? b : "");
296
330
  const branches = resolveBranchConfig({
297
331
  mainBranch: opts.mainBranch || savedBranch(existing?.branches?.main),
298
332
  developBranch: opts.developBranch || savedBranch(existing?.branches?.develop),
299
333
  defaultBranch: branch,
300
334
  });
301
- // pr-flow에서 develop이 원격에 없으면 자동 생성+push (--force 비대화형 — 질문 없음).
302
- // 원격에 브랜치가 하나도 없으면(빈 원격·origin 없음) push할 기준이 없으므로 만들지 않고 안내한다.
335
+ // In pr-flow, if develop is missing on the remote, create and push it automatically (--force non-interactive, no question).
336
+ // If the remote has no branches at all (empty remote / no origin) there is nothing to base the push on, so notify instead of creating.
303
337
  let developMissing = false;
304
338
  if (branches.mode === "pr-flow" && !opts.dryRun) {
305
339
  const remoteBranches = await detectRemoteBranches(cwd);
@@ -315,41 +349,41 @@ async function runInner(argv, {
315
349
  }
316
350
  }
317
351
 
318
- // 타입 전용 옵션 — CLI 플래그 → version.yml 저장값 → 기본값을 타입 훅이 정한다. 해당 타입이 없으면 비어 있다.
319
- // (Flutter는 저장값이 없는 기존 설치의 스토어를 설치된 워크플로우로 추론한다 — 대화형과 같은 결론.)
352
+ // Type-specific options: the type hook decides CLI flag -> saved version.yml value -> default. Empty if the type is absent.
353
+ // (For an existing Flutter install without a saved value, the stores are inferred from the installed workflows, same as interactive.)
320
354
  const typeOptions = mergeHookResults(types, "resolveOptions", {
321
355
  opts, existing, workflowsDir: join(cwd, PATHS.workflowsDir),
322
356
  });
323
357
 
324
- // 고른 배포 방식 — 플래그 → 저장값 → 기본값. 실제 설치되는 방식으로의 정리는 컨텍스트 조립에서 한다.
358
+ // Chosen deploy style: flag -> saved value -> default. Normalizing to the style actually installed happens when the context is assembled.
325
359
  const chosenDeployStyle = resolveDeployStyle({ payload, types, explicit: opts.deployStyle, existing });
326
360
 
327
361
  const context = buildInstallContext({
328
362
  payload, existing, templateVersion: readTemplateVersion(), types, deployStyle: chosenDeployStyle,
329
363
  typeOptions,
330
- // 옵션: CLI 플래그 최우선 → version.yml 저장 옵션 → 기본값 (대화형과 같은 규칙)
364
+ // Options: CLI flag first -> saved version.yml option -> default (same rule as interactive)
331
365
  releaseOptions: resolveReleaseOptions({ semverAuto: opts.includeSemverAuto, copilotAi: opts.includeCopilotAi }, existing),
332
366
  mode: opts.mode, force: opts.force, version, versionCode, branch,
333
367
  branches,
334
368
  paths,
335
369
  repoName,
336
- // @wizard ask/auto 토큰 값을 계산하는 resolver
370
+ // resolvers that compute @wizard ask/auto token values
337
371
  resolvers: makeResolvers(cwd, repoName, paths, typeOptions),
338
- now, today,
339
- // 설치 로그용 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
372
+ now, today, language,
373
+ // Extra context for the install log; does not change the install itself.
340
374
  markers: detectMarkers(cwd, types), detectWarnings,
341
375
  });
342
376
 
343
- // 무중단(nginx·traefik) 워크플로우가 없는 타입은 단일 서버 배포로 설치한다 — 조용히 넘어가지 않게 알린다.
377
+ // Types without a zero-downtime (nginx/traefik) workflow are installed with the single-server deploy; say so instead of passing silently.
344
378
  const fallbackTypes = fallbackStyleTypes(payload, types, chosenDeployStyle);
345
379
  if (fallbackTypes.length) {
346
- console.error(`⚠️ ${fallbackTypes.join(", ")}에는 ${chosenDeployStyle} 무중단 배포 워크플로우가 없어 단일 서버 배포(simple)로 설치합니다.`);
380
+ console.error(`⚠️ ${t("cli.index.deployFallback", { types: fallbackTypes.join(", "), style: chosenDeployStyle })}`);
347
381
  }
348
382
 
349
- // 비대화형 축약 배너 (1줄, 로그 오염 최소)
383
+ // Compact non-interactive banner (one line, minimal log noise)
350
384
  printBannerCompact({ version: context.templateVersion, mode: opts.mode });
351
385
 
352
- // Breaking Changes 게이트 (비대화형은 경고 후 진행)
386
+ // Breaking Changes gate (non-interactive warns, then proceeds)
353
387
  const proceed = await runBreakingCheck({ cwd, payloadRoot: payload, templateVersion: context.templateVersion });
354
388
  if (!proceed) return 0;
355
389
 
@@ -359,12 +393,12 @@ async function runInner(argv, {
359
393
  }
360
394
 
361
395
  startLog("install");
362
- // opts.mode는 parseArgs()에서 화이트리스트 검증을 통과했고, interactive/purge/uninstall/status/doctor는
363
- // 전부 위에서 조기 반환했으므로 이 시점엔 full 하나로 보장된다 (default 분기 제거,
364
- // 부분 설치 모드 제거로 분기 자체가 사라졌다).
396
+ // opts.mode passed the whitelist check in parseArgs(), and interactive/purge/uninstall/status/doctor all
397
+ // returned early above, so it is guaranteed to be full here (the default branch is gone along with the
398
+ // removed partial-install modes).
365
399
  const result = runFull(context, payload, cwd);
366
400
 
367
- // 완료 요약 (CLI 모드에서도 출력)
401
+ // Completion summary (also printed in CLI mode)
368
402
  printSummary({
369
403
  mode: opts.mode, types, version, versionCode, branches, developMissing,
370
404
  copiedFiles: result?.workflows?.copiedFiles ?? [],
@@ -382,8 +416,8 @@ async function runInner(argv, {
382
416
  scripts: result?.scripts ?? null,
383
417
  });
384
418
  for (const n of postInstallNotices(result)) console.error(n.startsWith(" ") ? n : `⚠️ ${n}`);
385
- // store_submit 배포 모드는 main push마다 심사를 자동 제출한다 — 비대화형에서도 같은 경고를 보여준다
386
- // (대화형 경로는 ui/prompts.js#deployModeWarning을 선택 시점에 note로 보여준다). 타입 훅이 해당 타입에만 낸다.
419
+ // The store_submit deploy mode auto-submits for review on every main push, so show the same warning non-interactively
420
+ // (the interactive path shows ui/prompts.js#deployModeWarning as a note at selection time). Only the matching type hook emits it.
387
421
  for (const { hook } of hooksFor(types, "installNotices")) {
388
422
  for (const w of hook(typeOptions)) {
389
423
  if (w) console.error(`⚠️ ${w}`);
@@ -392,20 +426,20 @@ async function runInner(argv, {
392
426
  return 0;
393
427
  }
394
428
 
395
- // 공개 진입점 — 어떤 경로로 끝나든(정상 반환·CliError·예외) 로거를 닫는다.
396
- // 본문을 통째로 try로 감싸면 들여쓰기가 전부 바뀌므로 얇은 래퍼로 분리했다.
429
+ // Public entry point: closes the logger however the run ends (normal return, CliError, exception).
430
+ // A thin wrapper, since wrapping the whole body in try would re-indent all of it.
397
431
  export async function run(argv, opts = {}) {
398
432
  try {
399
433
  return await runInner(argv, opts);
400
434
  } catch (e) {
401
- // Ctrl+C/EOF는 어느 질문에서든 즉시 중단한다 — 설치 파일을 쓰기 전에 빠져나오고, 셸 관례대로 130을 돌려준다.
435
+ // Ctrl+C/EOF aborts immediately at any question: exit before writing install files and return 130 by shell convention.
402
436
  if (isPromptAbort(e)) {
403
- prompts.cancelMessage("중단했습니다 — 변경 없이 종료합니다.");
437
+ prompts.cancelMessage(t("cli.index.aborted"));
404
438
  return e.signal === "SIGTERM" ? 143 : 130;
405
439
  }
406
- // 이미 기록을 시작한 실행이 도중에 죽으면 헤더만 남은 로그로는 원인을 알 수 없다 — 사유를 남긴다.
440
+ // If a run that already started logging dies midway, a header-only log hides the cause, so record the reason.
407
441
  if (currentLogPath()) log.fail("run", "error", e?.message || String(e));
408
- // 사용자가 고칠 수 있는 실패(권한 등)는 스택트레이스 대신 읽을 수 있는 문구로 끝낸다.
442
+ // Failures the user can fix (permissions, etc.) end with a readable message instead of a stack trace.
409
443
  if (e instanceof CliError) { console.error(e.message); return 1; }
410
444
  throw e;
411
445
  } finally {
package/src/ui/ansi.js CHANGED
@@ -1,4 +1,4 @@
1
- // 공용 ANSI 헬퍼 — banner/status-cards/summary가 공유 (readline-engine 내부 헬퍼와 독립, 의존성 0)
1
+ // Shared ANSI helpers used by banner/status-cards/summary (independent of readline-engine's own helpers, zero dependencies)
2
2
  const E = "\x1b[";
3
3
  export const A = {
4
4
  reset: `${E}0m`,
@@ -12,10 +12,10 @@ export const A = {
12
12
  gray: `${E}90m`,
13
13
  };
14
14
 
15
- // NO_COLOR(https://no-color.org) 환경변수 또는 대상 스트림이 TTY가 아니면 색상을 끈다.
16
- // no-color.org 규격상 NO_COLOR는 "값과 무관하게 존재 여부"만 본다 — NO_COLOR=""(빈 문자열)도
17
- // "설정됨"으로 취급해야 하므로 truthy 체크(`!process.env.NO_COLOR`)가 아니라 존재 체크를 쓴다.
18
- // TERM=dumb(Emacs shell·일부 CI 의사 TTY 등)은 ANSI 시퀀스를 해석하지 못해 그대로 찍힌다 — 색상도 끈다.
15
+ // Disable color when NO_COLOR (https://no-color.org) is set or the target stream is not a TTY.
16
+ // Per the no-color.org spec only the presence of NO_COLOR matters, not its value: NO_COLOR="" (empty)
17
+ // counts as set, so we check presence instead of truthiness (`!process.env.NO_COLOR`).
18
+ // TERM=dumb (Emacs shell, some CI pseudo-TTYs) cannot interpret ANSI sequences and prints them raw, so color is off too.
19
19
  export const isDumbTerminal = () => process.env.TERM === "dumb";
20
20
  export function colorEnabled(stream = process.stdout) {
21
21
  return process.env.NO_COLOR === undefined && !!stream.isTTY && !isDumbTerminal();
@@ -23,13 +23,13 @@ export function colorEnabled(stream = process.stdout) {
23
23
 
24
24
  export const paint = (s, color, enabled = colorEnabled()) => (enabled ? `${color}${s}${A.reset}` : String(s));
25
25
 
26
- // 대략적 표시 폭 (CJK 2칸 · ANSI 시퀀스 0칸) — 박스 우변 정렬용
26
+ // Approximate display width (CJK 2 columns, ANSI sequences 0) - used to align the right edge of boxes
27
27
  export function visualWidth(s) {
28
28
  const plain = String(s).replace(/\x1b\[[0-9;]*m/g, "");
29
29
  let w = 0;
30
30
  for (const ch of plain) {
31
31
  const cp = ch.codePointAt(0);
32
- // 한글·CJK·이모지 대략 2칸 (터미널 관례)
32
+ // Hangul/CJK/emoji take roughly 2 columns (terminal convention)
33
33
  w += (cp >= 0x1100 && (cp <= 0x115f || (cp >= 0x2e80 && cp <= 0xa4cf) || (cp >= 0xac00 && cp <= 0xd7a3)
34
34
  || (cp >= 0xf900 && cp <= 0xfaff) || (cp >= 0xff00 && cp <= 0xff60) || cp >= 0x1f300)) ? 2 : 1;
35
35
  }
package/src/ui/banner.js CHANGED
@@ -1,17 +1,18 @@
1
- // 첫 화면 배너 — 클래식 박스형
1
+ // First-screen banner - classic boxed style
2
2
  import { A, paint, visualWidth } from "./ansi.js";
3
+ import { t } from "../i18n/index.js";
3
4
 
4
- const INNER = 56; // 박스 내부 폭
5
+ const INNER = 56; // inner box width
5
6
 
6
7
  function boxLine(out, content = "") {
7
8
  const pad = Math.max(0, INNER - visualWidth(content));
8
9
  out(paint("║", A.cyan) + content + " ".repeat(pad) + paint("║", A.cyan) + "\n");
9
10
  }
10
11
 
11
- // 대화형 첫 화면 배너 — 박스 타이틀 + 메타 4줄
12
+ // Interactive first-screen banner - boxed title + meta lines
12
13
  export function printBanner({ version, modeLabel }, out = (s) => process.stderr.write(s), columns = process.stderr.columns) {
13
14
  out("\n");
14
- // 상자보다 좁은 터미널에서는 테두리가 접혀 모양이 무너지므로 상자 없이 제목만 찍는다.
15
+ // On terminals narrower than the box the border wraps and breaks the shape, so print just the title.
15
16
  if (columns && columns < INNER + 2) {
16
17
  out(`${paint("✦", A.yellow)} ${paint("PROJECT AUTO WIZARD", A.bold)}\n`);
17
18
  } else {
@@ -27,7 +28,7 @@ export function printBanner({ version, modeLabel }, out = (s) => process.stderr.
27
28
  out("\n");
28
29
  }
29
30
 
30
- // 비대화형(--force/CI) 축약 배너 — 1줄 (사용자 확정: 로그 오염 최소 + 버전 추적)
31
+ // Compact banner for non-interactive (--force/CI) runs - one line (minimal log noise, keeps the version traceable)
31
32
  export function printBannerCompact({ version, mode }, out = (s) => process.stderr.write(s)) {
32
- out(`${paint("✦", A.yellow)} ${paint("project-auto-wizard", A.bold)} v${version} — ${mode} 모드 (--force)\n`);
33
+ out(`${paint("✦", A.yellow)} ${paint("project-auto-wizard", A.bold)} v${version} — ${t("ui.banner.compact", { mode })}\n`);
33
34
  }