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.
- 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 +7 -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 +397 -97
- package/payload/version.yml.template +4 -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 +17 -10
- package/src/commands/dry-run.js +54 -1
- package/src/commands/full.js +127 -50
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +124 -56
- package/src/commands/purge.js +59 -17
- package/src/commands/status.js +16 -1
- package/src/commands/uninstall.js +58 -13
- 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/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 +90 -47
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +131 -47
- package/src/core/errors.js +3 -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-plan.js +75 -19
- package/src/core/types.js +113 -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 +122 -49
- 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,4 +1,4 @@
|
|
|
1
|
-
// 대화형
|
|
1
|
+
// 대화형 마법사.
|
|
2
2
|
// io 주입으로 테스트 가능. 실제 실행은 src/ui/prompts.js 함수를 io로 넘긴다.
|
|
3
3
|
// 새 시각 층(banner/detectionLog/analysisCard/installKind/summary)과 저수준 엔진(engineIo)은
|
|
4
4
|
// io의 "옵셔널 멤버" — 스텁이 생략하면 해당 층만 건너뛰고 실행 계약은 동일하다.
|
|
@@ -7,18 +7,21 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
7
7
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "../core/assets.js";
|
|
8
8
|
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber, detectMarkers } from "../core/detect-fs.js";
|
|
9
9
|
import { parseExisting } from "../core/version-yml.js";
|
|
10
|
+
import { pickReleaseOptions, resolveReleaseOptions } from "../core/release-options.js";
|
|
10
11
|
import { runBreakingCheck } from "../core/breaking-check.js";
|
|
11
12
|
import { resolveProjectPaths } from "../core/paths-resolve.js";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
resolveBranchConfig, detectRemoteBranches, ensureDevelopBranch, sortBranchesForSelection, isValidBranchName, developMissingNotice,
|
|
15
|
+
} from "../core/branches.js";
|
|
13
16
|
import { promptEnvPlan } from "../ui/env-plan.js";
|
|
14
17
|
import { surveyWorkflows } from "../core/copy/workflows.js";
|
|
15
18
|
import { createContext, VALID_TYPES } from "../context.js";
|
|
16
|
-
import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
19
|
+
import { isDeployStyle, DEFAULT_DEPLOY_STYLE, hasServerDeployWorkflows, hasNonstopWorkflows, effectiveDeployStyle } from "../core/deploy-style.js";
|
|
17
20
|
import { PATHS } from "../core/paths.js";
|
|
18
|
-
import { resolveFlutterOptions, DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
|
|
21
|
+
import { resolveFlutterOptions, DEFAULT_DEPLOY_MODE, STORE_PLATFORMS } from "../core/flutter-options.js";
|
|
19
22
|
import { inferInstalledStores } from "../core/installed-stores.js";
|
|
20
23
|
import { savedFlutterState, askUnsetFlutterOptions, editFlutterOption, FLUTTER_EDIT_ITEMS } from "./interactive-flutter.js";
|
|
21
|
-
import { runFull } from "./full.js";
|
|
24
|
+
import { runFull, postInstallNotices } from "./full.js";
|
|
22
25
|
import { runUninstallFlow } from "./uninstall.js";
|
|
23
26
|
import * as prompts from "../ui/prompts.js";
|
|
24
27
|
import { runStatus, printStatus } from "./status.js";
|
|
@@ -26,6 +29,9 @@ import { runDoctor, printDoctorReport } from "./doctor.js";
|
|
|
26
29
|
import { currentLogPath, hasLegacyMdLogs } from "../core/logger.js";
|
|
27
30
|
|
|
28
31
|
const CANCEL = prompts.CANCEL;
|
|
32
|
+
|
|
33
|
+
const SEMVER_AUTO_QUESTION = "자동 버전 승격을 사용하시겠습니까? (커밋 타입에 따라 major/minor/patch 자동 결정)";
|
|
34
|
+
const COPILOT_AI_QUESTION = "Copilot으로 AI 요약을 생성하시겠습니까? (GitHub Copilot AI Credits가 소비되며, 사용할 수 없으면 자동으로 규칙 기반 요약으로 전환됩니다)";
|
|
29
35
|
const isCancel = (v) => v === CANCEL || typeof v === "symbol";
|
|
30
36
|
|
|
31
37
|
// io 기본값 = 실제 prompts. 테스트는 스텁 io 주입.
|
|
@@ -37,7 +43,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
37
43
|
if (io.banner) io.banner({ version: templateVersion, modeLabel: "대화형 통합 마법사" });
|
|
38
44
|
else io.intro?.("project-auto-wizard — 대화형 통합 마법사");
|
|
39
45
|
|
|
40
|
-
// 기존 version.yml — version/version_code/paths/옵션 보존의 단일 진실
|
|
46
|
+
// 기존 version.yml — version/version_code/paths/옵션 보존의 단일 진실
|
|
41
47
|
const vyPath = join(cwd, "version.yml");
|
|
42
48
|
const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
|
|
43
49
|
|
|
@@ -65,7 +71,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
65
71
|
return 0;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
|
-
// Breaking Changes 게이트 (
|
|
74
|
+
// Breaking Changes 게이트 (모든 모드 공통, 대화형은 확인 질문)
|
|
69
75
|
const proceed = await runBreakingCheck({
|
|
70
76
|
cwd, payloadRoot: payload, templateVersion,
|
|
71
77
|
askYesNo: (msg, def) => io.askYesNo(msg, def),
|
|
@@ -78,14 +84,21 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
78
84
|
const detectWarnings = [];
|
|
79
85
|
let types = detectTypes(cwd);
|
|
80
86
|
let version = (existing?.version) || detectVersion(cwd, {
|
|
87
|
+
types,
|
|
81
88
|
warn: (m) => detectWarnings.push(m),
|
|
82
89
|
hint: "다음 화면의 '수정하기 > 버전'에서 바로 고칠 수 있습니다.",
|
|
83
90
|
});
|
|
84
|
-
|
|
91
|
+
// 경로가 확정된 뒤 모노레포 하위 폴더에서 다시 감지할지 — 저장값·직접 입력한 값은 건드리지 않는다.
|
|
92
|
+
let versionAutoDetected = !existing?.version;
|
|
93
|
+
let branch = detectDefaultBranch(cwd, {
|
|
94
|
+
warn: (m) => detectWarnings.push(m),
|
|
95
|
+
hint: "다르면 뒤의 '릴리스 브랜치' 질문에서 바꿀 수 있습니다.",
|
|
96
|
+
});
|
|
85
97
|
const repoName = detectRepoName(cwd);
|
|
86
|
-
// 선택 워크플로우 초기값: version.yml 저장 옵션
|
|
87
|
-
|
|
88
|
-
let
|
|
98
|
+
// 선택 워크플로우 초기값: CLI 플래그(--copilot 등) → version.yml 저장 옵션
|
|
99
|
+
// 플래그로 정한 값은 질문을 생략한다 — 비대화형과 같은 우선순위.
|
|
100
|
+
let { semverAuto: includeSemverAuto, copilotAi: includeCopilotAi } = pickReleaseOptions(
|
|
101
|
+
{ semverAuto: baseCtx?.includeSemverAuto, copilotAi: baseCtx?.includeCopilotAi }, existing);
|
|
89
102
|
// 서버 배포 방식 — 저장값(version.yml)이 있으면 재질문하지 않는다 (semver_auto와 같은 규약).
|
|
90
103
|
let deployStyle = isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
|
|
91
104
|
const showOptional = mode === "full";
|
|
@@ -99,12 +112,21 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
99
112
|
envModeDefault: resolveFlutterOptions({
|
|
100
113
|
cli: { envMode: "", stores: null, androidDeployMode: "", iosDeployMode: "" }, existing,
|
|
101
114
|
}).envMode,
|
|
102
|
-
|
|
115
|
+
// 신규 설치의 초기 선택은 CLI 기본값(--flutter-store 미지정 = 둘 다)과 같아야 한다 — 경로에 따라 설치 결과가 달라지면 안 된다.
|
|
116
|
+
inferredStores: existing && flutter.stores === null ? inferInstalledStores(join(cwd, PATHS.workflowsDir)) : [...STORE_PLATFORMS],
|
|
103
117
|
};
|
|
104
118
|
// 이미 정해진 값은 건너뛰므로 여러 번 불러도 같은 질문이 반복되지 않는다.
|
|
105
119
|
const askFlutterOptions = async () => {
|
|
106
120
|
if (types.includes("flutter")) flutter = await askUnsetFlutterOptions(io, flutter, flutterAsk);
|
|
107
121
|
};
|
|
122
|
+
// 서버 배포 방식 — 서버 배포(CD) 워크플로우가 있는 타입(spring·go·python 등)일 때만 묻는다.
|
|
123
|
+
// 그 외 타입에는 설치 결과에 영향이 없는 질문이라 묻지도 기록하지도 않는다.
|
|
124
|
+
const hasServerDeploy = () => hasServerDeployWorkflows(payload, types);
|
|
125
|
+
const askDeployStyle = async () => {
|
|
126
|
+
if (isDeployStyle(deployStyle) || !hasServerDeploy()) return;
|
|
127
|
+
const picked = await io.selectDeployStyle({ nonstop: hasNonstopWorkflows(payload, types) });
|
|
128
|
+
deployStyle = isDeployStyle(picked) ? picked : DEFAULT_DEPLOY_STYLE; // ESC = 기본값
|
|
129
|
+
};
|
|
108
130
|
|
|
109
131
|
// 감지 로그. markers = 실제로 존재를 확인한 파일.
|
|
110
132
|
let markers = detectMarkers(cwd, types);
|
|
@@ -127,11 +149,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
127
149
|
|
|
128
150
|
// 배포 방식·Flutter 옵션·semver/Copilot 질문 — 선택 항목을 묻는 경우(showOptional)만
|
|
129
151
|
if (showOptional) {
|
|
130
|
-
|
|
131
|
-
if (!isDeployStyle(deployStyle)) {
|
|
132
|
-
const picked = await io.selectDeployStyle();
|
|
133
|
-
deployStyle = isDeployStyle(picked) ? picked : DEFAULT_DEPLOY_STYLE; // ESC = 기본값
|
|
134
|
-
}
|
|
152
|
+
await askDeployStyle();
|
|
135
153
|
|
|
136
154
|
// Flutter 옵션 — 환경변수 방식 → 스토어 배포 대상 → 플랫폼별 배포 모드.
|
|
137
155
|
await askFlutterOptions();
|
|
@@ -139,31 +157,37 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
139
157
|
// 신규 질문 — 자동 semver 승격 (기본 ON). 저장값 있으면 재질문 생략.
|
|
140
158
|
// version.yml을 쓰지 않는 workflows 모드에서는 답변이 무의미하므로 full에서만 질문한다.
|
|
141
159
|
if (mode === "full" && includeSemverAuto === null) {
|
|
142
|
-
const y2 = await io.askYesNo(
|
|
160
|
+
const y2 = await io.askYesNo(SEMVER_AUTO_QUESTION, true);
|
|
143
161
|
includeSemverAuto = y2 === true;
|
|
144
162
|
}
|
|
145
163
|
|
|
146
164
|
// Copilot AI 요약 — AI Credits를 소비하므로 opt-in(기본 No). 저장값 있으면 재질문 생략.
|
|
147
165
|
if (mode === "full" && includeCopilotAi === null) {
|
|
148
|
-
const y3 = await io.askYesNo(
|
|
166
|
+
const y3 = await io.askYesNo(COPILOT_AI_QUESTION, false);
|
|
149
167
|
includeCopilotAi = y3 === true;
|
|
150
168
|
}
|
|
151
169
|
}
|
|
152
|
-
// 질문이 실제로 나온 경우(위 full 모드 질문) 답변을 그대로
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
// 질문이 실제로 나온 경우(위 full 모드 질문) 답변을 그대로 존중하고, 안 나온 옵션만 기본값으로 채운다
|
|
171
|
+
// — CLI 경로(index.js)와 같은 함수라 같은 규칙이다.
|
|
172
|
+
({ includeSemverAuto, includeCopilotAi } = resolveReleaseOptions(
|
|
173
|
+
{ semverAuto: includeSemverAuto, copilotAi: includeCopilotAi }, existing));
|
|
174
|
+
const showOptionToggles = mode === "full";
|
|
157
175
|
|
|
158
|
-
// 확인/수정 루프 — ESC는 '머무르기' (
|
|
176
|
+
// 확인/수정 루프 — ESC는 '머무르기' (명시적 '아니오'만 종료)
|
|
159
177
|
let paths = new Map();
|
|
160
178
|
let confirmed = false;
|
|
161
179
|
while (!confirmed) {
|
|
162
180
|
// 프로젝트 분석 개요 카드. 스텁엔 없음 → note 폴백.
|
|
163
181
|
if (io.analysisCard) {
|
|
164
|
-
io.analysisCard({
|
|
182
|
+
io.analysisCard({
|
|
183
|
+
mode, modeLabel: modeLabel(mode), types, version, branch, showOptional, paths, flutter, envModeDefault: flutterAsk.envModeDefault,
|
|
184
|
+
options: showOptionToggles ? { semverAuto: includeSemverAuto, copilotAi: includeCopilotAi } : null,
|
|
185
|
+
});
|
|
165
186
|
} else {
|
|
166
|
-
io.note?.(summarize({
|
|
187
|
+
io.note?.(summarize({
|
|
188
|
+
mode, types, version, branch, showOptional, flutter, envModeDefault: flutterAsk.envModeDefault,
|
|
189
|
+
options: showOptionToggles ? { semverAuto: includeSemverAuto, copilotAi: includeCopilotAi } : null,
|
|
190
|
+
}), "프로젝트 분석 결과");
|
|
167
191
|
}
|
|
168
192
|
const choice = await io.confirmProjectMenu();
|
|
169
193
|
if (choice === "cancel") { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
|
|
@@ -172,12 +196,12 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
172
196
|
// edit 루프
|
|
173
197
|
let editing = true;
|
|
174
198
|
while (editing) {
|
|
175
|
-
const what = await io.editMenu({ showFlutter: showOptional && types.includes("flutter") });
|
|
199
|
+
const what = await io.editMenu({ showFlutter: showOptional && types.includes("flutter"), showOptions: showOptionToggles });
|
|
176
200
|
if (isCancel(what) || what === "done") { editing = false; break; }
|
|
177
201
|
if (what === "type") {
|
|
178
202
|
const t = await io.selectTypes(types);
|
|
179
203
|
if (!isCancel(t) && Array.isArray(t) && t.length) {
|
|
180
|
-
// 타입 집합이 실제로 바뀌면 경로 재해석 대상으로 초기화 (
|
|
204
|
+
// 타입 집합이 실제로 바뀌면 경로 재해석 대상으로 초기화 (정렬 집합 비교)
|
|
181
205
|
const oldSorted = [...types].sort().join(",");
|
|
182
206
|
types = t.filter((x) => VALID_TYPES.includes(x));
|
|
183
207
|
if ([...types].sort().join(",") !== oldSorted) paths = new Map();
|
|
@@ -185,13 +209,19 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
185
209
|
} else if (what === "version") {
|
|
186
210
|
const v = await io.askText("새 버전 (예: 1.0.0)", version);
|
|
187
211
|
if (!isCancel(v) && v !== version) {
|
|
188
|
-
// semver 형식 검증
|
|
189
|
-
if (/^\d+\.\d+\.\d+$/.test(v)) version = v;
|
|
212
|
+
// semver 형식 검증
|
|
213
|
+
if (/^\d+\.\d+\.\d+$/.test(v)) { version = v; versionAutoDetected = false; }
|
|
190
214
|
else io.note?.("버전 형식이 올바르지 않습니다 (x.y.z 형태) — 기존 값을 유지합니다.", "⚠ 버전");
|
|
191
215
|
}
|
|
192
216
|
} else if (what === "branch") {
|
|
193
|
-
|
|
194
|
-
|
|
217
|
+
branch = await askBranchName(io, "기본 브랜치", branch, isCancel);
|
|
218
|
+
} else if (what === "semverAuto") {
|
|
219
|
+
// 저장값이 있으면 처음 질문을 건너뛰므로, 한 번 정한 값을 바꿀 수 있는 곳은 여기뿐이다.
|
|
220
|
+
const y = await io.askYesNo(SEMVER_AUTO_QUESTION, includeSemverAuto);
|
|
221
|
+
if (typeof y === "boolean") includeSemverAuto = y;
|
|
222
|
+
} else if (what === "copilotAi") {
|
|
223
|
+
const y = await io.askYesNo(COPILOT_AI_QUESTION, includeCopilotAi);
|
|
224
|
+
if (typeof y === "boolean") includeCopilotAi = y;
|
|
195
225
|
} else if (FLUTTER_EDIT_ITEMS.has(what)) {
|
|
196
226
|
flutter = await editFlutterOption(io, what, flutter, flutterAsk.envModeDefault);
|
|
197
227
|
}
|
|
@@ -199,7 +229,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
199
229
|
}
|
|
200
230
|
|
|
201
231
|
// 편집 루프에서 뒤늦게 flutter 타입이 추가된 경우에도 옵션을 확정한다 — 이미 정해진 값은 다시 묻지 않는다.
|
|
202
|
-
|
|
232
|
+
// 서버 배포 타입이 뒤늦게 추가된 경우도 같다.
|
|
233
|
+
if (showOptional) {
|
|
234
|
+
await askDeployStyle();
|
|
235
|
+
await askFlutterOptions();
|
|
236
|
+
}
|
|
203
237
|
// 질문이 나오지 않은 경우(비 full 모드 등)도 동작 보존 기본값으로 채워 워크플로우 치환이 어긋나지 않게 한다.
|
|
204
238
|
const flutterOptions = {
|
|
205
239
|
envMode: flutter.envMode || flutterAsk.envModeDefault,
|
|
@@ -208,14 +242,17 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
208
242
|
iosDeployMode: flutter.iosDeployMode || DEFAULT_DEPLOY_MODE,
|
|
209
243
|
};
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// 신규 질문 ① — 브랜치 설정 (DESIGN-SPEC §4). full/workflows만 질문, version은 기본값 기록.
|
|
245
|
+
// 브랜치 설정. full/workflows만 질문, version은 기본값 기록.
|
|
214
246
|
// 저장값(version.yml metadata.template.branches)이 있으면 재질문 없이 재사용 (업데이트 모드).
|
|
215
|
-
|
|
216
|
-
|
|
247
|
+
// 이전 버전이 저장한 감지 실패 값("(unknown)" 등)이 있으면 저장값이 없는 것으로 보고 다시 묻는다.
|
|
248
|
+
const savedBranches = existing?.branches
|
|
249
|
+
&& isValidBranchName(existing.branches.main) && isValidBranchName(existing.branches.develop)
|
|
250
|
+
? existing.branches : null;
|
|
251
|
+
let branches = savedBranches
|
|
252
|
+
? resolveBranchConfig({ mainBranch: savedBranches.main, developBranch: savedBranches.develop, defaultBranch: branch })
|
|
217
253
|
: resolveBranchConfig({ defaultBranch: branch });
|
|
218
|
-
|
|
254
|
+
let developMissing = false;
|
|
255
|
+
if (showOptional && !savedBranches) {
|
|
219
256
|
const remoteBranches = await detectRemoteBranches(cwd);
|
|
220
257
|
// 두 질문에 같은 이름을 입력해야만 trunk-based가 되는 암묵적 규칙 대신,
|
|
221
258
|
// 전략을 먼저 명시적으로 고르게 한다. 취소/그 외 값은 기존 기본 동작과 같은 pr-flow로 폴백
|
|
@@ -230,16 +267,21 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
230
267
|
branches = resolveBranchConfig({ mainBranch: mainB, developBranch: devB, defaultBranch: branch });
|
|
231
268
|
if (branches.mode === "trunk-based") {
|
|
232
269
|
io.note?.(`릴리스 브랜치(${branches.main}) 하나만 사용하는 trunk-based 모드로 설치합니다 (RELEASE-PUBLISH 단독).`, "브랜치 모드");
|
|
233
|
-
} else if (remoteBranches.length
|
|
234
|
-
|
|
270
|
+
} else if (!remoteBranches.length) {
|
|
271
|
+
// 원격이 없거나 비어 있으면 push할 기준이 없다 — 만들지 않았다는 사실과 방법을 알린다.
|
|
272
|
+
developMissing = true;
|
|
273
|
+
io.note?.(developMissingNotice(branches), "브랜치");
|
|
274
|
+
} else if (!remoteBranches.includes(branches.develop)) {
|
|
275
|
+
const r = await ensureDevelopBranch({
|
|
235
276
|
develop: branches.develop, remoteBranches, cwd,
|
|
236
277
|
confirm: (msg) => io.askYesNo(msg, true),
|
|
237
278
|
log: (m) => io.note?.(m, "브랜치"),
|
|
238
279
|
});
|
|
280
|
+
developMissing = r.created !== true || r.pushed === false;
|
|
239
281
|
}
|
|
240
282
|
}
|
|
241
283
|
|
|
242
|
-
// 경로 확정 (
|
|
284
|
+
// 경로 확정 (저장값·후보 스캔·질문)
|
|
243
285
|
if (mode === "full") {
|
|
244
286
|
paths = await resolveProjectPaths({
|
|
245
287
|
root: cwd, types, paths, existingPaths: existing?.paths ?? new Map(),
|
|
@@ -249,7 +291,13 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
249
291
|
for (const t of types) if (t !== "basic" && !paths.has(t)) paths.set(t, existing?.paths.get(t) || ".");
|
|
250
292
|
}
|
|
251
293
|
|
|
252
|
-
//
|
|
294
|
+
// 모노레포는 버전·빌드 번호 파일이 타입 폴더 안에 있다 — 경로가 확정된 지금 그 폴더에서 감지한다.
|
|
295
|
+
if (versionAutoDetected && [...paths.values()].some((p) => p && p !== ".")) {
|
|
296
|
+
version = detectVersion(cwd, { types, paths, warn: () => {} });
|
|
297
|
+
}
|
|
298
|
+
const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types, paths }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지
|
|
299
|
+
|
|
300
|
+
// @wizard env 계획 질문 (full/workflows만)
|
|
253
301
|
const resolvers = makeResolvers(cwd, repoName, paths, flutterOptions);
|
|
254
302
|
let envValues = new Map(), envUseDefaults = true, envAnswers = [];
|
|
255
303
|
if (showOptional) {
|
|
@@ -271,7 +319,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
271
319
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
272
320
|
// 설치 로그·완료 요약이 쓰는 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
273
321
|
markers, envAnswers, detectWarnings,
|
|
274
|
-
|
|
322
|
+
// 저장값이 무중단이어도 선택한 타입에 그 방식이 없으면 단일 서버 배포가 설치된다 — 설치된 방식을 기록한다.
|
|
323
|
+
deployStyle: hasServerDeploy() ? effectiveDeployStyle(payload, types, deployStyle || DEFAULT_DEPLOY_STYLE) : null,
|
|
275
324
|
envMode: flutterOptions.envMode, flutterStore: flutterOptions.stores,
|
|
276
325
|
androidDeployMode: flutterOptions.androidDeployMode, iosDeployMode: flutterOptions.iosDeployMode,
|
|
277
326
|
previousTemplateVersion: existing?.templateVersion || "",
|
|
@@ -301,7 +350,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
301
350
|
}
|
|
302
351
|
}
|
|
303
352
|
|
|
304
|
-
// (a) 진짜 충돌 3지선 — 타입당 1회 결정을 파일에 캐시 적용
|
|
353
|
+
// (a) 진짜 충돌 3지선 — 타입당 1회 결정을 파일에 캐시 적용
|
|
305
354
|
const decisions = new Map();
|
|
306
355
|
if (conflicts.length) {
|
|
307
356
|
const perType = new Map();
|
|
@@ -315,7 +364,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
315
364
|
{ value: "template", label: "기존 유지 + 새 버전을 .template.yaml로 참고 추가" },
|
|
316
365
|
],
|
|
317
366
|
});
|
|
318
|
-
perType.set(type, isCancel(sel) || sel == null ? "skip" : sel); // ESC = 건너뛰기
|
|
367
|
+
perType.set(type, isCancel(sel) || sel == null ? "skip" : sel); // ESC = 건너뛰기
|
|
319
368
|
}
|
|
320
369
|
decisions.set(filename, perType.get(type));
|
|
321
370
|
}
|
|
@@ -325,20 +374,26 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
325
374
|
|
|
326
375
|
const result = runFull(ctx, payload, cwd, hooks);
|
|
327
376
|
|
|
328
|
-
// 완료 요약
|
|
377
|
+
// 완료 요약
|
|
329
378
|
io.summary?.({
|
|
330
|
-
mode, types, version, versionCode, branches,
|
|
379
|
+
mode, types, version, versionCode, branches, developMissing,
|
|
331
380
|
copiedFiles: result?.workflows?.copiedFiles ?? [],
|
|
381
|
+
autoUpdated: result?.workflows?.autoUpdated ?? [],
|
|
332
382
|
gitignoreUpdated: result?.gitignoreUpdated === true,
|
|
333
383
|
answers: envAnswers,
|
|
334
384
|
unresolved: result?.unresolved ?? [],
|
|
335
385
|
secrets: result?.secrets ?? new Map(),
|
|
386
|
+
optionalSecrets: result?.optionalSecrets ?? new Map(),
|
|
336
387
|
logPath: currentLogPath(),
|
|
337
388
|
legacyMdLogs: hasLegacyMdLogs(cwd),
|
|
338
389
|
cleanup: result?.cleanup ?? null,
|
|
339
390
|
storeCleanup: result?.storeCleanup ?? null,
|
|
340
391
|
flutterApp: result?.flutterApp ?? null,
|
|
392
|
+
readme: result?.readme ?? null,
|
|
393
|
+
scripts: result?.scripts ?? null,
|
|
341
394
|
});
|
|
395
|
+
const notices = postInstallNotices(result, { interactive: true });
|
|
396
|
+
if (notices.length) io.note?.(notices.join("\n"), "이전 워크플로우 정리");
|
|
342
397
|
io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
|
|
343
398
|
return 0;
|
|
344
399
|
}
|
|
@@ -354,17 +409,26 @@ export async function pickBranch(io, message, def, remoteBranches, isCancel) {
|
|
|
354
409
|
options.push({ value: "__custom__", label: "직접 입력..." });
|
|
355
410
|
const initialIndex = Math.max(0, options.findIndex((o) => o.value === def));
|
|
356
411
|
const sel = await io.engineIo.select({ message, options, initialIndex });
|
|
357
|
-
if (sel === "__custom__")
|
|
358
|
-
const v = await io.askText("브랜치 이름", def);
|
|
359
|
-
return isCancel(v) || !v ? def : v;
|
|
360
|
-
}
|
|
412
|
+
if (sel === "__custom__") return askBranchName(io, "브랜치 이름", def, isCancel);
|
|
361
413
|
return isCancel(sel) || sel == null ? def : sel;
|
|
362
414
|
}
|
|
363
|
-
|
|
364
|
-
return isCancel(v) || !v ? def : v;
|
|
415
|
+
return askBranchName(io, message, def, isCancel);
|
|
365
416
|
}
|
|
366
417
|
|
|
367
|
-
|
|
418
|
+
// 브랜치 이름 텍스트 입력 — 앞뒤 공백을 떼고, 비었거나 ESC면 기본값, 쓸 수 없는 이름이면 다시 묻는다.
|
|
419
|
+
// 입력값이 워크플로우 트리거에 그대로 들어가므로 공백 포함 이름 등은 워크플로우가 영영 돌지 않는다.
|
|
420
|
+
async function askBranchName(io, message, def, isCancel) {
|
|
421
|
+
for (;;) {
|
|
422
|
+
const v = await io.askText(message, def);
|
|
423
|
+
if (isCancel(v)) return def;
|
|
424
|
+
const name = String(v ?? "").trim();
|
|
425
|
+
if (!name) return def;
|
|
426
|
+
if (isValidBranchName(name)) return name;
|
|
427
|
+
io.note?.(`'${name}'은(는) 브랜치 이름으로 쓸 수 없습니다 (공백·특수문자·'..' 등 불가) — 다시 입력하세요.`, "⚠ 브랜치");
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function summarize({ mode, types, version, branch, showOptional, flutter, envModeDefault, options = null }) {
|
|
368
432
|
const lines = [
|
|
369
433
|
`통합 모드 : ${modeLabel(mode)}`,
|
|
370
434
|
`프로젝트 타입 : ${types.join(", ")}${types.length > 1 ? " (멀티)" : ""}`,
|
|
@@ -380,6 +444,10 @@ function summarize({ mode, types, version, branch, showOptional, flutter, envMod
|
|
|
380
444
|
lines.push(`배포 모드 : ${modeParts.length ? modeParts.join(" ") : "없음"}`);
|
|
381
445
|
}
|
|
382
446
|
}
|
|
447
|
+
if (options) {
|
|
448
|
+
lines.push(`자동 버전 승격 : ${options.semverAuto ? "켜짐" : "꺼짐"}`);
|
|
449
|
+
lines.push(`Copilot AI 요약 : ${options.copilotAi ? "켜짐" : "꺼짐"}`);
|
|
450
|
+
}
|
|
383
451
|
return lines.join("\n");
|
|
384
452
|
}
|
|
385
453
|
|
package/src/commands/purge.js
CHANGED
|
@@ -1,29 +1,60 @@
|
|
|
1
1
|
// purge 모드 — planRemoval이 판별하는 전부(워크플로우·스크립트) + version.yml·README
|
|
2
|
-
// AUTO-VERSION-SECTION
|
|
3
|
-
//
|
|
2
|
+
// AUTO-VERSION-SECTION 블록·.gitignore 자동 추가 항목·릴리스 워크플로우가 만든 CHANGELOG를
|
|
3
|
+
// 추가로 제거해 설치 이전 상태로 완전히 되돌린다. 마법사가 만들지 않은 파일은 지우지 않는다.
|
|
4
|
+
// 개발·테스트 전용 숨김 모드.
|
|
4
5
|
// develop 브랜치 삭제는 파일 삭제와 성격이 달라(실행 시점 git 상태 판단 필요) 여기 plan에는
|
|
5
6
|
// 포함하지 않고 index.js의 purge 분기에서 직접 처리한다.
|
|
6
7
|
import { join } from "node:path";
|
|
7
|
-
import { existsSync } from "node:fs";
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
9
|
import { PATHS } from "../core/paths.js";
|
|
9
10
|
import { remove } from "../core/fsutil.js";
|
|
10
|
-
import { planRemoval } from "../core/removal-plan.js";
|
|
11
|
+
import { planRemoval, backupArtifacts } from "../core/removal-plan.js";
|
|
12
|
+
import { removeAppFiles, pruneInstallDirs } from "./uninstall.js";
|
|
11
13
|
import { removeVersionSectionFromReadme, hasVersionSection } from "../core/copy/readme.js";
|
|
12
|
-
import {
|
|
14
|
+
import { removeAutoAddedEntriesFromGitignore, hasAutoAddedEntries } from "../core/copy/gitignore.js";
|
|
15
|
+
import { logRemovals } from "../core/logger.js";
|
|
13
16
|
|
|
14
17
|
const CHANGELOG_FILES = ["CHANGELOG.json", "CHANGELOG.md"];
|
|
15
18
|
|
|
19
|
+
// changelog_manager.py가 만든 CHANGELOG인지 — 사용자가 원래 쓰던 CHANGELOG.md를 지우지 않기 위해 내용으로 가린다.
|
|
20
|
+
// CHANGELOG.json은 metadata·releases 구조로, CHANGELOG.md는 generate-md가 쓰는 머리말로 판단한다.
|
|
21
|
+
// JSON이 우리 것이면 MD는 릴리스마다 JSON에서 통째로 재생성되므로 함께 우리 것이다.
|
|
22
|
+
function readText(targetRoot, f) {
|
|
23
|
+
try { return readFileSync(join(targetRoot, f), "utf8").replace(/\r\n/g, "\n"); } catch { return null; }
|
|
24
|
+
}
|
|
25
|
+
function isGeneratedChangelogJson(targetRoot) {
|
|
26
|
+
const text = readText(targetRoot, "CHANGELOG.json");
|
|
27
|
+
if (text === null) return false;
|
|
28
|
+
try {
|
|
29
|
+
const data = JSON.parse(text);
|
|
30
|
+
return !!data && typeof data.metadata === "object" && Array.isArray(data.releases);
|
|
31
|
+
} catch { return false; }
|
|
32
|
+
}
|
|
33
|
+
function generatedChangelogFiles(targetRoot) {
|
|
34
|
+
const jsonOurs = isGeneratedChangelogJson(targetRoot);
|
|
35
|
+
const md = readText(targetRoot, "CHANGELOG.md");
|
|
36
|
+
const mdOurs = md !== null && (jsonOurs || md.startsWith("# Changelog\n\n**현재 버전:**"));
|
|
37
|
+
return CHANGELOG_FILES.filter((f) => (f === "CHANGELOG.json" ? jsonOurs : mdOurs));
|
|
38
|
+
}
|
|
39
|
+
|
|
16
40
|
// keepFlags: { versionYml, readme, changelog, workflows, scripts } — true인 카테고리는 후보에서 제외.
|
|
17
|
-
// 반환: { workflows, scripts, versionYml, readmeSection, changelog } — 아무것도 지우지 않는 순수 함수.
|
|
41
|
+
// 반환: { workflows, scripts, versionYml, readmeSection, gitignore, changelog } — 아무것도 지우지 않는 순수 함수.
|
|
18
42
|
export function planPurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
19
43
|
const removalPlan = planRemoval(payloadRoot, targetRoot);
|
|
44
|
+
// 워크플로우를 남기면 그 .bak/.template.yaml도 남는다 — .gitignore 항목까지 지우면 백업 파일이 git 상태에 드러난다.
|
|
45
|
+
const gitignoreKept = hasAutoAddedEntries(targetRoot) && keepFlags.workflows === true
|
|
46
|
+
&& backupArtifacts(removalPlan.workflows).length > 0;
|
|
20
47
|
return {
|
|
21
48
|
workflows: keepFlags.workflows ? [] : removalPlan.workflows,
|
|
49
|
+
// Flutter 앱 파일의 생성 기록은 baseline에 있으므로 baseline과 같은 플래그를 따른다.
|
|
50
|
+
appFiles: keepFlags.workflows ? [] : removalPlan.appFiles,
|
|
22
51
|
baseline: keepFlags.workflows ? [] : removalPlan.baseline,
|
|
23
52
|
scripts: keepFlags.scripts ? [] : removalPlan.scripts,
|
|
24
53
|
versionYml: !keepFlags.versionYml && existsSync(join(targetRoot, PATHS.versionFile)),
|
|
25
54
|
readmeSection: !keepFlags.readme && hasVersionSection(targetRoot),
|
|
26
|
-
|
|
55
|
+
gitignore: hasAutoAddedEntries(targetRoot) && !gitignoreKept,
|
|
56
|
+
gitignoreKept,
|
|
57
|
+
changelog: keepFlags.changelog ? [] : generatedChangelogFiles(targetRoot),
|
|
27
58
|
};
|
|
28
59
|
}
|
|
29
60
|
|
|
@@ -38,8 +69,11 @@ export function printPurgePlan(plan, { dryRun = false } = {}) {
|
|
|
38
69
|
for (const f of plan.workflows) lines.push(` - ${f}`);
|
|
39
70
|
lines.push(`스크립트 (${plan.scripts.length}개):`);
|
|
40
71
|
for (const f of plan.scripts) lines.push(` - ${f}`);
|
|
72
|
+
for (const f of plan.appFiles || []) lines.push(`Flutter 앱 파일: ${f}`);
|
|
41
73
|
if (plan.versionYml) lines.push("파일: version.yml");
|
|
42
74
|
if (plan.readmeSection) lines.push("README.md: AUTO-VERSION-SECTION 블록");
|
|
75
|
+
if (plan.gitignore) lines.push(".gitignore: 자동 추가 항목(*.bak, *.template.yaml)");
|
|
76
|
+
if (plan.gitignoreKept) lines.push(".gitignore: 자동 추가 항목 유지 (남겨 둔 워크플로우의 백업 파일을 가리는 항목)");
|
|
43
77
|
for (const f of plan.changelog) lines.push(`파일: ${f}`);
|
|
44
78
|
lines.push("");
|
|
45
79
|
console.log(lines.join("\n"));
|
|
@@ -49,33 +83,41 @@ export function printPurgePlan(plan, { dryRun = false } = {}) {
|
|
|
49
83
|
// planRemoval 결과를 그대로 지우지 않는 이유: 그 목록은 항상 전체라서
|
|
50
84
|
// --keep-* 로 선택적 카테고리만 보존하는 요구사항과 맞지 않는다.
|
|
51
85
|
// readmeSection은 plan의 판정을 그대로 되돌려주지 않고
|
|
52
|
-
// removeVersionSectionFromReadme()의 실제 반환값("removed"인지)을 반영한다 —
|
|
53
|
-
// "반환값은 실제 삭제 결과를 반영"하라고 명시하기 때문에, plan과 실제 제거 조건이
|
|
86
|
+
// removeVersionSectionFromReadme()의 실제 반환값("removed"인지)을 반영한다 — plan과 실제 제거 조건이
|
|
54
87
|
// 이론상 어긋나는 경우에도 printPurgeResult가 거짓으로 "제거됨"을 보고하지 않는다.
|
|
55
88
|
export function executePurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
56
89
|
const plan = planPurge(payloadRoot, targetRoot, keepFlags);
|
|
57
90
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
58
|
-
|
|
59
|
-
for (const name of plan.
|
|
60
|
-
for (const
|
|
61
|
-
|
|
91
|
+
const done = [];
|
|
92
|
+
for (const name of plan.workflows) { remove(join(wfDir, name)); done.push(["remove", "workflow", name]); }
|
|
93
|
+
for (const name of plan.scripts) { remove(join(targetRoot, PATHS.scriptsDir, name)); done.push(["remove", "script", name]); }
|
|
94
|
+
removeAppFiles(targetRoot, plan.appFiles, done);
|
|
95
|
+
for (const p of plan.baseline || []) { remove(join(targetRoot, p)); done.push(["remove", "metadata", p]); }
|
|
96
|
+
pruneInstallDirs(targetRoot, plan);
|
|
97
|
+
if (plan.versionYml) { remove(join(targetRoot, PATHS.versionFile)); done.push(["remove", "version", PATHS.versionFile]); }
|
|
62
98
|
const readmeSection = plan.readmeSection && removeVersionSectionFromReadme(targetRoot) === "removed";
|
|
63
|
-
|
|
64
|
-
|
|
99
|
+
const gitignoreStatus = plan.gitignore ? removeAutoAddedEntriesFromGitignore(targetRoot) : null;
|
|
100
|
+
const gitignore = gitignoreStatus === "removed" || gitignoreStatus === "file-deleted";
|
|
101
|
+
if (gitignore) done.push(["remove", "gitignore", ".gitignore 자동 추가 항목"]);
|
|
102
|
+
for (const f of plan.changelog) { remove(join(targetRoot, f)); done.push(["remove", "changelog", f]); }
|
|
103
|
+
if (readmeSection) done.push(["remove", "readme", "README.md 버전 섹션"]);
|
|
104
|
+
logRemovals(targetRoot, done);
|
|
105
|
+
return { ...plan, readmeSection, gitignore };
|
|
65
106
|
}
|
|
66
107
|
|
|
67
108
|
// 삭제 후 실제 제거된 목록 출력 — printPurgePlan과 완전히 동일한 형태(파일명 나열)로
|
|
68
|
-
// 맞춘다 (개수만 출력하면
|
|
109
|
+
// 맞춘다 (개수만 출력하면 무엇이 지워졌는지 확인할 수 없다).
|
|
69
110
|
export function printPurgeResult(result) {
|
|
70
111
|
const lines = ["", "제거됨:", ""];
|
|
71
112
|
lines.push(`워크플로우 (${result.workflows.length}개):`);
|
|
72
113
|
for (const f of result.workflows) lines.push(` - ${f}`);
|
|
73
114
|
lines.push(`스크립트 (${result.scripts.length}개):`);
|
|
74
115
|
for (const f of result.scripts) lines.push(` - ${f}`);
|
|
116
|
+
for (const f of result.appFiles || []) lines.push(`Flutter 앱 파일: ${f}`);
|
|
75
117
|
if (result.versionYml) lines.push("파일: version.yml");
|
|
76
118
|
if (result.readmeSection) lines.push("README.md: AUTO-VERSION-SECTION 블록");
|
|
119
|
+
if (result.gitignore) lines.push(".gitignore: 자동 추가 항목(*.bak, *.template.yaml)");
|
|
77
120
|
for (const f of result.changelog) lines.push(`파일: ${f}`);
|
|
78
|
-
lines.push("(.gitignore에 추가된 백업 파일 제외 항목(*.bak/*.template.yaml)은 purge 대상에서 제외되어 그대로 보존됩니다)");
|
|
79
121
|
lines.push("");
|
|
80
122
|
console.log(lines.join("\n"));
|
|
81
123
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -6,6 +6,9 @@ import { planWorkflows } from "../core/copy/workflows.js";
|
|
|
6
6
|
import { makeResolvers, detectRepoName, detectDefaultBranch } from "../core/detect-fs.js";
|
|
7
7
|
import { PATHS } from "../core/paths.js";
|
|
8
8
|
import { resolveFlutterOptions } from "../core/flutter-options.js";
|
|
9
|
+
import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
10
|
+
import { findStaleWorkflows } from "../core/removal-plan.js";
|
|
11
|
+
import { readBaseline } from "../core/baseline.js";
|
|
9
12
|
|
|
10
13
|
// payloadRoot: 패키지 payload/ 루트. targetRoot: 상태를 확인할 대상 레포.
|
|
11
14
|
export function runStatus(payloadRoot, targetRoot = ".") {
|
|
@@ -28,8 +31,13 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
28
31
|
types: existing.types, paths: existing.paths,
|
|
29
32
|
repoName, resolvers, branches: branchesForCompare,
|
|
30
33
|
flutterStore: flutterOptions.stores,
|
|
34
|
+
// 설치 때 고른 배포 방식으로 비교해야 nginx·traefik CD 수정도 드리프트로 잡힌다.
|
|
35
|
+
// 빠지면 기본값(simple)으로 걸러 설치된 무중단 CD가 비교 대상에서 사라진다.
|
|
36
|
+
deployStyle: isDeployStyle(existing.options.deployStyle) ? existing.options.deployStyle : DEFAULT_DEPLOY_STYLE,
|
|
31
37
|
};
|
|
32
38
|
const plan = planWorkflows(context, payloadRoot, targetRoot);
|
|
39
|
+
// 현재 버전에 없는 옛 워크플로우 — 다음 업데이트에서 정리된다. 그 전까지는 옛 트리거로 계속 돈다.
|
|
40
|
+
const stale = findStaleWorkflows(payloadRoot, targetRoot, readBaseline(targetRoot));
|
|
33
41
|
|
|
34
42
|
return {
|
|
35
43
|
installed: true,
|
|
@@ -49,6 +57,7 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
49
57
|
conflicts: plan.changed.map((f) => f.filename), // 양쪽 변경 — 검토 필요
|
|
50
58
|
removed: plan.removed.map((f) => f.filename), // 내가 지웠고 복원하지 않음
|
|
51
59
|
},
|
|
60
|
+
staleFiles: stale,
|
|
52
61
|
};
|
|
53
62
|
}
|
|
54
63
|
|
|
@@ -69,7 +78,8 @@ export function printStatus(status) {
|
|
|
69
78
|
const semverAutoLabel = status.options.semverAuto === null ? "미설정(기본 false)" : status.options.semverAuto;
|
|
70
79
|
const copilotAiLabel = boolLabel(status.options.copilotAi ?? null);
|
|
71
80
|
const flutterLabels = status.types.includes("flutter") ? flutterOptionLabels(status.options) : "";
|
|
72
|
-
|
|
81
|
+
const deployLabel = status.options.deployStyle ? ` deploy_style=${status.options.deployStyle}` : "";
|
|
82
|
+
lines.push(`옵션 : semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${deployLabel}${flutterLabels}`);
|
|
73
83
|
if (status.modifiedFiles.length) {
|
|
74
84
|
lines.push("", `사용자가 수정한 워크플로우 파일 (${status.modifiedFiles.length}개):`);
|
|
75
85
|
for (const f of status.modifiedFiles) lines.push(` - ${f}`);
|
|
@@ -77,6 +87,11 @@ export function printStatus(status) {
|
|
|
77
87
|
lines.push("", "모든 워크플로우 파일이 설치 시점 기본값과 동일합니다 (수정 없음).");
|
|
78
88
|
}
|
|
79
89
|
|
|
90
|
+
if (status.staleFiles?.length) {
|
|
91
|
+
lines.push("", `현재 버전에 없는 이전 워크플로우 (${status.staleFiles.length}개 — 다음 업데이트에서 정리됨):`);
|
|
92
|
+
for (const f of status.staleFiles) lines.push(` - ${f}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
// 업데이트하면 무슨 일이 일어나는지. baseline이 없는 설치는 전부 0이라 출력하지 않는다.
|
|
81
96
|
const b = status.buckets || { autoUpdatable: [], localKept: [], conflicts: [], removed: [] };
|
|
82
97
|
if (b.autoUpdatable.length || b.localKept.length || b.conflicts.length || b.removed.length) {
|