project-auto-wizard 0.13.3 → 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,19 +1,19 @@
1
- // 설치 시점 baseline — 업데이트에서 "누가 바꿨는지"를 가르는 기준점.
1
+ // Install-time baseline - the reference point that tells an update "who changed this".
2
2
  //
3
- // 왜 필요한가: isUnchanged()는 payload(theirs)와 설치본(ours)을 2-way로 비교한다.
4
- // base가 없으니 업스트림이 한 글자만 고쳐도 사용자가 손대지 않은 파일이 changed로 떨어지고,
5
- // 결국 "전부 skip(업데이트 못 받음)" 아니면 "전부 backup(사용자 수정 전멸)" 둘 중 하나만
6
- // 고를 수 있게 된다.
3
+ // Why it is needed: isUnchanged() compares the payload (theirs) and the installed copy (ours) 2-way.
4
+ // Without a base, upstream changing a single character drops a file the user never touched into
5
+ // "changed", so the only choices are "skip everything (no updates)" or "back up everything
6
+ // (all user edits lost)".
7
7
  //
8
- // 파일 사본이 아니라 해시만 남긴다 — 분류가 목적이지 자동 병합이 목적이 아니다.
8
+ // Only hashes are kept, not file copies - the goal is classification, not automatic merging.
9
9
  //
10
- // 해시를 두 개 두는 이유: env 치환으로 사용자 값이 들어간 파일은
11
- // 디스크 내용과 "기본값으로 렌더한 결과"가 애초에 다르다. 하나로는 두 질문에 동시에 답할 수 없다.
12
- // - installed : 설치 시점 우리가 디스크에 쓴 내용 → "사용자가 그 뒤에 손댔는가"
13
- // - rendered : 그 시점 payload를 기본값 치환한 결과 → "업스트림이 그 뒤에 바뀌었는가"
10
+ // Why two hashes: for files with user values substituted from env, the disk content and the
11
+ // "payload rendered with defaults" differ from the start, so one hash cannot answer both questions.
12
+ // - installed : what we wrote to disk at install time -> "did the user touch it since"
13
+ // - rendered : the payload rendered with defaults at that time -> "did upstream change it since"
14
14
  //
15
- // installed는 우리가 실제로 쓴 파일에만 채운다. 사용자 수정본을 installed로 기록하면
16
- // "우리가 쓴 것"이라고 거짓말하는 셈이고, 다음 업데이트에서 그 파일이 조용히 덮인다.
15
+ // installed is filled only for files we actually wrote. Recording a user-edited file as installed
16
+ // would claim "we wrote this", and the next update would silently overwrite it.
17
17
  import { createHash } from "node:crypto";
18
18
  import { join } from "node:path";
19
19
  import { existsSync, readFileSync } from "node:fs";
@@ -26,14 +26,14 @@ export function sha256(text) {
26
26
  return "sha256:" + createHash("sha256").update(String(text), "utf8").digest("hex");
27
27
  }
28
28
 
29
- // Flutter 앱 파일(Fastfile·ExportOptions.plist) 해시 — 체크아웃 줄바꿈(autocrlf)만 달라진 파일을
30
- // "사용자가 수정했다"로 보지 않도록 LF로 맞춘 뒤 해시한다.
29
+ // Hash of Flutter app files (Fastfile, ExportOptions.plist) - normalized to LF first so a file that
30
+ // differs only by checkout line endings (autocrlf) is not treated as "modified by the user".
31
31
  export function appFileHash(text) {
32
32
  return sha256(String(text).replace(/\r\n/g, "\n"));
33
33
  }
34
34
 
35
- // 없거나 깨졌으면 null — 호출부는 "base 미상"으로 폴백한다(조용히 빈 baseline을 쓰지 않는다.
36
- // 빈 baseline은 "기록이 없다"가 아니라 "전부 삭제됐다"로 오해될 수 있다).
35
+ // null when missing or corrupt - callers fall back to "base unknown" (we never silently use an empty
36
+ // baseline, which could be misread as "everything was deleted" rather than "no record").
37
37
  export function readBaseline(targetRoot = ".") {
38
38
  const p = join(targetRoot, BASELINE_PATH);
39
39
  if (!existsSync(p)) return null;
@@ -42,27 +42,27 @@ export function readBaseline(targetRoot = ".") {
42
42
  if (!data || typeof data !== "object" || typeof data.files !== "object" || data.files === null) return null;
43
43
  return data;
44
44
  } catch {
45
- return null; // 손상된 baseline은 없는 것으로 취급 — 업데이트를 막지 않는다
45
+ return null; // A corrupt baseline is treated as missing - it must not block the update
46
46
  }
47
47
  }
