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,9 +1,9 @@
1
- // purge 모드 — planRemoval이 판별하는 전부(워크플로우·스크립트) + version.yml·README
2
- // AUTO-VERSION-SECTION 블록·.gitignore 자동 추가 항목·릴리스 워크플로우가 만든 CHANGELOG를
3
- // 추가로 제거해 설치 이전 상태로 완전히 되돌린다. 마법사가 만들지 않은 파일은 지우지 않는다.
4
- // 개발·테스트 전용 숨김 모드.
5
- // develop 브랜치 삭제는 파일 삭제와 성격이 달라(실행 시점 git 상태 판단 필요) 여기 plan에는
6
- // 포함하지 않고 index.js의 purge 분기에서 직접 처리한다.
1
+ // purge mode: on top of everything planRemoval identifies (workflows, scripts), also removes version.yml, the README
2
+ // AUTO-VERSION-SECTION block, auto-added .gitignore entries and the CHANGELOG created by the release
3
+ // workflow, restoring the pre-install state completely. Files the wizard did not create are never deleted.
4
+ // Hidden development/testing mode.
5
+ // Deleting the develop branch differs in nature from deleting files (it needs a run-time git state
6
+ // judgment), so it is not part of this plan and is handled directly in the purge branch of index.js.
7
7
  import { join } from "node:path";
8
8
  import { existsSync, readFileSync } from "node:fs";
9
9
  import { PATHS } from "../core/paths.js";
@@ -11,12 +11,17 @@ import { planRemoval, backupArtifacts } from "../core/removal-plan.js";
11
11
  import { executeRemoval } from "../core/removal-exec.js";
12
12
  import { hasVersionSection } from "../core/copy/readme.js";
13
13
  import { hasAutoAddedEntries } from "../core/copy/gitignore.js";
14
+ import { t } from "../i18n/index.js";
14
15
 
15
16
  const CHANGELOG_FILES = ["CHANGELOG.json", "CHANGELOG.md"];
16
17
 
17
- // changelog_manager.py가 만든 CHANGELOG인지 — 사용자가 원래 쓰던 CHANGELOG.md를 지우지 않기 위해 내용으로 가린다.
18
- // CHANGELOG.json은 metadata·releases 구조로, CHANGELOG.md는 generate-md가 쓰는 머리말로 판단한다.
19
- // JSON이 우리 것이면 MD는 릴리스마다 JSON에서 통째로 재생성되므로 함께 우리 것이다.
18
+ // Whether the CHANGELOG was created by changelog_manager.py: judged by content so a CHANGELOG.md the user
19
+ // wrote themselves is not deleted. CHANGELOG.json is recognized by its metadata/releases structure, and
20
+ // CHANGELOG.md by the header generate-md writes. If the JSON is ours, the MD is regenerated wholesale from
21
+ // the JSON on every release, so it is ours too.
22
+ // The header's label may be localized by the script, so both the Korean and English wording are accepted
23
+ // (the Korean label is written as escapes to keep this source free of Hangul).
24
+ const GENERATED_MD_HEADER = /^# Changelog\n\n\*\*(?:\uD604\uC7AC \uBC84\uC804|Current version):\*\*/;
20
25
  function readText(targetRoot, f) {
21
26
  try { return readFileSync(join(targetRoot, f), "utf8").replace(/\r\n/g, "\n"); } catch { return null; }
22
27
  }
@@ -31,20 +36,20 @@ function isGeneratedChangelogJson(targetRoot) {
31
36
  function generatedChangelogFiles(targetRoot) {
32
37
  const jsonOurs = isGeneratedChangelogJson(targetRoot);
33
38
  const md = readText(targetRoot, "CHANGELOG.md");
34
- const mdOurs = md !== null && (jsonOurs || md.startsWith("# Changelog\n\n**현재 버전:**"));
39
+ const mdOurs = md !== null && (jsonOurs || GENERATED_MD_HEADER.test(md));
35
40
  return CHANGELOG_FILES.filter((f) => (f === "CHANGELOG.json" ? jsonOurs : mdOurs));
36
41
  }
37
42
 
38
- // keepFlags: { versionYml, readme, changelog, workflows, scripts } — true인 카테고리는 후보에서 제외.
39
- // 반환: { workflows, scripts, versionYml, readmeSection, gitignore, changelog } — 아무것도 지우지 않는 순수 함수.
43
+ // keepFlags: { versionYml, readme, changelog, workflows, scripts }: categories set to true are excluded from the candidates.
44
+ // Returns { workflows, scripts, versionYml, readmeSection, gitignore, changelog }: a pure function that deletes nothing.
40
45
  export function planPurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
41
46
  const removalPlan = planRemoval(payloadRoot, targetRoot);
42
- // 워크플로우를 남기면 그 .bak/.template.yaml도 남는다 — .gitignore 항목까지 지우면 백업 파일이 git 상태에 드러난다.
47
+ // If the workflows stay, so do their .bak/.template.yaml; removing the .gitignore entries too would surface the backup files in git status.
43
48
  const gitignoreKept = hasAutoAddedEntries(targetRoot) && keepFlags.workflows === true
44
49
  && backupArtifacts(removalPlan.workflows).length > 0;
45
50
  return {
46
51
  workflows: keepFlags.workflows ? [] : removalPlan.workflows,
47
- // Flutter 앱 파일의 생성 기록은 baseline에 있으므로 baseline과 같은 플래그를 따른다.
52
+ // The creation record of Flutter app files lives in the baseline, so they follow the same flag as the baseline.
48
53
  appFiles: keepFlags.workflows ? [] : removalPlan.appFiles,
49
54
  baseline: keepFlags.workflows ? [] : removalPlan.baseline,
50
55
  scripts: keepFlags.scripts ? [] : removalPlan.scripts,
@@ -56,55 +61,55 @@ export function planPurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
56
61
  };
57
62
  }
