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,28 +1,31 @@
1
1
  import { DEFAULT_DEPLOY_STYLE } from "./deploy-style.js";
2
2
  import { escapeYamlDoubleQuoted } from "./wizard-env.js";
3
3
  import { hooksFor, mergeHookResults, allHookValues } from "./types.js";
4
+ import { DEFAULT_LANGUAGE, isSupportedLanguage, normalizeLanguage } from "../i18n/languages.js";
5
+ // Aliased: `t` is used as a local variable name (type, trimmed line) throughout this file.
6
+ import { t as tr } from "../i18n/index.js";
4
7
 
5
- // version.yml 파싱·생성 (전체 재생성 전략).
6
- // ⚠️ YAML 재직렬화 금지 — 주석이 데이터.
7
- // 레이아웃 단일 진실 = payload/version.yml.template (호출부가 templateText로 주입).
8
+ // version.yml parsing and generation (full-regeneration strategy).
9
+ // WARNING: no YAML re-serialization - the comments are data.
10
+ // Single source of truth for the layout = payload/version.yml.template (injected by the caller as templateText).
8
11
 
9
- // version.yml.template이 아는 최상위 키 — 이 밖의 최상위 키는 사용자가 직접 추가한 것으로 간주한다.
10
- // "project_type"(단수)은 더 이상 렌더하지 않는 레거시 키지만 이 집합에는 남겨둔다:
11
- // 빼면 기존 파일의 단수 줄이 "사용자가 추가한 필드"로 오인돼 재생성 때 되살아난다. 아는 키로
12
- // 둬야 재통합 시 흡수되어 사라진다.
12
+ // Top-level keys the version.yml.template knows - any other top-level key is considered added by the user.
13
+ // "project_type" (singular) is a legacy key that is no longer rendered but stays in this set:
14
+ // if removed, the singular line of an existing file would be mistaken for a "field added by the user" and
15
+ // revived on regeneration. It must stay a known key so it is absorbed and disappears on re-integration.
13
16
  const KNOWN_TOP_LEVEL_KEYS = new Set([
14
- "version", "version_code", "project_types", "project_type", "project_paths", "metadata", "deploy",
17
+ "version", "version_code", "project_types", "project_type", "language", "project_paths", "metadata", "deploy",
15
18
  ]);
16
19
 
17
- // 최상위 레벨의 알려지지 않은 필드(사용자가 직접 추가한 임의 필드)를 원본 그대로 보존한다
18
- //. 각 알려지지 않은 최상위 키부터 다음 최상위 키 직전까지를 통째로 한 블록으로
19
- // 캡처한다(중첩 구조가 있어도 유효한 YAML로 남기기 위함). metadata/project_paths/deploy처럼
20
- // 이 모듈이 이미 아는 블록 "내부"의 알려지지 않은 하위 키는 대상이 아니다(범위 밖).
20
+ // Preserve unknown top-level fields (arbitrary fields the user added) verbatim.
21
+ // Everything from each unknown top-level key up to just before the next top-level key is captured
22
+ // as one block (so it stays valid YAML even with nested structure). Unknown sub-keys "inside" blocks this
23
+ // module already knows, such as metadata/project_paths/deploy, are out of scope.
21
24
  export function parseExtraTopLevel(content) {
22
25
  const blocks = [];
23
26
  let current = null;
24
27
  for (const line of String(content || "").split("\n")) {
25
- // 최상위 키는 하이픈을 포함할 수 있다(YAML 관례).
28
+ // Top-level keys may contain hyphens (YAML convention).
26
29
  const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):/);
