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,14 +1,15 @@
1
- // Flutter 옵션 — 환경변수 방식·스토어 배포 대상·배포 모드의 단일 진실.
2
- // 스토어 배포 대상은 deploy-style.js(배포 방식)와 같은 구조 — 값 목록, 파일 필터, 선택 해제 정리 — 를 따른다.
1
+ // Flutter options: single source of truth for the env-var mode, store deploy targets and deploy modes.
2
+ // Store deploy targets follow the same structure as deploy-style.js (deploy style): value list, file filter, deselection cleanup.
3
3
  import { join } from "node:path";
4
4
  import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
5
5
  import { sha256 } from "./baseline.js";
6
+ import { t } from "../i18n/index.js";
6
7
 
7
- // 환경변수 주입 방식. dart-define은 --dart-define-from-file, dotenv는 flutter_dotenv/envied용 .env 생성.
8
- // 둘을 동시에 쓰는 both는 만들지 않는다 — 환경변수를 두 곳에서 관리하게 되기 때문.
8
+ // Env-var injection mode. dart-define uses --dart-define-from-file; dotenv generates the .env for flutter_dotenv/envied.
9
+ // No "both" option that uses the two at once: it would mean managing env vars in two places.
9
10
  export const ENV_MODES = ["dart-define", "dotenv"];
10
- export const DEFAULT_ENV_MODE = "dart-define"; // 신규 설치 기본
11
- export const LEGACY_ENV_MODE = "dotenv"; // 기존 설치(version.yml 있음, 저장값 없음)가 조용히 깨지지 않도록 보존
11
+ export const DEFAULT_ENV_MODE = "dart-define"; // default for new installs
12
+ export const LEGACY_ENV_MODE = "dotenv"; // preserved so an existing install (has version.yml, no saved value) does not break silently
12
13
 
13
14
  export const STORE_PLATFORMS = ["android", "ios"];
14
15
  export const DEPLOY_MODES = ["store_only", "store_prepare", "store_submit"];
@@ -18,8 +19,8 @@ export const NO_STORE = "none";
18
19
  export const isEnvMode = (v) => ENV_MODES.includes(v);
19
20
  export const isDeployMode = (v) => DEPLOY_MODES.includes(v);
20
21
 