58
63
 
59
- // 삭제 전 요약 출력 — dry-run 미리보기와 실제 실행 전 요약 양쪽에서 재사용한다.
64
+ // Summary printed before deletion; reused by both the dry-run preview and the summary before a real run.
60
65
  export function printPurgePlan(plan, { dryRun = false } = {}) {
61
66
  const lines = ["",
62
67
  dryRun
63
- ? "project-auto-wizard --mode purge --dry-run — 미리보기, 실제 파일은 바뀌지 않았습니다"
64
- : "project-auto-wizard --mode purge — 아래 항목을 제거합니다",
68
+ ? t("cmd.purge.headerDry")
69
+ : t("cmd.purge.header"),
65
70
  ""];
66
- lines.push(`워크플로우 (${plan.workflows.length}개):`);
71
+ lines.push(t("cmd.purge.workflows", { n: plan.workflows.length }));
67
72
  for (const f of plan.workflows) lines.push(` - ${f}`);
68
- lines.push(`스크립트 (${plan.scripts.length}개):`);
73
+ lines.push(t("cmd.purge.scripts", { n: plan.scripts.length }));
69
74
  for (const f of plan.scripts) lines.push(` - ${f}`);
70
- for (const f of plan.appFiles || []) lines.push(`Flutter 앱 파일: ${f}`);
71
- if (plan.versionYml) lines.push("파일: version.yml");
72
- if (plan.readmeSection) lines.push("README.md: AUTO-VERSION-SECTION 블록");
73
- if (plan.gitignore) lines.push(".gitignore: 자동 추가 항목(*.bak, *.template.yaml)");
74
- if (plan.gitignoreKept) lines.push(".gitignore: 자동 추가 항목 유지 (남겨 둔 워크플로우의 백업 파일을 가리는 항목)");
75
- for (const f of plan.changelog) lines.push(`파일: ${f}`);
75
+ for (const f of plan.appFiles || []) lines.push(t("cmd.purge.appFile", { file: f }));
76
+ if (plan.versionYml) lines.push(t("cmd.purge.versionYml"));
77
+ if (plan.readmeSection) lines.push(t("cmd.purge.readmeSection"));
78
+ if (plan.gitignore) lines.push(t("cmd.purge.gitignore"));
79
+ if (plan.gitignoreKept) lines.push(t("cmd.purge.gitignoreKept"));
80
+ for (const f of plan.changelog) lines.push(t("cmd.purge.file", { file: f }));
76
81
  lines.push("");
77
82
  console.log(lines.join("\n"));
78
83
  }
79
84
 
80
- // 실제 삭제 수행 — planPurge()와 동일 shape을 반환하되 실제로 제거된 항목을 반영한다.
81
- // planRemoval 결과를 그대로 지우지 않는 이유: 그 목록은 항상 전체라서
82
- // --keep-* 로 선택적 카테고리만 보존하는 요구사항과 맞지 않는다.
83
- // readmeSection은 plan의 판정을 그대로 되돌려주지 않고
84
- // removeVersionSectionFromReadme()의 실제 반환값("removed"인지)을 반영한다 — plan과 실제 제거 조건이
85
- // 이론상 어긋나는 경우에도 printPurgeResult가 거짓으로 "제거됨"을 보고하지 않는다.
85
+ // Performs the actual deletion: returns the same shape as planPurge() but reflecting what was really removed.
86
+ // Why the planRemoval result is not deleted as is: that list is always the full set, which does not fit
87
+ // the requirement to preserve selected categories via --keep-*.
88
+ // readmeSection does not echo the plan's decision but reflects the actual return value of
89
+ // removeVersionSectionFromReadme() (whether it was "removed"), so even if the plan and the real removal
90
+ // condition theoretically diverge, printPurgeResult never falsely reports "removed".
86
91
  export function executePurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
87
92
  const plan = planPurge(payloadRoot, targetRoot, keepFlags);
88
93
  const { readme, gitignore } = executeRemoval(targetRoot, { ...plan, readme: plan.readmeSection }, {
89
94
  logOrder: ["version", "gitignore", "changelog", "readme"],
90
- gitignoreDetail: (status) => (status === "removed" || status === "file-deleted" ? ".gitignore 자동 추가 항목" : null),
95
+ gitignoreDetail: (status) => (status === "removed" || status === "file-deleted" ? t("cmd.purge.gitignoreLog") : null),
91
96
  });
92
97
  return { ...plan, readmeSection: readme, gitignore };
93
98
  }
94
99
 
