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
@@ -1,5 +1,5 @@
1
- // @wizard env 계획 질문 UI — ask 키 수집, 필드 카드 출력, 전부 기본값/일부만 변경 선택.
2
- // io 주입식 — 테스트는 {select, multiselect, text} 스텁을 넘긴다. 기본은 readline-engine 실물.
1
+ // @wizard env plan question UI - collects ask keys, prints field cards, lets the user keep all defaults or change some.
2
+ // io is injected - tests pass {select, multiselect, text} stubs. The default is the real readline-engine.
3
3
  import { join } from "node:path";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { stdin, stderr } from "node:process";
@@ -10,14 +10,15 @@ import { loadWizardPrompts, wfField, workflowDisplayName } from "../core/wizard-
10
10
  import { deployFilter, payloadWorkflowNames, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
11
11
  import { buildTypeRootFilter, readSavedDeployValues } from "../core/copy/workflows.js";
12
12
  import * as engine from "./readline-engine.js";
13
+ import { t } from "../i18n/index.js";
13
14
 
14
15
  const CANCEL = engine.CANCEL;
15
16
 
16
- // 진행 안내는 stderr로 출력 (stdout 파이프 오염 방지).
17
+ // Progress output goes to stderr (keeps stdout pipes clean).
17
18
  const defaultLog = (s = "") => stderr.write(s + "\n");
18
19
 
19
- // 사용처 문자열 조립.
20
- // usages: [{type, workflowName}] — 타입이 여러 개면 타입만 "t1·t2", 하나면 "타입 name1·name2".
20
+ // Build the usage string.
21
+ // usages: [{type, workflowName}] - with several types only "t1·t2", with one type "type name1·name2".
21
22
  export function scopeString(usages = []) {
22
23
  const types = []; const names = [];
23
24
  for (const { type, workflowName } of usages) {
@@ -28,19 +29,19 @@ export function scopeString(usages = []) {
28
29
  return `${types.join("·")} ${names.join("·")}`.trim();
29
30
  }
30
31
 
31
- // ask KEY 수집 — 실제 설치되는 워크플로우와 같은 소스를 스캔한다.
32
- // payloadRoot: 패키지 payload/ 루트. types: 설치 대상 타입 목록.
32
+ // Collect ask KEYs - scans the same sources as the workflows that actually get installed.
33
+ // payloadRoot: package payload/ root. types: target types to install.
33
34
  // opts:
34
- // resolvers - @접두 기본값(@repo 등) 해석용 (수집 시점에 해석)
35
- // flutterStore - Flutter 스토어 대상(string[]|null). 배열이면 선택 해제된 스토어 워크플로우는 스캔에서 제외 (null=현행)
36
- // prompts - wizard-labels 파싱 객체 (워크플로우 표시명용, null이면 확장자 제거 폴백)
37
- // saved - Map<type, Map<key,value>>: version.yml deploy 블록 저장값. 있으면 그 값을 기본값으로 보여준다
38
- // (업데이트 때 "기본값 그대로"를 고르면 설치 때 답한 값이 유지되어야 한다)
39
- // 반환: { keys:[], defaults:Map<key,default>, typeDefaults:Map<"type|key",default>,
35
+ // resolvers - resolves @-prefixed defaults (@repo etc.) at collection time
36
+ // flutterStore - Flutter store targets (string[]|null). An array excludes workflows of deselected stores from the scan (null = current behaviour)
37
+ // prompts - parsed wizard-labels object (for workflow display names; falls back to the extension-stripped name when null)
38
+ // saved - Map<type, Map<key,value>>: values saved in the version.yml deploy block. When present they are shown as defaults
39
+ // (choosing "keep defaults" on an update must preserve the values answered at install time)
40
+ // Returns: { keys:[], defaults:Map<key,default>, typeDefaults:Map<"type|key",default>,
40
41
  // usages:Map<key,[{type,workflowName}]> }
41
42
  export function collectAsks(payloadRoot, types = [], opts = {}) {
42
43
  const { resolvers = {}, deployStyle = "", flutterStore = null, prompts = null, saved = new Map() } = opts;
43
- // 설치하지 않을 배포 워크플로우의 질문까지 묻지 않는다 — 질문 수는 설치 범위를 따라간다.
44
+ // Do not ask about deploy workflows that will not be installed - the number of questions follows the install scope.
44
45
  const available = payloadWorkflowNames(payloadRoot);
45
46
  const keepDeploy = deployFilter(deployStyle, available);
46
47
  const baseDir = join(payloadRoot, PAYLOAD.workflowsDir);
@@ -49,18 +50,18 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
49
50
  const typeDefaults = new Map();
50
51
  const usages = new Map();
51
52
 
52
- // 스캔 단위: [타입, 폴더]. common/ 최상위는 타입 선택과 무관하게 항상 설치되므로(복사 엔진과
53
- // 동일 규칙) 무조건 스캔한다.
53
+ // Scan units: [type, folder]. The top level of common/ is always installed regardless of the chosen types
54
+ // (same rule as the copy engine), so it is always scanned.
54
55
  const units = [];
55
56
  const commonDir = join(baseDir, "common");
56
57
  if (exists(commonDir)) units.push(["common", commonDir, null]);
57
58
  for (const type of types) {
58
59
  const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
59
60
  if (!exists(typeDir)) continue;
60
- // 복사 엔진과 동일한 폴더 구성: 타입 직하위 + ("배포 안 함"이 아닐 때만) server-deploy.
61
- // go/python·react/next처럼 서버 배포 워크플로우가 타입 루트에 바로 있는 타입도 같은 배포 방식
62
- // 필터로 거른다 — "배포 안 함"이면 CD·PR 프리뷰 전용 ask 키(DEPLOY_PORT, SSH_AUTH_METHOD 등)를 묻지 않는다.
63
- // Flutter는 선택 해제된 스토어 워크플로우(PLAYSTORE·TESTFLIGHT)도 같은 필터로 걸러 질문 범위가 설치 범위와 같다.
61
+ // Same folder layout as the copy engine: type root + server-deploy (only when deploy is not "none").
62
+ // Types such as go/python and react/next keep server deploy workflows directly in the type root; they go
63
+ // through the same deploy-style filter - with "no deploy", CD/PR-preview ask keys (DEPLOY_PORT, SSH_AUTH_METHOD, ...) are not asked.
64
+ // Flutter also filters out deselected store workflows (PLAYSTORE, TESTFLIGHT) so the question scope equals the install scope.
64
65
  units.push([type, typeDir, buildTypeRootFilter(type, deployStyle, flutterStore, available)]);
65
66
  if (deployStyle !== NO_DEPLOY_STYLE) {
66
67
  units.push([type, serverDeployDir, keepDeploy]);
@@ -77,13 +78,13 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
77
78
  for (const line of content.split(/\r?\n/)) {
78
79
  const p = parseWizardLine(line);
79
80
  if (!p || p.action !== "ask") continue;
80
- // 타입별 기본값: @접두면 resolver 해석, 아니면 리터럴
81
+ // Per-type default: resolved via resolver when @-prefixed, otherwise a literal
81
82
  const rawDefault = p.arg.startsWith("@")
82
83
  ? resolveToken(p.arg.slice(1), type, resolvers)
83
84
  : p.arg;
84
- // 리터럴 기본값 안에 __PROJECT_NAME__ 등이 박혀 있으면실제 repoName으로
85
- // 풀어준다 — substituteEnv()가 설치 파일에 적용하는 것과 동일한 치환이라야 마법사
86
- // 화면 표시와 실제 설치 결과가 어긋나지 않는다.
85
+ // If a literal default embeds __PROJECT_NAME__ etc., expand it to the real repoName -
86
+ // it must be the same substitution substituteEnv() applies to installed files, otherwise the
87
+ // wizard display and the actual install result would diverge.
87
88
  const savedValue = saved.get(type)?.get(p.key);
88
89
  const typeDefault = savedValue != null && savedValue !== ""
89
90
  ? savedValue
@@ -99,14 +100,14 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
99
100
  return { keys, defaults, typeDefaults, usages };
100
101
  }
101
102
 
102
- // KEY가 처음 등장한 type — 라벨 조회 시 타입 오버라이드 우선순위용
103
+ // The type where KEY first appears - used for type-override priority when looking up labels
103
104
  function firstTypeFor(usages, key) {
104
105
  return usages.get(key)?.[0]?.type ?? "";
105
106
  }
106
107
 
107
- // 최종 답변 목록 — 완료 요약과 설치 로그가 같은 데이터를 쓰도록 여기서 만든다.
108
- // isDefault는 "기본값 그대로인가"다. 나중에 배포가 안 될 때 제일 먼저 확인하게 되는 정보라
109
- // 값만 남기면 부족하다.
108
+ // Final answer list - built here so the completion summary and the install log share the same data.
109
+ // isDefault means "left at the default". It is the first thing checked when a deploy later fails,
110
+ // so keeping only the value is not enough.
110
111
  function buildAnswers(prompts, asks, values, useDefaults) {
111
112
  return asks.keys.map((key) => {
112
113
  const def = asks.defaults.get(key) ?? "";
@@ -121,54 +122,54 @@ function buildAnswers(prompts, asks, values, useDefaults) {
121
122
  });
122
123
  }
123
124
 
124
- // KEY 1개를 'label·사용처·설명·예시·기본값' 카드로 출력.
125
- // info: { default, usages } — idx/tot 있으면 "(i/t)" 진행 표시. log 주입 가능(테스트 무음화).
125
+ // Print one KEY as a 'label, usage, description, example, default' card.
126
+ // info: { default, usages } - with idx/tot a "(i/t)" progress marker is shown. log is injectable (silences tests).
126
127
  export function printFieldCard(prompts, key, info, idx = null, tot = null, log = defaultLog) {
127
- const t = info.usages?.[0]?.type ?? "";
128
- const label = wfField(prompts, t, key, "label");
129
- const help = wfField(prompts, t, key, "help");
130
- const ex = wfField(prompts, t, key, "example");
128
+ const type = info.usages?.[0]?.type ?? "";
129
+ const label = wfField(prompts, type, key, "label");
130
+ const help = wfField(prompts, type, key, "help");
131
+ const ex = wfField(prompts, type, key, "example");
131
132
  const scope = scopeString(info.usages || []);
132
133
  const head = idx != null && tot != null
133
134
  ? ` ▸ (${idx}/${tot}) ${label} [${scope}]`
134
135
  : ` ▸ ${label} [${scope}]`;
135
136
  log(head);
136
137
  if (help) log(` ${help}`);
137
- if (ex) log(` 예) ${ex}`);
138
- log(` 기본값: ${info.default ?? ""}`);
138
+ if (ex) log(t("ui.env-plan.card.example", { example: ex }));
139
+ log(t("ui.env-plan.card.default", { value: info.default ?? "" }));
139
140
  log("");
140
141
  }
141
142
 
142
- // ask 필드의 기본값이 정확히 "true"/"false"면 boolean 필드로 간주한다.
143
- // 마커 문법(@wizard ask:...)을 바꾸지 않고 리터럴 값 형태만으로 판단 — 별도 타입 표기가 필요 없다.
143
+ // An ask field whose default is exactly "true"/"false" is treated as a boolean field.
144
+ // Decided from the literal value alone without changing the marker syntax (@wizard ask:...) - no separate type annotation needed.
144
145
  function isBooleanDefault(value) {
145
146
  return value === "true" || value === "false";
146
147
  }
147
148
 
148
- // 형식이 정해진 ask 값 검증 — 잘못된 값은 설치는 통과하고 배포 단계에서야 실패하므로 입력 시점에 다시 묻는다.
149
- // 반환: 오류 문구(문제 없으면 "").
149
+ // Validate ask values with a fixed format - a bad value passes install and only fails at deploy time, so re-ask at input time.
150
+ // Returns: the error message ("" when fine).
150
151
  export function validateAskValue(key, value) {
151
152
  const v = String(value);
152
153
  if (/_PORT$/.test(key)) {
153
154
  const n = Number(v);
154
- return /^\d+$/.test(v) && n >= 1 && n <= 65535 ? "" : "1~65535 사이의 숫자로 입력하세요.";
155
+ return /^\d+$/.test(v) && n >= 1 && n <= 65535 ? "" : t("ui.env-plan.validate.port");
155
156
  }
156
157
  if (key === "SSH_AUTH_METHOD") {
157
- return v === "password" || v === "key" ? "" : "password 또는 key 중 하나로 입력하세요.";
158
+ return v === "password" || v === "key" ? "" : t("ui.env-plan.validate.sshAuth");
158
159
  }
159
160
  if (key === "JAVA_VERSION") {
160
- return /^\d+(\.\d+)*$/.test(v) ? "" : "JDK 버전 숫자로 입력하세요 (예: 21, 17).";
161
+ return /^\d+(\.\d+)*$/.test(v) ? "" : t("ui.env-plan.validate.javaVersion");
161
162
  }
162
163
  return "";
163
164
  }
164
165
 
165
- // 지정 KEY들을 하나씩 입력받아 values에 기록.
166
- // 빈 입력(Enter)/ESC → KEY 공통 기본값 유지.
166
+ // Prompt for the given KEYs one by one and record them in values.
167
+ // Empty input (Enter) / ESC keeps the KEY's shared default.
167
168
  async function promptEach(io, prompts, asks, todoKeys, values, log) {
168
169
  const tot = todoKeys.length;
169
170
  if (tot === 0) return;
170
171
  log("");
171
- log(" 값을 입력하세요. 그대로 두려면 아무것도 입력하지 말고 Enter를 누르면 기본값이 적용됩니다.");
172
+ log(t("ui.env-plan.each.intro"));
172
173
  log("");
173
174
  let i = 0;
174
175
  for (const key of todoKeys) {
@@ -178,36 +179,36 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
178
179
  const label = wfField(prompts, firstTypeFor(asks.usages, key), key, "label");
179
180
  let input;
180
181
  if (isBooleanDefault(def)) {
181
- const answer = await io.confirm({ message: `↳ ${label} — 활성화할까요?`, initialValue: def === "true" });
182
+ const answer = await io.confirm({ message: t("ui.env-plan.each.enable", { label }), initialValue: def === "true" });
182
183
  input = answer === CANCEL ? def : (answer ? "true" : "false");
183
184
  } else {
184
185
  for (;;) {
185
- input = await io.text({ message: `↳ 값 입력 (Enter=기본값 «${def}» 유지):`, defaultValue: def });
186
+ input = await io.text({ message: t("ui.env-plan.each.input", { def }), defaultValue: def });
186
187
  if (input === CANCEL || input == null) { input = def; break; }
187
188
  input = String(input).trim();
188
189
  if (input === "") { input = def; break; }
189
190
  const problem = validateAskValue(key, input);
190
191
  if (!problem) break;
191
- log(` ⚠️ '${input}' — ${problem}`);
192
+ log(t("ui.env-plan.each.invalid", { input, problem }));
192
193
  }
193
194
  }
194
195
  values.set(key, input);
195
- log(` → ${label} = ${input}`);
196
+ log(t("ui.env-plan.each.result", { label, input }));
196
197
  log("");
197
198
  }
198
199
  }
199
200
 
200
- // 배포 env 설정 계획.
201
- // 반환: { values: Map<key,value>, useDefaults: boolean, answers: [{key,label,value,isDefault,scope}] }
202
- // - useDefaults=true → 호출부는 substituteEnv에 그대로 넘기면 타입별 기본값 경로
203
- // - useDefaults=false → values에 담긴 키만 사용자 확정값으로 치환, 나머지는 기본값
204
- // (⚠️ substituteEnv는 useDefaults=false일 때만 values를 참조하므로 이 플래그를 반드시 함께 전달)
205
- // 인자:
206
- // payloadRoot/types/resolvers/flutterStore — collectAsks와 동일 의미
207
- // targetRoot — wizard-prompts.yml 1차 탐색 위치(기본 ".")
208
- // force — true면 질문 없이 전부 기본값
209
- // io — {select, multiselect, text} 주입 (기본 readline-engine). 테스트 스텁 지점.
210
- // log — 카드·안내 출력 함수 주입 (기본 stderr)
201
+ // Deploy env setup plan.
202
+ // Returns: { values: Map<key,value>, useDefaults: boolean, answers: [{key,label,value,isDefault,scope}] }
203
+ // - useDefaults=true -> the caller passes it straight to substituteEnv, taking the per-type default path
204
+ // - useDefaults=false -> only keys in values are replaced with user-confirmed values, the rest use defaults
205
+ // (WARNING: substituteEnv reads values only when useDefaults=false, so always pass this flag along)
206
+ // Arguments:
207
+ // payloadRoot/types/resolvers/flutterStore - same meaning as collectAsks
208
+ // targetRoot - first place to look for wizard-prompts.yml (default ".")
209
+ // force - when true, take all defaults without asking
210
+ // io - injected {select, multiselect, text} (default readline-engine). Test stub point.
211
+ // log - injected card/notice output function (default stderr)
211
212
  export async function promptEnvPlan({
212
213
  payloadRoot, types = [], io = null, force = false, resolvers = {},
213
214
  deployStyle = "", flutterStore = null, targetRoot = ".", repoName = "", log = defaultLog,
@@ -217,11 +218,11 @@ export async function promptEnvPlan({
217
218
  const asks = collectAsks(payloadRoot, types, { resolvers, deployStyle, flutterStore, prompts, saved });
218
219
  const defaults = asks.defaults;
219
220
 
220
- // 수집 키 0개 → 질문 자체가 없음
221
+ // No collected keys -> nothing to ask
221
222
  if (asks.keys.length === 0) return { values: new Map(), useDefaults: true, answers: [] };
222
223
 
223
- // 비대화형: force 또는 (io 미주입 && 비TTY) → 전부 기본값
224
- // io가 주입돼 있으면(테스트/상위 마법사) TTY 여부와 무관하게 대화형으로 진행한다.
224
+ // Non-interactive: force or (no io injected && non-TTY) -> all defaults
225
+ // With io injected (tests / parent wizard) it runs interactively regardless of TTY.
225
226
  const interactive = !force && (io != null || stdin.isTTY);
226
227
  if (!interactive) {
227
228
  const values = new Map(defaults);
@@ -230,57 +231,57 @@ export async function promptEnvPlan({
230
231
 
231
232
  const ui = io ?? engine;
232
233
 
233
- // 기본값 미리보기 카드 전체 출력
234
+ // Print the full default-preview cards
234
235
  log("");
235
- log("▶ 워크플로우 환경설정을 채웁니다");
236
+ log(t("ui.env-plan.intro.title"));
236
237
  log("");
237
- log(" 설치되는 워크플로우가 사용할 값입니다. 항목마다 '무엇에 쓰이는지·설명·예시'와");
238
- log(" 기본값을 함께 보여드립니다. 그대로 둬도 되고, 원하는 것만 바꿀 수 있습니다.");
238
+ log(t("ui.env-plan.intro.line1"));
239
+ log(t("ui.env-plan.intro.line2"));
239
240
  log("");
240
241
  const tot = asks.keys.length;
241
242
  asks.keys.forEach((key, i) => {
242
243
  printFieldCard(prompts, key, { default: defaults.get(key), usages: asks.usages.get(key) || [] }, i + 1, tot, log);
243
244
  });
244
- log(" ─────────────────────────────────────────────");
245
+ log(t("ui.env-plan.intro.rule"));
245
246
 
246
247
  const choice = await ui.select({
247
- message: "어떻게 채울까요?",
248
+ message: t("ui.env-plan.choice.message"),
248
249
  options: [
249
- { value: "all", label: "① 위 기본값 그대로 전부 설치 (입력 없이 바로 진행)" },
250
- { value: "each", label: "② 하나씩 직접 입력 (모든 항목을 순서대로)" },
251
- { value: "some", label: "③ 몇 개만 골라서 바꾸기 (고른 것만 입력 · 나머지는 기본값)" },
250
+ { value: "all", label: t("ui.env-plan.choice.all") },
251
+ { value: "each", label: t("ui.env-plan.choice.each") },
252
+ { value: "some", label: t("ui.env-plan.choice.some") },
252
253
  ],
253
254
  });
254
- // ESC/취소 → 전부 기본값
255
+ // ESC/cancel -> all defaults
255
256
  if (choice === CANCEL || choice == null || choice === "all") {
256
257
  const values = new Map(defaults);
257
258
  return { values, useDefaults: true, answers: buildAnswers(prompts, asks, values, true) };
258
259
  }
259
260
 
260
- // 사용자가 확정한 키만 values에 담는다 — substituteEnv(useDefaults:false)가
261
- // values에 없는 키는 타입별 기본값으로 채운다(기본값 위에 답한 값만 덮어쓰는 것과 같다).
261
+ // Keep only user-confirmed keys in values - substituteEnv(useDefaults:false) fills keys
262
+ // missing from values with the per-type defaults (same as overriding defaults with just the answered values).
262
263
  const values = new Map();
263
264
  if (choice === "each") {
264
265
  await promptEach(ui, prompts, asks, asks.keys, values, log);
265
266
  return { values, useDefaults: false, answers: buildAnswers(prompts, asks, values, false) };
266
267
  }
267
268
 
268
- // some: 바꿀 항목만 멀티선택 → 고른 것만 입력
269
+ // some: multi-select the items to change -> prompt only for those
269
270
  const options = asks.keys.map((key) => ({
270
271
  value: key,
271
- label: `${wfField(prompts, firstTypeFor(asks.usages, key), key, "label")} (기본: ${defaults.get(key)})`,
272
+ label: t("ui.env-plan.some.option", { label: wfField(prompts, firstTypeFor(asks.usages, key), key, "label"), def: defaults.get(key) }),
272
273
  }));
273
274
  const selected = await ui.multiselect({
274
- message: "바꿀 항목을 고르세요 (Space로 선택 · Enter로 확정)",
275
+ message: t("ui.env-plan.some.message"),
275
276
  options,
276
277
  initialValues: [],
277
278
  });
278
- // ESC/빈 선택 → 전부 기본값
279
+ // ESC/empty selection -> all defaults
279
280
  if (selected === CANCEL || !Array.isArray(selected) || selected.length === 0) {
280
281
  const values = new Map(defaults);
281
282
  return { values, useDefaults: true, answers: buildAnswers(prompts, asks, values, true) };
282
283
  }
283
- // 수집 키 순서 유지 + 수집된 키만 인정
284
+ // Keep collection order + accept only collected keys
284
285
  const todo = asks.keys.filter((k) => selected.includes(k));
285
286
  await promptEach(ui, prompts, asks, todo, values, log);
286
287
  return { values, useDefaults: false, answers: buildAnswers(prompts, asks, values, false) };