48
48
 
49
49
  // entries: Map<filename, {installed?:string|null, rendered?:string|null}>
50
- // appFiles: Map<레포 기준 상대경로, appFileHash> — 이번에 새로 만든 Flutter 앱 파일. 완전 삭제가
51
- // "마법사가 만들었고 사용자가 손대지 않은 파일"만 지우는 근거다. files는 워크플로우 파일명 키라 섞지 않는다.
52
- // 기존 baseline은 병합 대상이다 — 이번 실행에서 건드리지 않은 파일의 기준점을 잃지 않는다.
50
+ // appFiles: Map<repo-relative path, appFileHash> - Flutter app files newly created this run. It is what lets a
51
+ // full removal delete only "files the wizard created and the user did not touch". files is keyed by workflow filename, so they are not mixed.
52
+ // The existing baseline is merged in - files not touched this run keep their reference point.
53
53
  export function writeBaseline(targetRoot, { templateVersion, installedAt, entries, previous = null, appFiles = new Map() }) {
54
54
  const files = { ...(previous?.files || {}) };
55
55
  for (const [filename, entry] of entries) {
56
56
  const prev = files[filename] || {};
57
57
  files[filename] = {
58
- // installed는 이번에 실제로 쓴 경우에만 갱신. 유지(skip)한 파일은 예전 기준점을 지킨다.
58
+ // installed is updated only when actually written this run. Files kept (skipped) retain the old reference point.
59
59
  installed: entry.installed ?? prev.installed ?? null,
60
- // rendered가 없으면(충돌로 업스트림 변경을 받지 않은 파일) 예전 기준점을 지킨다.
60
+ // Without rendered (a file that did not take the upstream change due to a conflict) the old reference point is kept.
61
61
  rendered: entry.rendered ?? prev.rendered ?? null,
62
62
  };
63
63
  }
64
64
  const apps = { ...(previous?.appFiles || {}), ...Object.fromEntries(appFiles) };
65
- // 기준점이 하나도 바뀌지 않은 재실행은 설치 시각도 그대로 둔다 — 매 실행마다 파일이 바뀌면 멱등이 아니다.
65
+ // A re-run that changes no reference point keeps the install time too - a file that changes on every run is not idempotent.
66
66
  const unchanged = previous && previous.templateVersion === (templateVersion || "unknown")
67
67
  && JSON.stringify(previous.files) === JSON.stringify(files)
68
68
  && JSON.stringify(previous.appFiles || {}) === JSON.stringify(apps);
@@ -76,14 +76,14 @@ export function writeBaseline(targetRoot, { templateVersion, installedAt, entrie
76
76
  return out;
77
77
  }
78
78
 
79
- // baseline에는 있는데 디스크에 없는 파일 = 사용자가 지운 것.
80
- // 별도의 삭제 이력 파일이 필요 없다는 것이 이 설계의 부산물이다.
81
- // candidates: payload가 이번에 설치하려는 파일명 목록 (그 밖의 baseline 항목은 관심 없다)
79
+ // In the baseline but missing on disk = deleted by the user.
80
+ // A side benefit of this design is that no separate deletion-history file is needed.
81
+ // candidates: filenames the payload intends to install this run (other baseline entries are ignored)
82
82
  export function detectRemoved(baseline, candidates, workflowsDir) {
83
83
  if (!baseline) return [];
84
84
  const removed = [];
85
85
  for (const filename of candidates) {
86
- if (!baseline.files[filename]) continue; // 우리가 설치한 적 없는 파일 — 판단 근거 없음
86
+ if (!baseline.files[filename]) continue; // A file we never installed - no basis for a judgement
87
87
  if (existsSync(join(workflowsDir, filename))) continue;
88
88
  removed.push(filename);
89
89
  }
@@ -1,10 +1,11 @@
1
- // 브랜치 구성 — main/develop 브랜치와 pr-flow·trunk-based 모드.
2
- // on: push: branches: 는 YAML 정적 값 — 마법사가 릴리스/개발 브랜치를 물어(또는 플래그로 받아)
3
- // {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} 플레이스홀더를 치환한다 (치환 자체는 branding.js).
4
- // main === develop 이면 trunk-based 모드 → RELEASE-PUBLISH 단독 설치.
1
+ // Branch configuration: main/develop branches and the pr-flow / trunk-based modes.
2
+ // on: push: branches: is a static YAML value, so the wizard asks for (or takes via flags) the release/develop
3
+ // branches and substitutes the {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} placeholders (the substitution itself lives in branding.js).
4
+ // When main === develop it is trunk-based mode, and RELEASE-PUBLISH is installed alone.
5
5
  import { execFile } from "node:child_process";
6
+ import { t } from "../i18n/index.js";
6
7
 
7
- // 기본 exec — git 명령 실행. 반환 {code, stdout, stderr}. 테스트는 mock 주입.
8
+ // Default exec: runs a git command. Returns {code, stdout, stderr}. Tests inject a mock.
8
9
  export function defaultExec(cmd, args, { cwd } = {}) {
9
10
  return new Promise((resolve) => {
10
11
  execFile(cmd, args, { cwd, windowsHide: true }, (err, stdout, stderr) => {
@@ -13,22 +14,23 @@ export function defaultExec(cmd, args, { cwd } = {}) {
13
14
  });
14
15
  }
15
16
 
16
- // 감지: 로컬 git에서 원격 브랜치 목록 (네트워크 없이 — 로컬이 아는 origin/* 기준).
17
- // git이 없거나 레포가 아니면 빈 목록 (질문 기본값 경로로 폴백).
17
+ // Detection: remote branch list from local git (no network, based on the origin/* refs local knows).
18
+ // Empty list when git is missing or this is not a repo (falls back to the prompt-default path).
18
19
  export async function detectRemoteBranches(cwd, exec = defaultExec) {
19
20
  const r = await exec("git", ["branch", "-r", "--format=%(refname:short)"], { cwd });
20
21
  if (r.code !== 0) return [];
21
22
  return r.stdout
22
23
  .split(/\r?\n/)
23
24
  .map((s) => s.trim())
24
- .filter((s) => s && !s.includes("->")) // "origin/HEAD -> origin/main" 제외
25
+ .filter((s) => s && !s.includes("->")) // exclude "origin/HEAD -> origin/main"
25
26
  .map((s) => s.replace(/^origin\//, ""))
26
27
  .filter((s, i, a) => a.indexOf(s) === i);
27
28
  }
28
29
 
29
- // 브랜치 이름 검증 — git check-ref-format --branch 규칙에 더해, 워크플로우 YAML·셸 명령에 그대로
30
- // 치환되는 값이라 공백·괄호·따옴표 같은 셸 특수문자도 거부한다. 빈 원격이 돌려주는 "(unknown)" 같은
31
- // 감지 실패 값이나 공백만 입력한 값이 트리거에 기록되면 워크플로우가 영영 돌지 않는다.
30
+ // Branch name validation: on top of the git check-ref-format --branch rules, shell special characters such as
31
+ // whitespace, parentheses and quotes are rejected because the value is substituted verbatim into workflow YAML and
32
+ // shell commands. If a detection-failure value like "(unknown)" from an empty remote, or a whitespace-only input,
33
+ // were written into the trigger, the workflow would never run.
32
34
  export function isValidBranchName(name) {
33
35
  if (typeof name !== "string" || name === "") return false;
34
36
  if (/[\s~^:?*[\\\x00-\x1f\x7f()"'`$;&|<>!{}]/.test(name)) return false;
@@ -38,23 +40,23 @@ export function isValidBranchName(name) {
38
40
  return !name.split("/").some((part) => part.startsWith("."));
39
41
  }
40
42
 
41
- // 원격에 브랜치가 하나도 없거나(빈 원격·origin 없음) develop 생성을 건너뛴 경우의 안내.
42
- // develop이 없으면 pr-flow 워크플로우가 트리거되지 않으므로 조용히 넘어가면 안 된다.
43
+ // Notice for when the remote has no branches at all (empty remote, no origin) or develop creation was skipped.
44
+ // Without develop the pr-flow workflows are never triggered, so this must not pass silently.
43
45
  export function developMissingNotice({ main, develop }) {
44
- return `원격에 '${develop}' 브랜치가 없습니다 — 설치 파일을 커밋해 '${main}'을 push한 뒤 만들어 주세요: git push origin ${main}:${develop}`;
46
+ return t("core.branches.developMissing", { main, develop });
45
47
  }
46
48
 
47
- // 결정 (순수 함수): 플래그/답변 → 최종 구성.
48
- // 우선순위: 명시값(mainBranch/developBranch) → 감지 default → 하드 폴백(main/develop).
49
+ // Decision (pure function): flags/answers to the final configuration.
50
+ // Priority: explicit values (mainBranch/developBranch), then detected default, then hard fallback (main/develop).
49
51
  export function resolveBranchConfig({ mainBranch = "", developBranch = "", defaultBranch = "" } = {}) {
50
52
  const main = mainBranch || defaultBranch || "main";
51
53
  const develop = developBranch || "develop";
52
54
  return { main, develop, mode: main === develop ? "trunk-based" : "pr-flow" };
53
55
  }
54
56
 
55
- // 브랜치 선택 프롬프트용 정렬. def를 최우선으로, 그다음 priority(main/develop)
56
- // 순서로 목록 앞에 배치한다. 나머지는 remoteBranches의 원래 순서(git이 준 알파벳순)를 유지한다.
57
- // 순수 함수 — remoteBranches 원본은 변경하지 않는다.
57
+ // Ordering for the branch selection prompt. def goes first, then priority (main/develop) in that order, at the
58
+ // front of the list. The rest keep remoteBranches' original order (alphabetical as given by git).
59
+ // Pure function: the remoteBranches original is not modified.
58
60
  export function sortBranchesForSelection(remoteBranches, def, priority = ["main", "develop"]) {
59
61
  const priorityOrder = [def, ...priority].filter((b, i, arr) => arr.indexOf(b) === i);
60
62
  const inPriority = priorityOrder.filter((b) => remoteBranches.includes(b));
@@ -62,27 +64,27 @@ export function sortBranchesForSelection(remoteBranches, def, priority = ["main"
62
64
  return [...inPriority, ...rest];
63
65
  }
64
66
 
65
- // 생성: develop이 원격에 없으면 현 HEAD 기준으로 생성 + push.
66
- // confirm: async(message)→bool — 대화형 확인 질문. null이면(--force) 질문 없이 자동 생성.
67
- // exec 주입 가능 (테스트 mock). 반환 { created, pushed?, skipped? }.
67
+ // Creation: when develop is missing on the remote, create it from the current HEAD and push.
68
+ // confirm: async(message)->bool, the interactive confirmation. When null (--force) it is created without asking.
69
+ // exec is injectable (test mock). Returns { created, pushed?, skipped? }.
68
70
  export async function ensureDevelopBranch({ develop, remoteBranches = [], confirm = null, cwd, exec = defaultExec, log = null }) {
69
71
  if (remoteBranches.includes(develop)) return { created: false };
70
72
 
71
73
  if (confirm) {
72
- const ok = await confirm(`원격에 '${develop}' 브랜치가 없습니다. 현재 HEAD 기준으로 생성하고 push할까요?`);
74
+ const ok = await confirm(t("core.branches.confirmCreate", { develop }));
73
75
  if (ok !== true) return { created: false, skipped: true };
74
76
  }
75
77
 
76
78
  const br = await exec("git", ["branch", develop], { cwd });
77
79
  if (br.code !== 0) {
78
- // 이미 로컬에 존재하는 경우 등 — push만 시도
79
- log?.(`'${develop}' 로컬 브랜치 생성 생략 (${(br.stderr || "").trim() || "이미 존재"})`);
80
+ // e.g. it already exists locally: just try the push
81
+ log?.(t("core.branches.localSkipped", { develop, reason: (br.stderr || "").trim() || t("core.branches.alreadyExists") }));
80
82
  }
81
83
  const push = await exec("git", ["push", "-u", "origin", develop], { cwd });
82
84
  if (push.code !== 0) {
83
- log?.(`⚠️ '${develop}' 브랜치 push 실패 — 원격 설정을 확인한 뒤 수동으로 push하세요 (git push -u origin ${develop})`);
85
+ log?.(t("core.branches.pushFailed", { develop }));
84
86
  return { created: true, pushed: false };
85
87
  }
86
- log?.(`'${develop}' 브랜치를 생성하고 push했습니다.`);
88
+ log?.(t("core.branches.created", { develop }));
87
89
  return { created: true, pushed: true };
88
90
  }
@@ -1,15 +1,16 @@
1
- // 브랜치 플레이스홀더 치환 파이프라인.
2
- // payload 워크플로우의 {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}}를 설치 시 실제 브랜치명으로 바꾼다.
3
- // ⚠️ GitHub Actions 표현식 `${{ ... }}`은 우리 토큰이 아니다 — `$` 선행 시 제외(lookbehind).
4
- // 치환 후 알 수 없는 {{TOKEN}}이 남으면 throw — 복사 무결성 가드 (오타·누락 조기 검출).
1
+ // Branch placeholder substitution pipeline.
2
+ // Replaces {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} in payload workflows with the real branch names at install time.
3
+ // WARNING: the GitHub Actions expression `${{ ... }}` is not our token, so a leading `$` excludes it (lookbehind).
4
+ // Throws when an unknown {{TOKEN}} remains after substitution: a copy-integrity guard (catches typos and omissions early).
5
+ import { t } from "../i18n/index.js";
5
6
 
6
7
  const TOKEN_RE = /(?<!\$)\{\{([A-Z][A-Z0-9_]*)\}\}/g;
7
8
 
8
- // substitute(text, {main, develop}) — 브랜치 토큰 치환. 미지 토큰 발견 시 throw.
9
+ // substitute(text, {main, develop}): substitutes branch tokens. Throws when an unknown token is found.
9
10
  export function substitute(text, { main, develop }) {
10
11
  const map = { MAIN_BRANCH: main, DEVELOP_BRANCH: develop };
11
12
  return String(text).replace(TOKEN_RE, (_, name) => {
12
13
  if (name in map) return map[name];
13
- throw new Error(`알 수 없는 플레이스홀더가 남았습니다: {{${name}}}`);
14
+ throw new Error(t("core.branding.unknownPlaceholder", { name }));
14
15
  });
15
16
  }
@@ -1,32 +1,33 @@
1
- // breaking-changes 확인 흐름 배선.
2
- // collectBreaking(순수 비교)은 breaking.js — 이 모듈은 로드(패키지 번들본)·표시·확인 게이트.
1
+ // Wiring for the breaking-changes confirmation flow.
2
+ // collectBreaking (pure comparison) is in breaking.js - this module handles loading (bundled copy), display and the confirmation gate.
3
3
  import { readFileSync, existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
- import { collectBreaking } from "./breaking.js";
5
+ import { collectBreaking, localizedField } from "./breaking.js";
6
6
  import { parseExisting } from "./version-yml.js";
7
+ import { t } from "../i18n/index.js";
7
8
 
8
- // 패키지에 동봉된 payload/config 번들본만 읽는다. 원격(main)을 읽으면 같은 패키지 버전이라도
9
- // 실행 시점마다 결과가 달라지고 설치 중 네트워크 요청이 생긴다. 읽기 실패 시 null(조용히 스킵).
9
+ // Reads only the payload/config copy bundled with the package. Reading remote (main) would make results
10
+ // differ per run even for the same package version, and add a network request during install. null on read failure (silent skip).
10
11
  export function loadBreakingJson(payloadRoot) {
11
12
  try {
12
13
  const p = join(payloadRoot, "config", "breaking-changes.json");
13
14
  if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
14
- } catch { /* 번들 읽기 실패 — 스킵 */ }
15
+ } catch { /* bundle read failed - skip */ }
15
16
  return null;
16
17
  }
17
18
 
18
- // 반환: true=진행, false=사용자 취소.
19
+ // Returns: true = proceed, false = user cancelled.
19
20
  // opts:
20
- // cwd - 통합 대상 루트 (기존 version.yml에서 현재 템플릿 버전 읽음)
21
- // payloadRoot - 패키지 payload/ 루트 (번들 breaking-changes.json 위치)
22
- // templateVersion - 설치하려는 템플릿 버전 (고정값이 아니라 실제 패키지 버전)
23
- // askYesNo - async(message, defaultYes)→bool. null이면 비대화형: 경고만 출력 후 진행
24
- // loader - 테스트 주입용 (기본 loadBreakingJson)
21
+ // cwd - integration target root (current template version is read from the existing version.yml)
22
+ // payloadRoot - package payload/ root (where the bundled breaking-changes.json lives)
23
+ // templateVersion - template version being installed (the actual package version, not a fixed value)
24
+ // askYesNo - async(message, defaultYes)->bool. null = non-interactive: print the warning and proceed
25
+ // loader - injected by tests (default loadBreakingJson)
25
26
  export async function runBreakingCheck({ cwd, payloadRoot, templateVersion, askYesNo = null, loader = loadBreakingJson }) {
26
27
  const vy = join(cwd, "version.yml");
27
- if (!existsSync(vy)) return true; // 신규 통합 — 비교 기준 없음
28
+ if (!existsSync(vy)) return true; // New integration - nothing to compare against
28
29
  const { templateVersion: current, types } = parseExisting(readFileSync(vy, "utf8"));
29
- if (!current) return true; // 템플릿 메타 없음(unknown) — 비교 기준이 없어 스킵
30
+ if (!current) return true; // No template metadata (unknown) - nothing to compare against, skip
30
31
 
31
32
  const json = await loader(payloadRoot);
32
33
  if (!json) return true;
@@ -34,26 +35,26 @@ export async function runBreakingCheck({ cwd, payloadRoot, templateVersion, askY
34
35
  const { critical, warnings } = collectBreaking(json, current, templateVersion, types);
35
36
  if (critical.length === 0 && warnings.length === 0) return true;
36
37
 
37
- // 박스 표시
38
+ // Box display
38
39
  const e = (s = "") => process.stderr.write(s + "\n");
39
40
  e("");
40
41
  e("╔══════════════════════════════════════════════════════════════════╗");
41
42
  e(`║ ⚠️ BREAKING CHANGES (v${current} → v${templateVersion})`);
42
43
  e("╠══════════════════════════════════════════════════════════════════╣");
43
- for (const c of critical) { e("║"); e(`║ [CRITICAL] ${c.version} - ${c.title || ""}`); e(`║ → ${c.message || ""}`); }
44
- for (const w of warnings) { e("║"); e(`║ [WARNING] ${w.version} - ${w.title || ""}`); e(`║ → ${w.message || ""}`); }
44
+ for (const c of critical) { e("║"); e(`║ [CRITICAL] ${c.version} - ${localizedField(c, "title")}`); e(`║ → ${localizedField(c, "message")}`); }
45
+ for (const w of warnings) { e("║"); e(`║ [WARNING] ${w.version} - ${localizedField(w, "title")}`); e(`║ → ${localizedField(w, "message")}`); }
45
46
  e("║");
46
47
  e("╚══════════════════════════════════════════════════════════════════╝");
47
48
  e("");
48
49
 
49
50
  if (critical.length > 0) {
50
51
  if (askYesNo) {
51
- // 대화형: 명시 확인 없으면 중단 (기본 N)
52
- const ok = await askYesNo("위 호환성 변경을 확인했고 계속 진행할까요?", false);
52
+ // Interactive: abort without explicit confirmation (default N)
53
+ const ok = await askYesNo(t("core.breakingCheck.confirm"), false);
53
54
  if (ok !== true) return false;
54
55
  } else {
55
- // 비대화형(--force): 게이트로 CI를 죽이지 않고 경고 후 진행 (CI 친화)
56
- e("⚠️ CRITICAL 호환성 변경이 있습니다 — 비대화형 실행이라 계속 진행합니다. 위 내용을 꼭 확인하세요.");
56
+ // Non-interactive (--force): do not kill CI with a gate, warn and proceed (CI friendly)
57
+ e(t("core.breakingCheck.nonInteractive"));
57
58
  }
58
59
  }
59
60
  return true;
@@ -1,4 +1,6 @@
1
- // 버전 비교: v 접두 제거, 3자리 숫자 비교, 누락 자리=0
1
+ import { getLanguage } from "../i18n/index.js";
2
+
3
+ // Version comparison: strips the v prefix, compares 3 numeric parts, missing part = 0
2
4
  export function compareVersions(a, b) {
3
5
  const parse = (v) => String(v).replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
4
6
  const pa = parse(a), pb = parse(b);
@@ -10,19 +12,24 @@ export function compareVersions(a, b) {
10
12
  return 0;
11
13
  }
12
14
 
13
- // breaking-changes.json에서 current < ver <= target 범위 항목 수집.
14
- // target은 고정값이 아니라 실제 templateVersion을 넘긴다 — 그래야 설치하려는 버전까지의 고지가 모두 잡힌다.
15
- // _ 로 시작하는 키(메타) 제외. severity critical / 그 외(warning).
16
- // 버전 키의 값은 항목 객체 또는 항목 객체의 배열(같은 릴리스에 고지가 여러 건일 때).
17
- // types: 설치된 프로젝트 타입. 항목에 types가 있으면 겹치는 타입이 있을 때만 보여준다 — spring 레포에
18
- // Flutter 전용 경고가 뜨면 진짜 알려야 할 고지가 묻힌다. types를 모르면(빈 배열) 전부 보여준다.
15
+ // Text of an entry in the given language: `title_ko` / `message_ko` when present, otherwise the plain (English) field.
16
+ export function localizedField(entry, field, lang = getLanguage()) {
17
+ return entry?.[`${field}_${lang}`] || entry?.[field] || "";
18
+ }
19
+
20
+ // Collect entries from breaking-changes.json with current < ver <= target.
21
+ // target is the actual templateVersion, not a fixed value - so every notice up to the version being installed is caught.
22
+ // Keys starting with _ (metadata) are skipped. severity critical / anything else (warning).
23
+ // A version key's value is an entry object or an array of entry objects (several notices in one release).
24
+ // types: installed project types. When an entry has types it is shown only if one overlaps - a
25
+ // Flutter-only warning on a spring repo would bury the notices that really matter. Unknown types (empty array) shows everything.
19
26
  export function collectBreaking(json, current, target, types = []) {
20
27
  const critical = [], warnings = [];
21
28
  const relevant = (entry) => !Array.isArray(entry?.types) || !types.length || entry.types.some((t) => types.includes(t));
22
29
  for (const [ver, value] of Object.entries(json || {})) {
23
30
  if (ver.startsWith("_")) continue;
24
31
  if (compareVersions(current, ver) < 0 && compareVersions(ver, target) <= 0) {
25
- // 한 릴리스에 고지가 여러 건이면 배열로 등록한다 — JSON 키(버전)는 유일해서 객체로는 한 건만 담긴다.
32
+ // Several notices in one release are registered as an array - JSON keys (versions) are unique, so an object holds only one.
26
33
  for (const entry of Array.isArray(value) ? value : [value]) {
27
34
  if (!relevant(entry)) continue;
28
35
  const rec = { version: ver, ...entry };
@@ -1,8 +1,9 @@
1
- // 타입 전용 앱 파일(사용자 소유, 없을 때만 생성) 설치 — 타입 훅(planAppFiles/copyAppFiles)을 돌려 합친다.
2
- // 어떤 파일이 대상인지는 훅이 정하므로 여기서는 타입 이름을 알지 못한다.
1
+ // Installs type-specific app files (user-owned, created only when missing) by running the type hooks
2
+ // (planAppFiles/copyAppFiles) and merging their results.
3
+ // Which files are targeted is decided by the hooks, so this module knows no type names.
3
4
  import { hooksFor, typeInfo } from "../types.js";
4
5
 
5
- // 읽기 전용 — dry-run이 쓴다. 반환: { created:[], kept:[] }
6
+ // Read-only, used by dry-run. Returns { created:[], kept:[] }
6
7
  export function planTypeAppFiles(context, payloadRoot, targetRoot = ".") {
7
8
  const result = { created: [], kept: [] };
8
9
  for (const { hook } of hooksFor(context.types || [], "planAppFiles")) {
@@ -13,7 +14,7 @@ export function planTypeAppFiles(context, payloadRoot, targetRoot = ".") {
13
14
  return result;
14
15
  }
15
16
 
16
- // onFile(tag, action, relPath): 파일마다 호출되는 로그 콜백. 반환: { created:[], kept:[] }
17
+ // onFile(tag, action, relPath): log callback invoked per file. Returns { created:[], kept:[] }
17
18
  export function copyTypeAppFiles(context, payloadRoot, targetRoot = ".", onFile = () => {}) {
18
19
  const result = { created: [], kept: [] };
19
20
  for (const { id, hook } of hooksFor(context.types || [], "copyAppFiles")) {
@@ -1,7 +1,7 @@
1
- // Flutter 앱 소유 파일(fastlane·ExportOptions.plist) 설치.
2
- // 워크플로우와 달리 사용자가 값을 채워 넣는 파일이라 "없을 때만 생성"한다 — 덮어쓰지 않고 baseline
3
- // 3-way도 적용하지 않는다(기존 사용자 파일을 덮어쓰지 않는 규약). 원본 갱신을 기존 사용자에게
4
- // 전파하는 것은 범위 밖이다.
1
+ // Installs Flutter app-owned files (fastlane, ExportOptions.plist).
2
+ // Unlike workflows these are files the user fills in, so they are created only when missing: never
3
+ // overwritten and no baseline 3-way applied (existing user files are not overwritten by convention).
4
+ // Propagating upstream changes to existing users is out of scope.
5
5
  import { join, posix } from "node:path";
6
6
  import { existsSync } from "node:fs";
7
7
  import { copyFileSync } from "../fsutil.js";
@@ -9,8 +9,8 @@ import { storeAppFilesFor } from "../flutter-options.js";
9
9
 
10
10
  const FLUTTER_APP_DIR = "flutter-app"; // payload/flutter-app/
11
11
 
12
- // 이번 실행에서 다룰 파일의 (보고용 상대경로, 원본, 목적지) 목록.
13
- // 대상은 Flutter 타입이 있을 때만이고, 선택된 플랫폼(context.flutterStore, null이면 둘 다)의 파일로 한정한다.
12
+ // List of (reported relative path, source, destination) for the files handled in this run.
13
+ // Only when the Flutter type is present, limited to the selected platforms (context.flutterStore, null = both).
14
14
  function flutterAppTargets(context, payloadRoot, targetRoot) {
15
15
  const { types = [], paths = new Map(), flutterStore = null } = context;
16
16
  if (!types.includes("flutter")) return [];
@@ -22,7 +22,7 @@ function flutterAppTargets(context, payloadRoot, targetRoot) {
22
22
  }));
23
23
  }
24
24
 
25
- // 읽기 전용 — status/dry-run이 쓴다. 원본 payload는 읽지 않는다.
25
+ // Read-only, used by status/dry-run. Does not read the source payload.
26
26
  export function planFlutterAppFiles(context, payloadRoot, targetRoot = ".") {
27
27
  const result = { created: [], kept: [] };
28
28
  for (const { reported, dst } of flutterAppTargets(context, payloadRoot, targetRoot)) {
@@ -35,7 +35,7 @@ export function copyFlutterAppFiles(context, payloadRoot, targetRoot = ".") {
35
35
  const result = { created: [], kept: [] };
36
36
  for (const { reported, src, dst } of flutterAppTargets(context, payloadRoot, targetRoot)) {
37
37
  if (existsSync(dst)) { result.kept.push(reported); continue; }
38
- copyFileSync(src, dst); // 부모 디렉토리 생성 + 바이트 그대로(CRLF 보존)
38
+ copyFileSync(src, dst); // creates parent directories and copies bytes as-is (preserves CRLF)
39
39
  result.created.push(reported);
40
40
  }
41
41
  return result;