95
- // 삭제 후 실제 제거된 목록 출력 — printPurgePlan과 완전히 동일한 형태(파일명 나열)로
96
- // 맞춘다 (개수만 출력하면 무엇이 지워졌는지 확인할 수 없다).
100
+ // Prints the list actually removed after deletion, in exactly the same form as printPurgePlan (file names
101
+ // listed); printing only counts would not show what was deleted.
97
102
  export function printPurgeResult(result) {
98
- const lines = ["", "제거됨:", ""];
99
- lines.push(`워크플로우 (${result.workflows.length}개):`);
103
+ const lines = ["", t("cmd.purge.removedHeading"), ""];
104
+ lines.push(t("cmd.purge.workflows", { n: result.workflows.length }));
100
105
  for (const f of result.workflows) lines.push(` - ${f}`);
101
- lines.push(`스크립트 (${result.scripts.length}개):`);
106
+ lines.push(t("cmd.purge.scripts", { n: result.scripts.length }));
102
107
  for (const f of result.scripts) lines.push(` - ${f}`);
103
- for (const f of result.appFiles || []) lines.push(`Flutter 앱 파일: ${f}`);
104
- if (result.versionYml) lines.push("파일: version.yml");
105
- if (result.readmeSection) lines.push("README.md: AUTO-VERSION-SECTION 블록");
106
- if (result.gitignore) lines.push(".gitignore: 자동 추가 항목(*.bak, *.template.yaml)");
107
- for (const f of result.changelog) lines.push(`파일: ${f}`);
108
+ for (const f of result.appFiles || []) lines.push(t("cmd.purge.appFile", { file: f }));
109
+ if (result.versionYml) lines.push(t("cmd.purge.versionYml"));
110
+ if (result.readmeSection) lines.push(t("cmd.purge.readmeSection"));
111
+ if (result.gitignore) lines.push(t("cmd.purge.gitignore"));
112
+ for (const f of result.changelog) lines.push(t("cmd.purge.file", { file: f }));
108
113
  lines.push("");
109
114
  console.log(lines.join("\n"));
110
115
  }
@@ -1,4 +1,4 @@
1
- // status 명령 — 읽기 전용 설치 상태 확인. 네트워크 접근 없음(로컬 파일 비교만).
1
+ // status command: read-only install state check. No network access (local file comparison only).
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { parseExisting } from "../core/version-yml.js";
@@ -9,32 +9,33 @@ import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
9
9
  import { findStaleWorkflows } from "../core/removal-plan.js";
10
10
  import { readBaseline } from "../core/baseline.js";
11
11
  import { hooksFor, mergeHookResults } from "../core/types.js";
12
+ import { t } from "../i18n/index.js";
12
13
 
