project-auto-wizard 0.13.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.ja.md +2 -0
  2. package/README.ko.md +2 -0
  3. package/README.md +3 -1
  4. package/README.zh-CN.md +2 -0
  5. package/bin/project-auto-wizard.js +18 -4
  6. package/package.json +2 -2
  7. package/payload/config/breaking-changes.json +28 -14
  8. package/payload/config/wizard-prompts.yml +102 -46
  9. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  10. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  11. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  12. package/payload/scripts/changelog_manager.py +304 -265
  13. package/payload/scripts/issue_helper.py +54 -50
  14. package/payload/scripts/messages.py +2585 -0
  15. package/payload/scripts/truncate_release_notes.py +7 -6
  16. package/payload/scripts/version_manager.py +110 -107
  17. package/payload/version.yml.template +2 -1
  18. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +23 -16
  19. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +106 -63
  20. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  21. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +55 -45
  22. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +90 -72
  23. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +59 -45
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  29. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  30. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  31. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  32. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  33. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  34. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  35. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  36. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  37. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  38. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  39. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  40. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  41. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  42. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  44. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  45. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  46. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  47. package/src/cli/args.js +98 -52
  48. package/src/cli/help.js +8 -41
  49. package/src/commands/doctor.js +90 -89
  50. package/src/commands/dry-run.js +64 -43
  51. package/src/commands/full.js +79 -77
  52. package/src/commands/install-settings.js +16 -15
  53. package/src/commands/interactive-flutter.js +20 -19
  54. package/src/commands/interactive.js +135 -132
  55. package/src/commands/purge.js +47 -42
  56. package/src/commands/status.js +36 -36
  57. package/src/commands/uninstall.js +43 -41
  58. package/src/context.js +12 -11
  59. package/src/core/assets.js +12 -11
  60. package/src/core/baseline.js +27 -27
  61. package/src/core/branches.js +29 -27
  62. package/src/core/branding.js +7 -6
  63. package/src/core/breaking-check.js +22 -21
  64. package/src/core/breaking.js +15 -8
  65. package/src/core/copy/app-files.js +5 -4
  66. package/src/core/copy/flutter-app.js +8 -8
  67. package/src/core/copy/gitignore.js +58 -50
  68. package/src/core/copy/readme.js +102 -36
  69. package/src/core/copy/simple.js +16 -15
  70. package/src/core/copy/workflows.js +131 -124
  71. package/src/core/deploy-style.js +51 -48
  72. package/src/core/detect-fs.js +57 -57
  73. package/src/core/detect.js +67 -65
  74. package/src/core/errors.js +2 -2
  75. package/src/core/flutter-doctor.js +17 -16
  76. package/src/core/flutter-hooks.js +33 -32
  77. package/src/core/flutter-options.js +34 -33
  78. package/src/core/fsutil.js +12 -12
  79. package/src/core/installed-stores.js +4 -4
  80. package/src/core/logger.js +42 -40
  81. package/src/core/paths-resolve.js +88 -86
  82. package/src/core/paths.js +10 -10
  83. package/src/core/release-options.js +8 -8
  84. package/src/core/removal-exec.js +14 -13
  85. package/src/core/removal-plan.js +37 -37
  86. package/src/core/types.js +47 -47
  87. package/src/core/verify.js +44 -38
  88. package/src/core/version-yml.js +76 -68
  89. package/src/core/wizard-env.js +46 -46
  90. package/src/core/wizard-labels.js +63 -41
  91. package/src/i18n/catalog/en/cli.js +50 -0
  92. package/src/i18n/catalog/en/commands.js +188 -0
  93. package/src/i18n/catalog/en/copy.js +20 -0
  94. package/src/i18n/catalog/en/core.js +74 -0
  95. package/src/i18n/catalog/en/core2.js +15 -0
  96. package/src/i18n/catalog/en/ui.js +221 -0
  97. package/src/i18n/catalog/en.js +12 -0
  98. package/src/i18n/catalog/index.js +5 -0
  99. package/src/i18n/catalog/ko/cli.js +50 -0
  100. package/src/i18n/catalog/ko/commands.js +188 -0
  101. package/src/i18n/catalog/ko/copy.js +20 -0
  102. package/src/i18n/catalog/ko/core.js +74 -0
  103. package/src/i18n/catalog/ko/core2.js +15 -0
  104. package/src/i18n/catalog/ko/ui.js +221 -0
  105. package/src/i18n/catalog/ko.js +10 -0
  106. package/src/i18n/index.js +44 -0
  107. package/src/i18n/languages.js +9 -0
  108. package/src/index.js +132 -97
  109. package/src/ui/ansi.js +7 -7
  110. package/src/ui/banner.js +7 -6
  111. package/src/ui/env-plan.js +82 -81
  112. package/src/ui/prompts.js +97 -122
  113. package/src/ui/readline-engine.js +59 -58
  114. package/src/ui/status-cards.js +37 -31
  115. package/src/ui/summary.js +90 -89
@@ -1,6 +1,6 @@
1
- // full 모드 오케스트레이터 — 대화형·비대화형이 확정한 context로 실제 설치를 수행한다.
2
- // 복사 순서: workflows(+env 치환) → flutter 앱 파일 → version.yml → readme → scripts → gitignore(조건부)
3
- // gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다.
1
+ // full mode orchestrator: performs the actual install from the context settled by interactive/non-interactive mode.
2
+ // Copy order: workflows (+env substitution) -> flutter app files -> version.yml -> readme -> scripts -> gitignore (conditional)
3
+ // gitignore is updated only when conflict backup byproducts (.bak/.template.yaml) were actually created in this run.
4
4
  import { join } from "node:path";
