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,10 +1,11 @@
1
+ import { t as tr } from "../i18n/index.js";
1
2
  import {
2
3
  typeInfo, FALLBACK_TYPE, MARKER_DETECTED_TYPES, PACKAGE_DETECTED_TYPES, PACKAGE_FALLBACK_TYPE,
3
4
  } from "./types.js";
4
5
 
5
- // package.json 분류 — 의존성 "키"를 정확히 비교한다. 원문 부분문자열로 보면 export 스크립트나
6
- // exponential-backoff가 expo로, react-native-web을 쓰는 웹앱이 react-native로, keywords의 "next"가
7
- // next로 오감지된다. 입력은 package.json 원문 문자열(raw). 판정 순서는 레지스트리의 detectOrder.
6
+ // package.json classification: compares dependency "keys" exactly. A raw substring match would misdetect an
7
+ // export script or exponential-backoff as expo, a web app using react-native-web as react-native, and
8
+ // "next" in keywords as next. Input is the raw package.json string. Evaluation order is the registry's detectOrder.
8
9
  export function classifyPackageText(raw) {
9
10
  let pkg;
10
11
  try { pkg = JSON.parse(String(raw || "")); } catch { return PACKAGE_FALLBACK_TYPE; }
@@ -17,14 +18,14 @@ export function classifyPackageText(raw) {
17
18
  return PACKAGE_DETECTED_TYPES.find((t) => deps.has(t.packageDep))?.id ?? PACKAGE_FALLBACK_TYPE;
18
19
  }
19
20
 
20
- // 편의: 파싱된 객체를 받는 경우 원문으로 재직렬화해 위 규칙 적용
21
+ // Convenience: when given a parsed object, re-serialize it and apply the rule above.
21
22
  export function classifyPackageJson(pkgOrRaw) {
22
23
  const raw = typeof pkgOrRaw === "string" ? pkgOrRaw : JSON.stringify(pkgOrRaw || {});
23
24
  return classifyPackageText(raw);
24
25
  }
25
26
 
26
- // 마커 스캔. has(relpath)=>bool 주입. node는 다른 타입 있으면 미추가.
27
- // read(relpath)=>string|null 로 package.json 원문을 받아 classifyPackageText에 넘긴다.
27
+ // Marker scan. has(relpath)=>bool is injected. node is not added when another type is present.
28
+ // read(relpath)=>string|null supplies the raw package.json passed to classifyPackageText.
28
29
  export function detectTypesFromMarkers({ has, read }) {
29
30
  const types = [];
30
31
  for (const t of MARKER_DETECTED_TYPES) if (t.markers.some(has)) types.push(t.id);
@@ -36,25 +37,25 @@ export function detectTypesFromMarkers({ has, read }) {
36
37
  return types.length ? [...new Set(types)] : [FALLBACK_TYPE];
37
38
  }
38
39
 
39
- // 1.2.3-rc.1·1.2.3+7·1.2.0-SNAPSHOT 같은 prerelease/빌드 메타데이터는 x.y.z 코어만 쓴다.
40
- // version.yml은 x.y.z만 받으므로 감지 실패(0.0.1)로 떨어지는 것보다 코어가 정확하다.
41
- // 릴리스 시 읽기(payload/scripts/version_manager.py core_version)와 같은 규칙이어야 설치 직후 값이 유지된다.
40
+ // For prerelease/build metadata such as 1.2.3-rc.1, 1.2.3+7, 1.2.0-SNAPSHOT only the x.y.z core is used.
41
+ // version.yml accepts only x.y.z, so the core is more accurate than falling back to 0.0.1.
42
+ // Must match the release-time read (payload/scripts/version_manager.py core_version) so the value survives right after install.
42
43
  function coreVersion(v) {
43
44
  const m = String(v ?? "").trim().match(/^v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.+-]*)?$/);
44
45
  return m ? m[1] : null;
45
46
  }
46
47
 
47
- // setup.py의 setup(version="x.y.z"). python_version 같은 다른 키는 단어 경계로 거른다.
48
+ // setup(version="x.y.z") in setup.py. Other keys such as python_version are filtered out by the word boundary.
48
49
  export function versionFromSetupPy(content) {
49
50
  if (!content) return null;
50
51
  const m = String(content).match(/(?<![\w.])version\s*=\s*["']([^"']+)["']/);
51
52
  return m ? coreVersion(m[1]) : null;
52
53
  }
53
54
 
