project-auto-wizard 0.12.2 → 0.13.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 (86) hide show
  1. package/README.ja.md +165 -0
  2. package/README.ko.md +165 -0
  3. package/README.md +107 -286
  4. package/README.zh-CN.md +165 -0
  5. package/bin/project-auto-wizard.js +1 -2
  6. package/package.json +7 -3
  7. package/payload/config/breaking-changes.json +24 -0
  8. package/payload/config/wizard-prompts.yml +45 -0
  9. package/payload/scripts/changelog_manager.py +141 -46
  10. package/payload/scripts/issue_helper.py +34 -5
  11. package/payload/scripts/version_manager.py +397 -97
  12. package/payload/version.yml.template +4 -2
  13. package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
  14. package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
  15. package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
  16. package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
  17. package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
  18. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
  19. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
  20. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
  21. package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
  22. package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
  23. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
  24. package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
  25. package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
  26. package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
  27. package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
  28. package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
  29. package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
  30. package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
  31. package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
  32. package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
  33. package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
  34. package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
  35. package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
  36. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
  37. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
  38. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
  39. package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
  40. package/src/cli/args.js +33 -24
  41. package/src/cli/help.js +16 -4
  42. package/src/commands/doctor.js +17 -10
  43. package/src/commands/dry-run.js +54 -1
  44. package/src/commands/full.js +127 -50
  45. package/src/commands/interactive-flutter.js +1 -1
  46. package/src/commands/interactive.js +124 -56
  47. package/src/commands/purge.js +59 -17
  48. package/src/commands/status.js +16 -1
  49. package/src/commands/uninstall.js +58 -13
  50. package/src/context.js +5 -5
  51. package/src/core/baseline.js +20 -5
  52. package/src/core/branches.js +19 -1
  53. package/src/core/branding.js +1 -1
  54. package/src/core/breaking-check.js +14 -19
  55. package/src/core/breaking.js +7 -3
  56. package/src/core/copy/gitignore.js +20 -12
  57. package/src/core/copy/readme.js +24 -5
  58. package/src/core/copy/simple.js +52 -13
  59. package/src/core/copy/workflows.js +90 -47
  60. package/src/core/deploy-style.js +83 -11
  61. package/src/core/detect-fs.js +127 -24
  62. package/src/core/detect.js +131 -47
  63. package/src/core/errors.js +3 -0
  64. package/src/core/flutter-options.js +4 -3
  65. package/src/core/fsutil.js +23 -6
  66. package/src/core/logger.js +55 -19
  67. package/src/core/paths-resolve.js +50 -47
  68. package/src/core/paths.js +26 -1
  69. package/src/core/release-options.js +23 -0
  70. package/src/core/removal-plan.js +75 -19
  71. package/src/core/types.js +113 -0
  72. package/src/core/verify.js +85 -19
  73. package/src/core/version-yml.js +42 -8
  74. package/src/core/wizard-env.js +21 -14
  75. package/src/core/wizard-labels.js +34 -25
  76. package/src/index.js +122 -49
  77. package/src/ui/ansi.js +3 -1
  78. package/src/ui/banner.js +12 -8
  79. package/src/ui/env-plan.js +68 -44
  80. package/src/ui/prompts.js +22 -12
  81. package/src/ui/readline-engine.js +109 -22
  82. package/src/ui/status-cards.js +10 -5
  83. package/src/ui/summary.js +71 -33
  84. package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
  85. package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
  86. package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
- // project-auto-wizard CLI 진입 파이프라인 (.sh main + execute_integration 등가).
1
+ // project-auto-wizard CLI 진입 파이프라인.
2
2
  // 감지 → payload 해석 → 모드 라우팅 → 통합 실행. 비대화형(--force) 우선.
3
- // 네트워크 접근 0 — 설치 자산은 전부 npm 패키지 동봉 payload/ (단일 진실).
3
+ // 설치 자산은 전부 npm 패키지 동봉 payload/ (단일 진실). 자체 네트워크 요청은 없고,
4
+ // 기본 브랜치 감지용 git 명령만 사용자 레포의 origin에 접속할 수 있다.
4
5
  import { join, dirname } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { readFileSync, existsSync } from "node:fs";