5
5
  import { existsSync, readFileSync, readdirSync } from "node:fs";
6
6
  import { writeText, findUnwritable } from "../core/fsutil.js";
@@ -20,33 +20,34 @@ import { cleanupOtherDeployWorkflows, payloadWorkflowNames, DEFAULT_DEPLOY_STYLE
20
20
  import { hooksFor } from "../core/types.js";
21
21
  import { findStaleWorkflows, cleanupStaleWorkflows } from "../core/removal-plan.js";
22
22
  import { log, maskValue } from "../core/logger.js";
23
+ import { t } from "../i18n/index.js";
23
24
 
24
25
  // context: { version, types, paths:Map, branch, versionCode,
25
26
  // force, repoName, resolvers, now, today }
26
- // payloadRoot: 패키지 payload/ 루트. targetRoot: 통합 대상.
27
+ // payloadRoot: the package's payload/ root. targetRoot: the install target.
27
28
  export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
28
29
  const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
29
30
  force = true, now, today, templateVersion = "unknown",
30
31
  includeSemverAuto } = context;
31
32
 
32
- // 아무것도 쓰기 전에 권한부터 확인한다 — 도중에 멈추면 반쯤 설치된 상태가 남는다.
33
+ // Check permissions before writing anything: stopping midway would leave a half-installed state.
33
34
  const blocked = findUnwritable(targetRoot,
34
35
  [".", PATHS.workflowsDir, PATHS.scriptsDir, ".github/.wizard"],
35
36
  [PATHS.versionFile, "README.md", ".gitignore", ".github/.wizard/baseline.json"]);
36
37
  if (blocked.length) {
37
- throw new CliError(`쓰기 권한이 없어 설치를 시작하지 않았습니다 (아무 파일도 바꾸지 않았습니다):\n${blocked.map((p) => ` - ${p}`).join("\n")}\n권한을 확인한 뒤 다시 실행하세요.`);
38
+ throw new CliError(t("cmd.full.noWritePermission", { list: blocked.map((p) => ` - ${p}`).join("\n") }));
38
39
  }
39
40
 
40
- // project_paths 마커 계산.
41
- // 대표 마커명이 아니라 그 폴더에 실제로 있는 파일을 쓴다 — build.gradle.kts만 있는 레포의
42
- // version.yml에 "# build.gradle"이라고 적히면 감지 로그와 같은 종류의 거짓말이 된다.
41
+ // Compute the project_paths markers.
42
+ // Use the file that actually exists in that folder, not the canonical marker name: writing "# build.gradle"
43
+ // into version.yml of a repo that only has build.gradle.kts would be the same kind of lie as in the detection log.
43
44
  const pathMarkers = new Map();
44
- for (const [t, p] of paths) {
45
- const marker = existingMarkerInDir(t, join(targetRoot, p || "."));
46
- pathMarkers.set(t, marker);
47
- // 파일이 실제로 없으면 사용자가 직접 고른 타입이다 — 대표 파일명을 근거로 적으면 감지된 것처럼 보인다.
45
+ for (const [type, p] of paths) {
46
+ const marker = existingMarkerInDir(type, join(targetRoot, p || "."));
47
+ pathMarkers.set(type, marker);
48
+ // If the file does not actually exist, the user picked the type by hand; citing the canonical file name as evidence would look like detection.
48
49
  const found = marker && existsSync(join(targetRoot, p || ".", marker));
49
- log.info("detect", "type", `${t} (근거: ${found ? marker : "직접 선택"})`);
50
+ log.info("detect", "type", t("cmd.full.log.typeEvidence", { type, evidence: found ? marker : t("cmd.full.log.userChosen") }));
50
51
  }
51
52
  log.info("detect", "version", `${version}${context.versionSource ? ` (${context.versionSource})` : ""}`);
52
53
  log.info("detect", "branch", `${branch}${context.branches ? ` | main=${context.branches.main} develop=${context.branches.develop} mode=${context.branches.mode}` : ""}`);
@@ -55,63 +56,63 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
55
56
  log.info("prompt", a.isDefault ? "default" : "answer", `${a.key}=${maskValue(a.key, a.value)}`);
56
57
  }
57
58
 
58
- // 1. 워크플로우 복사 (+ env 치환) — deploy 블록에 쓸 ask 값을 수집한다.
59
- // hooks.decisions: 대화형 충돌 3지선 결정 Map (미지정=skip — 현행 force 동작)
59
+ // 1. Copy workflows (+ env substitution) and collect the ask values for the deploy block.
60
+ // hooks.decisions: Map of interactive three-way conflict decisions (unset = skip, the current force behavior)
60
61
  const wfCounters = copyWorkflows(context, payloadRoot, targetRoot, hooks);
61
62
  const deployValues = wfCounters.deployValues || new Map(); // Map<type, Map<key,value>>
62
63
 