27
30
  if (m) {
28
31
  if (current) blocks.push(current.join("\n"));
@@ -35,22 +38,22 @@ export function parseExtraTopLevel(content) {
35
38
  return blocks;
36
39
  }
37
40
 
38
- // 타입 전용 옵션 키 → 반환 필드(타입 훅 savedOptionKeys). 값은 원문 문자열로 돌려주고, 유효성 판정은 타입 훅의 resolveOptions 몫이다.
41
+ // Type-specific option key -> returned field (type hook savedOptionKeys). The value is returned as the raw string; validity is up to the type hook's resolveOptions.
39
42
  const TYPE_OPTION_KEYS = allHookValues("savedOptionKeys");
40
43
  const TYPE_OPTION_LINE = new RegExp(`^\\s+(${Object.keys(TYPE_OPTION_KEYS).join("|")}):\\s*(.+)`);
41
44
 
42
- // metadata.template.options 상태머신 파싱.
43
- // 반환: { semverAuto: bool|null, copilotAi: bool|null, deployStyle: string|null,
44
- // 타입 전용 옵션 필드(예: envMode/flutterStore/androidDeployMode/iosDeployMode): string|null } — null=미기재.
45
- // 구 synology·coderabbit 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다(파싱 에러 없음).
45
+ // State-machine parse of metadata.template.options.
46
+ // Returns: { semverAuto: bool|null, copilotAi: bool|null, deployStyle: string|null,
47
+ // type-specific option fields (e.g. envMode/flutterStore/androidDeployMode/iosDeployMode): string|null } - null = not written.
48
+ // Other keys such as the old synology/coderabbit ones hit no branch and are naturally ignored (no parse error).
46
49
  export function parseTemplateOptions(content) {
47
50
  const out = {
48
51
  semverAuto: null, copilotAi: null, deployStyle: null,
49
52
  ...Object.fromEntries(Object.values(TYPE_OPTION_KEYS).map((field) => [field, null])),
50
53
  };
51
- // 값 정규화: 따옴표 제거 + 트림
52
- // 인라인 주석(` # ...`)을 먼저 떼고 따옴표·공백을 정리한다. 문자열 값을 받는 키(deploy_style)는
53
- // 주석을 안 떼면 "simple # simple | nginx ..." 가 통째로 값이 된다.
54
+ // Value normalization: strip quotes + trim
55
+ // Strip the inline comment (` # ...`) first, then clean quotes and whitespace. For keys taking a string value (deploy_style),
56
+ // without stripping the comment the whole "simple # simple | nginx ..." becomes the value.
54
57
  const strip = (s) => String(s).replace(/\s+#.*$/, "").replace(/["']/g, "").trim();
55
58
  let inTemplate = false;
56
59
  let inOptions = false;
@@ -76,36 +79,39 @@ export function parseTemplateOptions(content) {
76
79
  if (v === "false") out.copilotAi = false;
77
80
  continue;
78
81
  }
79
- // 들여쓰기 0~4칸의 다른 키 → options 섹션 종료
82
+ // another key indented 0-4 spaces -> end of the options section
80
83
  if (/^\s{0,4}[a-z_]+:/.test(line)) { inOptions = false; inTemplate = false; }
81
84
  }
82
- // 최상위 키 → template 섹션 종료
85
+ // top-level key -> end of the template section
83
86
  if (inTemplate && /^[a-z_]+:/.test(line)) { inTemplate = false; inOptions = false; }
84
87
  }
85
88
  return out;
86
89
  }
87
90
 
