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,20 +1,20 @@
1
- // 제거 대상 판별기 — 마법사가 설치한 파일이 무엇인지 가려낸다.
2
- // uninstall(항목 체크리스트)과 purge(숨김 개발 모드)의 공통 기반이며, 아무것도 지우지 않는다.
1
+ // Removal-target detector - works out which files the wizard installed.
2
+ // Shared base of uninstall (item checklist) and purge (hidden dev mode); it deletes nothing.
3
3
  //
4
- // 원칙: (a) 현재 payload에 존재하는 파일명과 정확히 일치하는 것, (b) 설치된 워크플로우 파일 중
5
- // 마법사 관리 마커(MANAGED_WORKFLOW_MARKER)로 시작하는 것 — 이 둘의 합집합을 제거 대상으로 삼는다.
6
- // (a)만으로는 과거 버전에서 설치된 뒤 이후 릴리스에서 payload 파일명이 바뀌거나 삭제된 파일을
7
- // 인식하지 못했다 (원래 버그). 반대로 (b)만으로는 이 수정 이전에 설치되어 마커가
8
- // 없는(그러나 파일명은 여전히 현재 payload와 일치하는) 기존 설치 전체를 인식하지 못하는 회귀가
9
- // 생긴다 — 그래서 두 방식을 합집합으로 병행한다. 마커는 이 수정 이후 배포되는 payload 템플릿부터
10
- // 포함되므로, (b) 경로가 실제로 새로 잡아내는 것은 "이름이 바뀌거나 삭제된, 마커가 있는" 파일뿐이다.
11
- // 단, 마커는 사용자가 워크플로우를 복사해 이름만 바꾼 파일에도 그대로 따라간다. 그래서 (b)는
12
- // baseline에 설치 기록이 있는 파일명만 인정하고, baseline이 없는 구버전 설치에서만 PROJECT-* 접두로 대신 판단한다.
13
- // 사용자가 직접 만든 워크플로우·version.yml·README·.gitignore는 대상이 아니다
14
- // (version.yml은 사용자 버전 데이터 — 제거 대상이 아니라 산출물이다).
4
+ // Principle: the union of (a) files whose name exactly matches one in the current payload and (b) installed
5
+ // workflow files that start with the wizard-managed marker (MANAGED_WORKFLOW_MARKER) is the removal target.
6
+ // (a) alone missed files installed by an older version whose payload filename was later renamed or deleted
7
+ // (the original bug). (b) alone would regress by missing every existing install made before the marker
8
+ // existed (whose filenames still match the current payload) - so both are used together as a union. The marker
9
+ // ships only in payload templates released after that fix, so what path (b) newly catches is only
10
+ // "renamed or deleted files that carry the marker".
11
+ // However, the marker follows a workflow the user copied and merely renamed. So (b) accepts only filenames
12
+ // recorded in the baseline, and only for old installs without a baseline falls back to the PROJECT-* prefix.
13
+ // User-made workflows, version.yml, README and .gitignore are not targets
14
+ // (version.yml is the user's version data - an artifact, not a removal target).
15
15
  //
16
- // 이 파일은 원래 src/commands/revert.js였다. revert 모드는 uninstall의 부분집합이라 제거됐고
17
- //, 판별 로직만 남아 commands가 아닌 core로 옮겨졌다.
16
+ // This file used to be src/commands/revert.js. The revert mode was removed as a subset of uninstall,
17
+ // and only the detection logic remained, moved to core rather than commands.
18
18
  import { join, isAbsolute } from "node:path";