63
- // 1-1. Flutter 앱 파일(fastlane·ExportOptions) — 사용자가 값을 채워 쓰는 파일이라 없을 때만 만든다.
64
- // 워크플로우 복사가 끝난 뒤에 실행한다(워크플로우가 이 파일들을 전제로 돈다).
64
+ // 1-1. Flutter app files (fastlane, ExportOptions): users fill these in, so create them only when missing.
65
+ // Runs after the workflow copy finishes (the workflows assume these files).
65
66
  const flutterApp = copyTypeAppFiles(context, payloadRoot, targetRoot, (tag, action, f) => {
66
- log.info(tag, action, action === "keep" ? `${f} (기존 파일 유지)` : f);
67
+ log.info(tag, action, action === "keep" ? t("cmd.full.log.keepExisting", { file: f }) : f);
67
68
  });
68
69
 
69
- // 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다.
70
+ // Preserve unknown top-level fields of an existing version.yml when regenerating it.
70
71
  const vyPath = join(targetRoot, PATHS.versionFile);
71
72
  const prevVy = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
72
73
  const extraTopLevel = prevVy != null ? parseExisting(prevVy).extraTopLevel : [];
73
74
 
74
- // 2. version.yml 생성 (payload/version.yml.template 렌더링 — 전체 재생성)
75
- // 날짜 줄만 달라지는 재실행은 다시 쓰지 않는다(멱등).
75
+ // 2. Generate version.yml (render payload/version.yml.template; full regeneration)
76
+ // A re-run where only the date lines differ does not rewrite it (idempotent).
76
77
  const vyText = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel });
77
78
  if (prevVy == null || !sameIgnoringTimestamps(prevVy, vyText)) {
78
79
  writeText(vyPath, vyText);
79
80
  log.info("version", "write", `version.yml (v${version}, code=${versionCode})`);
80
81
  } else {
81
- log.info("version", "skip", `version.yml (v${version}, code=${versionCode}, 변경 없음)`);
82
+ log.info("version", "skip", t("cmd.full.log.versionUnchanged", { version, code: versionCode }));
82
83
  }
83
84
 
84
- // 3. README 버전 섹션
85
+ // 3. README version section
85
86
  const readme = addVersionSectionToReadme(version, targetRoot);
86
87
  log.info("readme", readme === "added" ? "append" : "skip", README_STATUS_LABEL[readme] || readme);
87
88
 
88
- // 4. scripts (payload/scripts/*.py → .github/scripts/) — 항상 덮어쓰므로 사용자 수정이 사라진 사실도 남긴다.
89
+ // 4. scripts (payload/scripts/*.py -> .github/scripts/): always overwritten, so also record that user edits are lost.
89
90
  const scripts = copyScripts(payloadRoot, targetRoot);
90
91
  for (const { name, action } of scripts) {
91
- log.info("script", action, `${PATHS.scriptsDir}/${name}${action === "overwrite" ? " (기존 내용과 달라 새 버전으로 덮어씀)" : ""}`);
92
+ log.info("script", action, `${PATHS.scriptsDir}/${name}${action === "overwrite" ? t("cmd.full.log.scriptOverwritten") : ""}`);
92
93
  }
93
94
  const bytecodeRemoved = removeScriptBytecode(targetRoot);
94
- for (const rel of bytecodeRemoved) log.info("script", "remove", `${rel} (예전 버전이 커밋한 바이트코드 — 삭제를 함께 커밋하세요)`);
95
+ for (const rel of bytecodeRemoved) log.info("script", "remove", t("cmd.full.log.bytecodeRemoved", { file: rel }));
95
96
 
96
- // 5. gitignore — 워크플로우 충돌 처리가 .bak나 .template.yaml을 실제로 만든 경우에만 갱신한다.
97
- // 충돌 없는 설치(대부분의 최초 설치)는 .gitignore를 전혀 건드리지 않는다.
97
+ // 5. gitignore: updated only when conflict handling actually created .bak or .template.yaml files.
98
+ // Installs without conflicts (most first installs) do not touch .gitignore at all.
98
99
  const gitignoreUpdated0 = wfCounters.backupAdded > 0 || wfCounters.templateAdded > 0;
99
100
 
100
- // 6. 이전 배포 방식 정리 — 방식을 바꿔 재설치하면 이전 CD가 남아 배포가 두 번 돈다.
101
- // 옛 baseline이 살아 있는 지금이 "사용자가 손댔는가"를 판정할 수 있는 유일한 시점이다.
101
+ // 6. Clean up the previous deploy style: reinstalling with a different style would leave the old CD and deploy twice.
102
+ // Now, while the old baseline is still alive, is the only time we can judge "did the user touch it?".
102
103
  const previousBaseline = readBaseline(targetRoot);
103
104
  const { cleanup, storeCleanup, staleCleanup } = cleanupWorkflows(context, payloadRoot, targetRoot, previousBaseline,
104
105
  { justWritten: wfCounters.copiedFiles || [] });
105
- // 지운 파일의 기준점은 baseline에서도 빼야 다음 실행에서 "사용자가 지웠다"로 오인하지 않는다.
106
+ // Drop removed files' reference points from the baseline too, or the next run mistakes them for "deleted by the user".
106
107
  for (const r of [cleanup, storeCleanup, staleCleanup]) {
107
108
  for (const f of [...r.removed, ...r.backedUp]) delete previousBaseline?.files?.[f];
108
109
  }
109
110
 