21
- // "android,ios" | "android" | "none" | "" → 배열. 항상 STORE_PLATFORMS 순서라 직렬화가 결정적이다.
22
- // 알 수 없는 토큰이 하나라도 있거나 문자열이 아니면 null — 호출부가 "값 없음/잘못된 값"으로 처리한다.
22
+ // "android,ios" | "android" | "none" | "" to an array. Always in STORE_PLATFORMS order, so serialization is deterministic.
23
+ // null when any token is unknown or the input is not a string; callers treat that as "no value / invalid value".
23
24
  export function parseStoreList(csv) {
24
25
  if (typeof csv !== "string") return null;
25
26
  const tokens = csv.split(",").map((t) => t.trim()).filter((t) => t !== "");
@@ -28,14 +29,14 @@ export function parseStoreList(csv) {
28
29
  return STORE_PLATFORMS.filter((p) => tokens.includes(p));
29
30
  }
30
31
 
31
- // 저장용 직렬화 — 빈 배열은 빈 문자열이 아니라 "none"으로 적어 "선택 안 함"과 "값 없음"을 구분한다.
32
+ // Serialization for saving: an empty array is written as "none" rather than an empty string, to tell "nothing selected" from "no value".
32
33
  export function formatStoreList(stores) {
33
34
  const ordered = STORE_PLATFORMS.filter((p) => stores.includes(p));
34
35
  return ordered.length ? ordered.join(",") : NO_STORE;
35
36
  }
36
37
 
37
- // 플랫폼별 스토어 워크플로우 (payload/workflows/flutter/ 기준 파일명).
38
- // FIREBASE·SELFHOSTED·TEST-APK·APP-BUILD-TRIGGER·CI는 스토어와 무관해 여기 넣지 않는다 — 항상 설치된다.
38
+ // Store workflows per platform (file names relative to payload/workflows/flutter/).
39
+ // FIREBASE, SELFHOSTED, TEST-APK, APP-BUILD-TRIGGER and CI are unrelated to the stores and are not listed here: they are always installed.
39
40
  export const STORE_WORKFLOWS = {
40
41
  android: ["PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml"],
41
42
  ios: ["PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml", "PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml"],
@@ -43,20 +44,20 @@ export const STORE_WORKFLOWS = {
43
44
 
44
45
  export const isStoreWorkflow = (filename) => Object.values(STORE_WORKFLOWS).flat().includes(filename);
45
46
 
46
- // 파일 필터 — stores가 null이면 미결정이라 현행 동작(전부 설치)이다.
47
- // 배열이면 스토어 워크플로우는 선택된 플랫폼 것만 통과하고, 그 외 파일은 항상 통과한다.
47
+ // File filter: null stores means undecided, which keeps current behavior (install everything).
48
+ // With an array, store workflows pass only for the selected platforms and all other files always pass.
48
49
  export function storeWorkflowFilter(stores) {
49
50
  if (stores === null || stores === undefined) return () => true;
50
51
  const allowed = new Set(STORE_PLATFORMS.filter((p) => stores.includes(p)).flatMap((p) => STORE_WORKFLOWS[p]));
51
52
  return (filename) => !isStoreWorkflow(filename) || allowed.has(filename);
52
53
  }
53
54
 
54
- // 선택 해제한 스토어 워크플로우 정리 — cleanupOtherDeployWorkflows(deploy-style.js)와 같은 규칙이다.
55
- // 손대지 않은 것(baseline의 installed 해시와 동일) → 삭제
56
- // 손댄 것 → .bak으로 옮긴다 (내용 보존, 트리거만 죽인다)
57
- // 사용자 소유인 Fastfile·ExportOptions.plist는 여기서 다루지 않는다 (워크플로우 파일만 대상).
58
- // dryRun이면 판정만 하고 파일은 건드리지 않는다 (--dry-run 미리보기용).
59
- // 반환: { removed:[], backedUp:[] }
55
+ // Cleanup of deselected store workflows: same rule as cleanupOtherDeployWorkflows (deploy-style.js).
56
+ // untouched (same as the baseline installed hash) -> deleted
57
+ // modified -> moved to .bak (content kept, only the trigger is killed)
58
+ // User-owned Fastfile and ExportOptions.plist are not handled here (workflow files only).
59
+ // With dryRun only the decision is made and no files are touched (for the --dry-run preview).
60
+ // Returns: { removed:[], backedUp:[] }
60
61
  export function cleanupDeselectedStoreWorkflows(workflowsDir, installedFilenames, stores, baseline, { dryRun = false } = {}) {
61
62
  const keep = storeWorkflowFilter(stores);
62
63
  const removed = [];
@@ -80,13 +81,13 @@ export function cleanupDeselectedStoreWorkflows(workflowsDir, installedFilenames
80
81
  return { removed, backedUp };
81
82
  }
82
83
 
83
- // 스토어 앱 파일 (payload/flutter-app/ 기준 상대경로) — 플랫폼별 묶음.
84
+ // Store app files (paths relative to payload/flutter-app/), grouped per platform.
84
85
  export const STORE_APP_FILES = {
85
86
  android: ["android/fastlane/Fastfile.playstore"],
86
87
  ios: ["ios/fastlane/Fastfile", "ios/ExportOptions.plist"],
87
88
  };
88
89
 
89
- // 선택된 플랫폼의 앱 파일 목록. stores가 null이면 전부 (현행 동작 = 둘 다 설치).
90
+ // App file list for the selected platforms. All of them when stores is null (current behavior = install both).
90
91
  export function storeAppFilesFor(stores) {
91
92
  const platforms = stores === null || stores === undefined
92
93
  ? STORE_PLATFORMS
@@ -94,24 +95,24 @@ export function storeAppFilesFor(stores) {
94
95
  return platforms.flatMap((p) => STORE_APP_FILES[p]);
95
96
  }
96
97
 
97
- // store_submit은 main push마다 심사를 제출하므로 고른 직후 한 줄로 알린다. 다른 모드는 알릴 것이 없어 빈 문자열.
98
+ // store_submit submits a review on every main push, so a one-line notice is shown right after it is chosen. Other modes have nothing to announce, hence an empty string.
98
99
  export function deployModeWarning(mode) {
99
- return mode === "store_submit" ? "store_submit을 고르면 main push마다 심사가 자동 제출됩니다." : "";
100
+ return mode === "store_submit" ? t("core.flutterOptions.storeSubmitWarning") : "";
100
101
  }
101
102
 
102
103
  const validOr = (isValid, value, fallback) => (isValid(value) ? value : fallback);
103
104
 
104
- // 옵션 최종 결정 — 우선순위: CLI > version.yml 저장값 > 기본값.
105
- // cli { envMode:"", stores:null|string[], androidDeployMode:"", iosDeployMode:"" } (빈값/null = 미지정)
106
- // existing parseExisting() 결과 또는 null (version.yml이 없으면 신규 설치)
105
+ // Final option decision. Priority: CLI > value saved in version.yml > default.
106
+ // cli { envMode:"", stores:null|string[], androidDeployMode:"", iosDeployMode:"" } (empty/null = not given)
107
+ // existing parseExisting() result or null (no version.yml means a fresh install)
107
108
  //
108
- // - envMode 기본값: 신규 설치와 "Flutter를 새로 추가하는" 기존 설치는 dart-define, 이미 Flutter가
109
- // 설치돼 있던 프로젝트는 dotenv. 업데이트 한 번에 flutter_dotenv 프로젝트가 조용히 깨지지 않게
110
- // 하려는 것이라, 판단 기준은 "이 프로젝트에 Flutter가 이미 있었는가"다(단순 existing 존재 여부가
111
- // 아니다 — Spring 전용 프로젝트에 flutter 타입을 처음 추가하는 경우까지 dotenv로 묶으면 안 된다).
112
- // - stores: null은 "미결정" — 비대화형은 현행 동작(둘 다 설치), 대화형은 질문한다.
113
- // - 저장값은 유효할 때만 쓴다. version.yml은 사람이 고칠 수 있는데, 그 값이 워크플로우 표현식
114
- // (`|| 'store_only'` 폴백 자리)에 그대로 들어가므로 목록 밖 문자열은 걸러야 한다.
109
+ // - envMode default: fresh installs and existing installs that are "newly adding Flutter" get dart-define; projects
110
+ // that already had Flutter installed get dotenv. This keeps a single update from silently breaking a flutter_dotenv
111
+ // project, so the criterion is "did this project already have Flutter" (not merely whether existing is present:
112
+ // adding the flutter type for the first time to a Spring-only project must not be lumped into dotenv).
113
+ // - stores: null means "undecided": non-interactive keeps current behavior (install both), interactive asks.
114
+ // - Saved values are used only when valid. version.yml can be hand-edited and the value goes straight into a
115
+ // workflow expression (the `|| 'store_only'` fallback slot), so strings outside the list must be filtered out.
115
116
  export function resolveFlutterOptions({ cli = {}, existing = null } = {}) {
116
117
  const saved = existing?.options ?? {};
117
118
  const hadFlutterAlready = Array.isArray(existing?.types) && existing.types.includes("flutter");
@@ -1,4 +1,4 @@
1
- // 파일시스템 공용 유틸 (LF 보존 바이트 복사). 텍스트는 그대로 복사해 원본 줄바꿈 유지.
1
+ // Shared filesystem utilities (byte-for-byte copy, LF preserved). Text is copied as-is so original line endings survive.
2
2
  import {
3
3
  cpSync, existsSync, readFileSync, writeFileSync, mkdirSync,
4
4
  readdirSync, rmSync, accessSync, constants,
@@ -13,27 +13,27 @@ export function writeText(p, s) {
13
13
  writeFileSync(p, s);
14
14
  }
15
15
 
16
- // 단일 파일 복사 (부모 디렉토리 자동 생성, 바이트 그대로)
16
+ // Copy a single file (creates parent directories, bytes unchanged)
17
17
  export function copyFileSync(src, dst) {
18
18
  mkdirSync(dirname(dst), { recursive: true });
19
19
  cpSync(src, dst);
20
20
  }
21
21
 
22
- // 디렉토리 재귀 복사 (내용을 dst 하위로)
22
+ // Recursively copy a directory (contents go under dst)
23
23
  export function copyDirSync(src, dst) {
24
24
  mkdirSync(dst, { recursive: true });
25
25
  cpSync(src, dst, { recursive: true });
26
26
  }
27
27
 
28
- // 파일/폴더 삭제 (없어도 무해)
28
+ // Delete a file/folder (harmless if missing)
29
29
  export function remove(p) {
30
30
  rmSync(p, { recursive: true, force: true });
31
31
  }
32
32
 
33
- // 디렉토리 직하위 .yaml/.yml 파일명 목록. 하위 폴더 제외.
34
- // 정렬 순서는 고정한다: 확장자로 1차 그룹(.yaml 먼저 → .yml 나중), 각 그룹 안에서 알파벳순.
35
- // (단순 .sort()는 확장자를 섞어 정렬해 기존 설치와 파일 순회 순서가 갈리고,
36
- // 그 결과 version.yml deploy 블록의 키 순서까지 달라진다. 확장자 그룹핑으로 재실행 결과를 바이트 단위로 유지.)
33
+ // Names of the .yaml/.yml files directly inside a directory (subfolders excluded).
34
+ // Sort order is fixed: grouped by extension first (.yaml before .yml), alphabetical within each group.
35
+ // (A plain .sort() mixes extensions, so the traversal order diverges from existing installs and
36
+ // the key order of the version.yml deploy block changes with it. Grouping keeps re-runs byte-identical.)
37
37
  export function listYamlFiles(dir) {
38
38
  if (!existsSync(dir)) return [];
39
39
  const names = readdirSync(dir, { withFileTypes: true })
@@ -44,10 +44,10 @@ export function listYamlFiles(dir) {
44
44
  return [...yaml, ...yml];
45
45
  }
46
46
 
47
- // 쓰기 전에 권한을 미리 확인한다 — 중간에 EACCES로 멈추면 일부만 설치된 상태가 남고,
48
- // 다음 실행에서는 기준점(baseline)이 없어 이미 쓴 파일이 전부 충돌로 분류된다.
49
- // dirs: 파일을 만들 폴더(없으면 만들어질 가장 가까운 상위 폴더를 본다), files: 이미 있으면 덮어쓸 파일.
50
- // 반환: 쓸 수 없는 경로(root 기준 상대경로) 목록.
47
+ // Check permissions before writing - stopping midway on EACCES leaves a partial install, and the
48
+ // next run has no baseline, so every file already written would be classified as a conflict.
49
+ // dirs: folders to create files in (if missing, the nearest existing ancestor is checked), files: existing files to be overwritten.
50
+ // Returns the list of unwritable paths (relative to root).
51
51
  export function findUnwritable(root, dirs = [], files = []) {
52
52
  const writable = (p) => { try { accessSync(p, constants.W_OK); return true; } catch { return false; } };
53
53
  const blocked = new Set();
@@ -1,10 +1,10 @@
1
- // 이미 설치된 스토어 배포 워크플로우 파일명으로 플랫폼을 추론한다.
2
- // version.yml에 flutter_store 저장값이 없는 기존 설치가 대상이다 — 저장값이 없다고 "선택 없음"으로 보면
3
- // 잘 쓰던 스토어 워크플로우가 선택 해제 정리 규칙에 의해 삭제된다.
1
+ // Infers platforms from the file names of already-installed store deploy workflows.
2
+ // Targets existing installs whose version.yml has no flutter_store value: treating a missing value as "nothing
3
+ // selected" would make the deselection cleanup rule delete store workflows that were working fine.
4
4
  import { existsSync, readdirSync } from "node:fs";
5
5
  import { STORE_PLATFORMS, STORE_WORKFLOWS } from "./flutter-options.js";
6
6
 
7
- // 반환: 스토어 워크플로우가 하나라도 설치된 플랫폼 (STORE_PLATFORMS 순서).
7
+ // Returns the platforms with at least one store workflow installed (in STORE_PLATFORMS order).
8
8
  export function inferInstalledStores(workflowsDir) {
9
9
  if (!existsSync(workflowsDir)) return [];
10
10
  const installed = new Set(readdirSync(workflowsDir));
@@ -1,45 +1,46 @@
1
- // 실행 추적 로그 — "무엇을 어떤 순서로 왜 그렇게 했는지"를 시간순으로 남긴다.
1
+ // Execution trace log - records "what was done, in what order, and why" chronologically.
2
2
  //
3
- // 왜 즉시 append인가: 디버깅에서 가장 알고 싶은 순간은 크래시 직전이다. 끝나고 한 번에
4
- // 쓰는 구조는 예외가 나면 아무것도 남기지 못한다(구 install-log.js가 그랬다).
3
+ // Why append immediately: the moment you most want to see when debugging is right before a crash.
4
+ // A structure that writes everything at the end leaves nothing if an exception is thrown (the old install-log.js did).
5
5
  //
6
- // 왜 로컬 전용인가: 상세도를 제약하지 않기 위해서다. 로그 디렉토리에 .gitignore를 직접
7
- // 두어 그 폴더만 추적에서 뺀다 — 루트 .gitignore는 건드리지 않는다.
6
+ // Why local-only: so the level of detail is not constrained. A .gitignore placed inside the log
7
+ // directory takes only that folder out of tracking - the root .gitignore is left alone.
8
8
  import { appendFileSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
+ import { t } from "../i18n/index.js";
10
11
 
11
12
  export const LOG_DIR = ".github/.wizard/logs";
12
- const KEEP = 20; // 유지할 로그 파일 수
13
+ const KEEP = 20; // number of log files to keep
13
14
  const GITIGNORE_BODY = "*\n!.gitignore\n";
14
15
 
15
- // 값에 비밀이 들어갈 수 있는 키 — 현재 질문 항목에는 없지만(도메인·경로·포트·인증 '방식'),
16
- // 앞으로 추가될 때 그냥 평문으로 남지 않도록 처음부터 걸어둔다.
16
+ // Keys whose values may hold secrets - none of the current prompts do (domain, path, port, auth 'method'),
17
+ // but this is in place from the start so future additions are not logged in plain text.
17
18
  const SECRET_KEY_RE = /(PASSWORD|SECRET|TOKEN|KEY|CREDENTIAL)/i;
18
19
  const MASK = "***";
19
20
 
20
21
  let state = null; // { targetRoot, name, header, file, rel, clock, startedAt, disabled }
21
22
 
22
23
  export function maskValue(key, value) {
23
- // SSH_AUTH_METHOD처럼 "방식"만 담는 키는 비밀이 아니다 — 이름에 KEY가 들어가도 마스킹하지 않는다.
24
+ // Keys like SSH_AUTH_METHOD hold only a "method", not a secret - not masked even though the name contains KEY.
24
25
  if (key === "SSH_AUTH_METHOD") return value;
25
26
  return SECRET_KEY_RE.test(key) ? MASK : value;
26
27
  }
27
28
 
28
- // "2026-08-26 12:03:41" → "20260826-120341". 파일명이 곧 정렬 키가 되도록.
29
+ // "2026-08-26 12:03:41" -> "20260826-120341", so the filename doubles as the sort key.
29
30
  export function stampFrom(now = "") {
30
31
  const m = String(now).match(/(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/);
31
32
  if (!m) return "unknown";
32
33
  return `${m[1]}${m[2]}${m[3]}-${m[4]}${m[5]}${m[6]}`;
33
34
  }
34
35
 
35
- // 초 단위 이름만 쓰면 같은 초에 연달아 실행할 때 앞 실행의 로그를 덮어쓴다 — 밀리초를 붙인다.
36
- // 밀리초도 시각 순서라 이름 정렬이 곧 실행 순서라는 전제(rotate)는 그대로 유지된다.
36
+ // A seconds-only name would overwrite the previous run's log when runs follow within the same second - milliseconds are appended.
37
+ // Milliseconds are also in time order, so the premise that name order equals run order (rotate) still holds.
37
38
  export function logFilename(now, action = "install", ms = 0) {
38
39
  return `${stampFrom(now)}-${String(ms).padStart(3, "0")}-${action}.log`;
39
40
  }
40
41
 
41
- // 같은 이름이 이미 있으면(다른 프로세스가 같은 밀리초에 연 경우) -2, -3을 붙여 새로 만든다.
42
- // wx 플래그로 "없을 때만 생성"을 원자적으로 판정해 동시 실행끼리도 서로 덮어쓰지 않는다.
42
+ // If the name already exists (another process opened one in the same millisecond), -2, -3 is appended for a new file.
43
+ // The wx flag decides "create only if absent" atomically, so concurrent runs never overwrite each other.
43
44
  function createUnique(dir, base, header) {
44
45
  const stem = base.replace(/\.log$/, "");
45
46
  for (let n = 1; n < 100; n++) {
@@ -51,11 +52,11 @@ function createUnique(dir, base, header) {
51
52
  if (e.code !== "EEXIST") throw e;
52
53
  }
53
54
  }
54
- throw new Error(`로그 파일 이름이 모두 사용 중입니다: ${base}`);
55
+ throw new Error(t("core.logger.error.nameExhausted", { base }));
55
56
  }
56
57
 
57
- // 최근 KEEP개만 남기고 오래된 것부터 지운다. 파일명이 시각 오름차순이라 이름 정렬로 충분하다.
58
- // 새 파일이 곧 하나 추가되므로 KEEP-1개까지 줄인다.
58
+ // Keep only the latest KEEP files, deleting the oldest first. Filenames ascend by time, so name sorting is enough.
59
+ // A new file is about to be added, so trim down to KEEP-1.
59
60
  function rotate(dir) {
60
61
  const logs = readdirSync(dir).filter((f) => f.endsWith(".log")).sort();
61
62
  for (const f of logs.slice(0, Math.max(0, logs.length - (KEEP - 1)))) {
@@ -63,12 +64,12 @@ function rotate(dir) {
63
64
  }
64
65
  }
65
66
 
66
- // 파일은 첫 기록 때 만든다 — 읽기 전용 모드(status/doctor)나 인자 검증에서 거부된 실행처럼
67
- // 아무것도 바꾸지 않은 실행이 대상 레포에 로그 폴더와 헤더만 있는 파일을 남기지 않도록.
67
+ // The file is created on the first record - so runs that change nothing (read-only modes like status/doctor,
68
+ // or runs rejected during argument validation) do not leave a log folder and a header-only file in the target repo.
68
69
  export function initLogger(targetRoot, opts = {}) {
69
70
  const { action = "install", now = "", argv = [], templateVersion = "unknown", clock = () => new Date(),
70
71
  ms = new Date().getUTCMilliseconds() } = opts;
71
- // 시각은 UTC다 — 로컬 시간대로 읽으면 몇 시간 어긋나 보이므로 헤더에 밝혀 둔다.
72
+ // Times are UTC - reading them as local time looks off by hours, so the header says so.
72
73
  const header =
73
74
  `=== project-auto-wizard v${templateVersion} | ${action} | ${now} UTC ===\n` +
74
75
  `argv : ${["project-auto-wizard", ...argv].join(" ")}\n` +
@@ -79,12 +80,12 @@ export function initLogger(targetRoot, opts = {}) {
79
80
  return { get path() { return st.rel; } };
80
81
  }
81
82
 
82
- // 첫 기록 직전에 로그 파일을 연다. 실패하면 이후 기록을 끈다.
83
+ // Opens the log file just before the first record. On failure, later records are turned off.
83
84
  function open(st) {
84
85
  try {
85
86
  const dir = join(st.targetRoot, LOG_DIR);
86
87
  mkdirSync(dir, { recursive: true });
87
- // 사용자가 직접 둔 .gitignore가 있으면 존중한다.
88
+ // Respect a .gitignore the user placed themselves.
88
89
  const gi = join(dir, ".gitignore");
89
90
  if (!existsSync(gi)) writeFileSync(gi, GITIGNORE_BODY);
90
91
  rotate(dir);
@@ -92,9 +93,9 @@ function open(st) {
92
93
  st.file = join(st.targetRoot, st.rel);
93
94
  return true;
94
95
  } catch (e) {
95
- // 로그를 못 남긴 것이 설치를 되돌릴 이유는 아니다 — 다만 조용히 삼키지는 않는다.
96
+ // Failing to write the log is no reason to roll back the install - but it is not swallowed silently either.
96
97
  st.disabled = true;
97
- process.stderr.write(`[warn] 실행 로그를 시작하지 못했습니다: ${e.message}\n`);
98
+ process.stderr.write(`[warn] ${t("core.logger.warn.startFailed", { message: e.message })}\n`);
98
99
  return false;
99
100
  }
100
101
  }
@@ -103,13 +104,13 @@ export function resetLogger() {
103
104
  state = null;
104
105
  }
105
106
 
106
- // 이번 실행의 로그 경로(레포 상대). 설치 요약 화면이 사용자에게 안내할 때 쓴다.
107
+ // Log path of this run (repo-relative). Used by the install summary screen to tell the user.
107
108
  export function currentLogPath() {
108
109
  return state && !state.disabled ? state.rel : "";
109
110
  }
110
111
 
111
- // 구버전(.md) 설치 기록이 남아 있는지 — .gitignore는 이미 git이 추적 중인 파일에는
112
- // 영향이 없으므로, 있으면 사용자가 직접 추적을 끊도록 안내해야 한다.
112
+ // Whether old-format (.md) install records remain - .gitignore has no effect on files git already
113
+ // tracks, so if any exist the user must be told to untrack them manually.
113
114
  export function hasLegacyMdLogs(targetRoot) {
114
115
  try {
115
116
  const dir = join(targetRoot, LOG_DIR);
@@ -117,16 +118,16 @@ export function hasLegacyMdLogs(targetRoot) {
117
118
  } catch { return false; }
118
119
  }
119
120
 
120
- // 열 너비 — 사람이 훑을 때 컬럼이 맞고, 에이전트가 컬럼 단위로 끊어 읽을 수 있게 고정한다.
121
- const SCOPE_W = 8; // 가장 긴 scope('baseline')에 맞춘다 — 컬럼이 밀리면 훑기가 나빠진다
121
+ // Column widths - fixed so columns line up when skimmed and can be split by column when parsed.
122
+ const SCOPE_W = 8; // fits the longest scope ('baseline') - misaligned columns make skimming worse
122
123
  const ACTION_W = 10;
123
124
 
124
- // 동아시아 전각 문자는 폭 2로 센다 (요약 블록 정렬용).
125
+ // East Asian full-width characters count as width 2 (for aligning the summary block).
125
126
  const WIDE_RE = /[\u1100-\u115F\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE6F\uFF00-\uFF60\uFFE0-\uFFE6]/;
126
127
  const dispWidth = (s) => [...String(s)].reduce((n, c) => n + (WIDE_RE.test(c) ? 2 : 1), 0);
127
128
 
128
- // 헤더의 실행 시각과 파일명이 UTC 기준(utcNow)이므로 라인 시각도 UTC로 맞춘다 —
129
- // 로컬 시간을 쓰면 같은 파일 안에서 헤더와 라인이 시간대만큼 어긋난다.
129
+ // The header's run time and the filename are UTC (utcNow), so line times are UTC too -
130
+ // local time would put the header and lines off by the timezone offset within the same file.
130
131
  function hhmmss(date) {
131
132
  const p = (n, w = 2) => String(n).padStart(w, "0");
132
133
  return `${p(date.getUTCHours())}:${p(date.getUTCMinutes())}:${p(date.getUTCSeconds())}.${p(date.getUTCMilliseconds(), 3)}`;
@@ -139,9 +140,9 @@ function write(level, scope, action, detail = "") {
139
140
  const line = `${hhmmss(state.clock())} ${level} ${String(scope).padEnd(SCOPE_W)} ${String(action).padEnd(ACTION_W)} ${detail}`.trimEnd();
140
141
  appendFileSync(state.file, line + "\n");
141
142
  } catch (e) {
142
- // 첫 실패에서 한 번만 알리고 이후는 조용히 끈다 — 매 줄 경고를 뱉으면 설치 화면이 무너진다.
143
+ // Report only on the first failure, then quietly turn off - a warning on every line would wreck the install screen.
143
144
  state.disabled = true;
144
- process.stderr.write(`[warn] 실행 로그 기록을 중단합니다: ${e.message}\n`);
145
+ process.stderr.write(`[warn] ${t("core.logger.warn.writeStopped", { message: e.message })}\n`);
145
146
  }
146
147
  }
147
148
 
@@ -149,24 +150,25 @@ export const log = {
149
150
  info: (scope, action, detail) => write("INFO", scope, action, detail),
150
151
  warn: (scope, action, detail) => write("WARN", scope, action, detail),
151
152
  fail: (scope, action, detail) => write("FAIL", scope, action, detail),
152
- // rows: Array<[label, value]> — 라벨 폭을 맞춰 정렬한다.
153
+ // rows: Array<[label, value]> - labels are padded to align.
153
154
  summary(rows = []) {
154
155
  if (!state || state.disabled || !rows.length) return;
155
156
  if (!state.file && !open(state)) return;
156
- // 한글은 터미널에서 2칸을 차지한다 — 문자 수로 맞추면 눈으로 볼 때 어긋난다.
157
+ // Hangul takes 2 cells in a terminal - padding by character count looks misaligned.
157
158
  const w = Math.max(...rows.map(([k]) => dispWidth(k)));
158
159
  const body = rows.map(([k, v]) => `${k}${" ".repeat(w - dispWidth(k))} : ${v}`).join("\n");
159
160
  try {
160
- appendFileSync(state.file, `\n=== 요약 ===\n${body}\n`);
161
+ appendFileSync(state.file, `\n${t("core.logger.summary.title")}\n${body}\n`);
161
162
  } catch {
162
163
  state.disabled = true;
163
164
  }
164
165
  },
165
166
  };
166
167
 
167
- // 로그 폴더(.github/.wizard)째 지울 수 있는 삭제 실행용. 지우기 전에 쓰면 기록이 함께 사라지며
168
- // ENOENT 경고가 나고, 지운 뒤에 쓰면 폴더를 되살려 완전 삭제 뒤에도 흔적이 남는다.
169
- // 그래서 기록을 모았다가 폴더가 남아 있을 때만 쓴다(지워졌다면 결과는 화면 출력으로 대신한다).
168
+ // For removal runs that may delete the log folder (.github/.wizard) itself. Writing before the delete
169
+ // loses the records along with it and raises an ENOENT warning, and writing after would resurrect the folder,
170
+ // leaving a trace even after a full removal. So records are collected and written only if the folder survives
171
+ // (if it is gone, the result is shown on screen instead).
170
172
  export function logRemovals(targetRoot, entries = []) {
171
173
  if (!existsSync(dirname(join(targetRoot, LOG_DIR)))) return;
172
174
  for (const [scope, action, detail] of entries) write("INFO", scope, action, detail);