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