110
- for (const f of cleanup.removed || []) log.info("cleanup", "remove", `${f} (이전 배포 방식 정리)`);
111
+ for (const f of cleanup.removed || []) log.info("cleanup", "remove", `${f} (${t("cmd.dryRun.reason.cleanup")})`);
111
112
  for (const f of cleanup.backedUp || []) log.info("cleanup", "backup", `${f} → ${f}.bak`);
112
- for (const f of storeCleanup.removed) log.info("cleanup", "remove", `${f} (선택 해제된 스토어 워크플로우 정리)`);
113
+ for (const f of storeCleanup.removed) log.info("cleanup", "remove", `${f} (${t("cmd.dryRun.reason.storeCleanup")})`);
113
114
  for (const f of storeCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
114
- for (const f of staleCleanup.removed) log.info("cleanup", "remove", `${f} (현재 버전에 없는 이전 워크플로우 정리)`);
115
+ for (const f of staleCleanup.removed) log.info("cleanup", "remove", `${f} (${t("cmd.dryRun.reason.staleCleanup")})`);
115
116
  for (const f of staleCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
116
117
  const cleanupCount = (key) => [cleanup, storeCleanup, staleCleanup].reduce((n, r) => n + r[key].length, 0);
117
118
 
@@ -120,13 +121,13 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
120
121
  if (gitignoreUpdated) {
121
122
  const gi = ensureGitignore(targetRoot);
122
123
  log.info("gitignore", gi.created ? "create" : gi.added.length ? "append" : "skip",
123
- gi.added.length ? `.gitignore += ${gi.added.join(", ")}` : ".gitignore (이미 있는 항목)");
124
+ gi.added.length ? `.gitignore += ${gi.added.join(", ")}` : t("cmd.full.log.gitignoreExisting"));
124
125
  }
125
126
 
126
- // 7. baseline 기록 — 다음 업데이트에서 "누가 바꿨는지"를 가를 기준점.
127
- // env 치환까지 전부 끝난 뒤에 해시해야 디스크 내용이 최종형이다. 그래서 copyWorkflows 안이
128
- // 아니라 여기서 기록한다.
129
- // 기존 baseline은 병합 대상 — 이번에 건드리지 않은 파일의 기준점을 잃지 않는다.
127
+ // 7. Write the baseline: the reference point that tells "who changed it" on the next update.
128
+ // Hash only after env substitution is fully done so the on-disk content is final; that is why it is
129
+ // recorded here rather than inside copyWorkflows.
130
+ // The existing baseline is merged, so reference points for files not touched this time are not lost.
130
131
  writeBaseline(targetRoot, {
131
132
  templateVersion,
132
133
  installedAt: now || today || "",
@@ -136,52 +137,53 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
136
137
  makeSrcText(context.branches || null, context.deployStyle || ""),
137
138
  deployValues),
138
139
  previous: previousBaseline,
139
- // 새로 만든 Flutter 앱 파일만 기록한다 — 기존 사용자 파일(kept)은 완전 삭제 대상이 아니다.
140
+ // Record only newly created Flutter app files; existing user files (kept) are not candidates for full removal.
140
141
  appFiles: new Map(flutterApp.created.map((rel) => [rel, appFileHash(readFileSync(join(targetRoot, rel), "utf8"))])),
141
142
  });
142
143
 
143
- // 8. 설치 후 검증 — 디스크에 실제로 쓰인 내용을 다시 읽어 확인한다.
144
- // 미치환 플레이스홀더가 남았는지, 어떤 Secret이 있어야 워크플로우가 도는지.
145
- // 설치를 실패시키지는 않는다 — 사실을 알려주는 것이 목적이고, 판단은 사용자 몫이다.
144
+ // 8. Post-install verification: re-read what was actually written to disk.
145
+ // Whether unsubstituted placeholders remain, and which Secrets the workflows need to run.
146
+ // It does not fail the install; the goal is to report facts and leave the judgment to the user.
146
147
  const wfDir = join(targetRoot, PATHS.workflowsDir);
147
148
  const managed = [...(wfCounters.baselineTargets || new Map()).keys()];
148
149
  const unresolved = scanUnsubstituted(wfDir, managed);
149
- // 기본값이 있거나 둘 중 하나면 되는 secret은 필수와 나눠 안내한다.
150
+ // Secrets that have a default or where either of two is enough are reported separately from the required ones.
150
151
  const secretSets = classifySecrets(wfDir, managed);
151
152
  const secrets = narrowSecretsBySshAuth(secretSets.required, firstDeployValue(deployValues, "SSH_AUTH_METHOD"));
152
153
  const optionalSecrets = secretSets.optional;
153
154
 
154
- // 9. 요약 — 파일 끝에 결과 블록을 붙인다. tail만 봐도 결과가 보이도록.
155
+ // 9. Summary: append a result block at the end of the file so the outcome is visible from the tail alone.
155
156
  for (const u of unresolved) log.warn("verify", "unresolved", `${u.filename}:${u.line} ${u.token}`);
156
157
  for (const [name, users] of secrets) log.info("verify", "secret", `${name} ← ${users.join(", ")}`);
157
- for (const [name, users] of optionalSecrets) log.info("verify", "secret-opt", `${name} (선택) ← ${users.join(", ")}`);
158
- // copiedFiles에는 자동 갱신분도 들어 있다 — 빼지 않으면 같은 파일이 설치와 자동 갱신에 두 번 잡힌다.
158
+ for (const [name, users] of optionalSecrets) log.info("verify", "secret-opt", t("cmd.full.log.secretOptional", { name, users: users.join(", ") }));
159
+ // copiedFiles also contains the auto-updated ones; without excluding them the same file is counted as both installed and auto-updated.
159
160
  const autoUpdated = new Set(wfCounters.autoUpdated || []);