54
- // React Native 앱 버전 — 릴리스 때 version_manager가 쓰는 파일(ios/<앱>/Info.plist,
55
- // android/app/build.gradle)에서 읽는다. package.json version은 동기화 대상이 아니라 기준이 되면 어긋난다.
56
- // $(MARKETING_VERSION) 참조나 템플릿 기본값 "1.0"처럼 x.y.z가 아니면 건너뛴다.
57
- // list(relDir)=>string[]|null 로 ios 아래 앱 폴더 이름을 받는다(Pods 등 깊은 plist는 보지 않는다).
55
+ // React Native app version: read from the files version_manager writes at release time (ios/<app>/Info.plist,
56
+ // android/app/build.gradle). package.json version is not a sync target, so using it as the source would drift.
57
+ // Values that are not x.y.z, such as a $(MARKETING_VERSION) reference or the template default "1.0", are skipped.
58
+ // list(relDir)=>string[]|null supplies the app folder names under ios (deep plists such as Pods are not inspected).
58
59
  export function versionFromReactNative({ read, list }) {
59
60
  for (const dir of [...(list?.("ios") || [])].sort()) {
60
61
  const m = String(read(`ios/${dir}/Info.plist`) || "").match(/<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/);
@@ -65,12 +66,12 @@ export function versionFromReactNative({ read, list }) {
65
66
  return m ? coreVersion(m[1]) : null;
66
67
  }
67
68
 
68
- // 버전 감지 — 순서대로 첫 성공. read(relpath)=>string|null 주입.
69
- // package.json은 이미 Node JSON.parse로 파싱을 마친 값이므로 jq 설치 여부와 무관하게 항상 사용한다.
70
- // hint: 폴백 경고 뒤에 붙일 "그럼 어떻게 고치나" 한 줄. 대화형과 CLI가 서로 다른 방법을
71
- // 안내해야 하므로 호출부가 정한다. 미지정 시 CLI 문구를 쓴다.
72
- // types: 주 타입(첫 항목)의 버전 파일을 먼저 읽는다. 릴리스 때 version_manager가 주 타입
73
- // 파일과 version.yml을 비교하므로, 다른 타입 버전을 잡으면 첫 릴리스에서 버전이 뛴다.
69
+ // Version detection: first success in order. read(relpath)=>string|null is injected.
70
+ // package.json is already parsed with Node JSON.parse, so it is always used regardless of whether jq is installed.
71
+ // hint: one line appended to the fallback warning saying how to fix it. Interactive mode and the CLI
72
+ // suggest different fixes, so the caller decides. The CLI wording is used when omitted.
73
+ // types: the primary type's (first entry) version file is read first. At release time version_manager compares
74
+ // the primary type's file with version.yml, so picking another type's version would make the version jump on the first release.
74
75
  export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hint, types = [] }) {
75
76
  const grab = (content, re) => {
76
77
  for (const line of (content || "").split("\n")) {
@@ -79,17 +80,17 @@ export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hin
79
80
  }
80
81
  return null;
81
82
  };
82
- // 줄 시작 앵커가 없으면 ext.kotlin_version 같은 의존성 버전 변수가 먼저 걸린다.
83
- // 따옴표로 감싼 값만 본다 — 릴리스 때 동기화가 고칠 수 있는 형태가 이것뿐이다.
83
+ // Without a line-start anchor, dependency version variables such as ext.kotlin_version match first.
84
+ // Only quoted values are considered: that is the only form the release-time sync can rewrite.
84
85
  const gradleRe = /^(\s*)version\s*=\s*(["'])([^"'\n]*)\2/;
85
- // 들여쓰지 않은 `version =`을 우선하고, 없을 때만 allprojects/subprojects 블록 안의 들여쓴 줄을 쓴다.
86
- // `node { version = '20.11.0' }` 같은 플러그인 설정 블록이 프로젝트 버전으로 읽히면 첫 릴리스에서 버전이 뛴다.
87
- // 릴리스 때의 version_manager.py와 같은 규칙이어야 한다.
88
- // 주석을 떼고 따옴표 안 글자는 공백으로 바꾼다 — url 'https://…'의 `//`나 문자열 속 중괄호가
89
- // 블록 깊이 계산을 흐트러뜨리지 않게 한다. version_manager.py와 같은 규칙이어야 한다.
90
- // quote는 이 줄이 시작될 때 열려 있던 따옴표다. 삼중따옴표(""" ''')는 여러 줄에 걸치므로
91
- // { code, quote }로 줄 끝에서 열려 있는 따옴표를 돌려줘 다음 줄이 이어받게 한다.
92
- // 한 줄짜리 따옴표는 줄 끝에서 닫힌 것으로 본다.
86
+ // Prefer a non-indented `version =`; only when none exists use an indented line inside an allprojects/subprojects block.
87
+ // If a plugin config block such as `node { version = '20.11.0' }` were read as the project version, the version would jump on the first release.
88
+ // Must follow the same rule as version_manager.py at release time.
89
+ // Strip comments and blank out characters inside quotes so the `//` in url 'https://...' or braces inside strings
90
+ // do not throw off the block-depth count. Must follow the same rule as version_manager.py.
91
+ // quote is the quote that was open when this line started. Triple quotes (""" ''') span several lines, so
92
+ // { code, quote } returns the quote still open at the end of the line for the next line to continue.
93
+ // A single-line quote is treated as closed at the end of the line.
93
94
  const gradleCode = (line, quote) => {
94
95
  let out = "";
95
96
  for (let i = 0; i < line.length;) {
@@ -109,7 +110,7 @@ export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hin
109
110
  };
110
111
  const gradleVersion = (content) => {
111
112
  const top = [], shared = [], stack = [];
112
- let quote = ""; // 여러 줄 문자열 안이면 그 따옴표. 문자열 속 줄은 코드가 아니다
113
+ let quote = ""; // the open quote when inside a multi-line string; lines inside a string are not code
113
114
  for (const line of (content || "").split("\n")) {
114
115
  const m = quote ? null : line.match(gradleRe);
115
116
  if (m) {
@@ -131,8 +132,8 @@ export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hin
131
132
  const sources = {
132
133
  packageJson: () => coreVersion(readJson?.("package.json")?.version),
133
134
  appJson: () => coreVersion(readJson?.("app.json")?.expo?.version),
134
- // Groovy DSL과 Kotlin DSL은 같은 문법(`version = "x.y.z"`)이라 정규식을 공유한다.
135
- // .kts를 빼먹으면 Kotlin DSL Spring 프로젝트가 전부 0.0.1로 초기화된다.
135
+ // Groovy DSL and Kotlin DSL share the same syntax (`version = "x.y.z"`), so they share the regex.
136
+ // Omitting .kts would reset every Kotlin DSL Spring project to 0.0.1.
136
137
  gradle: () => gradleVersion(read("build.gradle")),
137
138
  gradleKts: () => gradleVersion(read("build.gradle.kts")),
138
139
  pom: () => versionFromPom(read("pom.xml")),
@@ -150,13 +151,13 @@ export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hin
150
151
  if (v) return v;
151
152
  }
152
153
  if (gitTag) { const t = coreVersion(gitTag); if (t) return t; }
153
- const tail = hint ?? "--project-version으로 직접 지정하거나 version.yml을 확인하세요.";
154
- warn?.(`⚠️ 버전을 자동 감지하지 못해 기본값 0.0.1을 사용합니다 — ${tail}`);
154
+ const tail = hint ?? tr("core.detect.versionFallbackHint");
155
+ warn?.(tr("core.detect.versionFallbackWarn", { tail }));
155
156
  return "0.0.1";
156
157
  }
157
158
 
158
- // pyproject.toml의 패키지 버전. [tool.*] 등 다른 섹션의 `version =`은 도구 설정이라
159
- // [project]·[tool.poetry] 섹션 안에서만 읽는다.
159
+ // Package version in pyproject.toml. `version =` in other sections such as [tool.*] is tool config,
160
+ // so it is read only inside the [project] and [tool.poetry] sections.
160
161
  export function versionFromPyproject(content) {
161
162
  if (!content) return null;
162
163
  let section = "";
@@ -170,9 +171,9 @@ export function versionFromPyproject(content) {
170
171
  return null;
171
172
  }
172
173
 
173
- // Maven pom.xml의 프로젝트 버전 — <project> 바로 아래의 <version>만 본다.
174
- // <parent>(스프링 부트 BOM)나 <dependencies> 안의 버전은 프로젝트 버전이 아니므로 깊이로 구분한다.
175
- // 프로젝트 버전이 없으면(부모에서 상속) null — 의존성 버전을 대신 고르지 않는다.
174
+ // Maven pom.xml project version: only the <version> directly under <project> counts.
175
+ // Versions inside <parent> (Spring Boot BOM) or <dependencies> are not the project version, so depth tells them apart.
176
+ // Returns null when there is no project version (inherited from the parent); a dependency version is never picked instead.
176
177
  export function versionFromPom(content) {
177
178
  if (!content) return null;
178
179
  const text = String(content);
@@ -197,42 +198,43 @@ export function versionFromPom(content) {
197
198
  return null;
198
199
  }
199
200
 
200
- // 타입의 대표 마커 파일. 마커가 없는 타입(basic)·미지 타입은 package.json을 돌려준다.
201
+ // A type's representative marker file. Types without markers (basic) and unknown types return package.json.
201
202
  export function markerForType(type) {
202
203
  return typeInfo(type)?.markers[0] || "package.json";
203
204
  }
204
205
 
205
- // 대표 파일 외의 보조 마커 (예: Spring의 build.gradle.kts·pom.xml).
206
+ // Secondary markers besides the representative file (e.g. Spring's build.gradle.kts and pom.xml).
206
207
  export function extraMarkers(type) {
207
208
  return typeInfo(type)?.markers.slice(1) || [];
208
209
  }
209
210
 
210
- // 그 타입을 감지하는 데 실제로 쓰인 파일. markerForType은 타입당 대표 파일 하나를
211
- // 고정 반환하므로, build.gradle.kts만 있는 레포에서도 "build.gradle 발견"이라고 출력돼
212
- // 같은 설치 로그 안에서 경로 확정 화면과 파일명이 어긋났다. has()로 실재하는 것을 고른다.
213
- // 실재하는 후보가 없으면(감지 전 화면 등) 대표 파일을 쓴다. 단 "근거"로 보여줄 때는(fallback:false)
214
- // 빈 문자열을 돌려준다 — 직접 고른 타입에 없는 파일을 근거로 붙이면 감지된 것처럼 보인다.
211
+ // The file actually used to detect that type. markerForType always returns one representative file per type,
212
+ // so even a repo with only build.gradle.kts printed "found build.gradle" and the file name disagreed with the
213
+ // path-confirmation screen in the same install log. has() picks the one that really exists.
214
+ // When no candidate exists (e.g. screens before detection) the representative file is used. When shown as
215
+ // "evidence" (fallback:false) an empty string is returned instead: attaching a file that does not exist for a
216
+ // manually chosen type as evidence would make it look detected.
215
217
  export function resolveMarker(type, has, { fallback = true } = {}) {
216
218
  const candidates = [markerForType(type), ...extraMarkers(type)];
217
219
  return candidates.find(has) ?? (fallback ? candidates[0] : "");
218
220
  }
219
221
 
220
- // 빌드 JDK 감지 — 배포 워크플로우의 JAVA_VERSION 기본값이 21로 고정돼 있어
221
- // toolchain이 다른 프로젝트(예: 25)는 그대로 Enter를 누르면 러너 JDK와 어긋나 빌드가 깨진다.
222
- // 빌드 번호를 프로젝트 파일에서 읽는 detectBuildNumberFromFiles와 같은 방식으로 실측한다.
223
- // 반환: "21" 같은 메이저 버전 문자열, 못 찾으면 null.
222
+ // Build JDK detection: the deploy workflow's JAVA_VERSION default is fixed at 21, so a project with a
223
+ // different toolchain (e.g. 25) that just presses Enter ends up with a runner JDK mismatch and a broken build.
224
+ // Measured from project files the same way detectBuildNumberFromFiles reads the build number.
225
+ // Returns a major version string such as "21", or null when not found.
224
226
  export function detectJdkFromFiles({ read }) {
225
227
  const pick = (content, patterns) => {
226
228
  if (!content) return null;
227
229
  for (const re of patterns) {
228
230
  const m = String(content).match(re);
229
- // JavaVersion.VERSION_1_8 처럼 1_8 표기는 8로 정규화한다.
231
+ // Normalize the 1_8 notation (JavaVersion.VERSION_1_8) to 8.
230
232
  if (m) return m[1] === "1_8" ? "8" : m[1].replace("1_", "");
231
233
  }
232
234
  return null;
233
235
  };
234
236
  const gradlePatterns = [
235
- /JavaLanguageVersion\.of\((\d+)\)/, // toolchain (Gradle 권장 표기)
237
+ /JavaLanguageVersion\.of\((\d+)\)/, // toolchain (recommended Gradle notation)
236
238
  /JavaVersion\.VERSION_(\d+(?:_\d+)?)/, // sourceCompatibility = JavaVersion.VERSION_21
237
239
  /(?:source|target)Compatibility\s*=?\s*["'](\d+)["']/, // sourceCompatibility = '17'
238
240
  ];
@@ -246,40 +248,40 @@ export function detectJdkFromFiles({ read }) {
246
248
  return null;
247
249
  }
248
250
 
249
- // 타입별 실제 마커 파일 맵 — 감지 로그·설치 로그가 같은 근거를 쓰도록 한 곳에서 만든다.
251
+ // Map of the real marker file per type: built in one place so the detection log and install log cite the same evidence.
250
252
  export function resolveMarkers(types = [], has) {
251
253
  const out = new Map();
252
254
  for (const t of types) {
253
255
  if (t === "basic") continue;
254
- // 실제로 있는 파일만 근거로 삼는다 — 없으면 맵에서 빠져 화면·로그가 "직접 선택"으로 다룬다.
256
+ // Only files that actually exist count as evidence: otherwise the type is left out of the map and screens/logs treat it as "manually chosen".
255
257
  const found = resolveMarker(t, has, { fallback: false });
256
258
  if (found) out.set(t, found);
257
259
  }
258
260
  return out;
259
261
  }
260
262
 
261
- // 빌드 번호 감지 — 신규 통합 시 pubspec.yaml/build.gradle/app.json에 이미 기록된
262
- // 빌드 번호를 읽어 version_code가 항상 1로 초기화되는 걸 막는다. types 배열에서 먼저 매칭되는
263
- // 첫 타입만 사용한다(다른 감지 로직의 types[0]=primary 관례와 동일). read(rel)=>string|null,
264
- // readJson(rel)=>object|null 로 주입.
263
+ // Build number detection: on a fresh integration, read the build number already recorded in
264
+ // pubspec.yaml/build.gradle/app.json so version_code is not always reset to 1. Only the first matching
265
+ // type in the types array is used (same types[0]=primary convention as the other detection logic).
266
+ // read(rel)=>string|null and readJson(rel)=>object|null are injected.
265
267
  export function detectBuildNumberFromFiles({ types = [], read, readJson, warn }) {
266
268
  const tryFlutter = () => {
267
269
  const content = read("pubspec.yaml");
268
270
  if (content == null) return null;
269
- // 1.2.3-rc.1+4처럼 prerelease가 있어도 +N은 빌드 번호다(릴리스 시 읽기와 같은 규칙).
271
+ // +N is the build number even with a prerelease such as 1.2.3-rc.1+4 (same rule as the release-time read).
270
272
  const m = content.match(/^version:\s*\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\+(\d+)/m);
271
273
  if (m) return parseInt(m[1], 10);
272
- warn?.("⚠️ pubspec.yaml에 빌드 번호(+N)가 없어 version_code를 감지하지 못했습니다 — 기본값 1을 사용합니다. 실제 빌드 번호를 확인하세요.");
274
+ warn?.(tr("core.detect.buildNumber.pubspecMissing"));
273
275
  return null;
274
276
  };
275
277
  const tryReactNative = () => {
276
278
  const content = read("android/app/build.gradle");
277
279
  if (content == null) return null;
278
- // 앵커 + m 플래그로 한 줄 전체가 "versionCode N"인 라인만 매칭 — 주석 처리된
279
- // "// versionCode 2"나 다른 블록의 versionCode 참조에 오매칭되지 않도록 함.
280
+ // Anchors + the m flag match only lines that are entirely "versionCode N", so a commented-out
281
+ // "// versionCode 2" or a versionCode reference in another block is not matched by mistake.
280
282
  const m = content.match(/^\s*versionCode\s+(\d+)\s*$/m);
281
283
  if (m) return parseInt(m[1], 10);
282
- warn?.("⚠️ android/app/build.gradle에 versionCode가 없어 version_code를 감지하지 못했습니다 — 기본값 1을 사용합니다. 실제 빌드 번호를 확인하세요.");
284
+ warn?.(tr("core.detect.buildNumber.gradleMissing"));
283
285
  return null;
284
286
  };
285
287
  const tryExpo = () => {
@@ -287,7 +289,7 @@ export function detectBuildNumberFromFiles({ types = [], read, readJson, warn })
287
289
  if (data == null) return null;
288
290
  const code = data?.expo?.android?.versionCode;
289
291
  if (Number.isInteger(code)) return code;
290
- warn?.("⚠️ app.json의 expo.android.versionCode가 없어 version_code를 감지하지 못했습니다 — 기본값 1을 사용합니다. 실제 빌드 번호를 확인하세요.");
292
+ warn?.(tr("core.detect.buildNumber.expoMissing"));
291
293
  return null;
292
294
  };
293
295
  const readers = { pubspec: tryFlutter, androidGradle: tryReactNative, expoAppJson: tryExpo };
@@ -1,3 +1,3 @@
1
- // 사용자 입력 오류 — 호출부(index.js)가 스택 없이 메시지만 출력하고 exit 1로 끝낸다.
2
- // core 모듈도 던지므로 cli가 아닌 core에 둔다(core → cli 역참조 방지).
1
+ // User input error - the caller (index.js) prints only the message, no stack, and exits 1.
2
+ // Core modules throw it too, so it lives in core rather than cli (avoids a core -> cli back-reference).
3
3
  export class CliError extends Error {}
@@ -1,19 +1,20 @@
1
- // Flutter 진단 항목 — doctor가 타입 훅(doctorChecks)으로 호출한다.
1
+ // Flutter diagnostic checks, called by doctor through the type hook (doctorChecks).
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join, posix } from "node:path";
4
4
  import { PATHS } from "./paths.js";
5
5
  import { STORE_PLATFORMS, STORE_APP_FILES, parseStoreList, storeAppFilesFor } from "./flutter-options.js";
6
6
  import { inferInstalledStores } from "./installed-stores.js";
7
+ import { t } from "../i18n/index.js";
7
8
 
8
- const PLATFORM_ROW_NAME = { android: "Flutter Android 배포 파일", ios: "Flutter iOS 배포 파일" };
9
- const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g; // 감지 규칙은 ExportOptions.plist 템플릿·IOS-TESTFLIGHT 검증과 동일
9
+ const PLATFORM_ROW_NAME = { android: "core.flutterDoctor.row.android", ios: "core.flutterDoctor.row.ios" }; // catalog keys, resolved lazily
10
+ const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g; // Same detection rule as the ExportOptions.plist template and the IOS-TESTFLIGHT validation
10
11
  const EXPORT_OPTIONS_REL = STORE_APP_FILES.ios.find((rel) => rel.endsWith("ExportOptions.plist"));
11
12
 
12
- // Flutter 스토어 배포 진단 — 선택한 플랫폼의 필수 파일과 ExportOptions.plist 플레이스홀더.
13
- // 스토어 시크릿 등록 여부는 이번 범위 밖이다. 반환: doctor 결과 행 배열.
13
+ // Flutter store deploy diagnostics: required files for the selected platforms and ExportOptions.plist placeholders.
14
+ // Whether store secrets are registered is out of scope. Returns an array of doctor result rows.
14
15
  export function flutterStoreChecks(cwd, existing, { docs }) {
15
16
  const saved = existing.options.flutterStore == null ? null : parseStoreList(existing.options.flutterStore);
16
- // 저장값 없는 기존 설치는 설치된 스토어 워크플로우로 추론한다 (interactive와 같은 규칙).
17
+ // An existing install with no saved value is inferred from the installed store workflows (same rule as interactive).
17
18
  const stores = saved ?? inferInstalledStores(join(cwd, PATHS.workflowsDir));
18
19
  const flutterRoot = existing.paths.get("flutter") || ".";
19
20
  const rows = [];
@@ -21,29 +22,29 @@ export function flutterStoreChecks(cwd, existing, { docs }) {
21
22
  for (const platform of STORE_PLATFORMS.filter((p) => stores.includes(p))) {
22
23
  const files = storeAppFilesFor([platform]).map((rel) => posix.join(flutterRoot, rel));
23
24
  const missing = files.filter((file) => !existsSync(join(cwd, file)));
24
- const head = { name: PLATFORM_ROW_NAME[platform], purpose: "fastlane 스토어 업로드에 필요한 파일" };
25
+ const head = { name: t(PLATFORM_ROW_NAME[platform]), purpose: t("core.flutterDoctor.purpose.storeFiles") };
25
26
  rows.push(missing.length
26
27
  ? {
27
- ...head, status: "WARN", value: `없는 파일: ${missing.join(", ")}`,
28
- impact: ["이 파일이 없으면 스토어 배포 워크플로우가 fastlane 단계에서 실패합니다."],
29
- actions: ["마법사를 다시 실행하면 없는 파일만 새로 만들어 줍니다 (이미 있는 파일은 덮어쓰지 않음)"],
28
+ ...head, status: "WARN", value: t("core.flutterDoctor.value.missing", { files: missing.join(", ") }),
29
+ impact: [t("core.flutterDoctor.impact.missing")],
30
+ actions: [t("core.flutterDoctor.action.missing")],
30
31
  doc: docs.flutterStore,
31
32
  }
32
- : { ...head, status: "OK", value: `${files.length}개 있음` });
33
+ : { ...head, status: "OK", value: t("core.flutterDoctor.value.present", { count: files.length }) });
33
34
  }
34
35
 
35
36
  const plistPath = posix.join(flutterRoot, EXPORT_OPTIONS_REL);
36
37
  if (stores.includes("ios") && existsSync(join(cwd, plistPath))) {
37
- const head = { name: "ExportOptions.plist", purpose: "iOS 서명·내보내기 설정" };
38
+ const head = { name: "ExportOptions.plist", purpose: t("core.flutterDoctor.purpose.exportOptions") };
38
39
  const left = [...new Set(readFileSync(join(cwd, plistPath), "utf8").match(PLACEHOLDER_RE) ?? [])];
39
40
  rows.push(left.length
40
41
  ? {
41
- ...head, status: "WARN", value: `채워지지 않은 값: ${left.join(", ")}`,
42
- impact: ["채우지 않으면 IOS-TESTFLIGHT 워크플로우가 ExportOptions.plist 검증 단계에서 중단됩니다."],
43
- actions: [`${plistPath} 의 플레이스홀더를 실제 값(Team ID · 번들 ID · 프로비저닝 프로파일 이름)으로 바꾸세요`],
42
+ ...head, status: "WARN", value: t("core.flutterDoctor.value.unfilled", { left: left.join(", ") }),
43
+ impact: [t("core.flutterDoctor.impact.unfilled")],
44
+ actions: [t("core.flutterDoctor.action.unfilled", { path: plistPath })],
44
45
  doc: docs.flutterStore,
45
46
  }
46
- : { ...head, status: "OK", value: "플레이스홀더 없음" });
47
+ : { ...head, status: "OK", value: t("core.flutterDoctor.value.noPlaceholder") });
47
48
  }
48
49
  return rows;
49
50
  }
@@ -1,5 +1,5 @@
1
- // Flutter 타입 훅 — types.js 레지스트리의 flutter 항목이 가리키는 구현 모음.
2
- // 공통 코드는 flutter를 직접 알지 못하고 훅 이름으로만 호출한다(각 훅의 계약은 types.js 머리말 참고).
1
+ // Flutter type hooks: the implementations the flutter entry of the types.js registry points to.
2
+ // Common code does not know flutter directly and calls it only by hook name (see the types.js header for each hook's contract).
3
3
  import { existsSync } from "node:fs";
4
4
  import {
5
5
  ENV_MODES, DEPLOY_MODES, DEFAULT_ENV_MODE, DEFAULT_DEPLOY_MODE, STORE_PLATFORMS, NO_STORE,
@@ -11,20 +11,21 @@ import { flutterStoreChecks } from "./flutter-doctor.js";
11
11
  import { inferInstalledStores } from "./installed-stores.js";
12
12
  import { escapeYamlDoubleQuoted } from "./wizard-env.js";
13
13
  import { CliError } from "./errors.js";
14
+ import { t } from "../i18n/index.js";
14
15
 
15
- // 저장값이 없으면 그 상태에서 실제로 적용되는 동작을 함께 알린다.
16
+ // When there is no saved value, also states the behavior that actually applies in that state.
16
17
  function statusLabels(options) {
17
18
  return [
18
- ` env_mode=${options.envMode ?? "미설정(dotenv 유지)"}`,
19
- ` flutter_store=${options.flutterStore ?? "미설정(둘 다 설치)"}`,
20
- ` android_deploy_mode=${options.androidDeployMode ?? "미설정(store_only)"}`,
21
- ` ios_deploy_mode=${options.iosDeployMode ?? "미설정(store_only)"}`,
19
+ ` env_mode=${options.envMode ?? t("core.flutterHooks.status.envModeUnset")}`,
20
+ ` flutter_store=${options.flutterStore ?? t("core.flutterHooks.status.storeUnset")}`,
21
+ ` android_deploy_mode=${options.androidDeployMode ?? t("core.flutterHooks.status.deployModeUnset")}`,
22
+ ` ios_deploy_mode=${options.iosDeployMode ?? t("core.flutterHooks.status.deployModeUnset")}`,
22
23
  ].join("");
23
24
  }
24
25
 
25
- // 스토어 저장값이 없는 기존 Flutter 설치는 설치돼 있는 스토어 워크플로우로 추론한다(대화형과 같은 결론).
26
- // 추론하지 않으면 미결정이 "둘 다"로 확정 저장되어, 지웠던 플랫폼의 워크플로우·fastlane 파일이 되살아난다.
27
- // workflowsDir가 없으면(status 등 읽기 전용 경로) 추론하지 않는다.
26
+ // An existing Flutter install with no saved store value is inferred from the installed store workflows (same conclusion as interactive).
27
+ // Without inference the undecided state would be saved as "both", resurrecting workflows and fastlane files of a platform that was removed.
28
+ // Without workflowsDir (read-only paths such as status) no inference is made.
28
29
  function resolveOptions({ opts = {}, existing = null, workflowsDir = null }) {
29
30
  const inferredStores = workflowsDir && opts.flutterStore == null && existing?.types?.includes("flutter")
30
31
  && existing.options?.flutterStore == null && existsSync(workflowsDir)
@@ -38,35 +39,35 @@ function resolveOptions({ opts = {}, existing = null, workflowsDir = null }) {
38
39
  });
39
40
  }
40
41
 
41
- // version.yml options 아래 Flutter 블록(6칸 들여쓰기). 값이 비었으면 워크플로우 템플릿 기본값과 같은 값으로 채운다 —
42
- // 저장값과 실제 설치 내용이 어긋나지 않게. stores가 null(미결정)이면 현행 동작대로 둘 다 설치되므로 "android,ios"로 기록한다.
42
+ // Flutter block under version.yml options (6-space indent). Empty values are filled with the same values as the workflow template defaults,
43
+ // so the saved values do not diverge from what was actually installed. When stores is null (undecided) both are installed as today, so "android,ios" is recorded.
43
44
  function versionOptionsBlock({ envMode, stores, androidDeployMode, iosDeployMode } = {}) {
44
45
  const quote = (v) => `"${escapeYamlDoubleQuoted(v)}"`;
45
46
  return [
46
- ` env_mode: ${quote(envMode || DEFAULT_ENV_MODE)} # ${ENV_MODES.join(" | ")} (Flutter 환경변수 주입 방식)`,
47
- ` flutter_store: ${quote(formatStoreList(stores ?? STORE_PLATFORMS))} # android | ios | android,ios | none (스토어 배포 대상)`,
48
- ` android_deploy_mode: ${quote(androidDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (Play Store 배포 모드)`,
49
- ` ios_deploy_mode: ${quote(iosDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (iOS 배포 모드)`,
47
+ ` env_mode: ${quote(envMode || DEFAULT_ENV_MODE)} # ${ENV_MODES.join(" | ")} ${t("core.flutterHooks.comment.envMode")}`,
48
+ ` flutter_store: ${quote(formatStoreList(stores ?? STORE_PLATFORMS))} # android | ios | android,ios | none ${t("core.flutterHooks.comment.store")}`,
49
+ ` android_deploy_mode: ${quote(androidDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} ${t("core.flutterHooks.comment.androidDeployMode")}`,
50
+ ` ios_deploy_mode: ${quote(iosDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} ${t("core.flutterHooks.comment.iosDeployMode")}`,
50
51
  ].join("\n");
51
52
  }
52
53
 
53
- // 값 목록 검증 플래그 하나를 만든다 — 값이 목록 밖이면 CliError.
54
+ // Builds one flag validated against a value list; CliError when the value is outside the list.
54
55
  const enumFlag = (flag, field, isValid, allowed) => ({
55
56
  flag, field, initial: "",
56
57
  parse(v) {
57
- if (!isValid(v)) throw new CliError(`${flag} 값이 올바르지 않습니다: ${v ?? "(없음)"} (${allowed.join(" | ")})`);
58
+ if (!isValid(v)) throw new CliError(t("core.flutterHooks.err.invalidFlag", { flag, value: v ?? t("core.flutterHooks.noValue"), allowed: allowed.join(" | ") }));
58
59
  return v;
59
60
  },
60
61
  });
61
62
 
62
63
  export const flutterHooks = {
63
64
  resolveOptions,
64
- // 확정된 옵션 → 설치 컨텍스트 필드. 기본값은 옵션이 없을 때의 "미결정" 상태다.
65
+ // Decided options to install-context fields. The defaults are the "undecided" state used when there are no options.
65
66
  contextDefaults: {
66
- envMode: "", // "dart-define" | "dotenv". ""=미결정 → 템플릿 기본값(dart-define)
67
- flutterStore: null, // 스토어 배포 대상 string[] (예: ["android","ios"]). null=미결정 → 둘 다(현행 동작)
68
- androidDeployMode: "", // store_only | store_prepare | store_submit. ""=미결정 → store_only
69
- iosDeployMode: "", // 위와 동일 (iOS)
67
+ envMode: "", // "dart-define" | "dotenv". ""=undecided, template default (dart-define) applies
68
+ flutterStore: null, // store deploy targets, string[] (e.g. ["android","ios"]). null=undecided, both (current behavior)
69
+ androidDeployMode: "", // store_only | store_prepare | store_submit. ""=undecided, store_only
70
+ iosDeployMode: "", // same as above (iOS)
70
71
  },
71
72
  contextFields: (options) => ({
72
73
  envMode: options.envMode,
@@ -78,21 +79,21 @@ export const flutterHooks = {
78
79
  envMode, stores: flutterStore, androidDeployMode, iosDeployMode,
79
80
  }),
80
81
  versionOptionsBlock,
81
- // version.yml 저장 키 → 파싱 결과 필드. 값은 원문 문자열로 돌려주고, 유효성 판정은 resolveOptions 몫이다.
82
+ // Saved version.yml key to parsed-result field. The value is returned as the raw string; validity is resolveOptions' job.
82
83
  savedOptionKeys: {
83
84
  env_mode: "envMode", flutter_store: "flutterStore",
84
85
  android_deploy_mode: "androidDeployMode", ios_deploy_mode: "iosDeployMode",
85
86
  },
86
- // CLI 플래그 — 파싱 결과 필드(opts)와 값 검증. initial은 미지정 값이다.
87
+ // CLI flags: parsed-result field (opts) and value validation. initial is the not-given value.
87
88
  cliFlags: [
88
89
  enumFlag("--flutter-env-mode", "flutterEnvMode", isEnvMode, ENV_MODES),
89
90
  {
90
91
  flag: "--flutter-store", field: "flutterStore", initial: null,
91
92
  parse(v) {
92
- // 빈 문자열은 parseStoreList가 []로 보지만, 스토어를 안 고르겠다는 뜻은 명시적인 none으로만 받는다.
93
+ // parseStoreList sees an empty string as [], but "select no store" is accepted only as an explicit none.
93
94
  const stores = v ? parseStoreList(v) : null;
94
95
  if (stores === null) {
95
- throw new CliError(`--flutter-store 값이 올바르지 않습니다: ${v || "(없음)"} (${[STORE_PLATFORMS.join(","), ...STORE_PLATFORMS, NO_STORE].join(" | ")})`);
96
+ throw new CliError(t("core.flutterHooks.err.invalidFlag", { flag: "--flutter-store", value: v || t("core.flutterHooks.noValue"), allowed: [STORE_PLATFORMS.join(","), ...STORE_PLATFORMS, NO_STORE].join(" | ") }));
96
97
  }
97
98
  return stores;
98
99
  },
@@ -100,20 +101,20 @@ export const flutterHooks = {
100
101
  enumFlag("--android-deploy-mode", "androidDeployMode", isDeployMode, DEPLOY_MODES),
101
102
  enumFlag("--ios-deploy-mode", "iosDeployMode", isDeployMode, DEPLOY_MODES),
102
103
  ],
103
- // 설치 직후 알릴 경고 — store_submit은 main push마다 심사를 자동 제출하므로 선택하지 않은 스토어에는 뜨면 안 된다.
104
+ // Warnings shown right after install: store_submit auto-submits a review on every main push, so it must not appear for a store that was not selected.
104
105
  installNotices: ({ stores, androidDeployMode, iosDeployMode }) => [
105
106
  (stores === null || stores.includes("android")) && deployModeWarning(androidDeployMode),
106
107
  (stores === null || stores.includes("ios")) && deployModeWarning(iosDeployMode),
107
108
  ],
108
- // 설치 로그에 남길 선택값 [이름, 값] 목록
109
+ // Selected values to record in the install log, as [name, value] pairs
109
110
  logChoices: (context) => {
110
- const stores = Array.isArray(context.flutterStore) ? (context.flutterStore.join(",") || "없음") : "미결정(둘 다)";
111
+ const stores = Array.isArray(context.flutterStore) ? (context.flutterStore.join(",") || t("core.flutterHooks.log.storesNone")) : t("core.flutterHooks.log.storesUndecided");
111
112
  return [["flutter",
112
113
  `env=${context.envMode || "-"} stores=${stores} android=${context.androidDeployMode || "-"} ios=${context.iosDeployMode || "-"}`]];
113
114
  },
114
- // 스토어 대상이 배열일 때만 스토어 워크플로우 필터를 건다(null=미결정 → 전부 설치).
115
+ // The store workflow filter applies only when the store target is an array (null=undecided, install everything).
115
116
  workflowFilter: ({ flutterStore }) => (Array.isArray(flutterStore) ? storeWorkflowFilter(flutterStore) : null),
116
- // 선택 해제된 스토어 워크플로우 정리 — 대상이 미결정(null)이면 아무것도 지우지 않는다.
117
+ // Cleans up deselected store workflows; when the target is undecided (null) nothing is deleted.
117
118
  cleanupWorkflows: (workflowsDir, installed, context, baseline, opts) => (Array.isArray(context.flutterStore)
118
119
  ? cleanupDeselectedStoreWorkflows(workflowsDir, installed, context.flutterStore, baseline, opts)
119
120
  : { removed: [], backedUp: [] }),