88
- // 기존 version.yml에서 값 추출 (라인 기반, 주석 라인 오탐 방지).
91
+ // Extract values from an existing version.yml (line-based, avoids false hits on comment lines).
89
92
  export function parseExisting(content) {
90
93
  const text = String(content || "");
91
94
  const line = (re) => {
92
95
  for (const l of text.split("\n")) {
93
- if (l.startsWith("#")) continue; // 주석 제외
96
+ if (l.startsWith("#")) continue; // exclude comments
94
97
  const m = l.match(re);
95
98
  if (m) return m[1];
96
99
  }
97
100
  return null;
98
101
  };
99
- // version: "x.y.z" (숫자.숫자.숫자 형태만)
102
+ // version: "x.y.z" (digits.digits.digits form only)
100
103
  const version = line(/^version:\s*["']?([0-9][0-9.]*)["']?/) || "";
101
- // version_code: N (양의 정수, 아니면 1)
104
+ // version_code: N (positive integer, otherwise 1)
102
105
  let versionCode = parseInt(line(/^version_code:\s*([0-9]+)/) || "", 10);
103
106
  if (!Number.isInteger(versionCode) || versionCode <= 0) versionCode = 1;
104
107
  // project_types: ["a","b"]
105
108
  const typesRaw = line(/^project_types:\s*(\[[^\]]*\])/);
106
109
  let types = [];
107
110
  if (typesRaw) types = [...typesRaw.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
108
- // project_paths 블록: " type: "path""
111
+ // language: "en" - only supported values count; a hand-edited unknown value is treated as unset
112
+ const langRaw = normalizeLanguage((line(/^language:\s*(.+)/) || "").replace(/\s+#.*$/, "").replace(/["']/g, ""));
113
+ const language = isSupportedLanguage(langRaw) ? langRaw : null;
114
+ // project_paths block: ` type: "path"`
109
115
  const paths = new Map();
110
116
  let inPaths = false;
111
117
  for (const l of text.split("\n")) {
@@ -113,10 +119,10 @@ export function parseExisting(content) {
113
119
  if (inPaths) {
114
120
  const m = l.match(/^\s+([a-z-]+):\s*"([^"]*)"/);
115
121
  if (m) paths.set(m[1], m[2]);
116
- else if (/^\S/.test(l)) inPaths = false; // 들여쓰기 끝 → 블록 종료
122
+ else if (/^\S/.test(l)) inPaths = false; // end of indentation -> end of block
117
123
  }
118
124
  }
119
- // template: 블록 내 version
125
+ // version inside the template: block
120
126
  let templateVersion = "";
121
127
  let inTemplate = false;
122
128
  for (const l of text.split("\n")) {
@@ -127,20 +133,20 @@ export function parseExisting(content) {
127
133
  if (/^\S/.test(l)) break;
128
134
  }
129
135
  }
130
- // 선택 워크플로우 옵션 (metadata.template.options)
136
+ // Optional workflow options (metadata.template.options)
131
137
  const options = parseTemplateOptions(text);
132
- // metadata.template.branches — main/develop/mode (업데이트 모드 재질문 생략용)
138
+ // metadata.template.branches - main/develop/mode (to skip re-asking in update mode)
133
139
  const branches = parseTemplateBranches(text);
134
140
  return {
135
- version, versionCode, types, paths, templateVersion, options, branches,
141
+ version, versionCode, types, language, paths, templateVersion, options, branches,
136
142
  deploy: parseDeployBlock(text), extraTopLevel: parseExtraTopLevel(text),
137
143
  };
138
144
  }
139
145
 
140
- // deploy 블록 파싱 — 설치 때 답한 배포 값을 다시 읽어 재실행·업데이트의 기본값으로 쓴다.
141
- // 쓰기만 하고 읽지 않으면 손대지 않은 파일은 치환을 건너뛰어 블록이 사라지고, 자동 갱신은
142
- // 사용자가 고른 값을 템플릿 기본값으로 되돌린다.
143
- // 반환: Map<type, Map<KEY, value>> (값은 buildVersionYml이 쓴 큰따옴표 이스케이프를 푼 원문)
146
+ // Parse the deploy block - re-reads the deploy values answered at install time for use as defaults on re-runs and updates.
147
+ // If it is only written and never read, an untouched file skips substitution so the block disappears, and auto-refresh
148
+ // reverts the user's chosen values to the template defaults.
149
+ // Returns: Map<type, Map<KEY, value>> (values are the raw text with the double-quote escapes written by buildVersionYml undone)
144
150
  export function parseDeployBlock(content) {
145
151
  const out = new Map();
146
152
  let inDeploy = false;
@@ -149,7 +155,7 @@ export function parseDeployBlock(content) {
149
155
  const l = raw.replace(/\r$/, "");
150
156
  if (/^deploy:/.test(l)) { inDeploy = true; current = null; continue; }
151
157
  if (!inDeploy) continue;
152
- if (/^\S/.test(l)) { inDeploy = false; continue; } // 다음 최상위 키 → 블록 종료
158
+ if (/^\S/.test(l)) { inDeploy = false; continue; } // next top-level key -> end of block
153
159
  const t = l.match(/^ {2}([a-z][a-z-]*):\s*(?:#.*)?$/);
154
160
  if (t) { current = new Map(); out.set(t[1], current); continue; }
155
161
  const kv = l.match(/^ {4}([A-Za-z_][A-Za-z0-9_]*):\s*"((?:[^"\\]|\\.)*)"/);
@@ -158,10 +164,10 @@ export function parseDeployBlock(content) {
158
164
  return out;
159
165
  }
160
166
 
161
- // metadata.template.branches 블록 파싱. 셋 다 있어야 유효 — 아니면 null.
167
+ // Parse the metadata.template.branches block. All three must be present to be valid - otherwise null.
162
168
  export function parseTemplateBranches(content) {
163
- // 인라인 주석(` # ...`)을 먼저 떼고 따옴표·공백을 정리한다. 문자열 값을 받는 키(deploy_style)는
164
- // 주석을 안 떼면 "simple # simple | nginx ..." 가 통째로 값이 된다.
169
+ // Strip the inline comment (` # ...`) first, then clean quotes and whitespace. For keys taking a string value (deploy_style),
170
+ // without stripping the comment the whole "simple # simple | nginx ..." becomes the value.
165
171
  const strip = (s) => String(s).replace(/\s+#.*$/, "").replace(/["']/g, "").trim();
166
172
  let inTemplate = false;
167
173
  let inBranches = false;
@@ -183,22 +189,23 @@ export function parseTemplateBranches(content) {
183
189
  return out.main && out.develop && out.mode ? out : null;
184
190
  }
185
191
 
186
- // version.yml 전체 생성 — payload/version.yml.template 렌더링.
192
+ // Generate the whole version.yml - renders payload/version.yml.template.
187
193
  // opts: { templateText, version, types:[], paths:Map, pathMarkers?:Map,
188
194
  // branch, branches?, versionCode, now, today, templateOptions?, deployValues?,
189
- // extraTopLevel?:string[], ← 기존 version.yml의 알려지지 않은 최상위 필드 보존
190
- // typeOptions?:object } ← 타입 훅(versionOptionsBlock)이 해당 타입일 때만 렌더하는 옵션 블록의 입력
191
- // templateText = payload/version.yml.template 원문 (readVersionYmlTemplate — 필수)
192
- // now = "YYYY-MM-DD HH:MM:SS" (UTC) — 결정성 위해 주입 / today = "YYYY-MM-DD"
193
- // branches = { main, develop, mode } (resolveBranchConfig 결과. 없으면 branch 기반 기본값)
194
- // pathMarkers = Map<type, markerFilename> (project_paths 주석용)
195
+ // extraTopLevel?:string[], <- preserves unknown top-level fields of the existing version.yml
196
+ // language?:string, <- message language (en|ko, default en)
197
+ // typeOptions?:object } <- input of the option block a type hook (versionOptionsBlock) renders only for its own type
198
+ // templateText = raw text of payload/version.yml.template (readVersionYmlTemplate - required)
199
+ // now = "YYYY-MM-DD HH:MM:SS" (UTC) - injected for determinism / today = "YYYY-MM-DD"
200
+ // branches = { main, develop, mode } (result of resolveBranchConfig. Without it, defaults based on branch)
201
+ // pathMarkers = Map<type, markerFilename> (for project_paths comments)
195
202
  // templateOptions = { templateVersion, optionsDate }
196
203
  export function buildVersionYml({
197
204
  templateText, version, types = [], paths = new Map(), pathMarkers = new Map(),
198
205
  branch = "main", branches = null, versionCode = 1, now, today,
199
- templateOptions = null, deployValues = new Map(), extraTopLevel = [], typeOptions = {},
206
+ templateOptions = null, deployValues = new Map(), extraTopLevel = [], typeOptions = {}, language = DEFAULT_LANGUAGE,
200
207
  }) {
201
- if (!templateText) throw new Error("version.yml.template 원문이 필요합니다 (payload/version.yml.template 누락?)");
208
+ if (!templateText) throw new Error(tr("core.versionYml.error.templateRequired"));
202
209
  const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(", ")}]` : `["basic"]`;
203
210
  const b = branches || { main: branch || "main", develop: "develop", mode: "pr-flow" };
204
211
  const {
@@ -206,10 +213,10 @@ export function buildVersionYml({
206
213
  includeSemverAuto = true, includeCopilotAi = false, deployStyle = "", optionsDate = today,
207
214
  } = templateOptions || {};
208
215
 
209
- // project_paths 블록 (full-line 토큰 {{PROJECT_PATHS}} — 없으면 라인 제거)
216
+ // project_paths block (full-line token {{PROJECT_PATHS}} - line removed when absent)
210
217
  let pathsBlock = "";
211
218
  if (paths.size) {
212
- const rows = [`project_paths: # 타입별 프로젝트 폴더 (레포 루트 기준 상대경로)`];
219
+ const rows = [`project_paths: # ${tr("core.versionYml.pathsComment")}`];
213
220
  for (const [t, p] of paths) {
214
221
  const marker = pathMarkers.get(t) || "";
215
222
  const pf = p === "." ? marker : (marker ? `${p}/${marker}` : p);
@@ -218,26 +225,27 @@ export function buildVersionYml({
218
225
  pathsBlock = rows.join("\n");
219
226
  }
220
227
 
221
- // deploy 블록 (full-line 토큰 {{DEPLOY}} — WF ask 값이 있는 타입만, 앞에 빈 줄 1개)
228
+ // deploy block (full-line token {{DEPLOY}} - only types with WF ask values, one blank line before)
222
229
  let deployBlock = "";
223
230
  const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t) && deployValues.get(t).size > 0);
224
231
  if (deployTypes.length) {
225
- const rows = ["", "deploy: # 마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)"];
232
+ const rows = ["", `deploy: # ${tr("core.versionYml.deployComment")}`];
226
233
  for (const t of deployTypes) {
227
234
  rows.push(` ${t}:`);
228
- // 동일한 이스케이프를 재사용 — deploy 값도 @wizard ask 값과 같은 경로로 들어오므로
229
- // 큰따옴표가 섞이면 setEnvLine과 동일하게 YAML이 깨진다 (두 번째 지점).
235
+ // Reuse the same escape - deploy values arrive by the same path as @wizard ask values, so
236
+ // a stray double quote breaks the YAML just like in setEnvLine (the second site).
230
237
  for (const [k, v] of deployValues.get(t)) rows.push(` ${k}: "${escapeYamlDoubleQuoted(v)}"`);
231
238
  }
232
239
  deployBlock = rows.join("\n");
233
240
  }
234
241
 
235
- // 타입 옵션 블록 (full-line 토큰 {{TYPE_OPTIONS}} — 블록을 내는 타입이 있을 때만, 아니면 줄 제거)
242
+ // Type option block (full-line token {{TYPE_OPTIONS}} - only when a type emits a block, otherwise the line is removed)
236
243
  const typeOptionsBlock = hooksFor(types, "versionOptionsBlock").map(({ hook }) => hook(typeOptions)).join("\n");
237
244
 
238
245
  const scalars = {
239
246
  VERSION: version, VERSION_CODE: String(versionCode),
240
247
  PROJECT_TYPES: typesJson,
248
+ LANGUAGE: isSupportedLanguage(language) ? language : DEFAULT_LANGUAGE,
241
249
  NOW: now, TODAY: today || optionsDate, DEFAULT_BRANCH: branch,
242
250
  TEMPLATE_VERSION: templateVersion,
243
251
  MAIN_BRANCH: b.main, DEVELOP_BRANCH: b.develop, BRANCH_MODE: b.mode,
@@ -252,10 +260,10 @@ export function buildVersionYml({
252
260
  if (t === "{{PROJECT_PATHS}}") { if (pathsBlock) out.push(pathsBlock); continue; }
253
261
  if (t === "{{TYPE_OPTIONS}}") { if (typeOptionsBlock) out.push(typeOptionsBlock); continue; }
254
262
  if (t === "{{DEPLOY}}") { if (deployBlock) out.push(deployBlock); continue; }
255
- if (t.startsWith("deploy_style:") && deployStyle === null) continue; // 서버 배포가 없는 타입은 기록하지 않는다
263
+ if (t.startsWith("deploy_style:") && deployStyle === null) continue; // types without a server deploy do not record it
256
264
  out.push(line.replace(/\{\{([A-Z][A-Z0-9_]*)\}\}/g, (_, name) => {
257
265
  if (name in scalars) return scalars[name];
258
- throw new Error(`version.yml.template에 알 수 없는 플레이스홀더: {{${name}}}`);
266
+ throw new Error(tr("core.versionYml.error.unknownPlaceholder", { name }));
259
267
  }));
260
268
  }
261
269
  let text = out.join("\n");
@@ -264,33 +272,33 @@ export function buildVersionYml({
264
272
  text += "\n" + extraTopLevel.join("\n\n");
265
273
  }
266
274
  if (!text.endsWith("\n")) text += "\n";
267
- return text.replace(/\n{3,}$/, "\n"); // 말미 과잉 빈 줄 정리
275
+ return text.replace(/\n{3,}$/, "\n"); // trim excess trailing blank lines
268
276
  }
269
277
 
270
- // 설치 시각 줄(metadata.last_updated 등)만 다른가 — 바뀐 게 없는 재실행이 매번 파일을 고쳐
271
- // 작업트리를 dirty하게 만들지 않도록 호출부가 쓰기를 건너뛰는 데 쓴다.
278
+ // Whether only the install-time lines (metadata.last_updated etc.) differ - callers use it to skip the write so a
279
+ // re-run with no changes does not rewrite the file every time and dirty the working tree.
272
280
  const TIMESTAMP_LINE = /^\s*(last_updated|integration_date|integrated_date|last_update_date):/;
273
281
  export function sameIgnoringTimestamps(a, b) {
274
282
  const norm = (t) => String(t).replace(/\r\n/g, "\n").split("\n").filter((l) => !TIMESTAMP_LINE.test(l)).join("\n");
275
283
  return norm(a) === norm(b);
276
284
  }
277
285
 
278
- // context 하나로 version.yml 최종형을 만든다 — 실제 설치(full)와 미리보기(dry-run)가
279
- // 같은 함수를 쓰게 해서 "미리보기와 결과가 다른" 상황을 구조적으로 막는다.
280
- // deployValues는 실제 설치에서만 존재한다(미리보기는 치환을 수행하지 않으므로 빈 Map).
286
+ // Builds the final version.yml from a single context - the real install (full) and the preview (dry-run)
287
+ // use the same function, structurally preventing "the preview differs from the result".
288
+ // deployValues exist only in a real install (the preview performs no substitution, so it is an empty Map).
281
289
  export function renderVersionYml(context, templateText, { pathMarkers, deployValues = new Map(), extraTopLevel = [] }) {
282
290
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
283
- now, today, templateVersion = "unknown", branches = null,
291
+ now, today, templateVersion = "unknown", branches = null, language,
284
292
  includeSemverAuto, includeCopilotAi, deployStyle } = context;
285
293
  return buildVersionYml({
286
294
  templateText, version, types, paths, pathMarkers, branch, branches, versionCode, now, today,
287
- deployValues, extraTopLevel,
295
+ deployValues, extraTopLevel, language,
288
296
  typeOptions: mergeHookResults(types, "optionsFromContext", context),
289
297
  templateOptions: {
290
298
  templateVersion,
291
299
  includeSemverAuto: includeSemverAuto !== false,
292
300
  includeCopilotAi: includeCopilotAi === true,
293
- // null = 서버 배포 워크플로우가 없는 타입이라 배포 방식이 의미 없음(기록 생략)
301
+ // null = a type with no server deploy workflow, so the deploy style is meaningless (record omitted)
294
302
  deployStyle: deployStyle === null ? null : (deployStyle || DEFAULT_DEPLOY_STYLE),
295
303
  optionsDate: today,
296
304
  },
@@ -1,55 +1,55 @@
1
- // @wizard env 토큰 엔진 — 워크플로우의 ask/auto/fallback 마커 치환과 unchanged 판정.
2
- // ⚠️ YAML 파싱/재직렬화 금지 — 라인 단위 문자열 처리 (포맷·주석 보존이 unchanged 판정 전제).
1
+ // @wizard env token engine - substitution of the workflow's ask/auto/fallback markers and the unchanged verdict.
2
+ // WARNING: no YAML parsing/re-serialization - line-based string handling (preserving format and comments is the premise of the unchanged verdict).
3
3
 
4
- // KEY 정규식: env 키(대문자)에 더해 workflow_dispatch 입력의 `default:`처럼 소문자 키도 받는다.
5
- // 마커가 붙은 줄만 대상이라 넓혀도 다른 줄에는 영향이 없다.
6
- // fallback은 `KEY: ${{ 런타임값 || 'literal' }}` 표현식 안의 기본 리터럴을 교체하는 마커다.
4
+ // KEY regex: besides env keys (uppercase) it accepts lowercase keys such as `default:` in a workflow_dispatch input.
5
+ // Only lines carrying a marker are targeted, so widening it does not affect other lines.
6
+ // fallback is a marker that replaces the default literal inside a `KEY: ${{ runtimeValue || 'literal' }}` expression.
7
7
  const MARKER_RE = /#\s*@wizard\s+(ask|auto|fallback):(.*)$/;
8
8
  const KEY_RE = /^(\s*)([A-Za-z_]+):/;
9
9
  const PATHS_ANCHOR_RE = /#\s*@wizard\s+paths-anchor/;
10
10
 
11
- // 한 라인을 파싱해 {indent,key,action,arg} 반환. ask/auto/fallback 마커 없으면 null.
11
+ // Parse one line into {indent,key,action,arg}. null when there is no ask/auto/fallback marker.
12
12
  export function parseWizardLine(line) {
13
13
  const marker = line.match(MARKER_RE);
14
14
  if (!marker) return null;
15
15
  const km = line.match(KEY_RE);
16
- if (!km) return null; // KEY: 형식 아니면 (예: paths-anchor 주석) 무시
16
+ if (!km) return null; // not in KEY: form (e.g. a paths-anchor comment) - ignore
17
17
  return { indent: km[1], key: km[2], action: marker[1], arg: marker[2].trim() };
18
18
  }
19
19
 
20
- // YAML 큰따옴표 문자열 안에 안전하게 넣기 위한 이스케이프(백슬래시 우선 — 그래야 그 다음에 붙이는
21
- // 큰따옴표 이스케이프가 깨지지 않는다). @wizard 치환(setEnvLine)과 version.yml의 deploy 블록
22
- // (buildVersionYml, src/core/version-yml.js) 양쪽에서 재사용한다 — 두 지점 모두에서
23
- // 발생하는 동일한 버그다.
20
+ // Escape for safely placing a value inside a YAML double-quoted string (backslash first - otherwise the
21
+ // double-quote escape added afterwards would be broken). Reused by both the @wizard substitution (setEnvLine)
22
+ // and the version.yml deploy block (buildVersionYml, src/core/version-yml.js) - the same bug
23
+ // occurs at both points.
24
24
  export function escapeYamlDoubleQuoted(value) {
25
25
  return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
26
26
  }
27
27
 
28
- // `KEY: "..."` 따옴표 안 값 치환 + 그 줄 끝 `# @wizard ...` 주석 제거.
29
- // 라인 하나에 대해 수행. value가 빈문자면 치환 스킵(템플릿 기본값을 남긴다).
28
+ // Substitute the value inside the quotes of `KEY: "..."` + strip the trailing `# @wizard ...` comment on that line.
29
+ // Works on a single line. An empty value skips substitution (the template default stays).
30
30
  export function setEnvLine(line, key, value) {
31
31
  if (value === "" || value == null) return line;
32
- // CRLF 안전: 라인 끝 \r을 분리해 처리 후 복원 (autocrlf 프로젝트 대응)
32
+ // CRLF safe: split off the trailing \r, process, then restore (for autocrlf projects)
33
33
  const cr = line.endsWith("\r") ? "\r" : "";
34
34
  const body = cr ? line.slice(0, -1) : line;
35
- // 값 치환: KEY: "기존값" → KEY: "value"
36
- // 홑따옴표(KEY: '기존값')도 받는다 — 템플릿에 두 표기가 섞여 있는데 겹따옴표만 보면
37
- // 홑따옴표 줄의 @wizard 마커가 아무 경고 없이 무시된다(같은 실패 형태).
38
- // 치환 결과는 겹따옴표로 통일하고, 값은 그에 맞게 이스케이프한다.
35
+ // Value substitution: KEY: "old" -> KEY: "value"
36
+ // Single quotes (KEY: 'old') are accepted too - the template mixes both notations, and looking only at
37
+ // double quotes would silently ignore the @wizard marker on single-quoted lines (the same failure shape).
38
+ // The result is unified to double quotes and the value is escaped accordingly.
39
39
  const escaped = escapeYamlDoubleQuoted(value);
40
40
  let out = body.replace(
41
41
  new RegExp(`^(\\s*${key}:\\s*)(["'])(?:(?!\\2).)*\\2`),
42
42
  (_m, head) => `${head}"${escaped}"`,
43
43
  );
44
- // 그 줄 끝 # @wizard ... 주석 제거 (앞 공백째)
44
+ // Strip the trailing # @wizard ... comment on that line (with the preceding whitespace)
45
45
  out = out.replace(/(\S)[^\S\r\n]*#[^\S\r\n]*@wizard[^\S\r\n].*$/, "$1");
46
46
  return out + cr;
47
47
  }
48
48
 
49
- // `KEY: ${{ a || b || 'literal' }} # @wizard fallback:<token>` — 표현식 안의 "마지막 홑따옴표 리터럴"만 교체하고
50
- // 마커 주석을 제거한다. setEnvLine은 `KEY: "값"` 형태의 따옴표 값만 다루므로 GitHub 표현식은 처리하지 못한다.
51
- // 리터럴은 `||` 체인의 맨 끝(런타임 입력·저장소 변수가 모두 비었을 때의 기본값)이라 런타임 우선순위는 바뀌지 않는다.
52
- // value가 빈 문자열이면 줄을 그대로 둔다 (setEnvLine과 같은 규약 — 템플릿 기본값이 남는다).
49
+ // `KEY: ${{ a || b || 'literal' }} # @wizard fallback:<token>` - replaces only the "last single-quoted literal" inside the expression
50
+ // and strips the marker comment. setEnvLine handles only quoted values of the `KEY: "value"` form, so it cannot process GitHub expressions.
51
+ // The literal sits at the very end of the `||` chain (the default when runtime input and repo variables are all empty), so runtime precedence does not change.
52
+ // An empty value leaves the line as is (same convention as setEnvLine - the template default stays).
53
53
  const WIZARD_COMMENT_RE = /[^\S\r\n]*#[^\S\r\n]*@wizard[^\S\r\n].*$/;
54
54
  const LAST_LITERAL_RE = /^(.*)'[^']*'([^']*)$/;
55
55
  export function setFallbackLine(line, value) {
@@ -57,11 +57,11 @@ export function setFallbackLine(line, value) {
57
57
  const cr = line.endsWith("\r") ? "\r" : "";
58
58
  const expression = (cr ? line.slice(0, -1) : line).replace(WIZARD_COMMENT_RE, "");
59
59
  if (!LAST_LITERAL_RE.test(expression)) return line;
60
- const escaped = String(value).replaceAll("'", "''"); // GitHub 표현식 문자열 리터럴의 따옴표 이스케이프
60
+ const escaped = String(value).replaceAll("'", "''"); // quote escape for a GitHub expression string literal
61
61
  return expression.replace(LAST_LITERAL_RE, (_m, head, tail) => `${head}'${escaped}'${tail}`) + cr;
62
62
  }
63
63
 
64
- // resolver — 값 계산은 주입된 resolvers로 위임(순수성 유지).
64
+ // resolver - value computation is delegated to the injected resolvers (keeps purity).
65
65
  // resolvers: { repo, "spring-app-yml-dir"(type), "spring-app-yml-path"(type), "flutter-root",
66
66
  // "project-path"(type), "flutter-env-mode", "android-deploy-mode", "ios-deploy-mode" }
67
67
  export function resolveToken(name, type, resolvers = {}) {
@@ -69,25 +69,25 @@ export function resolveToken(name, type, resolvers = {}) {
69
69
  return typeof fn === "function" ? (fn(type) ?? "") : "";
70
70
  }
71
71
 
72
- // __PROJECT_NAME__/__APP_ARTIFACT_NAME__ 전역 토큰을 repoName으로 치환.
73
- // substituteEnv()(설치 파일 본문)와 collectAsks()(마법사 화면 표시용 기본값) 양쪽에서
74
- // 재사용한다 — 두 곳이 서로 다른 로직으로 갈라지면 표시 불일치가 재발한다.
72
+ // Replace the global tokens __PROJECT_NAME__/__APP_ARTIFACT_NAME__ with repoName.
73
+ // Reused by both substituteEnv() (installed file body) and collectAsks() (defaults shown on the wizard screen) -
74
+ // if the two diverge into different logic, display mismatches come back.
75
75
  export function replaceProjectTokens(text, repoName) {
76
76
  if (!text.includes("__PROJECT_NAME__") && !text.includes("__APP_ARTIFACT_NAME__")) return text;
77
77
  return text.replaceAll("__PROJECT_NAME__", repoName).replaceAll("__APP_ARTIFACT_NAME__", repoName);
78
78
  }
79
79
 
80
- // 파일 전체 치환.
81
- // content: 원본 워크플로우 텍스트. 반환: 치환된 텍스트.
80
+ // Whole-file substitution.
81
+ // content: original workflow text. Returns: the substituted text.
82
82
  // opts:
83
- // type - 프로젝트 타입 (resolver·값 조회용)
84
- // values - Map<key,value>: ask 키의 사용자 선택값 (없으면 기본값=arg 또는 resolver)
85
- // useDefaults - true면 ask도 기본값 사용 (unchanged 비교의 전제)
86
- // resolvers - resolveToken용
87
- // repoName - __PROJECT_NAME__/__APP_ARTIFACT_NAME__ 치환값
88
- // projectPath - paths-anchor 치환용 ('.'이면 anchor 미변경)
89
- // savedValues - Map<key,value>: version.yml deploy 블록에 저장된 이 타입의 값. ask 기본값보다 우선한다
90
- // (재실행·자동 갱신이 설치 때 답한 값을 템플릿 기본값으로 되돌리지 않도록).
83
+ // type - project type (for resolvers/value lookup)
84
+ // values - Map<key,value>: the user's choices for ask keys (if absent, default = arg or resolver)
85
+ // useDefaults - when true, ask also uses defaults (the premise of the unchanged comparison)
86
+ // resolvers - for resolveToken
87
+ // repoName - substitution value for __PROJECT_NAME__/__APP_ARTIFACT_NAME__
88
+ // projectPath - for paths-anchor substitution (anchor unchanged when '.')
89
+ // savedValues - Map<key,value>: values of this type saved in the version.yml deploy block. Take precedence over ask defaults
90
+ // (so re-runs/auto-refresh do not revert answers given at install time to the template defaults).
91
91
  export function substituteEnv(content, opts = {}) {
92
92
  const {
93
93
  type = "", values = new Map(), useDefaults = true, resolvers = {}, repoName = "", projectPath = ".",
@@ -95,14 +95,14 @@ export function substituteEnv(content, opts = {}) {
95
95
  } = opts;
96
96
  if (!content.includes("@wizard")) return content;
97
97
 
98
- // CRLF 안전: EOL을 분리해 LF 기준으로 파싱·치환하고, 원래 EOL 스타일을 복원한다.
99
- // (JS 정규식의 `.`은 \r을 매칭하지 않아 `(.*)$` 마커 파싱이 CRLF에서 실패하기 때문.)
98
+ // CRLF safe: split EOLs, parse/substitute on LF basis, then restore the original EOL style.
99
+ // (JS regex `.` does not match \r, so `(.*)$` marker parsing would fail on CRLF.)
100
100
  const usesCRLF = content.includes("\r\n");
101
101
  const lines = content.split(/\r?\n/);
102
102
  for (let i = 0; i < lines.length; i++) {
103
- const p = parseWizardLine(lines[i]); // 이미 \r 제거된 라인
103
+ const p = parseWizardLine(lines[i]); // line with \r already stripped
104
104
  if (!p) continue;
105
- // fallback은 따옴표 값이 아니라 표현식 안 리터럴을 바꾸므로 ask/auto와 경로가 다르다.
105
+ // fallback changes a literal inside an expression, not a quoted value, so its path differs from ask/auto.
106
106
  if (p.action === "fallback") {
107
107
  lines[i] = setFallbackLine(lines[i], resolveToken(p.arg, type, resolvers));
108
108
  continue;
@@ -117,17 +117,17 @@ export function substituteEnv(content, opts = {}) {
117
117
  const chosen = values.get(p.key);
118
118
  if (chosen != null && chosen !== "" && !useDefaults) val = chosen;
119
119
  else val = def;
120
- // ask 키만 수집 (auto는 매번 다시 계산하므로 저장 안 함). deploy 블록용.
120
+ // Collect only ask keys (auto is recomputed every time, so it is not saved). For the deploy block.
121
121
  if (collectAsks) collectAsks.set(p.key, replaceProjectTokens(val, repoName));
122
122
  }
123
123
  lines[i] = setEnvLine(lines[i], p.key, val);
124
124
  }
125
125
  let out = lines.join(usesCRLF ? "\r\n" : "\n");
126
126
 
127
- // 잔여 전역 토큰
127
+ // Remaining global tokens
128
128
  out = replaceProjectTokens(out, repoName);
129
129
 
130
- // paths-anchor: 경로가 '.'이 아니면 주석 라인 전체를 paths 라인으로 교체
130
+ // paths-anchor: unless the path is '.', replace the whole comment line with a paths line
131
131
  if (PATHS_ANCHOR_RE.test(out) && projectPath && projectPath !== ".") {
132
132
  const eol = out.includes("\r\n") ? "\r\n" : "\n";
133
133
  out = out.split(/\r?\n/).map((line) => {
@@ -141,7 +141,7 @@ export function substituteEnv(content, opts = {}) {
141
141
  return out;
142
142
  }
143
143
 
144
- // unchanged 판정: 원본을 "기본값으로 가상 치환한 최종형"과 설치본을 바이트 비교.
144
+ // unchanged verdict: byte-compare the "final form virtually substituted with defaults" of the original against the installed copy.
145
145
  export function isUnchanged(templateContent, installedContent, opts = {}) {
146
146
  const virtual = substituteEnv(templateContent, { ...opts, useDefaults: true });
147
147
  return virtual === installedContent;