160
161
  log.summary([
161
- ["설치", `${(wfCounters.copiedFiles || []).filter((f) => !autoUpdated.has(f)).length}개 파일`],
162
- ["자동 갱신", `${autoUpdated.size}개 (사용자 미수정)`],
163
- ["유지", `${(wfCounters.keptLocal || []).length}개 (사용자 수정본)`],
164
- ["변경 없음", `${(wfCounters.unchangedFiles || []).length}개`],
165
- ["백업 교체", `${wfCounters.backupAdded || 0}개 (.bak 생성)`],
166
- // 배포 방식 변경·스토어 해제·옛 워크플로우 정리도 파일을 지우거나 .bak으로 옮긴다 — 충돌 백업과 따로 센다.
167
- ["정리", `삭제 ${cleanupCount("removed")}개, .bak 이동 ${cleanupCount("backedUp")}개`],
168
- ["미치환", `${unresolved.length}건${unresolved.length ? " ← 조치 필요" : ""}`],
169
- ["필요 Secret", `${secrets.size}개`],
170
- ["결과", unresolved.length ? `주의 (미치환 ${unresolved.length}건)` : "OK"],
162
+ [t("cmd.full.summary.installed"), t("cmd.full.summary.installedValue", { n: (wfCounters.copiedFiles || []).filter((f) => !autoUpdated.has(f)).length })],
163
+ [t("cmd.full.summary.autoUpdated"), t("cmd.full.summary.autoUpdatedValue", { n: autoUpdated.size })],
164
+ [t("cmd.full.summary.kept"), t("cmd.full.summary.keptValue", { n: (wfCounters.keptLocal || []).length })],
165
+ [t("cmd.full.summary.unchanged"), t("cmd.full.summary.unchangedValue", { n: (wfCounters.unchangedFiles || []).length })],
166
+ [t("cmd.full.summary.backup"), t("cmd.full.summary.backupValue", { n: wfCounters.backupAdded || 0 })],
167
+ // Deploy style changes, store deselection and old-workflow cleanup also delete files or move them to .bak; counted separately from conflict backups.
168
+ [t("cmd.full.summary.cleanup"), t("cmd.full.summary.cleanupValue", { removed: cleanupCount("removed"), backedUp: cleanupCount("backedUp") })],
169
+ [t("cmd.full.summary.unresolved"), t(unresolved.length ? "cmd.full.summary.unresolvedActionValue" : "cmd.full.summary.unresolvedValue", { n: unresolved.length })],
170
+ [t("cmd.full.summary.secrets"), t("cmd.full.summary.secretsValue", { n: secrets.size })],
171
+ [t("cmd.full.summary.result"), unresolved.length ? t("cmd.full.summary.resultWarn", { n: unresolved.length }) : "OK"],
171
172
  ]);
172
173
 
173
174
  return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup, storeCleanup, staleCleanup, flutterApp, readme, scripts, bytecodeRemoved, optionalSecrets };
174
175
  }
175
176
 
