project-auto-wizard 0.13.3 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.ja.md +2 -0
  2. package/README.ko.md +2 -0
  3. package/README.md +3 -1
  4. package/README.zh-CN.md +2 -0
  5. package/bin/project-auto-wizard.js +18 -4
  6. package/package.json +2 -2
  7. package/payload/config/breaking-changes.json +28 -14
  8. package/payload/config/wizard-prompts.yml +102 -46
  9. package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
  10. package/payload/flutter-app/ios/ExportOptions.plist +8 -8
  11. package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
  12. package/payload/scripts/changelog_manager.py +304 -265
  13. package/payload/scripts/issue_helper.py +54 -50
  14. package/payload/scripts/messages.py +2585 -0
  15. package/payload/scripts/truncate_release_notes.py +7 -6
  16. package/payload/scripts/version_manager.py +110 -107
  17. package/payload/version.yml.template +2 -1
  18. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +23 -16
  19. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +106 -63
  20. package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
  21. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +55 -45
  22. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +90 -72
  23. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +59 -45
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
  25. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
  26. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
  27. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
  28. package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
  29. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
  30. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
  31. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
  32. package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
  33. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
  34. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
  35. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
  36. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
  37. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
  38. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
  39. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
  40. package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
  41. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
  42. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
  43. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
  44. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
  45. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
  46. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
  47. package/src/cli/args.js +98 -52
  48. package/src/cli/help.js +8 -41
  49. package/src/commands/doctor.js +90 -89
  50. package/src/commands/dry-run.js +64 -43
  51. package/src/commands/full.js +79 -77
  52. package/src/commands/install-settings.js +16 -15
  53. package/src/commands/interactive-flutter.js +20 -19
  54. package/src/commands/interactive.js +135 -132
  55. package/src/commands/purge.js +47 -42
  56. package/src/commands/status.js +36 -36
  57. package/src/commands/uninstall.js +43 -41
  58. package/src/context.js +12 -11
  59. package/src/core/assets.js +12 -11
  60. package/src/core/baseline.js +27 -27
  61. package/src/core/branches.js +29 -27
  62. package/src/core/branding.js +7 -6
  63. package/src/core/breaking-check.js +22 -21
  64. package/src/core/breaking.js +15 -8
  65. package/src/core/copy/app-files.js +5 -4
  66. package/src/core/copy/flutter-app.js +8 -8
  67. package/src/core/copy/gitignore.js +58 -50
  68. package/src/core/copy/readme.js +102 -36
  69. package/src/core/copy/simple.js +16 -15
  70. package/src/core/copy/workflows.js +131 -124
  71. package/src/core/deploy-style.js +51 -48
  72. package/src/core/detect-fs.js +57 -57
  73. package/src/core/detect.js +67 -65
  74. package/src/core/errors.js +2 -2
  75. package/src/core/flutter-doctor.js +17 -16
  76. package/src/core/flutter-hooks.js +33 -32
  77. package/src/core/flutter-options.js +34 -33
  78. package/src/core/fsutil.js +12 -12
  79. package/src/core/installed-stores.js +4 -4
  80. package/src/core/logger.js +42 -40
  81. package/src/core/paths-resolve.js +88 -86
  82. package/src/core/paths.js +10 -10
  83. package/src/core/release-options.js +8 -8
  84. package/src/core/removal-exec.js +14 -13
  85. package/src/core/removal-plan.js +37 -37
  86. package/src/core/types.js +47 -47
  87. package/src/core/verify.js +44 -38
  88. package/src/core/version-yml.js +76 -68
  89. package/src/core/wizard-env.js +46 -46
  90. package/src/core/wizard-labels.js +63 -41
  91. package/src/i18n/catalog/en/cli.js +50 -0
  92. package/src/i18n/catalog/en/commands.js +188 -0
  93. package/src/i18n/catalog/en/copy.js +20 -0
  94. package/src/i18n/catalog/en/core.js +74 -0
  95. package/src/i18n/catalog/en/core2.js +15 -0
  96. package/src/i18n/catalog/en/ui.js +221 -0
  97. package/src/i18n/catalog/en.js +12 -0
  98. package/src/i18n/catalog/index.js +5 -0
  99. package/src/i18n/catalog/ko/cli.js +50 -0
  100. package/src/i18n/catalog/ko/commands.js +188 -0
  101. package/src/i18n/catalog/ko/copy.js +20 -0
  102. package/src/i18n/catalog/ko/core.js +74 -0
  103. package/src/i18n/catalog/ko/core2.js +15 -0
  104. package/src/i18n/catalog/ko/ui.js +221 -0
  105. package/src/i18n/catalog/ko.js +10 -0
  106. package/src/i18n/index.js +44 -0
  107. package/src/i18n/languages.js +9 -0
  108. package/src/index.js +132 -97
  109. package/src/ui/ansi.js +7 -7
  110. package/src/ui/banner.js +7 -6
  111. package/src/ui/env-plan.js +82 -81
  112. package/src/ui/prompts.js +97 -122
  113. package/src/ui/readline-engine.js +59 -58
  114. package/src/ui/status-cards.js +37 -31
  115. package/src/ui/summary.js +90 -89