13
- // payloadRoot: 패키지 payload/ 루트. targetRoot: 상태를 확인할 대상 레포.
14
+ // payloadRoot: the package's payload/ root. targetRoot: the repo whose state is checked.
14
15
  export function runStatus(payloadRoot, targetRoot = ".") {
15
16
  const vyPath = join(targetRoot, PATHS.versionFile);
16
17
  if (!existsSync(vyPath)) return { installed: false };
17
18
 
18
19
  const existing = parseExisting(readFileSync(vyPath, "utf8"));
19
20
  const repoName = detectRepoName(targetRoot);
20
- // 설치 때 워크플로우에 치환된 환경변수 방식·배포 모드와 스토어 선택을 비교 기준에도 똑같이 적용한다 —
21
- // 그렇지 않으면 미수정 파일이 드리프트로, 선택 해제한 스토어 워크플로우가 "삭제함"으로 오탐된다.
21
+ // Apply the env mode, deploy mode and store selection substituted into the workflows at install time to the
22
+ // comparison as well; otherwise unmodified files show up as drift and deselected store workflows as "deleted".
22
23
  const typeOptions = mergeHookResults(existing.types, "resolveOptions", { existing });
23
24
  const resolvers = makeResolvers(targetRoot, repoName, existing.paths, typeOptions);
24
- // version.yml에 branches 블록이 없으면(신기능 이전 설치·수기 편집) makeSrcText(null)이
25
- // {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}}를 치환하지 못해 모든 워크플로우가 드리프트로 오탐된다 —
26
- // 비교용 기본값으로 폴백(실제 저장값은 아니지만 드리프트 비교 목적에는 충분).
25
+ // Without a branches block in version.yml (install predating the feature, or hand-edited), makeSrcText(null)
26
+ // cannot substitute {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} and every workflow is falsely reported as drift, so
27
+ // fall back to defaults for the comparison (not the real saved values, but enough for drift detection).
27
28
  const branchesForCompare = existing.branches || { main: detectDefaultBranch(targetRoot) || "main", develop: "develop", mode: "pr-flow" };
28
29
  const context = {
29
30
  types: existing.types, paths: existing.paths,
30
31
  repoName, resolvers, branches: branchesForCompare,
31
32
  ...mergeHookResults(existing.types, "contextFields", typeOptions),
32
- // 설치 때 고른 배포 방식으로 비교해야 nginx·traefik CD 수정도 드리프트로 잡힌다.
33
- // 빠지면 기본값(simple)으로 걸러 설치된 무중단 CD가 비교 대상에서 사라진다.
33
+ // Compare using the deploy style chosen at install so nginx/traefik CD edits are caught as drift too.
34
+ // If omitted, the default (simple) filters out the installed zero-downtime CD from the comparison.
34
35
  deployStyle: isDeployStyle(existing.options.deployStyle) ? existing.options.deployStyle : DEFAULT_DEPLOY_STYLE,
35
36
  };
36
37
  const plan = planWorkflows(context, payloadRoot, targetRoot);
37
- // 현재 버전에 없는 옛 워크플로우 — 다음 업데이트에서 정리된다. 그 전까지는 옛 트리거로 계속 돈다.
38
+ // Old workflows absent from the current version are cleaned up on the next update; until then they keep running on the old triggers.
38
39
  const stale = findStaleWorkflows(payloadRoot, targetRoot, readBaseline(targetRoot));
39
40
 
40
41
  return {
@@ -44,60 +45,59 @@ export function runStatus(payloadRoot, targetRoot = ".") {
44
45
  types: existing.types,
45
46
  branches: existing.branches,
46
47
  options: existing.options,
47
- // 사용자가 손댄 파일 = 진짜 충돌(changed) + 업스트림은 그대로인데 내가 고친 것(localOnly).
48
- // baseline이 없으면 localOnly는 항상 비어 있어 종전과 동일하게 동작한다.
48
+ // Files the user touched = real conflicts (changed) + upstream unchanged but locally edited (localOnly).
49
+ // Without a baseline localOnly is always empty, so behavior is the same as before.
49
50
  modifiedFiles: [...plan.changed, ...plan.localOnly].map((f) => f.filename),
50
- // 업데이트 시 무슨 일이 일어날지 미리 보여주는 버킷들 — 판단 재료가 없어 사용자가
51
- // 직접 git diff를 떠야 했던 문제를 없앤다.
51
+ // Buckets that preview what an update would do, so users no longer have to run git diff themselves for lack of information.
52
52
  buckets: {
53
- autoUpdatable: plan.upstreamOnly.map((f) => f.filename), // 질문 없이 최신으로 교체됨
54
- localKept: plan.localOnly.map((f) => f.filename), // 질문 없이 내 수정본 유지됨
55
- conflicts: plan.changed.map((f) => f.filename), // 양쪽 변경 — 검토 필요
56
- removed: plan.removed.map((f) => f.filename), // 내가 지웠고 복원하지 않음
53
+ autoUpdatable: plan.upstreamOnly.map((f) => f.filename), // replaced with the latest without asking
54
+ localKept: plan.localOnly.map((f) => f.filename), // local edit kept without asking
55
+ conflicts: plan.changed.map((f) => f.filename), // changed on both sides; needs review
56
+ removed: plan.removed.map((f) => f.filename), // deleted by the user and not restored
57
57
  },
58
58
  staleFiles: stale,
59
59
  };
60
60
  }
61
61
 
62
62
  export function printStatus(status) {
63
- const lines = ["", "project-auto-wizard status — 설치 상태", ""];
63
+ const lines = ["", t("cmd.status.title"), ""];
64
64
  if (!status.installed) {
65
- lines.push("이 디렉터리에 project-auto-wizard가 설치되어 있지 않습니다 (version.yml 없음).", "");
65
+ lines.push(t("cmd.status.notInstalled"), "");
66
66
  console.log(lines.join("\n"));
67
67
  return;
68
68
  }
69
- lines.push(`버전 : ${status.version}`);
70
- lines.push(`템플릿 버전 : ${status.templateVersion}`);
71
- lines.push(`프로젝트 타입 : ${status.types.join(", ") || "(없음)"}`);
69
+ lines.push(t("cmd.status.version", { value: status.version }));
70
+ lines.push(t("cmd.status.templateVersion", { value: status.templateVersion }));
71
+ lines.push(t("cmd.status.types", { value: status.types.join(", ") || t("cmd.status.none") }));
72
72
  if (status.branches) {
73
- lines.push(`브랜치 모드 : ${status.branches.mode} (${status.branches.main} / ${status.branches.develop})`);
73
+ lines.push(t("cmd.status.branchMode", { mode: status.branches.mode, main: status.branches.main, develop: status.branches.develop }));
74
74
  }
75
- const boolLabel = (v) => (v === null ? "미설정(기본 false)" : v);
76
- const semverAutoLabel = status.options.semverAuto === null ? "미설정(기본 false)" : status.options.semverAuto;
75
+ const boolLabel = (v) => (v === null ? t("cmd.status.unsetDefaultFalse") : v);
76
+ const semverAutoLabel = status.options.semverAuto === null ? t("cmd.status.unsetDefaultFalse") : status.options.semverAuto;
77
77
  const copilotAiLabel = boolLabel(status.options.copilotAi ?? null);
78
78
  const typeLabels = hooksFor(status.types, "statusLabels").map(({ hook }) => hook(status.options)).join("");
79
79
  const deployLabel = status.options.deployStyle ? ` deploy_style=${status.options.deployStyle}` : "";
80
- lines.push(`옵션 : semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${deployLabel}${typeLabels}`);
80
+ lines.push(t("cmd.status.options", { value: `semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${deployLabel}${typeLabels}` }));
81
81
  if (status.modifiedFiles.length) {
82
- lines.push("", `사용자가 수정한 워크플로우 파일 (${status.modifiedFiles.length}개):`);
82
+ lines.push("", t("cmd.status.modified", { n: status.modifiedFiles.length }));
83
83
  for (const f of status.modifiedFiles) lines.push(` - ${f}`);
84
84
  } else {
85
- lines.push("", "모든 워크플로우 파일이 설치 시점 기본값과 동일합니다 (수정 없음).");
85
+ lines.push("", t("cmd.status.unmodified"));
86
86
  }
87
87
 
88
88
  if (status.staleFiles?.length) {
89
- lines.push("", `현재 버전에 없는 이전 워크플로우 (${status.staleFiles.length}개 — 다음 업데이트에서 정리됨):`);
89
+ lines.push("", t("cmd.status.stale", { n: status.staleFiles.length }));
90
90
  for (const f of status.staleFiles) lines.push(` - ${f}`);
91
91
  }
92
92
 
93
- // 업데이트하면 무슨 일이 일어나는지. baseline이 없는 설치는 전부 0이라 출력하지 않는다.
93
+ // What an update would do. Installs without a baseline are all zeros, so nothing is printed.
94
94
  const b = status.buckets || { autoUpdatable: [], localKept: [], conflicts: [], removed: [] };
95
95
  if (b.autoUpdatable.length || b.localKept.length || b.conflicts.length || b.removed.length) {
96
- lines.push("", "지금 업데이트하면:");
97
- lines.push(` 업스트림 변경 — 자동 적용 가능 ${String(b.autoUpdatable.length).padStart(3)}`);
98
- lines.push(` 내가 수정 · 업스트림 그대로 ${String(b.localKept.length).padStart(3)} (유지)`);
99
- lines.push(` 양쪽 변경 — 검토 필요 ${String(b.conflicts.length).padStart(3)}`);
100
- lines.push(` 내가 삭제함 — 복원 안 함 ${String(b.removed.length).padStart(3)}`);
96
+ lines.push("", t("cmd.status.bucketsTitle"));
97
+ lines.push(t("cmd.status.bucketAuto", { n: String(b.autoUpdatable.length).padStart(3) }));
98
+ lines.push(t("cmd.status.bucketLocalKept", { n: String(b.localKept.length).padStart(3) }));
99
+ lines.push(t("cmd.status.bucketConflicts", { n: String(b.conflicts.length).padStart(3) }));
100
+ lines.push(t("cmd.status.bucketRemoved", { n: String(b.removed.length).padStart(3) }));
101
101
  }
102
102
  lines.push("");
103
103
  console.log(lines.join("\n"));
@@ -1,6 +1,6 @@
1
- // uninstall 모드 — 마법사가 설치한 파일(planRemoval 판별분)에 더해 README/.gitignore/version.yml까지
2
- // 선택적으로 제거한다. 대화형 체크리스트 또는 --force + --purge-* 로 항목별 opt-in.
3
- // core/removal-plan.js는 읽기 전용으로만 재사용한다(아무것도 지우지 않는 순수 함수).
1
+ // uninstall mode: removes the files the wizard installed (as identified by planRemoval) and, optionally,
2
+ // README/.gitignore/version.yml too. Per-item opt-in via the interactive checklist or --force + --purge-*.
3
+ // core/removal-plan.js is only reused read-only (a pure function that deletes nothing).
4
4
  import { join } from "node:path";
5
5
  import { existsSync } from "node:fs";
6
6
  import { PATHS } from "../core/paths.js";
@@ -9,20 +9,21 @@ import { executeRemoval } from "../core/removal-exec.js";
9
9
  import { hasVersionSection } from "../core/copy/readme.js";
10
10
  import { hasAutoAddedEntries } from "../core/copy/gitignore.js";
11
11
  import { CANCEL } from "../ui/prompts.js";
12
+ import { t } from "../i18n/index.js";
12
13
 
13
- // selection: { workflows, scripts, readme, gitignore, versionYml } (모두 boolean).
14
- // 반환: 위와 동일한 키의 boolean/배열 — 실제로 제거 "대상"인지 여부(순수 함수, 아무것도 지우지 않음).
14
+ // selection: { workflows, scripts, readme, gitignore, versionYml } (all booleans).
15
+ // Returns: boolean/array values under the same keys, telling what is actually a removal "target" (pure function, deletes nothing).
15
16
  export function planUninstall(payloadRoot, targetRoot, selection) {
16
17
  const removalPlan = planRemoval(payloadRoot, targetRoot);
17
18
  return {
18
19
  workflows: selection.workflows ? removalPlan.workflows : [],
19
- // baseline은 워크플로우 해시 기록 — 워크플로우를 지우면 함께 사라져야 한다.
20
- // Flutter 앱 파일의 생성 기록도 baseline에만 있으므로 같은 항목으로 묶는다.
20
+ // baseline records the workflow hashes, so it must disappear together with the workflows.
21
+ // The creation record of Flutter app files also lives only in the baseline, so it goes under the same item.
21
22
  appFiles: selection.workflows ? removalPlan.appFiles : [],
22
23
  baseline: selection.workflows ? removalPlan.baseline : [],
23
24
  scripts: selection.scripts ? removalPlan.scripts : [],
24
25
  readme: selection.readme ? hasVersionSection(targetRoot) : false,
25
- // 워크플로우를 남기면 그 백업 파일(.bak 등)도 남는다 — .gitignore 항목을 지우면 git 상태에 드러나므로 유지한다.
26
+ // If the workflows stay, so do their backup files (.bak etc.); removing the .gitignore entries would surface them in git status, so keep them.
26
27
  gitignore: selection.gitignore
27
28
  ? hasAutoAddedEntries(targetRoot) && (selection.workflows || backupArtifacts(removalPlan.workflows).length === 0)
28
29
  : false,
@@ -30,29 +31,30 @@ export function planUninstall(payloadRoot, targetRoot, selection) {
30
31
  };
31
32
  }
32
33
 
33
- // 반환: planUninstall과 동일한 형태 — 실제로 제거된 항목.
34
+ // Returns: the same shape as planUninstall, the items actually removed.
34
35
  export function runUninstall(context, payloadRoot, targetRoot, selection) {
35
36
  const plan = planUninstall(payloadRoot, targetRoot, selection);
36
37
  const { readme, gitignore } = executeRemoval(targetRoot, plan, {
37
38
  logOrder: ["readme", "gitignore", "version"],
38
- gitignoreDetail: (status) => `.gitignore 자동 추가 항목 (${status})`,
39
+ gitignoreDetail: (status) => t("cmd.uninstall.gitignoreDetail", { status }),
39
40
  });
40
41
  return { ...plan, readme, gitignore };
41
42
  }
42
43
 
43
- // ── 대화형 체크리스트 흐름 ────────────────────────────────────────────
44
- const ITEM_DEFS = [
45
- // .github/.wizard 에는 baseline.json과 설치 로그(.wizard/logs)가 함께 들어 있다 —
46
- // 워크플로우를 지우면 함께 사라지므로 라벨에 명시한다.
47
- // Flutter 앱 파일은 마법사가 만들고 사용자가 수정하지 않은 것만 함께 지운다.
48
- { key: "workflows", label: "워크플로우 (.github/workflows/PROJECT-*.yaml) + 설치 기록 (.github/.wizard) + 수정하지 않은 Flutter 앱 파일" },
49
- { key: "scripts", label: "스크립트 (.github/scripts/*.py)" },
50
- { key: "readme", label: "README.md 버전 섹션 (AUTO-VERSION-SECTION)" },
51
- { key: "gitignore", label: ".gitignore 자동 추가 항목" },
52
- { key: "versionYml", label: "version.yml (버전/브랜치 설정 전체)" },
44
+ // ── Interactive checklist flow ────────────────────────────────────────
45
+ // Built lazily: labels are translated at use time, after the language is resolved.
46
+ const itemDefs = () => [
47
+ // .github/.wizard holds baseline.json and the install logs (.wizard/logs); they disappear together
48
+ // with the workflows, so the label says so.
49
+ // Flutter app files are removed too, but only those the wizard created and the user did not modify.
50
+ { key: "workflows", label: t("cmd.uninstall.item.workflows") },
51
+ { key: "scripts", label: t("cmd.uninstall.item.scripts") },
52
+ { key: "readme", label: t("cmd.uninstall.item.readme") },
53
+ { key: "gitignore", label: t("cmd.uninstall.item.gitignore") },
54
+ { key: "versionYml", label: t("cmd.uninstall.item.versionYml") },
53
55
  ];
54
56
 
55
- // 기본 체크 상태 — "안전 삭제" 2종만 기본 체크하고 나머지(readme/gitignore/versionYml)는 opt-in.
57
+ // Default checked state: only the two "safe delete" items are checked; the rest (readme/gitignore/versionYml) are opt-in.
56
58
  export const SAFE_ITEMS = ["workflows", "scripts"];
57
59
 
58
60
  function detectAvailableItems(payloadRoot, targetRoot) {
@@ -64,7 +66,7 @@ function detectAvailableItems(payloadRoot, targetRoot) {
64
66
  gitignore: hasAutoAddedEntries(targetRoot),
65
67
  versionYml: existsSync(join(targetRoot, PATHS.versionFile)),
66
68
  };
67
- return ITEM_DEFS.filter((d) => presence[d.key]).map((d) => ({ value: d.key, label: d.label }));
69
+ return itemDefs().filter((d) => presence[d.key]).map((d) => ({ value: d.key, label: d.label }));
68
70
  }
69
71
 
70
72
  function toSelection(checkedKeys) {
@@ -76,51 +78,51 @@ function toSelection(checkedKeys) {
76
78
  }
77
79
 
78
80
  function summarizeSelection(selection) {
79
- const labelOf = Object.fromEntries(ITEM_DEFS.map((d) => [d.key, d.label]));
81
+ const labelOf = Object.fromEntries(itemDefs().map((d) => [d.key, d.label]));
80
82
  const chosen = Object.keys(selection).filter((k) => selection[k]).map((k) => `- ${labelOf[k]}`);
81
- return chosen.length ? chosen.join("\n") : "(선택된 항목 없음)";
83
+ return chosen.length ? chosen.join("\n") : t("cmd.uninstall.noneSelected");
82
84
  }
83
85
 
84
86
  function summarizeResult(result) {
85
87
  const lines = [];
86
- if (result.workflows.length) lines.push(`워크플로우 ${result.workflows.length}개 제거`);
87
- if (result.scripts.length) lines.push(`스크립트 ${result.scripts.length}개 제거`);
88
- if (result.appFiles?.length) lines.push(`Flutter 앱 파일 ${result.appFiles.length}개 제거`);
89
- if (result.readme) lines.push("README.md 버전 섹션 제거");
90
- if (result.gitignore) lines.push(".gitignore 자동 추가 항목 제거");
91
- if (result.versionYml) lines.push("version.yml 제거");
92
- return lines.length ? lines.join("\n") : "제거된 항목이 없습니다.";
88
+ if (result.workflows.length) lines.push(t("cmd.uninstall.result.workflows", { n: result.workflows.length }));
89
+ if (result.scripts.length) lines.push(t("cmd.uninstall.result.scripts", { n: result.scripts.length }));
90
+ if (result.appFiles?.length) lines.push(t("cmd.uninstall.result.appFiles", { n: result.appFiles.length }));
91
+ if (result.readme) lines.push(t("cmd.uninstall.result.readme"));
92
+ if (result.gitignore) lines.push(t("cmd.uninstall.result.gitignore"));
93
+ if (result.versionYml) lines.push(t("cmd.uninstall.result.versionYml"));
94
+ return lines.length ? lines.join("\n") : t("cmd.uninstall.result.none");
93
95
  }
94
96
 
95
- // io 계약: engineIo.multiselect({message,options,initialValues}), askYesNo(msg,def),
96
- // note(text,title)?, cancelMessage(text)? — src/ui/prompts.js가 실물, 테스트는 스텁 주입.
97
- // preset: CLI의 --purge-* 플래그 { readme, gitignore, versionYml } — 체크리스트 초기 선택에 반영한다.
97
+ // io contract: engineIo.multiselect({message,options,initialValues}), askYesNo(msg,def),
98
+ // note(text,title)?, cancelMessage(text)? - src/ui/prompts.js is the real one, tests inject stubs.
99
+ // preset: the CLI --purge-* flags { readme, gitignore, versionYml }, reflected in the checklist's initial selection.
98
100
  export async function runUninstallFlow(payloadRoot, targetRoot, io, preset = {}) {
99
101
  const available = detectAvailableItems(payloadRoot, targetRoot);
100
102
  if (available.length === 0) {
101
- io.note?.("제거할 항목이 없습니다.", "완전 삭제");
103
+ io.note?.(t("cmd.uninstall.flow.nothing"), t("cmd.uninstall.flow.title"));
102
104
  return null;
103
105
  }
104
106
 
105
107
  const checked = await io.engineIo.multiselect({
106
- message: "삭제할 항목을 선택하세요 (Space 토글, Enter 확정)",
108
+ message: t("cmd.uninstall.flow.select"),
107
109
  options: available,
108
110
  initialValues: available.map((o) => o.value).filter((v) => SAFE_ITEMS.includes(v) || preset[v] === true),
109
111
  });
110
112
  if (checked === CANCEL || !Array.isArray(checked) || checked.length === 0) {
111
- io.cancelMessage?.("완전 삭제를 취소했습니다.");
113
+ io.cancelMessage?.(t("cmd.uninstall.flow.cancelled"));
112
114
  return null;
113
115
  }
114
116
 
115
117
  const selection = toSelection(checked);
116
- io.note?.(summarizeSelection(selection), "삭제 예정 항목");
117
- const ok = await io.askYesNo("정말 삭제할까요? 되돌릴 수 없습니다.", false);
118
+ io.note?.(summarizeSelection(selection), t("cmd.uninstall.flow.plannedTitle"));
119
+ const ok = await io.askYesNo(t("cmd.uninstall.flow.confirm"), false);
118
120
  if (ok !== true) {
119
- io.cancelMessage?.("완전 삭제를 취소했습니다.");
121
+ io.cancelMessage?.(t("cmd.uninstall.flow.cancelled"));
120
122
  return null;
121
123
  }
122
124
 
123
125
  const result = runUninstall({}, payloadRoot, targetRoot, selection);
124
- io.note?.(summarizeResult(result), "완전 삭제 완료");
126
+ io.note?.(summarizeResult(result), t("cmd.uninstall.flow.doneTitle"));
125
127
  return result;
126
128
  }
package/src/context.js CHANGED
@@ -1,19 +1,19 @@
1
- // 마법사 전역 상태를 하나의 객체로 명시화
1
+ // Wizard-wide state made explicit as a single object
2
2
  import { TYPE_IDS, allHookValues } from "./core/types.js";
3
3
 
4
- // --type 화이트리스트 — 타입 레지스트리(core/types.js)의 표시 순서 그대로.
4
+ // --type whitelist: the display order of the type registry (core/types.js).
5
5
  export const VALID_TYPES = TYPE_IDS;
6
6
 
7
- // --mode 화이트리스트 — 알 수 없는 값은 부수효과(브랜치 조회 등) 이전에 즉시 거부해야 한다.
8
- // purge는 --help/대화형 메뉴에 노출하지 않는 숨김 모드이지만 검증 대상에는 포함한다.
9
- // version/workflows(부분 설치)와 revert는 제거됐다 — 부분 설치는 설치 시점 baseline을
10
- // 반쪽만 갱신해 업데이트 판정을 흐리고, revert는 uninstall의 부분집합이었다.
7
+ // --mode whitelist: unknown values must be rejected before any side effect (branch lookup, etc.).
8
+ // purge is a hidden mode (not in --help or the interactive menu) but is still validated.
9
+ // version/workflows (partial install) and revert were removed: a partial install refreshes the
10
+ // install-time baseline only halfway and blurs update detection, and revert was a subset of uninstall.
11
11
  export const VALID_MODES = [
12
12
  "interactive", "full",
13
13
  "uninstall", "status", "doctor", "purge",
14
14
  ];
15
15
 
16
- export const DEFAULT_VERSION = "0.0.0"; // 패키지 버전 읽기 실패 시 폴백 (배너용 — breaking 비교엔 안 씀)
16
+ export const DEFAULT_VERSION = "0.0.0"; // fallback when reading the package version fails (banner only; not used for breaking comparison)
17
17
 
18
18
  export function createContext(overrides = {}) {
19
19
  return {
@@ -22,12 +22,13 @@ export function createContext(overrides = {}) {
22
22
  types: [],
23
23
  version: "",
24
24
  branch: "",
25
- branches: null, // { main, develop, mode: "pr-flow"|"trunk-based" } — resolveBranchConfig 결과
25
+ branches: null, // { main, develop, mode: "pr-flow"|"trunk-based" } - result of resolveBranchConfig
26
26
  paths: new Map(), // type -> path
27
- includeSemverAuto: null, // null=미설정(다운스트림에서 true로 해석), true/false=명시
28
- includeCopilotAi: null, // null=미설정(다운스트림에서 false로 해석), true/false=명시 — Copilot AI 요약 opt-in
29
- // 타입 전용 옵션 필드 — 해당 타입이 없는 프로젝트에서는 전부 무시된다. 필드와 기본값은 타입 훅(contextDefaults)이 정한다.
27
+ includeSemverAuto: null, // null = unset (resolved to true downstream), true/false = explicit
28
+ includeCopilotAi: null, // null = unset (resolved to false downstream), true/false = explicit; Copilot AI summary opt-in
29
+ // Type-specific option fields; all ignored in projects without that type. Fields and defaults come from the type hook (contextDefaults).
30
30
  ...allHookValues("contextDefaults"),
31
+ language: "en", // message language (--lang / env / version.yml), default en
31
32
  templateVersion: "",
32
33
  deployValues: new Map(), // "type.KEY" -> value
33
34
  counters: {},
@@ -1,19 +1,20 @@
1
- // payload 자산 해석 — 네트워크 접근 0 (구 acquireTemplate/git clone 대체).
2
- // 마법사가 설치하는 모든 자산은 npm 패키지에 동봉된 payload/ 가 단일 진실이다.
3
- // npx 글로벌 캐시에서 실행돼도 import.meta.url 기준으로 패키지 내 payload를 정확히 가리킨다.
1
+ // Payload asset resolution: zero network access (replaces the old acquireTemplate/git clone).
2
+ // The payload/ bundled in the npm package is the single source of truth for every asset the wizard installs.
3
+ // Even when run from the npx global cache, import.meta.url points precisely at the payload inside the package.
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { join } from "node:path";
6
6
  import { readFileSync } from "node:fs";
7
7
  import { exists, readText, listYamlFiles } from "./fsutil.js";
8
8
  import { DEFAULT_VERSION } from "../context.js";
9
+ import { t } from "../i18n/index.js";
9
10
 
10
- // 패키지 내 payload/ 절대경로. 테스트·픽스처는 인자 주입으로 대체 가능.
11
+ // Absolute path of payload/ inside the package. Tests and fixtures can substitute it by argument injection.
11
12
  export function resolvePayloadRoot() {
12
13
  return fileURLToPath(new URL("../../payload/", import.meta.url));
13
14
  }
14
15
 
15
- // 마법사(=템플릿) 버전 — 패키지 package.json의 version.
16
- // 구 readTemplateVersion(tempDir/version.yml)과 동일한 소비처(배너·breaking 비교·version.yml 기록)를 채운다.
16
+ // Wizard (= template) version: the version in the package's package.json.
17
+ // Serves the same consumers as the former readTemplateVersion(tempDir/version.yml): banner, breaking-change comparison, version.yml record.
17
18
  export function readTemplateVersion() {
18
19
  try {
19
20
  const pkgPath = fileURLToPath(new URL("../../package.json", import.meta.url));
@@ -24,23 +25,23 @@ export function readTemplateVersion() {
24
25
  }
25
26
  }
26
27
 
27
- // payload/workflows/common 직하위 yaml 목록.
28
+ // List of yaml files directly under payload/workflows/common.
28
29
  export function listCommonWorkflows(payloadRoot = resolvePayloadRoot()) {
29
30
  return listYamlFiles(join(payloadRoot, "workflows", "common"));
30
31
  }
31
32
 
32
- // payload 구조 자가 점검 — 필수 폴더 누락 시 명확히 실패 (배포 패키징 오류 조기 발견).
33
+ // Payload structure self-check: fails clearly when a required folder is missing (catches packaging errors early).
33
34
  export function assertPayload(payloadRoot = resolvePayloadRoot()) {
34
35
  if (!exists(join(payloadRoot, "workflows"))) {
35
- throw new Error("패키지 구조 오류 — payload/workflows 폴더를 찾지 못했습니다.");
36
+ throw new Error(t("core.assets.missingWorkflows"));
36
37
  }
37
38
  if (!exists(join(payloadRoot, "scripts"))) {
38
- throw new Error("패키지 구조 오류 — payload/scripts 폴더를 찾지 못했습니다.");
39
+ throw new Error(t("core.assets.missingScripts"));
39
40
  }
40
41
  return payloadRoot;
41
42
  }
42
43
 
43
- // payload/version.yml.template 원문.
44
+ // Raw text of payload/version.yml.template.
44
45
  export function readVersionYmlTemplate(payloadRoot = resolvePayloadRoot()) {
45
46
  const p = join(payloadRoot, "version.yml.template");
46
47
  return exists(p) ? readText(p) : null;