19
19
  import { existsSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
20
20
  import { PATHS } from "./paths.js";
@@ -23,12 +23,12 @@ import { LOG_DIR } from "./logger.js";
23
23
  import { payloadWorkflowNames } from "./deploy-style.js";
24
24
  import { SCRIPT_NAMES } from "./copy/simple.js";
25
25
 
26
- // payload/workflows/**/*.yaml 첫 줄에 심어둔 고정 마커 — 이 값이 바뀌면 과거 설치분과의 매칭이 끊긴다.
26
+ // Fixed marker planted on the first line of payload/workflows/**/*.yaml - changing this value breaks matching with past installs.
27
27
  export const MANAGED_WORKFLOW_MARKER = "# project-auto-wizard:managed-workflow";
28
28
 
29
- // 설치된 워크플로우 디렉토리(평면 구조)에서 관리 마커로 시작하는 파일명 집합.
30
- // .bak/.template.yaml 백업본도 원본 텍스트를 그대로 복사한 것이라 마커를 그대로 갖고 있어
31
- // 확장자나 파일명 유추 없이 동일한 방식으로 인식된다.
29
+ // Set of filenames starting with the managed marker in the installed workflows directory (flat layout).
30
+ // .bak/.template.yaml backups are verbatim copies of the original text and keep the marker,
31
+ // so they are recognized the same way without inferring from extension or filename.
32
32
  function markedWorkflowNames(wfDir) {
33
33
  const names = new Set();
34
34
  if (!existsSync(wfDir)) return names;
@@ -40,8 +40,8 @@ function markedWorkflowNames(wfDir) {
40
40
  return names;
41
41
  }
42
42
 
43
- // 마커가 있는 파일이 마법사가 설치한 것인지 — .bak/.template.yaml 백업본은 원본 파일명으로 따진다.
44
- // recorded: baseline의 파일명 집합, baseline이 없으면 null.
43
+ // Whether a marked file was installed by the wizard - .bak/.template.yaml backups are judged by the original filename.
44
+ // recorded: set of filenames in the baseline, null when there is no baseline.
45
45
  function isInstalledName(name, recorded) {
46
46
  if (!recorded) return name.startsWith("PROJECT-");
47
47
  const stem = name.endsWith(".bak") ? name.slice(0, -".bak".length) : name;
@@ -53,18 +53,18 @@ function isInstalledName(name, recorded) {
53
53
  return candidates.some((c) => recorded.has(c));
54
54
  }
55
55
 
56
- // 충돌 처리·정리로 생긴 백업 파생 파일(.bak/.template.yaml) — .gitignore 자동 추가 항목이 가리는 대상이다.
57
- // 워크플로우를 남기는 삭제에서 이 파일들이 남으면 .gitignore 항목도 남겨야 git 상태에 드러나지 않는다.
56
+ // Backup-derived files created by conflict handling/cleanup (.bak/.template.yaml) - what the auto-added .gitignore entries hide.
57
+ // If these files remain after a removal that keeps the workflows, the .gitignore entries must stay too, or they show up in git status.
58
58
  export const backupArtifacts = (names) => names.filter((n) => n.endsWith(".bak") || n.endsWith(".template.yaml"));
59
59
 
60
- // 아무것도 지우지 않는 순수 함수 — uninstall/purge/--dry-run이 공유한다.
60
+ // Pure function that deletes nothing - shared by uninstall/purge/--dry-run.
61
61
  export function planRemoval(payloadRoot, targetRoot = ".") {
62
62
  const removedWf = new Set();
63
63
  const removedScripts = [];
64
64
  const wfDir = join(targetRoot, PATHS.workflowsDir);
65
65
  const baseline = readBaseline(targetRoot);
66
66
  if (existsSync(wfDir)) {
67
- // (a) 현재 payload와 파일명이 일치하는 것 — 마커 유무 무관(기존 설치 회귀 방지).
67
+ // (a) Files whose name matches the current payload - marker or not (prevents regressing existing installs).
68
68
  for (const name of payloadWorkflowNames(payloadRoot)) {
69
69
  const p = join(wfDir, name);
70
70
  if (existsSync(p)) removedWf.add(name);
@@ -72,7 +72,7 @@ export function planRemoval(payloadRoot, targetRoot = ".") {
72
72
  if (existsSync(join(wfDir, templateName))) removedWf.add(templateName);
73
73
  if (existsSync(p + ".bak")) removedWf.add(name + ".bak");
74
74
  }
75
- // (b) 관리 마커로 시작하는 것 — payload에서 이름이 바뀌거나 삭제된 파일도 인식.
75
+ // (b) Files starting with the managed marker - also recognizes files renamed or deleted in the payload.
76
76
  const recorded = baseline ? new Set(Object.keys(baseline.files)) : null;
77
77
  for (const name of markedWorkflowNames(wfDir)) {
78
78
  if (isInstalledName(name, recorded)) removedWf.add(name);
@@ -81,24 +81,24 @@ export function planRemoval(payloadRoot, targetRoot = ".") {
81
81
  for (const s of SCRIPT_NAMES) {
82
82
  if (existsSync(join(targetRoot, PATHS.scriptsDir, s))) removedScripts.push(s);
83
83
  }
84
- // Flutter 앱 파일(Fastfile 등)은 마법사가 새로 만들었고 내용이 그대로인 것만 — 값을 채워 넣은 파일은 사용자 것이다.
84
+ // Flutter app files (Fastfile etc.) only if the wizard newly created them and the content is unchanged - a file with filled-in values belongs to the user.
85
85
  const appFiles = [];
86
86
  for (const [rel, hash] of Object.entries(baseline?.appFiles || {})) {
87
- if (rel.split(/[\\/]/).includes("..") || isAbsolute(rel)) continue; // 레포 밖 경로는 신뢰하지 않는다
87
+ if (rel.split(/[\\/]/).includes("..") || isAbsolute(rel)) continue; // paths outside the repo are not trusted
88
88
  const p = join(targetRoot, rel);
89
89
  if (existsSync(p) && appFileHash(readFileSync(p, "utf8")) === hash) appFiles.push(rel);
90
90
  }
91
- // baseline은 마법사가 만든 내부 상태 파일이다 — 설치물을 지우면 함께 사라져야 한다.
92
- // 남겨두면 다음 설치가 "예전에 깔았다가 사용자가 지운 파일"로 오인해 전부 removed로 분류한다.
93
- // 실행 로그(.wizard/logs)만 남은 경우도 같은 폴더째 지운다 — 설치물을 다 지운 뒤 로그 폴더만 남으면
94
- // "완전 삭제"가 아니다.
91
+ // The baseline is an internal state file made by the wizard - it must disappear along with the installed files.
92
+ // If left, the next install would mistake everything for "files installed before and deleted by the user" and classify all as removed.
93
+ // If only the run logs (.wizard/logs) remain, the whole folder goes too - a log folder left after all
94
+ // installed files are gone is not a "complete removal".
95
95
  const baselineDirs = existsSync(join(targetRoot, BASELINE_PATH)) || existsSync(join(targetRoot, LOG_DIR)) ? [BASELINE_DIR] : [];
96
96
  return { workflows: [...removedWf], scripts: removedScripts, appFiles, baseline: baselineDirs };
97
97
  }
98
98
 
99
- // 업데이트 때 정리할 옛 워크플로우 — 마법사가 설치했지만(baseline 기록 + 관리 마커) 지금 payload에는 없는 파일.
100
- // payload에서 이름이 바뀌거나 빠진 파일이라, 남겨두면 옛 트리거·옛 절차로 계속 돈다.
101
- // baseline에 없는 파일은 대상이 아니다 — 사용자가 마법사 파일을 복사해 만든 워크플로우일 수 있다.
99
+ // Old workflows to clean up on update - files the wizard installed (baseline record + managed marker) that are not in the payload now.
100
+ // They were renamed or dropped in the payload, so leaving them keeps running with the old triggers and procedure.
101
+ // Files not in the baseline are not targets - they may be workflows the user made by copying a wizard file.
102
102
  export function findStaleWorkflows(payloadRoot, targetRoot = ".", baseline = null) {
103
103
  if (!baseline?.files) return [];
104
104
  const current = payloadWorkflowNames(payloadRoot);
@@ -107,8 +107,8 @@ export function findStaleWorkflows(payloadRoot, targetRoot = ".", baseline = nul
107
107
  .sort();
108
108
  }
109
109
 
110
- // 배포 방식 정리와 같은 규칙 — 손대지 않은 파일(installed 해시 일치)은 삭제, 손댄 파일은 .bak으로 옮겨
111
- // 내용은 지키고 트리거만 끈다. dryRun이면 판정만 하고 파일은 건드리지 않는다 (--dry-run 미리보기용).
110
+ // Same rule as deploy-style cleanup - untouched files (installed hash matches) are deleted, touched files are moved to .bak
111
+ // to keep the content while switching off the trigger. With dryRun only the verdict is made and no file is touched (for the --dry-run preview).
112
112
  export function cleanupStaleWorkflows(targetRoot, names, baseline, { dryRun = false } = {}) {
113
113
  const wfDir = join(targetRoot, PATHS.workflowsDir);
114
114
  const removed = [];
package/src/core/types.js CHANGED
@@ -1,40 +1,40 @@
1
1
  import { flutterHooks } from "./flutter-hooks.js";
2
2
 
3
- // 프로젝트 타입 레지스트리 — 타입별 지식(감지 마커, 감지 순서, 버전·빌드 번호 파일)을 한 곳에 둔다.
4
- // 타입 목록·마커 맵·감지 체인은 모두 이 표에서 만들어지므로, 새 타입은 여기에 한 줄을 더하는 것에서 시작한다.
5
- // (릴리스 시점의 버전 파일 쓰기는 payload/scripts/version_manager.py가 따로 담당한다.)
3
+ // Project type registry: keeps per-type knowledge (detection markers, detection order, version and build-number files) in one place.
4
+ // The type list, marker map and detection chain are all built from this table, so a new type starts with one more line here.
5
+ // (Writing version files at release time is handled separately by payload/scripts/version_manager.py.)
6
6
  //
7
- // 필드:
8
- // id CLI·version.yml에 쓰이는 타입 이름. 배열 순서가 곧 --help·대화형 선택지 표시 순서다.
9
- // markers 그 타입의 근거 파일. [0]이 대표 파일이고, 모노레포 경로 탐색은 이 순서로 우선순위를 둔다.
10
- // detectBy 자동 감지 방식 — "markers": markers 중 하나라도 있으면 감지
11
- // "package": package.json 의존성 키(packageDep)로 감지
12
- // "package-fallback": package.json은 있지만 위 어느 것도 아닐 때
13
- // 없음: 자동 감지하지 않는다(basic은 아무것도 감지되지 않았을 때의 결과).
14
- // detectOrder 같은 detectBy 안에서의 판정 순서. 앞선 규칙이 이긴다(expo 앱은 react-native 의존성도 가짐).
15
- // versionSources 설치 시 버전을 읽을 파일 키(detect.js의 sources) — 주 타입일 때 이 순서로 먼저 읽는다.
16
- // buildNumberSource 모바일 빌드 번호를 읽을 파일 키(detect.js의 detectBuildNumberFromFiles).
17
- // singleServerCd 배포 방식 변형 없이 서버 배포 CD가 하나뿐일 때 그 워크플로우 파일명.
18
- // hooks 타입 전용 동작. 없는 훅은 "할 일 없음"이라 공통 코드는 hooksFor()로 있는 것만 호출한다.
19
- // workflowFilter(context) 타입 루트 워크플로우 파일 필터(filename → bool) 또는 null
7
+ // Fields:
8
+ // id Type name used in the CLI and version.yml. Array order is also the display order of --help and interactive choices.
9
+ // markers Evidence files for the type. [0] is the representative file, and monorepo path search prioritizes in this order.
10
+ // detectBy Auto-detection method - "markers": detected when any of markers exists
11
+ // "package": detected by a package.json dependency key (packageDep)
12
+ // "package-fallback": package.json exists but none of the above matched
13
+ // absent: not auto-detected (basic is the result when nothing was detected).
14
+ // detectOrder Evaluation order within the same detectBy. The earlier rule wins (an expo app also has a react-native dependency).
15
+ // versionSources File keys to read the version from at install (sources in detect.js); read first in this order for the primary type.
16
+ // buildNumberSource File key to read the mobile build number from (detectBuildNumberFromFiles in detect.js).
17
+ // singleServerCd The workflow file name when there is exactly one server-deploy CD with no deploy-style variants.
18
+ // hooks Type-specific behavior. A missing hook means "nothing to do", so common code calls only the existing ones through hooksFor().
19
+ // workflowFilter(context) Filter for the type's root workflow files (filename -> bool) or null
20
20
  // cleanupWorkflows(dir, installed, context, baseline, opts)
21
- // 선택에서 빠진 워크플로우 정리 → { removed, backedUp }
21
+ // Cleanup of workflows dropped from the selection -> { removed, backedUp }
22
22
  // planAppFiles / copyAppFiles(context, payloadRoot, targetRoot)
23
- // 사용자 소유 앱 파일(없을 때만 생성) 계획/복사 → { created, kept }
24
- // appFilesTag 앱 파일 복사 로그의 분류 이름
25
- // statusLabels(options) status 옵션 줄에 덧붙일 문자열
26
- // doctorChecks(cwd, existing, { docs }) doctor 진단 행 배열
27
- // resolveOptions({ opts, existing, workflowsDir }) CLI 옵션 → 저장값 → 기본값 순으로 확정한 타입 옵션 객체
28
- // (workflowsDir는 설치된 워크플로우로 추론할 때만 넘긴다)
29
- // contextDefaults createContext의 미결정 기본 필드
23
+ // Plan/copy of user-owned app files (created only when missing) -> { created, kept }
24
+ // appFilesTag Category name in the app-file copy log
25
+ // statusLabels(options) String appended to the status options line
26
+ // doctorChecks(cwd, existing, { docs }) Array of doctor diagnostic rows
27
+ // resolveOptions({ opts, existing, workflowsDir }) Type option object decided as CLI options, then saved values, then defaults
28
+ // (workflowsDir is passed only when inferring from installed workflows)
29
+ // contextDefaults Undecided default fields of createContext
30
30
  // contextFields(options) / optionsFromContext(context)
31
- // 옵션 객체 ↔ 설치 컨텍스트 필드 변환
32
- // versionOptionsBlock(options) version.yml options 아래에 덧붙일 블록(6칸 들여쓰기)
33
- // savedOptionKeys version.yml 저장 키 → parseExisting 옵션 필드
34
- // cliFlags[{ flag, field, initial, parse(v) }] 타입 전용 CLI 플래그(파싱 결과 opts[field]에 담긴다)
35
- // installNotices(options) 설치 직후 알릴 경고 배열(빈 값은 무시)
36
- // logChoices(context) 설치 로그에 남길 선택값 [이름, 값] 배열
37
- // (타입 전용 대화형 질문 흐름은 아직 interactive.js에 남아 있다.)
31
+ // Conversion between the option object and install-context fields
32
+ // versionOptionsBlock(options) Block appended under version.yml options (6-space indent)
33
+ // savedOptionKeys Saved version.yml key to parseExisting option field
34
+ // cliFlags[{ flag, field, initial, parse(v) }] Type-specific CLI flags (the parsed result goes into opts[field])
35
+ // installNotices(options) Array of warnings shown right after install (empty values ignored)
36
+ // logChoices(context) Array of selected [name, value] pairs to record in the install log
37
+ // (The type-specific interactive question flow still lives in interactive.js.)
38
38
  export const TYPES = [
39
39
  {
40
40
  id: "spring",
@@ -68,13 +68,13 @@ export const TYPES = [
68
68
  id: "react-native",
69
69
  markers: ["package.json"],
70
70
  detectBy: "package", packageDep: "react-native", detectOrder: 2,
71
- // 릴리스 때 동기화하는 네이티브 파일이 기준이고, 거기서 못 읽을 때만 package.json을 본다.
71
+ // The native files synced at release time are the reference; package.json is consulted only when they cannot be read.
72
72
  versionSources: ["reactNative", "packageJson"],
73
73
  buildNumberSource: "androidGradle",
74
74
  },
75
75
  {
76
- // Expo는 최신 create-expo-app 템플릿처럼 app.json 없이 app.config.ts/js만 쓸 수 있다.
77
- // 구성 파일이 아예 없어도 expo 의존성이 있는 package.json이 곧 근거다.
76
+ // Expo may use only app.config.ts/js without app.json, as in the latest create-expo-app template.
77
+ // Even with no config file at all, a package.json with an expo dependency is the evidence.
78
78
  id: "react-native-expo",
79
79
  markers: ["app.json", "app.config.ts", "app.config.js", "package.json"],
80
80
  detectBy: "package", packageDep: "expo", detectOrder: 1,
@@ -94,7 +94,7 @@ export const TYPES = [
94
94
  versionSources: ["pyproject", "setupPy"],
95
95
  },
96
96
  {
97
- // 마커 파일이 없는 타입 — 아무것도 감지되지 않았을 때의 결과이자 경로가 필요 없는 타입.
97
+ // A type with no marker files: the result when nothing was detected, and a type that needs no path.
98
98
  id: "basic",
99
99
  markers: [],
100
100
  },
@@ -106,15 +106,15 @@ export const TYPES = [
106
106
  },
107
107
  ];
108
108
 
109
- // 감지 결과가 하나도 없을 때의 타입
109
+ // Type used when nothing was detected
110
110
  export const FALLBACK_TYPE = "basic";
111
111
 
112
112
  const BY_ID = new Map(TYPES.map((t) => [t.id, t]));
113
113
 
114
- // 알 수 없는 타입은 undefined — 호출부가 각자의 기본값을 정한다.
114
+ // Unknown types give undefined; callers decide their own defaults.
115
115
  export const typeInfo = (id) => BY_ID.get(id);
116
116
 
117
- // types 중 훅 name을 가진 것만 [{ id, hook }]으로 — 주어진 타입 순서를 유지한다.
117
+ // Only those of types that have hook `name`, as [{ id, hook }], keeping the given type order.
118
118
  export function hooksFor(types, name) {
119
119
  const found = [];
120
120
  for (const id of types) {
@@ -124,31 +124,31 @@ export function hooksFor(types, name) {
124
124
  return found;
125
125
  }
126
126
 
127
- // types 중 훅 name을 가진 타입들의 반환값(객체)을 하나로 합친다 — 옵션·컨텍스트 필드처럼 타입마다 다른 키를 낼 때 쓴다.
127
+ // Merges into one object the return values of the hook `name` from the types that have it; used when each type contributes different keys, such as options and context fields.
128
128
  export function mergeHookResults(types, name, ...args) {
129
129
  return Object.assign({}, ...hooksFor(types, name).map(({ hook }) => hook(...args)));
130
130
  }
131
131
 
132
- // 모든 타입이 선언한 훅 상수(contextDefaults·savedOptionKeys 등)를 하나로 합친다 — 타입이 정해지기 전에 필요한 기본 형태용.
132
+ // Merges the hook constants declared by all types (contextDefaults, savedOptionKeys, etc.) into one; for the default shape needed before the types are decided.
133
133
  export const allHookValues = (name) => Object.assign({}, ...TYPES.map((t) => t.hooks?.[name]));
134
134
 
135
- // 표시 순서 그대로의 타입 이름 목록
136
- // VALID_TYPES·ALL_TYPES가 같은 배열을 공유하므로 한쪽에서 바꿔 다른 쪽이 오염되지 않게 고정한다.
135
+ // Type names in display order.
136
+ // VALID_TYPES and ALL_TYPES share the same array, so it is frozen to keep a change on one side from polluting the other.
137
137
  export const TYPE_IDS = Object.freeze(TYPES.map((t) => t.id));
138
138
 
139
139
  const byDetectOrder = (a, b) => a.detectOrder - b.detectOrder;
140
140
 
141
- // 파일 존재만으로 감지하는 타입 (감지 순서대로)
141
+ // Types detected by file existence alone (in detection order)
142
142
  export const MARKER_DETECTED_TYPES = TYPES.filter((t) => t.detectBy === "markers").sort(byDetectOrder);
143
143
 
144
- // package.json 의존성으로 가르는 타입 (판정 순서대로)
144
+ // Types told apart by package.json dependencies (in evaluation order)
145
145
  export const PACKAGE_DETECTED_TYPES = TYPES.filter((t) => t.detectBy === "package").sort(byDetectOrder);
146
146
 
147
- // package.json이 있지만 알려진 프레임워크 의존성이 없을 때의 타입
147
+ // Type used when package.json exists but has no known framework dependency
148
148
  export const PACKAGE_FALLBACK_TYPE = TYPES.find((t) => t.detectBy === "package-fallback").id;
149
149
 
150
- // 모바일 빌드 번호(version_code)를 가진 타입
150
+ // Types that have a mobile build number (version_code)
151
151
  export const BUILD_NUMBER_TYPES = new Set(TYPES.filter((t) => t.buildNumberSource).map((t) => t.id));
152
152
 
153
- // 배포 방식 변형 없이 서버 배포 CD가 하나뿐인 워크플로우 파일명
153
+ // Workflow file names for which there is exactly one server-deploy CD, with no deploy-style variants
154
154
  export const SINGLE_SERVER_CD_FILES = new Set(TYPES.filter((t) => t.singleServerCd).map((t) => t.singleServerCd));
@@ -1,26 +1,28 @@
1
- // 설치 후 검증 — 설치된 워크플로우를 다시 읽어 "이대로 돌아가는가"를 본다.
1
+ // Post-install verification - re-reads the installed workflows to check "will this run as-is".
2
2
  //
3
- // 왜 설치 전이 아니라 후인가: 치환은 파일 단위로 흩어져 일어나고 auto 토큰은 resolver 결과에
4
- // 의존한다. 최종 디스크 내용을 보는 것이 실제로 배포될 것과 같은 것을 보는 유일한 방법이다.
3
+ // Why after install rather than before: substitution happens scattered per file and auto tokens
4
+ // depend on resolver results. Looking at the final disk content is the only way to see the same
5
+ // thing that will actually be deployed.
5
6
  import { join } from "node:path";
6
7
  import { existsSync, readFileSync } from "node:fs";
8
+ import { t } from "../i18n/index.js";
7
9
 
8
- // 치환 대상이 아닌 토큰 — 워크플로우 스크립트 안의 heredoc 구분자다. 값이 아니라 문법이므로
9
- // 미치환 검사에서 제외한다. (예: cat <<'__WIZARD_FILE_CONTENT_EOF__')
10
+ // Tokens that are not substitution targets - heredoc delimiters inside workflow scripts. They are
11
+ // syntax, not values, so they are excluded from the unsubstituted check. (e.g. cat <<'__WIZARD_FILE_CONTENT_EOF__')
10
12
  const SENTINEL_RE = /^__WIZARD_[A-Z0-9_]*__$/;
11
13
  const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g;
12
14
 
13
- // 주석으로 죽어 있는 줄 — 실행되지 않으므로 검사 대상이 아니다.
14
- // 템플릿에는 "[선택] ..." 예시 스텝이 통째로 주석 처리돼 들어 있는데, 이걸 세면
15
- // 쓰지도 않는 Secret을 "등록하세요"라고 안내하게 된다.
15
+ // Lines dead as comments - they never run, so they are not checked.
16
+ // The template contains whole optional example steps commented out; counting them
17
+ // would tell the user to "register" secrets that are never used.
16
18
  const isCommented = (line) => /^\s*#/.test(line);
17
19
 
18
- // 미치환 플레이스홀더 스캔.
19
- // 종전에는 auto 토큰 계산이 실패해도(예: application.yaml을 못 찾아 경로가 빈 문자열) 그 줄을
20
- // 건드리지 않고 넘어가, __APPLICATION_YML_DIR__ 이 그대로 남은 워크플로우가 "설치 성공"으로
21
- // 끝났다. 배포 시점에야 그 이름의 디렉토리가 만들어지며 문제가 드러난다.
20
+ // Scan for unsubstituted placeholders.
21
+ // Previously, even when an auto token computation failed (e.g. application.yaml not found, so the path was an empty string),
22
+ // the line was left untouched, and a workflow still containing __APPLICATION_YML_DIR__ ended as an
23
+ // "install success". The problem only surfaced at deploy time, when a directory with that name got created.
22
24
  //
23
- // 반환: [{ filename, line, token, text }] — 사람이 바로 고칠 수 있게 줄 번호까지 준다.
25
+ // Returns: [{ filename, line, token, text }] - with the line number so a person can fix it right away.
24
26
  export function scanUnsubstituted(workflowsDir, filenames = []) {
25
27
  const found = [];
26
28
  for (const filename of filenames) {
@@ -39,25 +41,29 @@ export function scanUnsubstituted(workflowsDir, filenames = []) {
39
41
  return found;
40
42
  }
41
43
 
42
- // GITHUB_TOKEN은 Actions가 자동 주입하므로 사용자가 등록할 대상이 아니다.
44
+ // GITHUB_TOKEN is injected by Actions automatically, so it is not something the user registers.
43
45
  const AUTO_SECRETS = new Set(["GITHUB_TOKEN"]);
44
- // 없어도 워크플로우가 도는 secret — 폴백이 문서화돼 있다. 필수와 섞어 "등록해야 동작합니다"라고
45
- // 하면 안내 자체를 못 믿게 되므로 분리한다.
46
- // AI_API_KEY → 없으면 Copilot(opt-in, copilot_ai) → 규칙 fallback
47
- // WORKFLOW_PAT → 없으면 GITHUB_TOKEN
46
+ // Secrets the workflow runs without - the fallback is documented. Mixing them with required ones under
47
+ // "must be registered to work" would make the guidance itself untrustworthy, so they are kept apart.
48
+ // AI_API_KEY -> if absent: Copilot (opt-in, copilot_ai) -> rule-based fallback
49
+ // WORKFLOW_PAT -> if absent: GITHUB_TOKEN
48
50
  export const OPTIONAL_SECRETS = new Set(["AI_API_KEY", "WORKFLOW_PAT"]);
49
51
  const SECRET_RE = /secrets\.([A-Z][A-Z0-9_]*)/g;
50
52
 
51
- // 워크플로우 머리 주석의 "NAME (선택)" 표기 — 워크플로우가 비어 있어도 동작하도록 작성된 secret이다.
52
- const OPTIONAL_NOTE_RE = /^\s*#\s*-?\s*([A-Z][A-Z0-9_]*)\s*\(선택/;
53
- // `secrets.A || secrets.B` — 둘 중 하나만 있으면 된다.
53
+ // The "NAME (optional)" note in a workflow header comment - a secret the workflow is written to work without.
54
+ // The payload uses "(optional)"; the legacy Korean marker ("\uC120\uD0DD") is still accepted so workflows installed
55
+ // by older versions keep being classified correctly.
56
+ const OPTIONAL_NOTE_RE = /^\s*#\s*-?\s*([A-Z][A-Z0-9_]*)\s*\((?:\uC120\uD0DD|optional)/i;
57
+ // `secrets.A || secrets.B` - only one of the two is needed.
54
58
  const EITHER_RE = /^\s*\|\|\s*secrets\.([A-Z][A-Z0-9_]*)/;
55
- // `secrets.A || '3000'`, `secrets.A || vars.A` — 기본값·대체값이 있다.
59
+ // `secrets.A || '3000'`, `secrets.A || vars.A` - there is a default/alternative value.
56
60
  const FALLBACK_AFTER_RE = /^\s*\|\|/;
57
61
  const FALLBACK_BEFORE_RE = /\|\|\s*$/;
58
- export const EITHER_SEP = " 또는 ";
62
+ // Separator of a "A or B" secret pair. It is shown to the user and also used to split keys, so it follows
63
+ // the current language; evaluated lazily (via toString) because it must not be fixed at import time.
64
+ export const EITHER_SEP = { toString: () => t("core.verify.eitherSep") };
59
65
 
60
- // 한 파일 안의 secret 참조를 필수 / 둘 중 하나 / 선택으로 가른다.
66
+ // Sorts the secret references in one file into required / either-of-two / optional.
61
67
  function classifyFile(content) {
62
68
  const lines = content.split(/\r?\n/);
63
69
  const noted = new Set();
@@ -78,7 +84,7 @@ function classifyFile(content) {
78
84
  const either = rest.match(EITHER_RE);
79
85
  if (either) {
80
86
  pairs.push([name, either[1]]);
81
- i++; // 짝의 오른쪽은 이미 묶었다
87
+ i++; // the right side of the pair is already bound
82
88
  } else if (FALLBACK_AFTER_RE.test(rest) || FALLBACK_BEFORE_RE.test(line.slice(0, m.index))) {
83
89
  optional.add(name);
84
90
  } else {
@@ -96,10 +102,10 @@ function classifyFile(content) {
96
102
  return out;
97
103
  }
98
104
 
99
- // 설치된 워크플로우의 secret을 필수와 선택으로 나눈다.
100
- // 참조한 이름을 전부 필수로 세면 기본값이 있는 것·둘 중 하나면 되는 폴백 쌍까지 "등록해야 동작합니다"에
101
- // 섞여 개수가 부풀고, 처음 쓰는 사용자가 필요 없는 secret까지 등록하게 된다.
102
- // 반환: { required: Map<이름|"A 또는 B", 파일명[]>, optional: Map<같은 형식> }
105
+ // Splits the secrets of the installed workflows into required and optional.
106
+ // Counting every referenced name as required would mix ones with defaults and either-of-two fallback pairs into
107
+ // "must be registered to work", inflating the count and making first-time users register secrets they do not need.
108
+ // Returns: { required: Map<name|"A or B", filenames[]>, optional: Map<same format> }
103
109
  export function classifySecrets(workflowsDir, filenames = []) {
104
110
  const required = new Map();
105
111
  const optional = new Map();
@@ -117,7 +123,7 @@ export function classifySecrets(workflowsDir, filenames = []) {
117
123
  for (const k of r.required) if (!skip(k)) add(required, k, filename);
118
124
  for (const k of r.optional) if (!skip(k)) add(optional, k, filename);
119
125
  }
120
- // 한 파일에서라도 단독으로 필수인 이름은 필수다 — 그 이름이 든 폴백 쌍·선택 항목은 이미 충족되므로 뺀다.
126
+ // A name that is required on its own in even one file is required - fallback pairs/optional entries containing it are already satisfied, so drop them.
121
127
  const requiredNames = new Set([...required.keys()].filter((k) => !k.includes(EITHER_SEP)));
122
128
  const coveredByRequired = (k) => k.split(EITHER_SEP).some((n) => requiredNames.has(n));
123
129
  for (const k of [...required.keys()]) if (k.includes(EITHER_SEP) && coveredByRequired(k)) required.delete(k);
@@ -126,23 +132,23 @@ export function classifySecrets(workflowsDir, filenames = []) {
126
132
  return { required: sorted(required), optional: sorted(optional) };
127
133
  }
128
134
 
129
- // 설치된 워크플로우가 요구하는 GitHub Secret 목록.
130
- // 완료 화면이 WORKFLOW_PAT와 권한만 안내하는 바람에, 배포 워크플로우가 실제로 필요로 하는
131
- // SERVER_HOST·SSH_KEY 같은 값이 하나도 안내되지 않았다. 설치 직후 상태로는 배포가 돌지 않는데
132
- // 그 사실이 어디에도 드러나지 않는다.
135
+ // List of GitHub Secrets the installed workflows require.
136
+ // The completion screen only mentioned WORKFLOW_PAT and permissions, so values the deploy workflow really needs
137
+ // (SERVER_HOST, SSH_KEY, ...) were never announced. Right after install the deploy does not run,
138
+ // and that fact was shown nowhere.
133
139
  //
134
- // 반환: Map<secretName 또는 "A 또는 B", string[] 그 secret을 쓰는 파일명>
140
+ // Returns: Map<secretName or "A or B", string[] filenames that use the secret>
135
141
  export function collectRequiredSecrets(workflowsDir, filenames = []) {
136
142
  return classifySecrets(workflowsDir, filenames).required;
137
143
  }
138
144
 
139
- // 없어도 워크플로우가 도는 secret(기본값·대체값이 있거나 머리 주석에 "(선택)"으로 적힌 것).
145
+ // Secrets the workflow runs without (having a default/alternative value, or marked optional in the header comment).
140
146
  export function collectOptionalSecrets(workflowsDir, filenames = []) {
141
147
  return classifySecrets(workflowsDir, filenames).optional;
142
148
  }
143
149
 
144
- // SSH 인증 방식에 따라 둘 중 하나만 필요한 secret — 사용자가 이미 답한 값으로 목록을 좁힌다.
145
- // 안 쓸 secret까지 "등록하세요"라고 하면 안내 자체를 신뢰하지 않게 된다.
150
+ // Secrets where only one of two is needed depending on the SSH auth method - narrows the list using the answer the user already gave.
151
+ // Telling the user to "register" secrets that will not be used makes the guidance itself untrusted.
146
152
  export function narrowSecretsBySshAuth(secrets, sshAuthMethod) {
147
153
  if (!sshAuthMethod) return secrets;
148
154
  const drop = sshAuthMethod === "key" ? "SERVER_PASSWORD" : "SSH_KEY";