@@ -8,21 +9,27 @@ import { createInterface } from "node:readline/promises";
8
9
  import { parseArgs, parsePathsCsv, CliError } from "./cli/args.js";
9
10
  import { HELP_TEXT } from "./cli/help.js";
10
11
  import { createContext } from "./context.js";
11
- import { DEFAULT_DEPLOY_STYLE, isDeployStyle } from "./core/deploy-style.js";
12
+ import { DEFAULT_DEPLOY_STYLE, isDeployStyle, hasServerDeployWorkflows, fallbackStyleTypes, effectiveDeployStyle } from "./core/deploy-style.js";
12
13
  import { resolveFlutterOptions } from "./core/flutter-options.js";
14
+ import { inferInstalledStores } from "./core/installed-stores.js";
15
+ import { PATHS } from "./core/paths.js";
13
16
  import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "./core/assets.js";
14
17
  import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber, detectMarkers } from "./core/detect-fs.js";
15
18
  import { parseExisting } from "./core/version-yml.js";
19
+ import { resolveReleaseOptions } from "./core/release-options.js";
16
20
  import { runBreakingCheck } from "./core/breaking-check.js";
17
21
  import { resolveProjectPaths } from "./core/paths-resolve.js";
18
- import { resolveBranchConfig, detectRemoteBranches, ensureDevelopBranch, defaultExec } from "./core/branches.js";
22
+ import {
23
+ resolveBranchConfig, detectRemoteBranches, ensureDevelopBranch, defaultExec, isValidBranchName, developMissingNotice,
24
+ } from "./core/branches.js";
19
25
  import { printBannerCompact } from "./ui/banner.js";
20
26
  import { printSummary } from "./ui/summary.js";
21
- import { runFull } from "./commands/full.js";
27
+ import { runFull, postInstallNotices } from "./commands/full.js";
22
28
  import { runUninstall, runUninstallFlow } from "./commands/uninstall.js";
23
29
  import * as prompts from "./ui/prompts.js";
30
+ import { isPromptAbort } from "./ui/readline-engine.js";
24
31
  import { runInteractive } from "./commands/interactive.js";
25
- import { initLogger, closeLogger, currentLogPath, hasLegacyMdLogs } from "./core/logger.js";
32
+ import { initLogger, closeLogger, currentLogPath, hasLegacyMdLogs, log } from "./core/logger.js";
26
33
  import { runStatus, printStatus } from "./commands/status.js";
27
34
  import { runDoctor, printDoctorReport } from "./commands/doctor.js";
28
35
  import { planDryRun, printDryRun } from "./commands/dry-run.js";
@@ -44,7 +51,8 @@ function utcNow(date = new Date()) {
44
51
  const p = (n) => String(n).padStart(2, "0");
45
52
  const d = `${date.getUTCFullYear()}-${p(date.getUTCMonth() + 1)}-${p(date.getUTCDate())}`;
46
53
  const t = `${p(date.getUTCHours())}:${p(date.getUTCMinutes())}:${p(date.getUTCSeconds())}`;
47
- return { now: `${d} ${t}`, today: d };
54
+ // ms: 로그 파일명이 같은 초의 연속 실행끼리 겹치지 않도록 쓴다 (now와 같은 시각에서 뽑는다).
55
+ return { now: `${d} ${t}`, today: d, ms: date.getUTCMilliseconds() };
48
56
  }
49
57
 
50
58
  // purge TTY 확인 — 실제 stdin에서 한 줄 입력을 받는다 (테스트는 promptRepoName 주입으로 대체).