176
- // 워크플로우 정리 3종 — 실제 설치(runFull)와 --dry-run 미리보기가 같은 판정을 쓴다.
177
- // 6 배포 방식 정리 — 방식을 바꿔 재설치하면 이전 CD가 남아 배포가 두 번 돈다.
178
- // 6-1 선택 해제된 스토어 워크플로우 정리 — 스토어 대상을 줄여 재설치하면 이전 워크플로우가 main push마다
179
- // 계속 돈다. 선택이 미결정(null)이거나 Flutter가 없으면 아무것도 지우지 않는다.
180
- // Fastfile·ExportOptions는 사용자 소유라 여기서 다루지 않는다.
181
- // 6-2 payload에서 이름이 바뀌거나 빠진 옛 워크플로우 정리.
182
- // 규칙은 모두 같다(미수정 삭제, 수정본 .bak). 세 대상은 파일명이 겹치지 않아 순서와 무관하게 판정이 같다.
183
- // baseline은 정리 전의 것을 넘겨야 한다 — "사용자가 손댔는가"를 판정할 수 있는 유일한 기준이다.
184
- // dryRun이면 판정만 하고 파일은 건드리지 않는다.
177
+ // Three workflow cleanups: the real install (runFull) and the --dry-run preview use the same decisions.
178
+ // 6 Deploy style cleanup: reinstalling with a different style would leave the old CD and deploy twice.
179
+ // 6-1 Deselected store workflow cleanup: reinstalling with fewer store targets would let the old workflows
180
+ // keep running on every main push. Nothing is deleted when the selection is undecided (null) or Flutter is absent.
181
+ // Fastfile and ExportOptions belong to the user and are not handled here.
182
+ // 6-2 Cleanup of old workflows that were renamed or dropped from the payload.
183
+ // The rules are the same for all (delete if unmodified, .bak if modified). The three sets never share file
184
+ // names, so the outcome is independent of order.
185
+ // Pass the pre-cleanup baseline: it is the only reference that can judge "did the user touch it?".
186
+ // With dryRun it only decides and leaves the files alone.
185
187
  export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { justWritten = [], dryRun = false } = {}) {
186
188
  const workflowsDir = join(targetRoot, PATHS.workflowsDir);
187
189
  const listDir = () => (existsSync(workflowsDir) ? readdirSync(workflowsDir) : []);
@@ -198,8 +200,8 @@ export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { j
198
200
  return { cleanup, storeCleanup, staleCleanup };
199
201
  }
200
202
 
201
- // 설치 결과를 가른 선택(배포 방식·자동 승격·Copilot·타입 전용 옵션)을 남긴다 — 대화형에서 고른 값도
202
- // 여기로 모이므로, 나중에 "왜 이렇게 설치됐나"를 로그만으로 따라갈 수 있다.
203
+ // Record the choices that shaped the install result (deploy style, auto bump, Copilot, type-specific options).
204
+ // Values picked interactively converge here too, so "why was it installed like this?" can later be traced from the log alone.
203
205
  function logChoices(context, types) {
204
206
  if (context.deployStyle) log.info("option", "deploy", context.deployStyle);
205
207
  if (context.includeSemverAuto != null) log.info("option", "semver", context.includeSemverAuto ? "on" : "off");
@@ -209,25 +211,25 @@ function logChoices(context, types) {
209
211
  }
210
212
  }
211
213
 
212
- // 완료 요약과 별도로 알려야 하는 사실 — 비대화형 실행이 조용히 넘어가면 사용자는 업데이트를 다 받았다고 믿는다.
213
- // interactive: 대화형은 충돌을 사용자가 직접 골랐으므로 충돌 안내를 빼고 정리 결과만 알린다.
214
+ // Facts to report separately from the completion summary: if a non-interactive run passes silently, users believe they received the whole update.
215
+ // interactive: in interactive mode the user picked the conflict outcome, so omit the conflict notice and report only the cleanup result.
214
216
  export function postInstallNotices(result, { interactive = false } = {}) {
215
217
  const lines = [];
216
218
  const stale = result?.staleCleanup || { removed: [], backedUp: [] };
217
219
  if (stale.removed.length || stale.backedUp.length) {
218
- lines.push("현재 버전에 없는 이전 워크플로우를 정리했습니다 (이름이 바뀌었거나 제거된 파일):");
219
- for (const f of stale.removed) lines.push(` • ${f} — 삭제 (손대지 않은 파일)`);
220
- for (const f of stale.backedUp) lines.push(` • ${f} → ${f}.bak — 수정하신 내용이 있어 백업`);
220
+ lines.push(t("cmd.full.notice.staleHeading"));
221
+ for (const f of stale.removed) lines.push(t("cmd.full.notice.staleRemoved", { file: f }));
222
+ for (const f of stale.backedUp) lines.push(t("cmd.full.notice.staleBackedUp", { file: f }));
221
223
  }
222
224
  const kept = interactive ? [] : result?.workflows?.conflictKept || [];
223
225
  if (kept.length) {
224
- lines.push(`충돌 ${kept.length}개 — 기존 파일을 유지했습니다 (업스트림 변경 미반영): ${kept.join(", ")}`);
225
- lines.push(" 파일마다 고르려면 대화형(npx project-auto-wizard)으로 다시 실행하세요.");
226
+ lines.push(t("cmd.full.notice.conflictKept", { n: kept.length, files: kept.join(", ") }));
227
+ lines.push(t("cmd.full.notice.conflictHint"));
226
228
  }
227
229
  return lines;
228
230
  }
229
231
 
230
- // deployValues는 Map<type, Map<key,value>> — 타입 구분 없이 첫 값만 필요할 때 쓴다.
232
+ // deployValues is Map<type, Map<key,value>>; used when only the first value is needed regardless of type.
231
233
  function firstDeployValue(deployValues, key) {
232
234
  for (const [, asks] of deployValues) {
233
235
  const v = asks.get(key);
@@ -1,45 +1,46 @@
1
- // 설치 설정 해석 단계 — 비대화형(index.js)과 대화형(interactive.js)이 공유한다.
2
- // 값을 채우는 방법(플래그·질문)은 경로마다 다르지만, 타입·버전·배포 방식·옵션을 확정하고
3
- // 설치 컨텍스트로 조립하는 규칙은 한 곳에 둬야 한쪽만 고쳐 두 모드의 결과가 갈라지지 않는다.
1
+ // Install settings resolution step, shared by non-interactive (index.js) and interactive (interactive.js) mode.
2
+ // How values are filled in (flags vs. questions) differs per path, but the rules that settle type, version,
3
+ // deploy style and options and assemble the install context belong in one place, so fixing only one side
4
+ // cannot make the two modes diverge.
4
5
  import { detectVersion, detectBuildNumber } from "../core/detect-fs.js";
5
6
  import { createContext } from "../context.js";
6
7
  import { mergeHookResults } from "../core/types.js";
7
8
  import { isDeployStyle, DEFAULT_DEPLOY_STYLE, hasServerDeployWorkflows, effectiveDeployStyle } from "../core/deploy-style.js";
8
9
 
9
- // version.yml에 저장된 배포 방식 — 없거나 알 수 없는 값이면 "".
10
+ // Deploy style saved in version.yml; "" when missing or unknown.
10
11
  export const savedDeployStyle = (existing) =>
11
12
  isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
12
13
 
13
- // 배포 방식 — 명시값(플래그·질문 답변) → 저장값 → 기본값.
14
- // 서버 배포 워크플로우가 없는 타입은 설치 결과에 영향이 없으므로 기록하지 않는다(null).
14
+ // Deploy style: explicit value (flag or question answer) -> saved value -> default.
15
+ // Types without server deploy workflows do not affect the install result, so nothing is recorded (null).
15
16
  export function resolveDeployStyle({ payload, types, explicit = "", existing }) {
16
17
  if (!hasServerDeployWorkflows(payload, types)) return null;
17
18
  return explicit || savedDeployStyle(existing) || DEFAULT_DEPLOY_STYLE;
18
19
  }
19
20
 
20
- // 버전 — 기존 version.yml 최우선(재실행 시 덮어쓰기 방지) → 명시값 → 파일 감지.
21
- // detectOpts는 감지 함수로 그대로 넘긴다(types·paths·warn·hint).
21
+ // Version: existing version.yml first (prevents overwriting on re-run) -> explicit value -> file detection.
22
+ // detectOpts is passed to the detection function as is (types, paths, warn, hint).
22
23
  export function resolveVersion({ cwd, existing, explicit = "", ...detectOpts }) {
23
24
  return existing?.version || explicit || detectVersion(cwd, detectOpts);
24
25
  }
25
26
 
26
- // 빌드 번호 — 기존 값 보존, 신규 통합이면 프로젝트 파일에서 감지, 못 찾으면 1.
27
+ // Build number: keep the existing value; for a new install detect it from the project files; 1 if not found.
27
28
  export function resolveVersionCode({ cwd, existing, types, paths }) {
28
29
  return existing?.versionCode ?? detectBuildNumber(cwd, { types, paths }) ?? 1;
29
30
  }
30
31
 
31
- // 확정된 값들을 설치 컨텍스트로 조립한다.
32
- // deployStyle: resolveDeployStyle 결과(고른 값). 실제 설치되는 방식으로 바꿔 기록한다.
33
- // typeOptions: 타입 훅(resolveOptions)이 확정한 타입 전용 옵션 — 두 경로 모두 확정된 값. 컨텍스트 필드로의 변환도 타입 훅이 한다.
34
- // releaseOptions: { includeSemverAuto, includeCopilotAi } — resolveReleaseOptions 결과.
35
- // extra: 경로별로만 쓰는 필드(envValues 등).
32
+ // Assemble the settled values into the install context.
33
+ // deployStyle: result of resolveDeployStyle (the chosen value); recorded as the style actually installed.
34
+ // typeOptions: type-specific options settled by the type hook (resolveOptions), settled on both paths; the type hook also converts them into context fields.
35
+ // releaseOptions: { includeSemverAuto, includeCopilotAi }, the resolveReleaseOptions result.
36
+ // extra: fields used by one path only (envValues etc.).
36
37
  export function buildInstallContext({
37
38
  payload, existing, templateVersion, types, deployStyle, typeOptions, releaseOptions, ...rest
38
39
  }) {
39
40
  return createContext({
40
41
  types,
41
42
  ...releaseOptions,
42
- // 안내는 고른 값으로 하되, 기록은 실제로 설치되는 방식으로 한다.
43
+ // Guidance uses the chosen value, but the record uses the style actually installed.
43
44
  deployStyle: effectiveDeployStyle(payload, types, deployStyle),
44
45
  ...mergeHookResults(types, "contextFields", typeOptions),
45
46
  previousTemplateVersion: existing?.templateVersion || "",
@@ -1,21 +1,22 @@
1
- // 대화형 마법사의 Flutter 옵션 질문 — 환경변수 방식 · 스토어 배포 대상 · 배포 모드.
2
- // 상태는 { envMode, stores, androidDeployMode, iosDeployMode } 불변 객체로 주고받는다.
3
- // envMode·*DeployMode의 ""와 stores의 null = 미결정 (stores의 빈 배열 = "스토어 배포 안 함"으로 이미 결정됨).
4
- // 이미 결정된 값은 다시 묻지 않는다 (version.yml 저장값 재질문 생략 규약). ESC(취소)는 항상 기본값:
5
- // 처음 묻는 질문이면 기본값, 수정 중이면 현재값을 유지한다.
1
+ // Flutter option questions of the interactive wizard - env mode, store deploy targets, deploy mode.
2
+ // State is passed around as an immutable { envMode, stores, androidDeployMode, iosDeployMode } object.
3
+ // "" for envMode/*DeployMode and null for stores mean undecided (an empty stores array means "no store deploy", already decided).
4
+ // Already-decided values are not asked again (convention: saved version.yml values skip the question). ESC (cancel) always means default:
5
+ // the default for a first-time question, the current value while editing.
6
6
  import {
7
7
  isEnvMode, isDeployMode, parseStoreList, STORE_PLATFORMS, DEFAULT_DEPLOY_MODE,
8
8
  } from "../core/flutter-options.js";
9
9
  import { deployModeWarning } from "../ui/prompts.js";
10
+ import { t } from "../i18n/index.js";
10
11
 
11
12
  export const FLUTTER_EDIT_ITEMS = new Set(["envMode", "flutterStore", "deployMode"]);
12
13
 
13
14
  const DEPLOY_MODE_KEY = { android: "androidDeployMode", ios: "iosDeployMode" };
14
15
 
15
- // STORE_PLATFORMS 순서로 정렬하고 모르는 값은 버린다 — 플랫폼별 질문 순서를 고정한다.
16
+ // Sort by STORE_PLATFORMS order and drop unknown values - keeps the per-platform question order fixed.
16
17
  const normalizeStores = (picked) => STORE_PLATFORMS.filter((platform) => picked.includes(platform));
17
18
 
18
- // 기존 version.yml의 저장값을 상태로 옮긴다. 잘못된 값은 저장이 없는 것으로 보고 다시 묻는다.
19
+ // Move the saved values of the existing version.yml into state. Invalid values are treated as not saved and asked again.
19
20
  export function savedFlutterState(existing) {
20
21
  const options = existing?.options ?? {};
21
22
  return {
@@ -28,13 +29,13 @@ export function savedFlutterState(existing) {
28
29
 
29
30
  async function askDeployMode(io, platform, initialValue) {
30
31
  const picked = await io.selectDeployMode({ platform, initialValue });
31
- const mode = isDeployMode(picked) ? picked : initialValue; // ESC = 기본값
32
+ const mode = isDeployMode(picked) ? picked : initialValue; // ESC = default
32
33
  const warning = deployModeWarning(mode);
33
- if (warning) io.note?.(warning, "배포 모드");
34
+ if (warning) io.note?.(warning, t("interactive.flutter.deployModeTitle"));
34
35
  return mode;
35
36
  }
36
37
 
37
- // 고른 플랫폼 중 배포 모드가 아직 없는 것만 묻는다.
38
+ // Ask only for chosen platforms that have no deploy mode yet.
38
39
  async function askUnsetDeployModes(io, state) {
39
40
  const next = { ...state };
40
41
  for (const platform of state.stores ?? []) {
@@ -44,23 +45,23 @@ async function askUnsetDeployModes(io, state) {
44
45
  return next;
45
46
  }
46
47
 
47
- // 아직 정해지지 않은 옵션만 묻는다.
48
- // envModeDefault — 신규 설치 dart-define / 기존 설치 dotenv (동작 보존)
49
- // inferredStores — 저장값 없는 기존 설치가 이미 쓰던 스토어 (신규 설치는 CLI 기본값과 같은 전체)
48
+ // Ask only the options that are not decided yet.
49
+ // envModeDefault - dart-define for new installs / dotenv for existing installs (preserves behaviour)
50
+ // inferredStores - stores an existing install without saved values already used (new installs get all, same as the CLI default)
50
51
  export async function askUnsetFlutterOptions(io, state, { envModeDefault, inferredStores }) {
51
52
  const next = { ...state };
52
53
  if (!next.envMode) {
53
54
  const picked = await io.selectEnvMode({ initialValue: envModeDefault });
54
- next.envMode = isEnvMode(picked) ? picked : envModeDefault; // ESC = 기본값
55
+ next.envMode = isEnvMode(picked) ? picked : envModeDefault; // ESC = default
55
56
  }
56
57
  if (next.stores === null) {
57
58
  const picked = await io.selectFlutterStores({ initialValues: inferredStores });
58
- next.stores = Array.isArray(picked) ? normalizeStores(picked) : inferredStores; // ESC = 초기 선택
59
+ next.stores = Array.isArray(picked) ? normalizeStores(picked) : inferredStores; // ESC = initial selection
59
60
  }
60
61
  return askUnsetDeployModes(io, next);
61
62
  }
62
63
 
63
- // 수정하기 메뉴에서 고른 항목 하나를 다시 묻는다. 현재값이 초기 선택이고 ESC는 현재값 유지.
64
+ // Re-ask one item chosen in the edit menu. The current value is the initial selection and ESC keeps it.
64
65
  export async function editFlutterOption(io, what, state, envModeDefault) {
65
66
  if (what === "envMode") {
66
67
  const picked = await io.selectEnvMode({ initialValue: state.envMode || envModeDefault });
@@ -70,18 +71,18 @@ export async function editFlutterOption(io, what, state, envModeDefault) {
70
71
  const picked = await io.selectFlutterStores({ initialValues: state.stores ?? [] });
71
72
  if (!Array.isArray(picked)) return state;
72
73
  const stores = normalizeStores(picked);
73
- // 해제된 플랫폼의 배포 모드는 초기화한다 — 그대로 두면 재선택 시 옛 값이 남아 다시 묻지 않는다.
74
+ // Reset the deploy mode of deselected platforms - otherwise the old value survives re-selection and is never asked again.
74
75
  const reset = { ...state, stores };
75
76
  for (const platform of STORE_PLATFORMS) {
76
77
  if (!stores.includes(platform)) reset[DEPLOY_MODE_KEY[platform]] = "";
77
78
  }
78
- // 새로 추가되거나 방금 초기화된 플랫폼만 배포 모드를 묻는다 — 이미 정한 플랫폼의 모드는 그대로 둔다.
79
+ // Ask the deploy mode only for platforms newly added or just reset - modes of already-decided platforms are kept.
79
80
  return askUnsetDeployModes(io, reset);
80
81
  }
81
82
  if (what === "deployMode") {
82
83
  const stores = state.stores ?? [];
83
84
  if (!stores.length) {
84
- io.note?.("스토어 배포 대상을 먼저 선택하세요 ('스토어 배포 대상' 항목).", "배포 모드");
85
+ io.note?.(t("interactive.flutter.storeFirst"), t("interactive.flutter.deployModeTitle"));
85
86
  return state;
86
87
  }
87
88
  const next = { ...state };