@@ -1,54 +1,56 @@
1
- // 배포 방식 — 서버 배포 CD 워크플로우는 서로 대체재다.
2
- // Nginx 무중단과 Traefik 무중단을 동시에 쓰는 경우는 없으므로 하나만 설치한다.
3
- // 고른 것은 push 트리거까지 켜서 설치한다 — 설치했는데 안 도는 상태를 만들지 않는다.
1
+ // Deploy style: the server-deploy CD workflows are alternatives to each other.
2
+ // Nginx zero-downtime and Traefik zero-downtime are never used together, so only one is installed.
3
+ // The chosen one is installed with its push trigger enabled, so nothing is installed that then never runs.
4
4
  import { join } from "node:path";
5
5
  import { existsSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
6
6
  import { sha256 } from "./baseline.js";
7
7
  import { PAYLOAD, typeWorkflowDirs } from "./paths.js";
8
8
  import { SINGLE_SERVER_CD_FILES } from "./types.js";
9
+ import { t } from "../i18n/index.js";
9
10
 
10
- // 파일명 접미사로 식별한다 — 타입 접두사(PROJECT-SPRING- 등)는 타입마다 다르기 때문.
11
+ // Identified by file name suffix, because the type prefix (PROJECT-SPRING- etc.) differs per type.
12
+ // label is a getter so the text follows the language resolved after import.
11
13
  export const DEPLOY_STYLES = [
12
- { value: "simple", suffix: "-SIMPLE-CICD.yaml", label: "단일 서버 배포 — 컨테이너를 내렸다 올린다 (가장 단순, 짧은 다운타임)" },
13
- { value: "nginx", suffix: "-NONSTOP-NGINX-CICD.yaml", label: "무중단 배포 (Nginx) — nginx config의 proxy_pass 포트를 Blue/Green으로 토글" },
14
- { value: "traefik", suffix: "-NONSTOP-TRAEFIK-CICD.yaml", label: "무중단 배포 (Traefik) — Traefik 라우팅으로 Blue/Green 전환" },
14
+ { value: "simple", suffix: "-SIMPLE-CICD.yaml", get label() { return t("core.deploy.label.simple"); } },
15
+ { value: "nginx", suffix: "-NONSTOP-NGINX-CICD.yaml", get label() { return t("core.deploy.label.nginx"); } },
16
+ { value: "traefik", suffix: "-NONSTOP-TRAEFIK-CICD.yaml", get label() { return t("core.deploy.label.traefik"); } },
15
17
  ];
16
18
 
17
19
  export const DEFAULT_DEPLOY_STYLE = "simple";
18
20
 
19
- // 서버 배포 자체를 하지 않는 프로젝트(프론트엔드 전용, 라이브러리 등)를 위한 값.
20
- // DEPLOY_STYLES에는 넣지 않는다 — isDeployWorkflow/suffixOf가 이 배열을 순회하는데, 빈 접미사를
21
- // 돌려주면 endsWith("")가 항상 참이라 모든 파일이 CD로 오판되어 cleanupOtherDeployWorkflows가
22
- // 설치된 워크플로우 전체를 지우는 회귀가 생긴다.
21
+ // Value for projects that do no server deploy at all (frontend-only, libraries, etc.).
22
+ // Not put into DEPLOY_STYLES: isDeployWorkflow/suffixOf iterate that array, and an empty suffix would make
23
+ // endsWith("") always true, so every file would be misjudged as CD and cleanupOtherDeployWorkflows would
24
+ // delete all installed workflows (a regression).
23
25
  export const NO_DEPLOY_STYLE = "none";
24
26
 
25
27
  export const isDeployStyle = (v) => v === NO_DEPLOY_STYLE || DEPLOY_STYLES.some((s) => s.value === v);
26
28
 
27
- // 이 파일이 CD 본체인가 (= 택1 대상인가). PR 프리뷰는 배포 방식과 직교하는 축이라 제외한다.
29
+ // Whether this file is a CD body (= subject of the pick-one choice). PR preview is orthogonal to the deploy style, so it is excluded.
28
30
  export const isDeployWorkflow = (filename) => DEPLOY_STYLES.some((s) => filename.endsWith(s.suffix));
29
31
 
30
32
  const PREVIEW_SUFFIX = "-PR-PREVIEW.yaml";
31
33
 
32
- // 서버에 배포하는 워크플로우 전부 — CD 본체, 단일 CD(react·next), PR 프리뷰.
33
- // "배포 안 함"은 이 전부를 모든 타입에서 똑같이 뺀다. 일부만 빼면 서버 Secret 요구가 남는다.
34
+ // All workflows that deploy to a server: CD bodies, single CDs (react, next) and PR preview.
35
+ // "No deploy" removes all of them identically for every type. Removing only some would leave server Secret requirements behind.
34
36
  export const isServerDeployWorkflow = (filename) =>
35
37
  isDeployWorkflow(filename) || SINGLE_SERVER_CD_FILES.has(filename) || filename.endsWith(PREVIEW_SUFFIX);
36
38
 
37
- // 선택한 타입 중 서버 배포 워크플로우를 가진 타입이 있는가 — payload 파일로 판정한다.
38
- // 없으면(node·flutter 등) 배포 방식은 설치 결과에 아무 영향이 없으므로 묻지도 기록하지도 않는다.
39
+ // Whether any selected type has server deploy workflows, judged from the payload files.
40
+ // If none (node, flutter, etc.) the deploy style has no effect on the install result, so it is neither asked nor recorded.
39
41
  export function hasServerDeployWorkflows(payloadRoot, types = []) {
40
42
  return types.some((type) => typeWorkflowDirs(payloadRoot, type)
41
43
  .some((dir) => existsSync(dir) && readdirSync(dir).some(isServerDeployWorkflow)));
42
44
  }
43
45
 
44
- // 선택한 타입 중 무중단(nginx·traefik) 워크플로우가 있는 타입이 있는가 — 없으면 무중단 선택지를 보이지 않는다.
46
+ // Whether any selected type has zero-downtime (nginx, traefik) workflows; if none, the zero-downtime choices are not shown.
45
47
  export function hasNonstopWorkflows(payloadRoot, types = []) {
46
48
  const nonstop = DEPLOY_STYLES.filter((s) => s.value !== DEFAULT_DEPLOY_STYLE).map((s) => s.suffix);
47
49
  return types.some((type) => typeWorkflowDirs(payloadRoot, type)
48
50
  .some((dir) => existsSync(dir) && readdirSync(dir).some((f) => nonstop.some((sfx) => f.endsWith(sfx)))));
49
51
  }
50
52
 
51
- // payload/workflows/** 의 워크플로우 파일명 전체. 파일명은 PROJECT-{TYPE}- 접두사로 타입 간 유일하다.
53
+ // All workflow file names under payload/workflows/**. File names are unique across types thanks to the PROJECT-{TYPE}- prefix.
52
54
  export function payloadWorkflowNames(payloadRoot) {
53
55
  const names = new Set();
54
56
  const root = join(payloadRoot, PAYLOAD.workflowsDir);
@@ -59,24 +61,24 @@ export function payloadWorkflowNames(payloadRoot) {
59
61
  return names;
60
62
  }
61
63
 
62
- // 모르는 값은 기본값으로 수렴시킨다. 빈 접미사를 돌려주면 endsWith("")가 항상 참이라
63
- // "전부 통과"가 되어, 잘못된 값이 조용히 CD 전부 설치로 새어나간다.
64
+ // Unknown values converge to the default. Returning an empty suffix would make endsWith("") always true,
65
+ // i.e. "everything passes", and an invalid value would silently leak into installing every CD.
64
66
  const suffixOf = (style) =>
65
67
  (DEPLOY_STYLES.find((s) => s.value === style) ?? DEPLOY_STYLES.find((s) => s.value === DEFAULT_DEPLOY_STYLE)).suffix;
66
68
  const SIMPLE_SUFFIX = suffixOf(DEFAULT_DEPLOY_STYLE);
67
69
 
68
- // 고른 방식의 CD가 없는 타입의 단일 서버 배포 CD인가 (예: nginx를 골랐는데 python·go에는 NONSTOP이 없다).
69
- // 그런 타입은 단일 서버 배포로 대신 설치한다 — 설치 직후 지우거나 .bak으로 옮기면 배포가 사라진다.
70
+ // Whether this is the single-server deploy CD of a type that lacks the chosen style's CD (e.g. nginx was chosen but python and go have no NONSTOP).
71
+ // Such types get the single-server deploy instead: deleting it or moving it to .bak right after install would leave no deploy at all.
70
72
  function isFallbackSimple(filename, style, available) {
71
73
  if (!available || !filename.endsWith(SIMPLE_SUFFIX)) return false;
72
74
  const prefix = filename.slice(0, -SIMPLE_SUFFIX.length);
73
75
  return !available.has(prefix + suffixOf(style));
74
76
  }
75
77
 
76
- // 파일 필터 — 고른 방식의 CD만 통과. CD가 아닌 파일(PR 프리뷰·common 등)은 항상 통과.
77
- // "none"은 서버 배포 워크플로우(CD·단일 CD·PR 프리뷰)를 하나도 설치하지 않는다.
78
- // available: payload 워크플로우 파일명 집합(payloadWorkflowNames). 주면 고른 방식이 없는 타입은
79
- // 단일 서버 배포로 대체한다. 없으면 접미사만으로 가른다.
78
+ // File filter: only the chosen style's CD passes. Non-CD files (PR preview, common, etc.) always pass.
79
+ // "none" installs no server-deploy workflow at all (CD, single CD, PR preview).
80
+ // available: set of payload workflow file names (payloadWorkflowNames). When given, types without the chosen
81
+ // style fall back to the single-server deploy. Without it, only the suffix decides.
80
82
  export function deployFilter(style, available = null) {
81
83
  if (style === NO_DEPLOY_STYLE) return (filename) => !isServerDeployWorkflow(filename);
82
84
  const suffix = suffixOf(style);
@@ -84,7 +86,7 @@ export function deployFilter(style, available = null) {
84
86
  || isFallbackSimple(filename, style, available);
85
87
  }
86
88
 
87
- // 고른 방식(nginx·traefik)의 워크플로우가 없어 단일 서버 배포로 대신 설치하는 타입 목록 — 안내용.
89
+ // Types that get the single-server deploy instead because they lack workflows for the chosen style (nginx, traefik); for notices.
88
90
  export function fallbackStyleTypes(payloadRoot, types = [], style) {
89
91
  if (!style || style === NO_DEPLOY_STYLE || suffixOf(style) === SIMPLE_SUFFIX) return [];
90
92
  return types.filter((type) => {
@@ -94,9 +96,10 @@ export function fallbackStyleTypes(payloadRoot, types = [], style) {
94
96
  });
95
97
  }
96
98
 
97
- // 실제로 설치되는 배포 방식 — 고른 무중단 방식의 워크플로우가 선택한 타입 어디에도 없으면 전부 단일 서버
98
- // 배포로 대신 설치되므로 simple이다. 고른 값을 그대로 기록하면 version.yml·status가 설치 상태와 어긋나고,
99
- // 다음 실행도 그 값을 기본값으로 이어받는다. 한 타입이라도 그 방식이 있으면 고른 값을 유지한다.
99
+ // The deploy style that is actually installed: if no selected type has workflows for the chosen zero-downtime
100
+ // style, all of them get the single-server deploy, so the result is simple. Recording the chosen value as is would
101
+ // make version.yml and status disagree with what was installed, and the next run would inherit it as the default.
102
+ // If even one type has that style, the chosen value is kept.
100
103
  export function effectiveDeployStyle(payloadRoot, types = [], style) {
101
104
  if (!style || style === NO_DEPLOY_STYLE || suffixOf(style) === SIMPLE_SUFFIX) return style;
102
105
  const installed = types.some((type) => typeWorkflowDirs(payloadRoot, type)
@@ -104,12 +107,12 @@ export function effectiveDeployStyle(payloadRoot, types = [], style) {
104
107
  return installed ? style : DEFAULT_DEPLOY_STYLE;
105
108
  }
106
109
 
107
- // 무중단 템플릿은 push 트리거가 주석 처리된 채 들어 있다(기본 배포가 단일 서버라서).
108
- // 사용자가 그 방식을 고른 이상 트리거는 켜져 있어야 한다 — 안 그러면 설치해도 아무 일이
109
- // 일어나지 않고 사용자가 YAML을 직접 고쳐야 한다.
110
+ // Zero-downtime templates ship with the push trigger commented out (because the default deploy is the single server).
111
+ // Once the user has chosen that style the trigger must be on: otherwise nothing happens after install and the
112
+ // user has to edit the YAML by hand.
110
113
  //
111
- // 첫 `on:` 블록 안에서 `# ` 두 글자만 떼므로 안쪽 들여쓰기 계층이 그대로 보존된다.
112
- // 설명문 주석은 벗겨낸 내용이 push/branches/- 로 시작하지 않아 건드리지 않는다.
114
+ // Inside the first `on:` block only the two characters `# ` are removed, so the inner indentation hierarchy is preserved.
115
+ // Explanatory comments are left alone because their stripped content does not start with push/branches/-.
113
116
  const TRIGGER_CONTENT = /^\s*(push:|branches:|- )/;
114
117
 
115
118
  export function activateDeployTrigger(content) {
@@ -121,7 +124,7 @@ export function activateDeployTrigger(content) {
121
124
  const line = lines[i];
122
125
  if (/^on:\s*$/.test(line)) { inOn = true; continue; }
123
126
  if (!inOn) continue;
124
- if (/^\S/.test(line)) break; // 최상위 키를 다시 만나면 on 블록 종료
127
+ if (/^\S/.test(line)) break; // a top-level key ends the on block
125
128
  const m = line.match(/^(\s*)# ?(.*)$/);
126
129
  if (!m || !TRIGGER_CONTENT.test(m[2])) continue;
127
130
  lines[i] = `${m[1]}${m[2]}`;
@@ -130,20 +133,20 @@ export function activateDeployTrigger(content) {
130
133
  return changed ? lines.join(eol) : content;
131
134
  }
132
135
 
133
- // 방식을 바꿔 재설치했을 때 이전 CD를 정리한다.
136
+ // Cleans up the previous CD when reinstalling with a different style.
134
137
  //
135
- // 남겨두면 이전 방식의 push 트리거가 살아 있어 배포가 두 번 돈다. 그렇다고 사용자에게
136
- // "직접 지우세요"라고 떠넘기면 설치가 끝나도 레포가 정상이 아닌 상태로 남는다. 마법사가
137
- // 깐 파일은 마법사가 정리한다.
138
+ // Leaving it would keep the old style's push trigger alive and deploy twice. Telling the user to "delete it
139
+ // yourself" would leave the repo in a broken state even after install finishes. Files the wizard installed are
140
+ // cleaned up by the wizard.
138
141
  //
139
- // 손대지 않은 것(baseline의 installed 해시와 동일) → 삭제. 물어볼 이유가 없다.
140
- // 손댄 것 → .bak으로 옮긴다. 내용은 지키고 트리거만 죽인다.
142
+ // untouched (same as the baseline installed hash) -> deleted. No reason to ask.
143
+ // modified -> moved to .bak. Content is kept and only the trigger is killed.
141
144
  //
142
- // opts.available — payload 워크플로우 파일명 집합. 주면 그 안의 파일(마법사가 까는 파일)만 정리하고,
143
- // 고른 방식이 없는 타입의 단일 서버 배포는 남긴다.
144
- // opts.justWritten — 이번 실행에서 마법사가 방금 쓴 파일. 첫 설치처럼 baseline이 없어도 수정본으로 오인하지 않는다.
145
- // opts.dryRun — 판정만 하고 파일은 건드리지 않는다. --dry-run 미리보기가 실제 실행과 같은 판정을 쓰게 한다.
146
- // 반환: { removed:[], backedUp:[] } — 완료 화면·설치 기록에 그대로 보고한다.
145
+ // opts.available - set of payload workflow file names. When given, only files in it (files the wizard installs)
146
+ // are cleaned up, and the single-server deploy of types lacking the chosen style is kept.
147
+ // opts.justWritten - files the wizard just wrote in this run. Not mistaken for modified files even without a baseline, as on a first install.
148
+ // opts.dryRun - only decide, do not touch files. Makes the --dry-run preview use the same decision as a real run.
149
+ // Returns { removed:[], backedUp:[] }, reported as is on the completion screen and in the install record.
147
150
  export function cleanupOtherDeployWorkflows(workflowsDir, installedFilenames, style, baseline, { available = null, justWritten = [], dryRun = false } = {}) {
148
151
  const keep = deployFilter(style, available);
149
152
  const written = new Set(justWritten);
@@ -152,7 +155,7 @@ export function cleanupOtherDeployWorkflows(workflowsDir, installedFilenames, st
152
155
 
153
156
  for (const filename of installedFilenames) {
154
157
  if (!isServerDeployWorkflow(filename) || keep(filename)) continue;
155
- if (available && !available.has(filename)) continue; // 사용자가 만든 비슷한 이름의 워크플로우는 건드리지 않는다
158
+ if (available && !available.has(filename)) continue; // never touch a similarly named workflow the user created
156
159
  const p = join(workflowsDir, filename);
157
160
  if (!existsSync(p)) continue;
158
161
 
@@ -1,11 +1,12 @@
1
- // 실 파일시스템 프로젝트 감지.
2
- // detect.js 순수 함수를 fs/git으로 구동한다.
1
+ // Project detection on the real filesystem.
2
+ // Drives the pure functions of detect.js with fs/git.
3
3
  import { existsSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { join, basename } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { detectTypesFromMarkers, detectVersionFromFiles, detectBuildNumberFromFiles, detectJdkFromFiles, resolveMarkers } from "./detect.js";
7
7
  import { parseExisting } from "./version-yml.js";
8
8
  import { isValidBranchName } from "./branches.js";
9
+ import { t as tr } from "../i18n/index.js";
9
10
 
10
11
  const hasFile = (root) => (rel) => existsSync(join(root, rel));
11
12
  const readFile = (root) => (rel) => {
@@ -18,19 +19,19 @@ function gitOut(root, args) {
18
19
  } catch { return ""; }
19
20
  }
20
21
 
21
- // 타입 감지 — version.yml의 project_types 최우선(source of truth), 없으면 마커 스캔.
22
- // paths: --paths로 받은 Map<type,path>. 모노레포는 루트에 마커가 없으므로 사용자가 적은 타입을 쓴다.
23
- // warn: 루트에 마커가 없어 basic으로 떨어질 때 하위 폴더에서 찾은 프로젝트를 알린다.
22
+ // Type detection: project_types in version.yml wins (source of truth), otherwise a marker scan.
23
+ // paths: Map<type,path> from --paths. A monorepo has no markers at the root, so the types the user listed are used.
24
+ // warn: when no root marker exists and detection falls back to basic, reports projects found in subfolders.
24
25
  export function detectTypes(root, { paths = new Map(), warn } = {}) {
25
26
  const vy = join(root, "version.yml");
26
27
  if (existsSync(vy)) {
27
28
  const { types } = parseExisting(readFileSync(vy, "utf8"));
28
- if (types.length) return types; // basic 포함, 명시돼 있으면 그대로
29
+ if (types.length) return types; // includes basic; used as-is when explicit
29
30
  }
30
31
  const fromRoot = detectTypesFromMarkers({ has: hasFile(root), read: readFile(root) });
31
32
  if (paths.size) {
32
- // --paths 순서가 주 타입을 정한다. 루트 package.json의 node는 다른 타입이 있으면 빼는
33
- // 마커 스캔 규칙과 맞춘다.
33
+ // The --paths order decides the primary type. Matches the marker-scan rule that drops the root
34
+ // package.json's node when another type exists.
34
35
  const merged = [...new Set([...paths.keys(), ...fromRoot])].filter((t) => t !== "basic");
35
36
  const types = merged.length > 1 ? merged.filter((t) => t !== "node" || paths.has("node")) : merged;
36
37
  return types.length ? types : ["basic"];
@@ -42,15 +43,14 @@ export function detectTypes(root, { paths = new Map(), warn } = {}) {
42
43
  for (const { dir, types } of found) for (const t of types) if (!firstDir.has(t)) firstDir.set(t, dir);
43
44
  const list = found.map(({ dir, types }) => `${dir}(${types.join(", ")})`).join(", ");
44
45
  const hint = [...firstDir].map(([t, d]) => `${t}=${d}`).join(",");
45
- warn(`⚠️ 루트에서 프로젝트 파일을 찾지 못해 basic으로 설치합니다. 하위 폴더에서 발견: ${list}\n` +
46
- ` 모노레포라면 --paths "${hint}"로 다시 실행하세요.`);
46
+ warn(tr("core.detectFs.monorepoWarn", { list, hint }));
47
47
  }
48
48
  }
49
49
  return fromRoot;
50
50
  }
51
51
 
52
- // 루트 아래 2단계까지 프로젝트 마커가 있는 폴더를 찾는다 (모노레포 안내용).
53
- // 빌드 산출물·의존성·네이티브 폴더는 오탐만 늘리므로 들어가지 않고, 찾은 폴더의 하위도 보지 않는다.
52
+ // Finds folders with project markers up to 2 levels below the root (for monorepo hints).
53
+ // Build output, dependency and native folders only add false positives, so they are skipped, and the children of a found folder are not scanned.
54
54
  const SUBDIR_PRUNE = new Set(["node_modules", "build", "dist", "android", "ios", "venv", "__pycache__"]);
55
55
  function findSubdirProjects(root, maxDepth = 2) {
56
56
  const found = [];
@@ -70,10 +70,10 @@ function findSubdirProjects(root, maxDepth = 2) {
70
70
  return found.sort((a, b) => a.dir.localeCompare(b.dir));
71
71
  }
72
72
 
73
- // 버전 감지 — 타입별 버전 파일을 순서대로 읽는다.
74
- // hint: 폴백 경고에 붙일 해결 방법 안내 (대화형/CLI가 다르다).
75
- // 모노레포(--paths)는 버전 파일이 타입 폴더 안에 있다 — 루트만 보면 0.0.1/1로 초기화된다.
76
- // 주 타입 폴더 → 나머지 타입 폴더 → 루트 순으로 찾는다.
73
+ // Version detection: reads each type's version files in order.
74
+ // hint: fix guidance appended to the fallback warning (differs between interactive mode and the CLI).
75
+ // In a monorepo (--paths) the version files live inside the type folders; looking only at the root would reset to 0.0.1/1.
76
+ // Search order: primary type folder, then the other type folders, then the root.
77
77
  function projectBases(types = [], paths = null) {
78
78
  const bases = [];
79
79
  for (const t of types) {
@@ -83,7 +83,7 @@ function projectBases(types = [], paths = null) {
83
83
  return bases.length ? [...bases, "."] : ["."];
84
84
  }
85
85
 
86
- // 첫 번째로 결과가 있는 기준 폴더의 값을 쓴다 — read와 list가 같은 폴더 우선순위를 따른다.
86
+ // Uses the value from the first base folder that has a result, so read and list follow the same folder priority.
87
87
  function firstFromBases(bases, fn) {
88
88
  return (rel) => {
89
89
  for (const b of bases) {
@@ -98,7 +98,7 @@ function readFromProject(root, types = [], paths = null) {
98
98
  return firstFromBases(projectBases(types, paths), readFile(root));
99
99
  }
100
100
 
101
- // 하위 폴더 이름 목록 (React Native의 ios/<앱>/Info.plist 탐색용). 폴더가 없으면 null.
101
+ // Subfolder names (for React Native's ios/<app>/Info.plist lookup). null when the folder does not exist.
102
102
  function listFromProject(root, types = [], paths = null) {
103
103
  const listDirs = (rel) => {
104
104
  try {
@@ -116,45 +116,45 @@ export function detectVersion(root, { warn = (m) => console.error(m), hint, type
116
116
  return detectVersionFromFiles({ read, readJson, list, gitTag, warn, hint, types });
117
117
  }
118
118
 
119
- // 타입별 실제 마커 파일 — 감지 로그·설치 로그가 같은 근거 파일을 인용하도록.
119
+ // Real marker file per type, so the detection log and install log cite the same evidence file.
120
120
  export function detectMarkers(root, types = []) {
121
121
  return resolveMarkers(types, hasFile(root));
122
122
  }
123
123
 
124
- // 빌드 JDK 감지 — 배포 워크플로우 JAVA_VERSION 기본값에 실측값을 쓰기 위해.
125
- // base: 모노레포에서 spring 프로젝트 루트 (레포 루트 기준 상대경로).
124
+ // Build JDK detection, so the measured value can be used as the deploy workflow's JAVA_VERSION default.
125
+ // base: the spring project root in a monorepo (relative to the repo root).
126
126
  export function detectJdk(root, base = ".") {
127
127
  const rel = base && base !== "." ? (r) => `${base}/${r}` : (r) => r;
128
128
  const read = readFile(root);
129
129
  return detectJdkFromFiles({ read: (r) => read(rel(r)) });
130
130
  }
131
131
 
132
- // 빌드 번호 감지 — 신규 통합 시 pubspec.yaml/build.gradle/app.json에서 실제 빌드 번호를 읽는다.
132
+ // Build number detection: on a fresh integration, reads the real build number from pubspec.yaml/build.gradle/app.json.
133
133
  export function detectBuildNumber(root, { types = [], paths = null, warn = (m) => console.error(m) } = {}) {
134
134
  const read = readFromProject(root, types, paths);
135
135
  const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
136
136
  return detectBuildNumberFromFiles({ types, read, readJson, warn });
137
137
  }
138
138
 
139
- // 기본 브랜치 감지 — symbolic-ref → remote show → main.
140
- // 빈 원격(remote add만 하고 push 전)은 remote show가 "HEAD branch: (unknown)"을 돌려준다. 이 값이
141
- // 워크플로우 트리거에 기록되면 릴리스 자동화가 조용히 멈추므로, 유효한 브랜치 이름만 인정하고
142
- // 아니면 로컬 현재 브랜치(첫 push 대상) → main 순으로 폴백하며 경고한다.
143
- // hint: 경고에 붙일 해결 방법 안내 (대화형/CLI가 다르다).
139
+ // Default branch detection: symbolic-ref, then remote show, then main.
140
+ // For an empty remote (remote add without a push yet) remote show returns "HEAD branch: (unknown)". If that value
141
+ // were written into the workflow trigger, release automation would silently stop, so only valid branch names are
142
+ // accepted; otherwise it falls back to the current local branch (first push target), then main, and warns.
143
+ // hint: fix guidance appended to the warning (differs between interactive mode and the CLI).
144
144
  export function detectDefaultBranch(root, { warn = null, hint = "" } = {}) {
145
145
  const b = gitOut(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]).replace(/^refs\/remotes\/origin\//, "");
146
146
  if (isValidBranchName(b)) return b;
147
147
  const show = gitOut(root, ["remote", "show", "origin"]);
148
148
  const m = show.match(/HEAD branch:\s*(\S+)/);
149
149
  if (m && isValidBranchName(m[1])) return m[1];
150
- if (!m) return "main"; // origin 없음 — 기존 규칙 그대로
150
+ if (!m) return "main"; // no origin: keep the existing rule
151
151
  const local = gitOut(root, ["symbolic-ref", "--short", "HEAD"]);
152
152
  const fallback = isValidBranchName(local) ? local : "main";
153
- warn?.(`⚠️ 원격 기본 브랜치를 확인할 수 없어(빈 원격 레포 등) 릴리스 브랜치를 '${fallback}'(으)로 가정합니다.${hint ? ` ${hint}` : ""}`);
153
+ warn?.(tr("core.detectFs.defaultBranchWarn", { fallback, hint: hint ? ` ${hint}` : "" }));
154
154
  return fallback;
155
155
  }
156
156
 
157
- // 레포명 — git remote get-url origin 마지막 세그먼트, 실패 시 폴더명.
157
+ // Repo name: last segment of git remote get-url origin, or the folder name on failure.
158
158
  export function detectRepoName(root) {
159
159
  const url = gitOut(root, ["remote", "get-url", "origin"]);
160
160
  if (url) {
@@ -164,32 +164,32 @@ export function detectRepoName(root) {
164
164
  return basename(root);
165
165
  }
166
166
 
167
- // Spring application*.yml 탐색
168
- // find {base} -path "*/src/main/resources/application*.yml" | head -1 의 fs 재귀 구현.
169
- // 반환: root 기준 상대경로 (예: "server/src/main/resources/application.yml") 또는 "".
167
+ // Spring application*.yml lookup.
168
+ // Recursive fs implementation of: find {base} -path "*/src/main/resources/application*.yml" | head -1
169
+ // Returns a path relative to root (e.g. "server/src/main/resources/application.yml") or "".
170
170
  //
171
- // .yaml도 인정한다. Spring은 .yml/.yaml을 모두 공식 지원하는데 종전 정규식이
172
- // .yml만 봐서, application.yaml을 쓰는 프로젝트는 이 값이 빈 문자열이 되고 그 결과
173
- // __APPLICATION_YML_DIR__ 가 치환되지 않은 채 설치됐다.
171
+ // .yaml is accepted too. Spring officially supports both .yml and .yaml, but the old regex only looked
172
+ // for .yml, so projects using application.yaml got an empty string here and __APPLICATION_YML_DIR__
173
+ // was installed unreplaced.
174
174
  //
175
- // 같은 디렉토리에서는 프로파일 없는 기본 파일(application.yml/.yaml)을 우선한다. 파일명 정렬만
176
- // 쓰면 'application-dev.yml'이 'application.yml'보다 앞서(`-` < `.`) 프로파일 파일이 잡힌다.
175
+ // Within one directory the profile-less base file (application.yml/.yaml) wins. With filename sorting alone,
176
+ // 'application-dev.yml' sorts before 'application.yml' (`-` < `.`) and a profile file would be picked.
177
177
  export function findSpringAppYml(root, base = ".") {
178
178
  return findSpringConfig(root, base, /^application(-[^/]*)?\.ya?ml$/, /^application\.ya?ml$/);
179
179
  }
180
180
 
181
- // src/main/resources 아래에서 pattern에 맞는 설정 파일을 찾는다. basePattern(프로파일 없는 기본 파일)이
182
- // 나오면 그걸로 확정한다.
181
+ // Finds a config file matching pattern under src/main/resources. Once basePattern (the profile-less base file)
182
+ // turns up, that one is final.
183
183
  function findSpringConfig(root, base, pattern, basePattern) {
184
184
  const startRel = base === "." ? "" : base;
185
185
  const PRUNE = new Set(["node_modules", ".git", "build", ".gradle", "target", ".idea"]);
186
186
  let hit = "";
187
187
  let hitIsBase = false;
188
188
  const walk = (rel, depth) => {
189
- if (hitIsBase || depth > 8) return; // 기본 파일을 찾았으면 더 볼 필요가 없다
189
+ if (hitIsBase || depth > 8) return; // once the base file is found there is no need to look further
190
190
  let entries;
191
191
  try { entries = readdirSync(join(root, rel), { withFileTypes: true }); } catch { return; }
192
- // 정렬로 순회 순서 결정화 (find 순서 플랫폼 편차 제거)
192
+ // Sort to make traversal order deterministic (removes platform differences in find order)
193
193
  for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
194
194
  if (hitIsBase) return;
195
195
  const childRel = rel ? `${rel}/${e.name}` : e.name;
@@ -198,7 +198,7 @@ function findSpringConfig(root, base, pattern, basePattern) {
198
198
  walk(childRel, depth + 1);
199
199
  } else if (pattern.test(e.name) && childRel.includes("src/main/resources/")) {
200
200
  const isBase = basePattern.test(e.name);
201
- // 첫 매치는 일단 채택하고, 이후 기본 파일이 나오면 그걸로 승격한다.
201
+ // Take the first match for now, and promote to the base file if one turns up later.
202
202
  if (!hit || isBase) { hit = childRel; hitIsBase = isBase; }
203
203
  }
204
204
  }
@@ -207,9 +207,9 @@ function findSpringConfig(root, base, pattern, basePattern) {
207
207
  return hit;
208
208
  }
209
209
 
210
- // 배포 워크플로우가 application-prod.yml을 만들 리소스 폴더.
211
- // Spring Initializr 기본 산출물은 application.properties라 yml만 찾으면 빈 값이 되어
212
- // __APPLICATION_YML_DIR__가 치환되지 않은 채 설치된다. yml → properties → 표준 경로 순으로 정한다.
210
+ // Resource folder where the deploy workflow will create application-prod.yml.
211
+ // Spring Initializr produces application.properties by default, so looking only for yml gives an empty value and
212
+ // __APPLICATION_YML_DIR__ is installed unreplaced. Decided in order: yml, then properties, then the standard path.
213
213
  export function findSpringResourcesDir(root, base = ".") {
214
214
  const f = findSpringAppYml(root, base)
215
215
  || findSpringConfig(root, base, /^application(-[^/]*)?\.properties$/, /^application\.properties$/);
@@ -217,27 +217,27 @@ export function findSpringResourcesDir(root, base = ".") {
217
217
  return base === "." ? "src/main/resources" : `${base}/src/main/resources`;
218
218
  }
219
219
 
220
- // @wizard 토큰 resolver 세트 생성 — index/interactive 공용.
221
- // paths: Map<type, path> (모노레포 경로).
222
- // flutterOptions: resolveFlutterOptions 결과 또는 같은 필드를 가진 context. null이면 Flutter 토큰이
223
- // 빈 값이라 템플릿 기본값(dart-define, store_only)이 그대로 남는다.
220
+ // Builds the set of @wizard token resolvers, shared by index and interactive.
221
+ // paths: Map<type, path> (monorepo paths).
222
+ // flutterOptions: the resolveFlutterOptions result or a context with the same fields. When null the Flutter tokens
223
+ // are empty, so the template defaults (dart-define, store_only) remain.
224
224
  export function makeResolvers(root, repoName, paths, flutterOptions = null) {
225
225
  const springBase = (t) => paths.get(t || "spring") || paths.get("spring") || ".";
226
226
  return {
227
227
  repo: () => repoName,
228
- // 빌드 JDK — 배포 워크플로우 JAVA_VERSION의 기본값. 프로젝트 툴체인을 실측한다.
229
- // ⚠️ 빈 문자열을 돌려주면 setEnvLine이 그 줄을 건너뛰어 __JAVA_VERSION__이 그대로 남는다
230
- // (같은 실패 형태). 감지 실패 시 반드시 종전 기본값 21로 폴백한다.
228
+ // Build JDK: default for the deploy workflow's JAVA_VERSION, measured from the project toolchain.
229
+ // WARNING: returning an empty string makes setEnvLine skip that line, leaving __JAVA_VERSION__ as is
230
+ // (the same failure shape). On detection failure always fall back to the previous default, 21.
231
231
  jdk: (t) => detectJdk(root, springBase(t)) || "21",
232
232
  "spring-app-yml-dir": (t) => findSpringResourcesDir(root, springBase(t)),
233
- // yml이 없는 프로젝트(properties 전용)는 리소스 폴더의 application.yml로 만든다 —
234
- // properties 파일 자리에 YAML 내용을 쓰면 설정이 깨진다.
233
+ // A project without yml (properties only) gets application.yml in the resources folder:
234
+ // writing YAML content in place of a properties file would break the config.
235
235
  "spring-app-yml-path": (t) => findSpringAppYml(root, springBase(t))
236
236
  || `${findSpringResourcesDir(root, springBase(t))}/application.yml`,
237
237
  "flutter-root": () => paths.get("flutter") || ".",
238
- // CI changes job의 경로 필터 — 타입별 프로젝트 루트. 단일 레포·common은 "."(항상 변경됨으로 판정).
238
+ // Path filter for the CI changes job: the project root per type. Single repos and common use "." (always treated as changed).
239
239
  "project-path": (t) => paths.get(t) || ".",
240
- // 빈 문자열이면 setEnvLine/setFallbackLine이 줄을 건너뛰어 템플릿 기본값이 남는다.
240
+ // An empty string makes setEnvLine/setFallbackLine skip the line, so the template default remains.
241
241
  "flutter-env-mode": () => flutterOptions?.envMode || "",
242
242
  "android-deploy-mode": () => flutterOptions?.androidDeployMode || "",
243
243
  "ios-deploy-mode": () => flutterOptions?.iosDeployMode || "",