@@ -78,17 +86,11 @@ async function runInner(argv, {
78
86
  const payload = assertPayload(payloadRoot ?? resolvePayloadRoot());
79
87
 
80
88
  // 시각은 여기서 한 번만 계산한다 — 로그 파일명과 설치 기록이 같은 값을 쓰도록.
81
- const { now, today } = clock || utcNow();
89
+ const { now, today, ms } = clock || utcNow();
82
90
 
83
- // dry-run은 "파일을 바꾸지 않는다"가 계약이므로 로그도 남기지 않는다.
84
- // --version/--help는 이 지점 이전에 이미 반환되므로 자연히 제외된다.
85
- const loggedAction = opts.dryRun ? null
86
- : opts.mode === "uninstall" ? "uninstall"
87
- : opts.mode === "purge" ? "purge"
88
- : "install";
89
- if (loggedAction) {
90
- initLogger(cwd, { action: loggedAction, now, argv, templateVersion: readTemplateVersion() });
91
- }
91
+ // 로그는 인자 검증과 모드별 게이트를 모두 통과한 "실제로 파일을 바꾸는" 실행만 남긴다.
92
+ // status/doctor(읽기 전용)·dry-run·거부된 실행은 대상 레포에 아무 파일도 만들지 않아야 한다.
93
+ const startLog = (action) => initLogger(cwd, { action, now, ms, argv, templateVersion: readTemplateVersion() });
92
94
 
93
95
  // 대화형 모드 — 인자 없이 실행 or --mode interactive
94
96
  if (opts.mode === "interactive") {
@@ -101,7 +103,23 @@ async function runInner(argv, {
101
103
  console.error("대화형 입력이 불가능한 환경입니다. --mode <full|uninstall> 와 --force 를 지정하세요.");
102
104
  return 1;
103
105
  }
104
- return await runInteractive({}, { cwd, payloadRoot: payload, clock });
106
+ // 켜고 끄기 옵션은 대화형에서도 반영한다(해당 질문 생략). 나머지 설치 플래그는 대화형 질문이 정하므로
107
+ // 조용히 무시하지 않고 알린다.
108
+ const ignored = [
109
+ [opts.types.length, "--type"], [opts.version, "--project-version"], [opts.pathsCsv, "--paths"],
110
+ [opts.mainBranch, "--main-branch"], [opts.developBranch, "--develop-branch"], [opts.deployStyle, "--deploy-style"],
111
+ [opts.flutterEnvMode, "--flutter-env-mode"], [opts.flutterStore, "--flutter-store"],
112
+ [opts.androidDeployMode, "--android-deploy-mode"], [opts.iosDeployMode, "--ios-deploy-mode"],
113
+ ].filter(([v]) => v).map(([, flag]) => flag);
114
+ if (ignored.length) {
115
+ console.error(`⚠️ 대화형 모드에서는 ${ignored.join(", ")}를 사용하지 않습니다 — 질문에서 고르거나 --mode full --force와 함께 쓰세요.`);
116
+ }
117
+ // 로그 파일은 첫 기록 때 생기므로 메뉴에서 status/doctor만 보고 나가면 아무것도 남지 않는다.
118
+ startLog("install");
119
+ return await runInteractive(
120
+ { includeSemverAuto: opts.includeSemverAuto, includeCopilotAi: opts.includeCopilotAi },
121
+ { cwd, payloadRoot: payload, clock },
122
+ );
105
123
  }
106
124
 
107
125
  // purge 모드 — 마법사가 만든 모든 산출물을 지워 설치 이전 상태로 완전히 되돌린다.
@@ -122,7 +140,7 @@ async function runInner(argv, {
122
140
  const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
123
141
  if (opts.dryRun) {
124
142
  printPurgePlan(planPurge(payload, cwd, keepFlags), { dryRun: true });
125
- // develop 브랜치 삭제는 plan에 포함되지 않으므로(§6 — git 상태는 실행 시점에만
143
+ // develop 브랜치 삭제는 plan에 포함되지 않으므로(git 상태는 실행 시점에만
126
144
  // 판단 가능) 별도로 예고하지 않으면 dry-run 미리보기가 유일한 파괴적 동작을 사용자에게 숨기게 된다.
127
145
  if (opts.deleteDevelopBranch) {
128
146
  const developBranch = existing?.branches?.develop || "develop";
@@ -159,6 +177,7 @@ async function runInner(argv, {
159
177
  return 1;
160
178
  }
161
179
  }
180
+ startLog("purge");
162
181
  const plan = planPurge(payload, cwd, keepFlags);
163
182
  printPurgePlan(plan, { dryRun: false });
164
183
  const result = executePurge(payload, cwd, keepFlags);
@@ -190,9 +209,11 @@ async function runInner(argv, {
190
209
  return 0;
191
210
  }
192
211
  if (opts.force) {
212
+ startLog("uninstall");
193
213
  const r = runUninstall({}, payload, cwd, safeSelection);
194
214
  const removed = [
195
215
  `워크플로우 ${r.workflows.length}개`, `스크립트 ${r.scripts.length}개`,
216
+ r.appFiles.length > 0 && `Flutter 앱 파일 ${r.appFiles.length}개`,
196
217
  r.readme && "README 버전 섹션",
197
218
  r.gitignore && ".gitignore 자동 추가 항목", r.versionYml && "version.yml",
198
219
  ].filter(Boolean).join(", ");
@@ -203,7 +224,11 @@ async function runInner(argv, {
203
224
  console.error("비대화형 환경에서는 --force 옵션이 필요합니다.");
204
225
  return 1;
205
226
  }
206
- await runUninstallFlow(payload, cwd, prompts);
227
+ startLog("uninstall");
228
+ // --purge-* 플래그는 체크리스트 초기 선택으로 반영한다(조용히 무시하지 않는다).
229
+ await runUninstallFlow(payload, cwd, prompts, {
230
+ readme: opts.purgeReadme, gitignore: opts.purgeGitignore, versionYml: opts.purgeVersion,
231
+ });
207
232
  return 0;
208
233
  }
209
234
 
@@ -217,7 +242,7 @@ async function runInner(argv, {
217
242
  printDoctorReport(runDoctor(cwd));
218
243
  return 0;
219
244
  }
220
- // 명시 모드(full/version/workflows)인데 --force 없으면 TTY 여부와 무관하게 즉시 거부한다
245
+ // 명시 모드(full)인데 --force 없으면 TTY 여부와 무관하게 즉시 거부한다
221
246
  // (TTY에서 확인 없이 즉시 설치되던 결함 수정).
222
247
  // --dry-run은 파일을 쓰지 않으므로 --force 게이트를 우회한다 (status/doctor와 동일한 안전성).
223
248
  if (!opts.force && !opts.dryRun) {
@@ -225,25 +250,32 @@ async function runInner(argv, {
225
250
  return 1;
226
251
  }
227
252
 
228
- // 기존 version.yml 로드 — version/version_code/project_paths 보존의 단일 진실 (.sh SSoT)
253
+ // 기존 version.yml 로드 — version/version_code/project_paths 보존의 단일 진실
229
254
  const vyPath = join(cwd, "version.yml");
230
255
  const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
231
256
 
232
257
  // 감지 (CLI 인자 우선, 없으면 자동 감지 — version.yml 우선 규칙은 detectTypes/detectVersion 내부)
233
- const types = opts.types.length ? opts.types : detectTypes(cwd);
234
- // version: 기존 version.yml 최우선(SSoT — 재실행 시 덮어쓰기 방지) → CLI 지정 → 파일 감지
235
- // 비대화형이므로 폴백 안내는 CLI 문구(--project-version)를 그대로 쓴다.
258
+ // --paths만 주고 --type을 생략한 모노레포도 그 타입으로 설치되도록 감지에 넘긴다.
259
+ let cliPaths;
260
+ try {
261
+ cliPaths = parsePathsCsv(opts.pathsCsv);
262
+ } catch (e) {
263
+ if (e instanceof CliError) { console.error(e.message); return 1; }
264
+ throw e;
265
+ }
266
+ const types = opts.types.length ? opts.types
267
+ : detectTypes(cwd, { paths: cliPaths, warn: (m) => console.error(m) });
236
268
  const detectWarnings = [];
237
- const version = (existing?.version) || opts.version
238
- || detectVersion(cwd, { warn: (m) => { detectWarnings.push(m); console.error(m); } });
239
- const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지 (.sh L2208~2221)
240
- const branch = detectDefaultBranch(cwd);
269
+ const branch = detectDefaultBranch(cwd, {
270
+ warn: (m) => { detectWarnings.push(m); console.error(m); },
271
+ hint: "다르면 --main-branch로 지정하세요.",
272
+ });
241
273
  const repoName = detectRepoName(cwd);
242
- // 경로 확정 (.sh resolve_project_paths 비대화형 경로 — --paths 우선 → 저장값 → 후보 1개 자동 → 에러)
274
+ // 경로 확정 (비대화형 — --paths 우선 → 저장값 → 후보 1개 자동 → 에러)
243
275
  let paths;
244
276
  try {
245
277
  paths = await resolveProjectPaths({
246
- root: cwd, types, paths: parsePathsCsv(opts.pathsCsv),
278
+ root: cwd, types, paths: cliPaths,
247
279
  existingPaths: existing?.paths ?? new Map(), force: true, tty: false, io: {},
248
280
  });
249
281
  } catch (e) {
@@ -251,29 +283,49 @@ async function runInner(argv, {
251
283
  throw e;
252
284
  }
253
285
 
286
+ // version: 기존 version.yml 최우선(SSoT — 재실행 시 덮어쓰기 방지) → CLI 지정 → 파일 감지
287
+ // 비대화형이므로 폴백 안내는 CLI 문구(--project-version)를 그대로 쓴다.
288
+ // 경로 확정 뒤에 감지해야 모노레포 하위 폴더의 버전·빌드 번호를 읽는다.
289
+ const version = (existing?.version) || opts.version
290
+ || detectVersion(cwd, { types, paths, warn: (m) => { detectWarnings.push(m); console.error(m); } });
291
+ const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types, paths }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지
292
+
254
293
  // 브랜치 구성 (--main-branch/--develop-branch → version.yml 저장값 → 감지 default → main/develop)
294
+ // 이전 버전이 저장한 감지 실패 값("(unknown)" 등)은 저장값으로 인정하지 않는다 — 그대로 두면 재실행해도 복구되지 않는다.
295
+ const savedBranch = (b) => (isValidBranchName(b) ? b : "");
255
296
  const branches = resolveBranchConfig({
256
- mainBranch: opts.mainBranch || existing?.branches?.main || "",
257
- developBranch: opts.developBranch || existing?.branches?.develop || "",
297
+ mainBranch: opts.mainBranch || savedBranch(existing?.branches?.main),
298
+ developBranch: opts.developBranch || savedBranch(existing?.branches?.develop),
258
299
  defaultBranch: branch,
259
300
  });
260
301
  // pr-flow에서 develop이 원격에 없으면 자동 생성+push (--force 비대화형 — 질문 없음).
261
- // 원격 목록을 못 읽는 환경(git 없음·origin 없음)은 remoteBranches=[]지만 push 실패를 조용히 보고.
302
+ // 원격에 브랜치가 하나도 없으면(빈 원격·origin 없음) push할 기준이 없으므로 만들지 않고 안내한다.
303
+ let developMissing = false;
262
304
  if (branches.mode === "pr-flow" && !opts.dryRun) {
263
305
  const remoteBranches = await detectRemoteBranches(cwd);
264
- if (remoteBranches.length && !remoteBranches.includes(branches.develop)) {
265
- await ensureDevelopBranch({
306
+ if (!remoteBranches.length) {
307
+ developMissing = true;
308
+ console.error(`⚠️ ${developMissingNotice(branches)}`);
309
+ } else if (!remoteBranches.includes(branches.develop)) {
310
+ const r = await ensureDevelopBranch({
266
311
  develop: branches.develop, remoteBranches, confirm: null, cwd,
267
312
  log: (m) => console.error(m),
268
313
  });
314
+ developMissing = r.pushed === false;
269
315
  }
270
316
  }
271
317
 
272
318
  // Flutter 옵션 — CLI 플래그 → version.yml 저장값 → 기본값(신규 dart-define, 기존 설치 dotenv 보존).
273
319
  // Flutter 타입이 없는 프로젝트에서는 렌더·치환 단계가 전부 무시한다.
320
+ // 스토어 저장값이 없는 기존 Flutter 설치는 설치돼 있는 스토어 워크플로우로 추론한다(대화형과 같은 결론).
321
+ // 추론하지 않으면 미결정이 "둘 다"로 확정 저장되어, 지웠던 플랫폼의 워크플로우·fastlane 파일이 되살아난다.
322
+ const workflowsDir = join(cwd, PATHS.workflowsDir);
323
+ const inferredStores = opts.flutterStore == null && existing?.types?.includes("flutter")
324
+ && existing.options?.flutterStore == null && existsSync(workflowsDir)
325
+ ? inferInstalledStores(workflowsDir) : null;
274
326
  const flutterOptions = resolveFlutterOptions({
275
327
  cli: {
276
- envMode: opts.flutterEnvMode, stores: opts.flutterStore,
328
+ envMode: opts.flutterEnvMode, stores: opts.flutterStore ?? inferredStores,
277
329
  androidDeployMode: opts.androidDeployMode, iosDeployMode: opts.iosDeployMode,
278
330
  },
279
331
  existing,
@@ -283,20 +335,16 @@ async function runInner(argv, {
283
335
  mode: opts.mode, force: opts.force, types, version, versionCode, branch,
284
336
  branches,
285
337
  paths,
286
- // 옵션: CLI 플래그 최우선 → version.yml 저장 옵션 → 기본값
287
- // 기존 version.yml이 있는데 semver_auto 키가 아예 없었던 경우(신규 기능 추가 이전 설치·
288
- // workflows-only 재실행) 조용히 true로 켜지면 애매한 커밋 하나로 major가 승격될 위험이 있다 —
289
- // 기존 설치는 false로 안전하게 폴백, 완전 신규 설치만 true(기존 설계) 유지.
290
- includeSemverAuto: opts.includeSemverAuto ?? existing?.options?.semverAuto ?? (existing ? false : true),
291
- // Copilot AI 요약은 AI Credits를 소비하는 opt-in — 신규·기존 설치 모두 명시하지 않으면 false다.
292
- includeCopilotAi: opts.includeCopilotAi ?? existing?.options?.copilotAi ?? false,
338
+ // 옵션: CLI 플래그 최우선 → version.yml 저장 옵션 → 기본값 (대화형과 같은 규칙)
339
+ ...resolveReleaseOptions({ semverAuto: opts.includeSemverAuto, copilotAi: opts.includeCopilotAi }, existing),
293
340
  repoName,
294
- // 실 resolver 4종 (.sh resolve_token 등가)
341
+ // @wizard ask/auto 토큰 값을 계산하는 resolver
295
342
  resolvers: makeResolvers(cwd, repoName, paths, flutterOptions),
296
343
  now, today,
297
344
  // 설치 로그용 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
298
345
  markers: detectMarkers(cwd, types), detectWarnings,
299
- deployStyle: opts.deployStyle
346
+ // 서버 배포 워크플로우가 없는 타입은 배포 방식이 설치에 영향이 없으므로 기록하지 않는다(null).
347
+ deployStyle: !hasServerDeployWorkflows(payload, types) ? null : opts.deployStyle
300
348
  || (isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : DEFAULT_DEPLOY_STYLE),
301
349
  previousTemplateVersion: existing?.templateVersion || "",
302
350
  envMode: flutterOptions.envMode,
@@ -307,10 +355,18 @@ async function runInner(argv, {
307
355
 
308
356
  context.templateVersion = readTemplateVersion();
309
357
 
358
+ // 무중단(nginx·traefik) 워크플로우가 없는 타입은 단일 서버 배포로 설치한다 — 조용히 넘어가지 않게 알린다.
359
+ const fallbackTypes = fallbackStyleTypes(payload, types, context.deployStyle);
360
+ if (fallbackTypes.length) {
361
+ console.error(`⚠️ ${fallbackTypes.join(", ")}에는 ${context.deployStyle} 무중단 배포 워크플로우가 없어 단일 서버 배포(simple)로 설치합니다.`);
362
+ }
363
+ // 안내는 고른 값으로 하되, 기록은 실제로 설치되는 방식으로 한다.
364
+ context.deployStyle = effectiveDeployStyle(payload, types, context.deployStyle);
365
+
310
366
  // 비대화형 축약 배너 (1줄, 로그 오염 최소)
311
367
  printBannerCompact({ version: context.templateVersion, mode: opts.mode });
312
368
 
313
- // Breaking Changes 게이트 (.sh execute_integration L4415~4420 등가 — 비대화형은 경고 후 진행)
369
+ // Breaking Changes 게이트 (비대화형은 경고 후 진행)
314
370
  const proceed = await runBreakingCheck({ cwd, payloadRoot: payload, templateVersion: context.templateVersion });
315
371
  if (!proceed) return 0;
316
372
 
@@ -319,24 +375,30 @@ async function runInner(argv, {
319
375
  return 0;
320
376
  }
321
377
 
378
+ startLog("install");
322
379
  // opts.mode는 parseArgs()에서 화이트리스트 검증을 통과했고, interactive/purge/uninstall/status/doctor는
323
380
  // 전부 위에서 조기 반환했으므로 이 시점엔 full 하나로 보장된다 (default 분기 제거,
324
381
  // 부분 설치 모드 제거로 분기 자체가 사라졌다).
325
382
  const result = runFull(context, payload, cwd);
326
383
 
327
- // 완료 요약 (.sh print_summary — CLI 모드에서도 출력)
384
+ // 완료 요약 (CLI 모드에서도 출력)
328
385
  printSummary({
329
- mode: opts.mode, types, version, versionCode, branches,
386
+ mode: opts.mode, types, version, versionCode, branches, developMissing,
330
387
  copiedFiles: result?.workflows?.copiedFiles ?? [],
388
+ autoUpdated: result?.workflows?.autoUpdated ?? [],
331
389
  gitignoreUpdated: result?.gitignoreUpdated === true,
332
390
  unresolved: result?.unresolved ?? [],
333
391
  secrets: result?.secrets ?? new Map(),
392
+ optionalSecrets: result?.optionalSecrets ?? new Map(),
334
393
  logPath: currentLogPath(),
335
394
  legacyMdLogs: hasLegacyMdLogs(cwd),
336
395
  cleanup: result?.cleanup ?? null,
337
396
  storeCleanup: result?.storeCleanup ?? null,
338
397
  flutterApp: result?.flutterApp ?? null,
398
+ readme: result?.readme ?? null,
399
+ scripts: result?.scripts ?? null,
339
400
  });
401
+ for (const n of postInstallNotices(result)) console.error(n.startsWith(" ") ? n : `⚠️ ${n}`);
340
402
  // store_submit 배포 모드는 main push마다 심사를 자동 제출한다 — 비대화형에서도 같은 경고를 보여준다
341
403
  // (대화형 경로는 ui/prompts.js#deployModeWarning을 선택 시점에 note로 보여준다).
342
404
  // Flutter 타입이 아니거나 해당 스토어를 선택하지 않은 프로젝트에는 뜨면 안 된다.
@@ -359,6 +421,17 @@ async function runInner(argv, {
359
421
  export async function run(argv, opts = {}) {
360
422
  try {
361
423
  return await runInner(argv, opts);
424
+ } catch (e) {
425
+ // Ctrl+C/EOF는 어느 질문에서든 즉시 중단한다 — 설치 파일을 쓰기 전에 빠져나오고, 셸 관례대로 130을 돌려준다.
426
+ if (isPromptAbort(e)) {
427
+ prompts.cancelMessage("중단했습니다 — 변경 없이 종료합니다.");
428
+ return e.signal === "SIGTERM" ? 143 : 130;
429
+ }
430
+ // 이미 기록을 시작한 실행이 도중에 죽으면 헤더만 남은 로그로는 원인을 알 수 없다 — 사유를 남긴다.
431
+ if (currentLogPath()) log.fail("run", "error", e?.message || String(e));
432
+ // 사용자가 고칠 수 있는 실패(권한 등)는 스택트레이스 대신 읽을 수 있는 문구로 끝낸다.
433
+ if (e instanceof CliError) { console.error(e.message); return 1; }
434
+ throw e;
362
435
  } finally {
363
436
  closeLogger();
364
437
  }
package/src/ui/ansi.js CHANGED
@@ -15,8 +15,10 @@ export const A = {
15
15
  // NO_COLOR(https://no-color.org) 환경변수 또는 대상 스트림이 TTY가 아니면 색상을 끈다.
16
16
  // no-color.org 규격상 NO_COLOR는 "값과 무관하게 존재 여부"만 본다 — NO_COLOR=""(빈 문자열)도
17
17
  // "설정됨"으로 취급해야 하므로 truthy 체크(`!process.env.NO_COLOR`)가 아니라 존재 체크를 쓴다.
18
+ // TERM=dumb(Emacs shell·일부 CI 의사 TTY 등)은 ANSI 시퀀스를 해석하지 못해 그대로 찍힌다 — 색상도 끈다.
19
+ export const isDumbTerminal = () => process.env.TERM === "dumb";
18
20
  export function colorEnabled(stream = process.stdout) {
19
- return process.env.NO_COLOR === undefined && !!stream.isTTY;
21
+ return process.env.NO_COLOR === undefined && !!stream.isTTY && !isDumbTerminal();
20
22
  }
21
23
 
22
24
  export const paint = (s, color, enabled = colorEnabled()) => (enabled ? `${color}${s}${A.reset}` : String(s));
package/src/ui/banner.js CHANGED
@@ -1,5 +1,4 @@
1
1
  // 첫 화면 배너 — 클래식 박스형
2
- // .ps1 Print-Banner 계승 + 브랜딩을 project-auto-wizard로 갱신.
3
2
  import { A, paint, visualWidth } from "./ansi.js";
4
3
 
5
4
  const INNER = 56; // 박스 내부 폭
@@ -9,14 +8,19 @@ function boxLine(out, content = "") {
9
8
  out(paint("║", A.cyan) + content + " ".repeat(pad) + paint("║", A.cyan) + "\n");
10
9
  }
11
10
 
12
- // 대화형 첫 화면 배너 — 박스 타이틀 + 메타 4줄 (.ps1 Print-Banner 등가)
13
- export function printBanner({ version, modeLabel }, out = (s) => process.stderr.write(s)) {
11
+ // 대화형 첫 화면 배너 — 박스 타이틀 + 메타 4줄
12
+ export function printBanner({ version, modeLabel }, out = (s) => process.stderr.write(s), columns = process.stderr.columns) {
14
13
  out("\n");
15
- out(paint(`╔${"═".repeat(INNER)}╗`, A.cyan) + "\n");
16
- boxLine(out);
17
- boxLine(out, ` ${paint("✦", A.yellow)} ${paint("P R O J E C T · A U T O · W I Z A R D", A.bold)} ${paint("✦", A.yellow)}`);
18
- boxLine(out);
19
- out(paint(`╚${"═".repeat(INNER)}╝`, A.cyan) + "\n");
14
+ // 상자보다 좁은 터미널에서는 테두리가 접혀 모양이 무너지므로 상자 없이 제목만 찍는다.
15
+ if (columns && columns < INNER + 2) {
16
+ out(`${paint("✦", A.yellow)} ${paint("PROJECT AUTO WIZARD", A.bold)}\n`);
17
+ } else {
18
+ out(paint(`╔${"═".repeat(INNER)}╗`, A.cyan) + "\n");
19
+ boxLine(out);
20
+ boxLine(out, ` ${paint("✦", A.yellow)} ${paint("P R O J E C T · A U T O · W I Z A R D", A.bold)} ${paint("✦", A.yellow)}`);
21
+ boxLine(out);
22
+ out(paint(`╚${"═".repeat(INNER)}╝`, A.cyan) + "\n");
23
+ }
20
24
  out(` 🌙 Version : ${paint(`v${version}`, A.green)}\n`);
21
25
  out(` 🪐 Mode : ${modeLabel}\n`);
22
26
  out(` 📦 Repo : ${paint("github.com/Twin-Fang/project-auto-wizard", A.dim)}\n`);