project-auto-wizard 0.12.2 → 0.13.1
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.
- package/README.ja.md +165 -0
- package/README.ko.md +165 -0
- package/README.md +107 -286
- package/README.zh-CN.md +165 -0
- package/bin/project-auto-wizard.js +1 -2
- package/package.json +9 -3
- package/payload/config/breaking-changes.json +24 -0
- package/payload/config/wizard-prompts.yml +45 -0
- package/payload/scripts/changelog_manager.py +141 -46
- package/payload/scripts/issue_helper.py +34 -5
- package/payload/scripts/version_manager.py +482 -107
- package/payload/version.yml.template +5 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
- package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
- package/src/cli/args.js +33 -24
- package/src/cli/help.js +16 -4
- package/src/commands/doctor.js +26 -60
- package/src/commands/dry-run.js +59 -5
- package/src/commands/full.js +135 -55
- package/src/commands/install-settings.js +51 -0
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +135 -69
- package/src/commands/purge.js +50 -21
- package/src/commands/status.js +18 -12
- package/src/commands/uninstall.js +22 -22
- package/src/context.js +5 -5
- package/src/core/baseline.js +20 -5
- package/src/core/branches.js +19 -1
- package/src/core/branding.js +1 -1
- package/src/core/breaking-check.js +14 -19
- package/src/core/breaking.js +7 -3
- package/src/core/copy/app-files.js +28 -0
- package/src/core/copy/gitignore.js +20 -12
- package/src/core/copy/readme.js +24 -5
- package/src/core/copy/simple.js +52 -13
- package/src/core/copy/workflows.js +133 -111
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +177 -47
- package/src/core/errors.js +3 -0
- package/src/core/flutter-doctor.js +49 -0
- package/src/core/flutter-hooks.js +29 -0
- package/src/core/flutter-options.js +4 -3
- package/src/core/fsutil.js +23 -6
- package/src/core/logger.js +55 -19
- package/src/core/paths-resolve.js +50 -47
- package/src/core/paths.js +26 -1
- package/src/core/release-options.js +23 -0
- package/src/core/removal-exec.js +67 -0
- package/src/core/removal-plan.js +75 -19
- package/src/core/types.js +136 -0
- package/src/core/verify.js +85 -19
- package/src/core/version-yml.js +42 -8
- package/src/core/wizard-env.js +21 -14
- package/src/core/wizard-labels.js +34 -25
- package/src/index.js +128 -60
- package/src/ui/ansi.js +3 -1
- package/src/ui/banner.js +12 -8
- package/src/ui/env-plan.js +68 -44
- package/src/ui/prompts.js +22 -12
- package/src/ui/readline-engine.js +109 -22
- package/src/ui/status-cards.js +10 -5
- package/src/ui/summary.js +71 -33
- package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// wizard-prompts.yml 라벨 메타
|
|
2
|
-
//
|
|
3
|
-
// 2934~2960(_wf_read_field), 2962~2970(wf_field).
|
|
4
|
-
// ⚠️ YAML 라이브러리 금지 — .sh와 동일하게 라인 기반 파싱만 한다(외부 의존성 0 + 포맷 관용성 동일).
|
|
1
|
+
// wizard-prompts.yml 라벨 메타 파서.
|
|
2
|
+
// ⚠️ YAML 라이브러리 금지 — 라인 기반 파싱만 한다(외부 의존성 0, 형식이 조금 어긋난 사용자 파일도 읽힌다).
|
|
5
3
|
import { join } from "node:path";
|
|
6
4
|
import * as nodeFs from "node:fs";
|
|
7
5
|
import { PAYLOAD } from "./paths.js";
|
|
@@ -9,7 +7,7 @@ import { PAYLOAD } from "./paths.js";
|
|
|
9
7
|
// wizard-prompts.yml 위치 — 사용자 레포(커스텀 오버라이드용) 기준 상대경로
|
|
10
8
|
export const LABELS_FILE = ".github/config/wizard-prompts.yml";
|
|
11
9
|
|
|
12
|
-
// 따옴표 감싸진 값이면 벗기고 trim ("값" → 값).
|
|
10
|
+
// 따옴표 감싸진 값이면 벗기고 trim ("값" → 값).
|
|
13
11
|
function unquote(s) {
|
|
14
12
|
const t = s.trim();
|
|
15
13
|
if (t.startsWith('"') && t.endsWith('"') && t.length >= 2) return t.slice(1, -1);
|
|
@@ -19,7 +17,7 @@ function unquote(s) {
|
|
|
19
17
|
// wizard-prompts.yml 텍스트 → 파싱 객체 (순수 함수 — 테스트 직접 사용 가능).
|
|
20
18
|
// 반환: { fields: Map<조회키, {label?,help?,example?}>, workflowNames: [{key,value}] }
|
|
21
19
|
// - 조회키: "PROJECT_NAME" 또는 "flutter.APP_ARTIFACT_NAME" (dotted 타입 오버라이드)
|
|
22
|
-
// - 구형 1줄(KEY: "라벨")은 fields의 label로 흡수 (
|
|
20
|
+
// - 구형 1줄(KEY: "라벨")은 fields의 label로 흡수 (label만 의미)
|
|
23
21
|
export function parseWizardPrompts(text) {
|
|
24
22
|
const fields = new Map();
|
|
25
23
|
const workflowNames = [];
|
|
@@ -28,10 +26,10 @@ export function parseWizardPrompts(text) {
|
|
|
28
26
|
|
|
29
27
|
for (const raw of String(text).split(/\r?\n/)) {
|
|
30
28
|
const line = raw.replace(/\r$/, "");
|
|
31
|
-
if (!line.trim() || line.trim().startsWith("#")) continue; // 빈 줄·주석은 블록을 끊지 않음
|
|
29
|
+
if (!line.trim() || line.trim().startsWith("#")) continue; // 빈 줄·주석은 블록을 끊지 않음
|
|
32
30
|
|
|
33
31
|
if (!/^\s/.test(line)) {
|
|
34
|
-
// 최상위 키 라인 — 이전 블록 종료
|
|
32
|
+
// 최상위 키 라인 — 이전 블록 종료
|
|
35
33
|
current = null; inWfNames = false;
|
|
36
34
|
const m = line.match(/^([A-Za-z_][A-Za-z0-9_.\-]*):(.*)$/);
|
|
37
35
|
if (!m) continue;
|
|
@@ -40,7 +38,7 @@ export function parseWizardPrompts(text) {
|
|
|
40
38
|
if (key === "_workflow_names") { inWfNames = true; continue; }
|
|
41
39
|
const entry = fields.get(key) || {};
|
|
42
40
|
if (rest) {
|
|
43
|
-
// 구형 1줄: KEY: "라벨" (
|
|
41
|
+
// 구형 1줄: KEY: "라벨" (label로만 사용)
|
|
44
42
|
const q = rest.match(/^"([^"]*)"\s*$/);
|
|
45
43
|
if (q) entry.label = q[1];
|
|
46
44
|
}
|
|
@@ -53,35 +51,46 @@ export function parseWizardPrompts(text) {
|
|
|
53
51
|
const m = line.match(/^\s+([A-Za-z_][A-Za-z0-9_.\-]*):\s*(.*)$/);
|
|
54
52
|
if (!m) continue;
|
|
55
53
|
if (inWfNames) {
|
|
56
|
-
// " KEY: "값"" 형식만 인정
|
|
54
|
+
// " KEY: "값"" 형식만 인정
|
|
57
55
|
const q = m[2].match(/^"(.*)"\s*$/);
|
|
58
56
|
if (q) workflowNames.push({ key: m[1], value: q[1] });
|
|
59
57
|
continue;
|
|
60
58
|
}
|
|
61
59
|
if (current && (m[1] === "label" || m[1] === "help" || m[1] === "example")) {
|
|
62
|
-
// 블록 내 첫 등장만 채택
|
|
60
|
+
// 블록 내 첫 등장만 채택
|
|
63
61
|
if (current[m[1]] == null) current[m[1]] = unquote(m[2]);
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
64
|
return { fields, workflowNames };
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
// wizard-prompts.yml을 찾아 읽고
|
|
70
|
-
//
|
|
71
|
-
// WHY
|
|
72
|
-
//
|
|
67
|
+
// wizard-prompts.yml을 찾아 읽고 파싱.
|
|
68
|
+
// 번들본(payload/config)을 바탕으로 대상 프로젝트의 사용자 파일(커스텀 오버라이드)을 키 단위로 덮어쓴다.
|
|
69
|
+
// WHY 병합: 사용자 파일에는 바꾸고 싶은 키만 적는 것이 자연스럽다 — 파일째 대체하면 적지 않은 나머지
|
|
70
|
+
// 질문의 label/help/example과 워크플로우 표시명이 전부 사라져 KEY명만 보인다.
|
|
73
71
|
// fs 주입 가능(테스트용) — 기본 node:fs.
|
|
74
72
|
export function loadWizardPrompts(targetRoot = ".", payloadRoot = "", fs = nodeFs) {
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const read = (p) => (fs.existsSync(p) ? parseWizardPrompts(fs.readFileSync(p, "utf8")) : null);
|
|
74
|
+
const bundled = payloadRoot ? read(join(payloadRoot, PAYLOAD.configDir, "wizard-prompts.yml")) : null;
|
|
75
|
+
const user = read(join(targetRoot, LABELS_FILE));
|
|
76
|
+
if (!bundled || !user) return user || bundled;
|
|
77
|
+
return mergeWizardPrompts(bundled, user);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 번들 + 사용자 파싱 결과 병합. 같은 키는 사용자가 적은 필드만 덮어쓴다(label만 바꾸면 help/example은 번들 유지).
|
|
81
|
+
export function mergeWizardPrompts(base, override) {
|
|
82
|
+
const fields = new Map([...base.fields].map(([k, v]) => [k, { ...v }]));
|
|
83
|
+
for (const [key, entry] of override.fields) {
|
|
84
|
+
const merged = { ...(fields.get(key) || {}) };
|
|
85
|
+
for (const [f, v] of Object.entries(entry)) if (v != null && v !== "") merged[f] = v;
|
|
86
|
+
fields.set(key, merged);
|
|
80
87
|
}
|
|
81
|
-
|
|
88
|
+
const overridden = new Set(override.workflowNames.map((w) => w.key));
|
|
89
|
+
const workflowNames = [...base.workflowNames.filter((w) => !overridden.has(w.key)), ...override.workflowNames];
|
|
90
|
+
return { fields, workflowNames };
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
// 필드
|
|
93
|
+
// 필드 조회. field: "label" | "help" | "example".
|
|
85
94
|
// 우선순위: "{type}.KEY" 블록 → "KEY" 블록(구형 1줄 포함) → 폴백(label이면 KEY명, 아니면 "").
|
|
86
95
|
export function wfField(prompts, type, key, field) {
|
|
87
96
|
if (prompts && prompts.fields) {
|
|
@@ -93,11 +102,11 @@ export function wfField(prompts, type, key, field) {
|
|
|
93
102
|
return field === "label" ? key : "";
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
// 워크플로우 파일명 → 사람이 읽는 짧은
|
|
105
|
+
// 워크플로우 파일명 → 사람이 읽는 짧은 이름.
|
|
97
106
|
// _workflow_names에서 "키가 파일명에 포함되면" 그 값 사용 — 최장 키 우선(REACT-CI vs REACT-CICD 구분).
|
|
98
|
-
// 미매칭이면 .yaml/.yml 확장자만 제거해
|
|
107
|
+
// 미매칭이면 .yaml/.yml 확장자만 제거해 반환.
|
|
99
108
|
export function workflowDisplayName(prompts, filename) {
|
|
100
|
-
const base = String(filename).split("/").pop().split("\\").pop(); // 경로 제거
|
|
109
|
+
const base = String(filename).split("/").pop().split("\\").pop(); // 경로 제거
|
|
101
110
|
let best = null; let bestLen = 0;
|
|
102
111
|
for (const { key, value } of prompts?.workflowNames ?? []) {
|
|
103
112
|
if (base.includes(key) && key.length > bestLen) { best = value; bestLen = key.length; }
|
package/src/index.js
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
|
-
// project-auto-wizard CLI 진입
|
|
1
|
+
// project-auto-wizard CLI 진입 파이프라인.
|
|
2
2
|
// 감지 → payload 해석 → 모드 라우팅 → 통합 실행. 비대화형(--force) 우선.
|
|
3
|
-
//
|
|
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";
|
|
7
8
|
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
|
-
import {
|
|
11
|
-
import { DEFAULT_DEPLOY_STYLE, isDeployStyle } from "./core/deploy-style.js";
|
|
11
|
+
import { fallbackStyleTypes } from "./core/deploy-style.js";
|
|
12
12
|
import { resolveFlutterOptions } from "./core/flutter-options.js";
|
|
13
|
+
import { inferInstalledStores } from "./core/installed-stores.js";
|
|
14
|
+
import { PATHS } from "./core/paths.js";
|
|
13
15
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "./core/assets.js";
|
|
14
|
-
import { detectTypes,
|
|
16
|
+
import { detectTypes, detectDefaultBranch, detectRepoName, makeResolvers, detectMarkers } from "./core/detect-fs.js";
|
|
15
17
|
import { parseExisting } from "./core/version-yml.js";
|
|
18
|
+
import { resolveReleaseOptions } from "./core/release-options.js";
|
|
16
19
|
import { runBreakingCheck } from "./core/breaking-check.js";
|
|
17
20
|
import { resolveProjectPaths } from "./core/paths-resolve.js";
|
|
18
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
resolveBranchConfig, detectRemoteBranches, ensureDevelopBranch, defaultExec, isValidBranchName, developMissingNotice,
|
|
23
|
+
} from "./core/branches.js";
|
|
19
24
|
import { printBannerCompact } from "./ui/banner.js";
|
|
20
25
|
import { printSummary } from "./ui/summary.js";
|
|
21
|
-
import { runFull } from "./commands/full.js";
|
|
26
|
+
import { runFull, postInstallNotices } from "./commands/full.js";
|
|
22
27
|
import { runUninstall, runUninstallFlow } from "./commands/uninstall.js";
|
|
23
28
|
import * as prompts from "./ui/prompts.js";
|
|
29
|
+
import { isPromptAbort } from "./ui/readline-engine.js";
|
|
24
30
|
import { runInteractive } from "./commands/interactive.js";
|
|
25
|
-
import {
|
|
31
|
+
import { resolveDeployStyle, resolveVersion, resolveVersionCode, buildInstallContext } from "./commands/install-settings.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
|
-
|
|
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
|
-
//
|
|
84
|
-
//
|
|
85
|
-
const
|
|
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
|
-
|
|
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에 포함되지 않으므로(
|
|
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
|
-
|
|
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
|
|
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 보존의 단일 진실
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
// 경로 확정 (
|
|
274
|
+
// 경로 확정 (비대화형 — --paths 우선 → 저장값 → 후보 1개 자동 → 에러)
|
|
243
275
|
let paths;
|
|
244
276
|
try {
|
|
245
277
|
paths = await resolveProjectPaths({
|
|
246
|
-
root: cwd, types, paths:
|
|
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,66 +283,85 @@ 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 = resolveVersion({
|
|
290
|
+
cwd, existing, explicit: opts.version,
|
|
291
|
+
types, paths, warn: (m) => { detectWarnings.push(m); console.error(m); },
|
|
292
|
+
});
|
|
293
|
+
const versionCode = resolveVersionCode({ cwd, existing, types, paths });
|
|
294
|
+
|
|
254
295
|
// 브랜치 구성 (--main-branch/--develop-branch → version.yml 저장값 → 감지 default → main/develop)
|
|
296
|
+
// 이전 버전이 저장한 감지 실패 값("(unknown)" 등)은 저장값으로 인정하지 않는다 — 그대로 두면 재실행해도 복구되지 않는다.
|
|
297
|
+
const savedBranch = (b) => (isValidBranchName(b) ? b : "");
|
|
255
298
|
const branches = resolveBranchConfig({
|
|
256
|
-
mainBranch: opts.mainBranch || existing?.branches?.main
|
|
257
|
-
developBranch: opts.developBranch || existing?.branches?.develop
|
|
299
|
+
mainBranch: opts.mainBranch || savedBranch(existing?.branches?.main),
|
|
300
|
+
developBranch: opts.developBranch || savedBranch(existing?.branches?.develop),
|
|
258
301
|
defaultBranch: branch,
|
|
259
302
|
});
|
|
260
303
|
// pr-flow에서 develop이 원격에 없으면 자동 생성+push (--force 비대화형 — 질문 없음).
|
|
261
|
-
//
|
|
304
|
+
// 원격에 브랜치가 하나도 없으면(빈 원격·origin 없음) push할 기준이 없으므로 만들지 않고 안내한다.
|
|
305
|
+
let developMissing = false;
|
|
262
306
|
if (branches.mode === "pr-flow" && !opts.dryRun) {
|
|
263
307
|
const remoteBranches = await detectRemoteBranches(cwd);
|
|
264
|
-
if (remoteBranches.length
|
|
265
|
-
|
|
308
|
+
if (!remoteBranches.length) {
|
|
309
|
+
developMissing = true;
|
|
310
|
+
console.error(`⚠️ ${developMissingNotice(branches)}`);
|
|
311
|
+
} else if (!remoteBranches.includes(branches.develop)) {
|
|
312
|
+
const r = await ensureDevelopBranch({
|
|
266
313
|
develop: branches.develop, remoteBranches, confirm: null, cwd,
|
|
267
314
|
log: (m) => console.error(m),
|
|
268
315
|
});
|
|
316
|
+
developMissing = r.pushed === false;
|
|
269
317
|
}
|
|
270
318
|
}
|
|
271
319
|
|
|
272
320
|
// Flutter 옵션 — CLI 플래그 → version.yml 저장값 → 기본값(신규 dart-define, 기존 설치 dotenv 보존).
|
|
273
321
|
// Flutter 타입이 없는 프로젝트에서는 렌더·치환 단계가 전부 무시한다.
|
|
322
|
+
// 스토어 저장값이 없는 기존 Flutter 설치는 설치돼 있는 스토어 워크플로우로 추론한다(대화형과 같은 결론).
|
|
323
|
+
// 추론하지 않으면 미결정이 "둘 다"로 확정 저장되어, 지웠던 플랫폼의 워크플로우·fastlane 파일이 되살아난다.
|
|
324
|
+
const workflowsDir = join(cwd, PATHS.workflowsDir);
|
|
325
|
+
const inferredStores = opts.flutterStore == null && existing?.types?.includes("flutter")
|
|
326
|
+
&& existing.options?.flutterStore == null && existsSync(workflowsDir)
|
|
327
|
+
? inferInstalledStores(workflowsDir) : null;
|
|
274
328
|
const flutterOptions = resolveFlutterOptions({
|
|
275
329
|
cli: {
|
|
276
|
-
envMode: opts.flutterEnvMode, stores: opts.flutterStore,
|
|
330
|
+
envMode: opts.flutterEnvMode, stores: opts.flutterStore ?? inferredStores,
|
|
277
331
|
androidDeployMode: opts.androidDeployMode, iosDeployMode: opts.iosDeployMode,
|
|
278
332
|
},
|
|
279
333
|
existing,
|
|
280
334
|
});
|
|
281
335
|
|
|
282
|
-
|
|
283
|
-
|
|
336
|
+
// 고른 배포 방식 — 플래그 → 저장값 → 기본값. 실제 설치되는 방식으로의 정리는 컨텍스트 조립에서 한다.
|
|
337
|
+
const chosenDeployStyle = resolveDeployStyle({ payload, types, explicit: opts.deployStyle, existing });
|
|
338
|
+
|
|
339
|
+
const context = buildInstallContext({
|
|
340
|
+
payload, existing, templateVersion: readTemplateVersion(), types, deployStyle: chosenDeployStyle,
|
|
341
|
+
flutterOptions,
|
|
342
|
+
// 옵션: CLI 플래그 최우선 → version.yml 저장 옵션 → 기본값 (대화형과 같은 규칙)
|
|
343
|
+
releaseOptions: resolveReleaseOptions({ semverAuto: opts.includeSemverAuto, copilotAi: opts.includeCopilotAi }, existing),
|
|
344
|
+
mode: opts.mode, force: opts.force, version, versionCode, branch,
|
|
284
345
|
branches,
|
|
285
346
|
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,
|
|
293
347
|
repoName,
|
|
294
|
-
//
|
|
348
|
+
// @wizard ask/auto 토큰 값을 계산하는 resolver
|
|
295
349
|
resolvers: makeResolvers(cwd, repoName, paths, flutterOptions),
|
|
296
350
|
now, today,
|
|
297
351
|
// 설치 로그용 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
298
352
|
markers: detectMarkers(cwd, types), detectWarnings,
|
|
299
|
-
deployStyle: opts.deployStyle
|
|
300
|
-
|| (isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : DEFAULT_DEPLOY_STYLE),
|
|
301
|
-
previousTemplateVersion: existing?.templateVersion || "",
|
|
302
|
-
envMode: flutterOptions.envMode,
|
|
303
|
-
flutterStore: flutterOptions.stores,
|
|
304
|
-
androidDeployMode: flutterOptions.androidDeployMode,
|
|
305
|
-
iosDeployMode: flutterOptions.iosDeployMode,
|
|
306
353
|
});
|
|
307
354
|
|
|
308
|
-
|
|
355
|
+
// 무중단(nginx·traefik) 워크플로우가 없는 타입은 단일 서버 배포로 설치한다 — 조용히 넘어가지 않게 알린다.
|
|
356
|
+
const fallbackTypes = fallbackStyleTypes(payload, types, chosenDeployStyle);
|
|
357
|
+
if (fallbackTypes.length) {
|
|
358
|
+
console.error(`⚠️ ${fallbackTypes.join(", ")}에는 ${chosenDeployStyle} 무중단 배포 워크플로우가 없어 단일 서버 배포(simple)로 설치합니다.`);
|
|
359
|
+
}
|
|
309
360
|
|
|
310
361
|
// 비대화형 축약 배너 (1줄, 로그 오염 최소)
|
|
311
362
|
printBannerCompact({ version: context.templateVersion, mode: opts.mode });
|
|
312
363
|
|
|
313
|
-
// Breaking Changes 게이트 (
|
|
364
|
+
// Breaking Changes 게이트 (비대화형은 경고 후 진행)
|
|
314
365
|
const proceed = await runBreakingCheck({ cwd, payloadRoot: payload, templateVersion: context.templateVersion });
|
|
315
366
|
if (!proceed) return 0;
|
|
316
367
|
|
|
@@ -319,24 +370,30 @@ async function runInner(argv, {
|
|
|
319
370
|
return 0;
|
|
320
371
|
}
|
|
321
372
|
|
|
373
|
+
startLog("install");
|
|
322
374
|
// opts.mode는 parseArgs()에서 화이트리스트 검증을 통과했고, interactive/purge/uninstall/status/doctor는
|
|
323
375
|
// 전부 위에서 조기 반환했으므로 이 시점엔 full 하나로 보장된다 (default 분기 제거,
|
|
324
376
|
// 부분 설치 모드 제거로 분기 자체가 사라졌다).
|
|
325
377
|
const result = runFull(context, payload, cwd);
|
|
326
378
|
|
|
327
|
-
// 완료 요약 (
|
|
379
|
+
// 완료 요약 (CLI 모드에서도 출력)
|
|
328
380
|
printSummary({
|
|
329
|
-
mode: opts.mode, types, version, versionCode, branches,
|
|
381
|
+
mode: opts.mode, types, version, versionCode, branches, developMissing,
|
|
330
382
|
copiedFiles: result?.workflows?.copiedFiles ?? [],
|
|
383
|
+
autoUpdated: result?.workflows?.autoUpdated ?? [],
|
|
331
384
|
gitignoreUpdated: result?.gitignoreUpdated === true,
|
|
332
385
|
unresolved: result?.unresolved ?? [],
|
|
333
386
|
secrets: result?.secrets ?? new Map(),
|
|
387
|
+
optionalSecrets: result?.optionalSecrets ?? new Map(),
|
|
334
388
|
logPath: currentLogPath(),
|
|
335
389
|
legacyMdLogs: hasLegacyMdLogs(cwd),
|
|
336
390
|
cleanup: result?.cleanup ?? null,
|
|
337
391
|
storeCleanup: result?.storeCleanup ?? null,
|
|
338
392
|
flutterApp: result?.flutterApp ?? null,
|
|
393
|
+
readme: result?.readme ?? null,
|
|
394
|
+
scripts: result?.scripts ?? null,
|
|
339
395
|
});
|
|
396
|
+
for (const n of postInstallNotices(result)) console.error(n.startsWith(" ") ? n : `⚠️ ${n}`);
|
|
340
397
|
// store_submit 배포 모드는 main push마다 심사를 자동 제출한다 — 비대화형에서도 같은 경고를 보여준다
|
|
341
398
|
// (대화형 경로는 ui/prompts.js#deployModeWarning을 선택 시점에 note로 보여준다).
|
|
342
399
|
// Flutter 타입이 아니거나 해당 스토어를 선택하지 않은 프로젝트에는 뜨면 안 된다.
|
|
@@ -359,6 +416,17 @@ async function runInner(argv, {
|
|
|
359
416
|
export async function run(argv, opts = {}) {
|
|
360
417
|
try {
|
|
361
418
|
return await runInner(argv, opts);
|
|
419
|
+
} catch (e) {
|
|
420
|
+
// Ctrl+C/EOF는 어느 질문에서든 즉시 중단한다 — 설치 파일을 쓰기 전에 빠져나오고, 셸 관례대로 130을 돌려준다.
|
|
421
|
+
if (isPromptAbort(e)) {
|
|
422
|
+
prompts.cancelMessage("중단했습니다 — 변경 없이 종료합니다.");
|
|
423
|
+
return e.signal === "SIGTERM" ? 143 : 130;
|
|
424
|
+
}
|
|
425
|
+
// 이미 기록을 시작한 실행이 도중에 죽으면 헤더만 남은 로그로는 원인을 알 수 없다 — 사유를 남긴다.
|
|
426
|
+
if (currentLogPath()) log.fail("run", "error", e?.message || String(e));
|
|
427
|
+
// 사용자가 고칠 수 있는 실패(권한 등)는 스택트레이스 대신 읽을 수 있는 문구로 끝낸다.
|
|
428
|
+
if (e instanceof CliError) { console.error(e.message); return 1; }
|
|
429
|
+
throw e;
|
|
362
430
|
} finally {
|
|
363
431
|
closeLogger();
|
|
364
432
|
}
|
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줄
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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`);
|