project-auto-wizard 0.12.0 → 0.12.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.md +3 -7
- package/package.json +1 -1
- package/payload/config/breaking-changes.json +1 -1
- 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
- package/payload/scripts/changelog_manager.py +1 -1
- package/payload/scripts/version_manager.py +2 -2
- package/payload/version.yml.template +0 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +0 -3
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +1 -4
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +3 -6
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +0 -12
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +2 -5
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +3 -7
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +7 -34
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +2 -25
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +2 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +5 -30
- package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +0 -4
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +7 -51
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +5 -29
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +9 -29
- package/payload/workflows/go/PROJECT-GO-CI.yaml +5 -28
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +19 -99
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +18 -89
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +11 -41
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +9 -45
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +1 -27
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +14 -97
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +14 -92
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +11 -40
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +9 -43
- package/payload/workflows/spring/{nexus/PROJECT-SPRING-NEXUS-CI.yml → PROJECT-SPRING-CI.yml} +2 -25
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +19 -63
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +19 -59
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +23 -86
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +20 -80
- package/src/cli/args.js +1 -15
- package/src/cli/help.js +0 -2
- package/src/commands/doctor.js +5 -5
- package/src/commands/dry-run.js +1 -1
- package/src/commands/full.js +9 -10
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +25 -46
- package/src/commands/purge.js +2 -2
- package/src/commands/status.js +4 -6
- package/src/commands/uninstall.js +3 -4
- package/src/context.js +4 -6
- package/src/core/assets.js +2 -2
- package/src/core/baseline.js +1 -1
- package/src/core/branches.js +3 -3
- package/src/core/breaking-check.js +1 -1
- package/src/core/breaking.js +1 -1
- package/src/core/copy/flutter-app.js +2 -2
- package/src/core/copy/gitignore.js +8 -8
- package/src/core/copy/workflows.js +24 -69
- package/src/core/deploy-style.js +2 -2
- package/src/core/detect-fs.js +11 -11
- package/src/core/detect.js +7 -7
- package/src/core/flutter-options.js +1 -1
- package/src/core/installed-stores.js +1 -1
- package/src/core/logger.js +1 -1
- package/src/core/paths-resolve.js +1 -1
- package/src/core/removal-plan.js +4 -4
- package/src/core/verify.js +3 -3
- package/src/core/version-yml.js +17 -33
- package/src/core/wizard-env.js +4 -4
- package/src/index.js +12 -14
- package/src/ui/banner.js +2 -2
- package/src/ui/env-plan.js +10 -16
- package/src/ui/prompts.js +9 -14
- package/src/ui/status-cards.js +10 -12
- package/src/ui/summary.js +9 -9
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +0 -210
- package/payload/workflows/spring/nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +0 -92
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +0 -70
- package/src/core/options-ask.js +0 -102
|
@@ -10,7 +10,6 @@ import { parseExisting } from "../core/version-yml.js";
|
|
|
10
10
|
import { runBreakingCheck } from "../core/breaking-check.js";
|
|
11
11
|
import { resolveProjectPaths } from "../core/paths-resolve.js";
|
|
12
12
|
import { resolveBranchConfig, detectRemoteBranches, ensureDevelopBranch, sortBranchesForSelection } from "../core/branches.js";
|
|
13
|
-
import { askAllOptionalWorkflows } from "../core/options-ask.js";
|
|
14
13
|
import { promptEnvPlan } from "../ui/env-plan.js";
|
|
15
14
|
import { surveyWorkflows } from "../core/copy/workflows.js";
|
|
16
15
|
import { createContext, VALID_TYPES } from "../context.js";
|
|
@@ -34,7 +33,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
34
33
|
const payload = assertPayload(payloadRoot ?? resolvePayloadRoot());
|
|
35
34
|
const templateVersion = readTemplateVersion();
|
|
36
35
|
|
|
37
|
-
//
|
|
36
|
+
// 시작 배너. 스텁엔 banner 없음 → intro 폴백.
|
|
38
37
|
if (io.banner) io.banner({ version: templateVersion, modeLabel: "대화형 통합 마법사" });
|
|
39
38
|
else io.intro?.("project-auto-wizard — 대화형 통합 마법사");
|
|
40
39
|
|
|
@@ -42,12 +41,12 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
42
41
|
const vyPath = join(cwd, "version.yml");
|
|
43
42
|
const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
|
|
44
43
|
|
|
45
|
-
//
|
|
44
|
+
// 신규/업데이트 판별
|
|
46
45
|
io.installKind?.({ currentTemplateVersion: existing?.templateVersion || "", templateVersion });
|
|
47
46
|
|
|
48
47
|
// 1) 모드 선택
|
|
49
48
|
// status/doctor — 읽기 전용이라 감지·breaking 게이트가 필요 없다. 결과를 보여준 뒤 메뉴로
|
|
50
|
-
//
|
|
49
|
+
// 돌아온다: 진단의 목적이 설치 준비이므로 확인 → 설치를 한 세션에서 끝내야 한다.
|
|
51
50
|
// CLI 경로(index.js의 --mode status/doctor)는 단발 명령이므로 지금처럼 즉시 종료한다.
|
|
52
51
|
let mode;
|
|
53
52
|
for (let round = 0; ; round++) {
|
|
@@ -75,7 +74,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
75
74
|
|
|
76
75
|
// full/version/workflows — 감지 (version은 기존 version.yml 최우선)
|
|
77
76
|
// 감지 경고는 즉시 찍지 않고 모았다가 감지 박스 안에서 출력한다 — 종전에는 경고가 박스보다
|
|
78
|
-
// 먼저 나와 앞선 질문에 대한 경고처럼
|
|
77
|
+
// 먼저 나와 앞선 질문에 대한 경고처럼 보였다. 안내 문구도 대화형용으로 바꾼다.
|
|
79
78
|
const detectWarnings = [];
|
|
80
79
|
let types = detectTypes(cwd);
|
|
81
80
|
let version = (existing?.version) || detectVersion(cwd, {
|
|
@@ -85,16 +84,14 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
85
84
|
let branch = detectDefaultBranch(cwd);
|
|
86
85
|
const repoName = detectRepoName(cwd);
|
|
87
86
|
// 선택 워크플로우 초기값: version.yml 저장 옵션 (.sh read_template_options L2361 등가)
|
|
88
|
-
let includeNexus = existing?.options?.nexus ?? false;
|
|
89
|
-
let includeSecretBackup = existing?.options?.secretBackup ?? false;
|
|
90
87
|
let includeSemverAuto = existing?.options?.semverAuto ?? null;
|
|
91
88
|
let includeCopilotAi = existing?.options?.copilotAi ?? null;
|
|
92
|
-
// 서버 배포 방식 — 저장값(version.yml)이 있으면 재질문하지 않는다 (
|
|
89
|
+
// 서버 배포 방식 — 저장값(version.yml)이 있으면 재질문하지 않는다 (semver_auto와 같은 규약).
|
|
93
90
|
let deployStyle = isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
|
|
94
91
|
const showOptional = mode === "full";
|
|
95
92
|
const realTty = process.stdout.isTTY === true;
|
|
96
93
|
|
|
97
|
-
// Flutter 옵션
|
|
94
|
+
// Flutter 옵션 — 저장값이 있으면 재질문하지 않는다 (deploy_style과 같은 규약).
|
|
98
95
|
// 저장값 없는 기존 설치는 동작 보존을 위해 dotenv를 초기 선택으로, 스토어는 설치된 워크플로우로 추론한다.
|
|
99
96
|
// 환경변수 기본값 규칙(신규=dart-define, 기존 설치·저장값 없음=dotenv)은 resolveFlutterOptions가 단일 진실이다.
|
|
100
97
|
let flutter = savedFlutterState(existing);
|
|
@@ -109,11 +106,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
109
106
|
if (types.includes("flutter")) flutter = await askUnsetFlutterOptions(io, flutter, flutterAsk);
|
|
110
107
|
};
|
|
111
108
|
|
|
112
|
-
//
|
|
109
|
+
// 감지 로그. markers = 실제로 존재를 확인한 파일.
|
|
113
110
|
let markers = detectMarkers(cwd, types);
|
|
114
111
|
io.detectionLog?.({ types, version, branch, markers, warnings: detectWarnings });
|
|
115
112
|
|
|
116
|
-
// 타입 확정
|
|
113
|
+
// 타입 확정 — 감지는 추정이므로 다른 질문보다 먼저 확인받는다. 종전에는 확정 UI가
|
|
117
114
|
// '수정하기 > 프로젝트 타입' 두 단계 뒤에 숨어 있어, 타입이 틀린 채로 설치가 끝나는 일이 많았다.
|
|
118
115
|
// 타입이 뒤에 나올 질문(선택 워크플로우·경로·env)의 범위를 정하므로 순서상 여기가 맞다.
|
|
119
116
|
// 저장값이 있는 업데이트 설치와 비대화형에서는 묻지 않는다 — 기존 동작 그대로.
|
|
@@ -128,24 +125,15 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
128
125
|
}
|
|
129
126
|
}
|
|
130
127
|
|
|
131
|
-
//
|
|
128
|
+
// 배포 방식·Flutter 옵션·semver/Copilot 질문 — 선택 항목을 묻는 경우(showOptional)만
|
|
132
129
|
if (showOptional) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
current: { nexus: existing?.options?.nexus ?? null, secretBackup: existing?.options?.secretBackup ?? null },
|
|
136
|
-
force: false, tty: realTty,
|
|
137
|
-
io: { confirm: ({ message, initialValue }) => io.askYesNo(message, initialValue) },
|
|
138
|
-
});
|
|
139
|
-
includeNexus = r.nexus;
|
|
140
|
-
includeSecretBackup = r.secretBackup;
|
|
141
|
-
|
|
142
|
-
// 서버 배포 방식 (이슈 #80). Nexus(라이브러리)를 고르면 server-deploy가 통째로 빠지므로 묻지 않는다.
|
|
143
|
-
if (!includeNexus && !isDeployStyle(deployStyle)) {
|
|
130
|
+
// 서버 배포 방식.
|
|
131
|
+
if (!isDeployStyle(deployStyle)) {
|
|
144
132
|
const picked = await io.selectDeployStyle();
|
|
145
133
|
deployStyle = isDeployStyle(picked) ? picked : DEFAULT_DEPLOY_STYLE; // ESC = 기본값
|
|
146
134
|
}
|
|
147
135
|
|
|
148
|
-
// Flutter 옵션
|
|
136
|
+
// Flutter 옵션 — 환경변수 방식 → 스토어 배포 대상 → 플랫폼별 배포 모드.
|
|
149
137
|
await askFlutterOptions();
|
|
150
138
|
|
|
151
139
|
// 신규 질문 — 자동 semver 승격 (기본 ON). 저장값 있으면 재질문 생략.
|
|
@@ -171,11 +159,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
171
159
|
let paths = new Map();
|
|
172
160
|
let confirmed = false;
|
|
173
161
|
while (!confirmed) {
|
|
174
|
-
//
|
|
162
|
+
// 프로젝트 분석 개요 카드. 스텁엔 없음 → note 폴백.
|
|
175
163
|
if (io.analysisCard) {
|
|
176
|
-
io.analysisCard({ mode, modeLabel: modeLabel(mode), types, version, branch,
|
|
164
|
+
io.analysisCard({ mode, modeLabel: modeLabel(mode), types, version, branch, showOptional, paths, flutter, envModeDefault: flutterAsk.envModeDefault });
|
|
177
165
|
} else {
|
|
178
|
-
io.note?.(summarize({ mode, types, version, branch,
|
|
166
|
+
io.note?.(summarize({ mode, types, version, branch, showOptional, flutter, envModeDefault: flutterAsk.envModeDefault }), "프로젝트 분석 결과");
|
|
179
167
|
}
|
|
180
168
|
const choice = await io.confirmProjectMenu();
|
|
181
169
|
if (choice === "cancel") { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
|
|
@@ -184,7 +172,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
184
172
|
// edit 루프
|
|
185
173
|
let editing = true;
|
|
186
174
|
while (editing) {
|
|
187
|
-
const what = await io.editMenu({
|
|
175
|
+
const what = await io.editMenu({ showFlutter: showOptional && types.includes("flutter") });
|
|
188
176
|
if (isCancel(what) || what === "done") { editing = false; break; }
|
|
189
177
|
if (what === "type") {
|
|
190
178
|
const t = await io.selectTypes(types);
|
|
@@ -204,12 +192,6 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
204
192
|
} else if (what === "branch") {
|
|
205
193
|
const b = await io.askText("기본 브랜치", branch);
|
|
206
194
|
if (!isCancel(b) && b) branch = b;
|
|
207
|
-
} else if (what === "nexus") {
|
|
208
|
-
const y = await io.askYesNo("Nexus publish 워크플로우를 포함할까요?", includeNexus);
|
|
209
|
-
if (!isCancel(y)) includeNexus = y === true;
|
|
210
|
-
} else if (what === "secret") {
|
|
211
|
-
const y = await io.askYesNo("Secret 백업 워크플로우를 포함할까요?", includeSecretBackup);
|
|
212
|
-
if (!isCancel(y)) includeSecretBackup = y === true;
|
|
213
195
|
} else if (FLUTTER_EDIT_ITEMS.has(what)) {
|
|
214
196
|
flutter = await editFlutterOption(io, what, flutter, flutterAsk.envModeDefault);
|
|
215
197
|
}
|
|
@@ -226,7 +208,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
226
208
|
iosDeployMode: flutter.iosDeployMode || DEFAULT_DEPLOY_MODE,
|
|
227
209
|
};
|
|
228
210
|
|
|
229
|
-
const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지
|
|
211
|
+
const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지
|
|
230
212
|
|
|
231
213
|
// 신규 질문 ① — 브랜치 설정 (DESIGN-SPEC §4). full/workflows만 질문, version은 기본값 기록.
|
|
232
214
|
// 저장값(version.yml metadata.template.branches)이 있으면 재질문 없이 재사용 (업데이트 모드).
|
|
@@ -235,7 +217,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
235
217
|
: resolveBranchConfig({ defaultBranch: branch });
|
|
236
218
|
if (showOptional && !existing?.branches) {
|
|
237
219
|
const remoteBranches = await detectRemoteBranches(cwd);
|
|
238
|
-
//
|
|
220
|
+
// 두 질문에 같은 이름을 입력해야만 trunk-based가 되는 암묵적 규칙 대신,
|
|
239
221
|
// 전략을 먼저 명시적으로 고르게 한다. 취소/그 외 값은 기존 기본 동작과 같은 pr-flow로 폴백
|
|
240
222
|
// (selectDeployStyle의 "ESC = 기본값" 패턴과 동일).
|
|
241
223
|
const strategyPick = await io.selectBranchStrategy();
|
|
@@ -273,22 +255,21 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
273
255
|
if (showOptional) {
|
|
274
256
|
const plan = await promptEnvPlan({
|
|
275
257
|
payloadRoot: payload, types, io: io.engineIo ?? null, force: false,
|
|
276
|
-
resolvers,
|
|
277
|
-
flutterStore: flutterOptions.stores, // 선택 해제된 스토어 워크플로우의 ask 질문은 묻지 않는다
|
|
258
|
+
resolvers, deployStyle, targetRoot: cwd, repoName,
|
|
259
|
+
flutterStore: flutterOptions.stores, // 선택 해제된 스토어 워크플로우의 ask 질문은 묻지 않는다
|
|
278
260
|
});
|
|
279
261
|
envValues = plan.values;
|
|
280
262
|
envUseDefaults = plan.useDefaults;
|
|
281
|
-
envAnswers = plan.answers || []; // 완료 요약·설치 로그가 같은 답변 데이터를 쓴다
|
|
263
|
+
envAnswers = plan.answers || []; // 완료 요약·설치 로그가 같은 답변 데이터를 쓴다
|
|
282
264
|
}
|
|
283
265
|
|
|
284
266
|
const { now, today } = clock || utcNow();
|
|
285
267
|
const ctx = createContext({
|
|
286
268
|
mode, force: true, types, version, versionCode, branch, branches, paths,
|
|
287
|
-
includeNexus, includeSecretBackup,
|
|
288
269
|
includeSemverAuto,
|
|
289
270
|
includeCopilotAi,
|
|
290
271
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
291
|
-
// 설치
|
|
272
|
+
// 설치 로그·완료 요약이 쓰는 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
292
273
|
markers, envAnswers, detectWarnings,
|
|
293
274
|
deployStyle: deployStyle || DEFAULT_DEPLOY_STYLE,
|
|
294
275
|
envMode: flutterOptions.envMode, flutterStore: flutterOptions.stores,
|
|
@@ -297,7 +278,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
297
278
|
});
|
|
298
279
|
ctx.templateVersion = templateVersion;
|
|
299
280
|
|
|
300
|
-
// 사용자가 답해야 하는 것만
|
|
281
|
+
// 사용자가 답해야 하는 것만 묻는다. baseline 3-way가 자동으로 안전한 경우를
|
|
301
282
|
// 걸러내므로, 여기 오는 것은 (a) 양쪽이 다 바뀐 진짜 충돌과 (b) 사용자가 지운 파일뿐이다.
|
|
302
283
|
let hooks = {};
|
|
303
284
|
if (showOptional && io.engineIo?.select) {
|
|
@@ -363,7 +344,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
363
344
|
}
|
|
364
345
|
|
|
365
346
|
// 브랜치 선택 — 원격 목록이 있으면 select(+직접 입력), 없으면 텍스트 입력. ESC/빈값 = 기본값.
|
|
366
|
-
//
|
|
347
|
+
// def를 최우선으로, main/develop을 그다음으로 정렬하고 커서를 def에 고정한다.
|
|
367
348
|
export async function pickBranch(io, message, def, remoteBranches, isCancel) {
|
|
368
349
|
if (io.engineIo?.select && remoteBranches.length) {
|
|
369
350
|
const sorted = sortBranchesForSelection(remoteBranches, def);
|
|
@@ -383,7 +364,7 @@ export async function pickBranch(io, message, def, remoteBranches, isCancel) {
|
|
|
383
364
|
return isCancel(v) || !v ? def : v;
|
|
384
365
|
}
|
|
385
366
|
|
|
386
|
-
function summarize({ mode, types, version, branch,
|
|
367
|
+
function summarize({ mode, types, version, branch, showOptional, flutter, envModeDefault }) {
|
|
387
368
|
const lines = [
|
|
388
369
|
`통합 모드 : ${modeLabel(mode)}`,
|
|
389
370
|
`프로젝트 타입 : ${types.join(", ")}${types.length > 1 ? " (멀티)" : ""}`,
|
|
@@ -391,8 +372,6 @@ function summarize({ mode, types, version, branch, includeNexus, includeSecretBa
|
|
|
391
372
|
`기본 브랜치 : ${branch}`,
|
|
392
373
|
];
|
|
393
374
|
if (showOptional) {
|
|
394
|
-
lines.push(`Nexus publish : ${includeNexus ? "포함" : "제외"}`);
|
|
395
|
-
lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
|
|
396
375
|
if (types.includes("flutter")) {
|
|
397
376
|
const stores = flutter.stores ?? [];
|
|
398
377
|
const modeParts = stores.map((p) => `${p}=${(p === "android" ? flutter.androidDeployMode : flutter.iosDeployMode) || DEFAULT_DEPLOY_MODE}`);
|
package/src/commands/purge.js
CHANGED
|
@@ -48,7 +48,7 @@ export function printPurgePlan(plan, { dryRun = false } = {}) {
|
|
|
48
48
|
// 실제 삭제 수행 — planPurge()와 동일 shape을 반환하되 실제로 제거된 항목을 반영한다.
|
|
49
49
|
// planRemoval 결과를 그대로 지우지 않는 이유: 그 목록은 항상 전체라서
|
|
50
50
|
// --keep-* 로 선택적 카테고리만 보존하는 요구사항과 맞지 않는다.
|
|
51
|
-
//
|
|
51
|
+
// readmeSection은 plan의 판정을 그대로 되돌려주지 않고
|
|
52
52
|
// removeVersionSectionFromReadme()의 실제 반환값("removed"인지)을 반영한다 — 스펙 §6이
|
|
53
53
|
// "반환값은 실제 삭제 결과를 반영"하라고 명시하기 때문에, plan과 실제 제거 조건이
|
|
54
54
|
// 이론상 어긋나는 경우에도 printPurgeResult가 거짓으로 "제거됨"을 보고하지 않는다.
|
|
@@ -65,7 +65,7 @@ export function executePurge(payloadRoot, targetRoot = ".", keepFlags = {}) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// 삭제 후 실제 제거된 목록 출력 — printPurgePlan과 완전히 동일한 형태(파일명 나열)로
|
|
68
|
-
// 맞춘다 (
|
|
68
|
+
// 맞춘다 (개수만 출력하면 스펙 §5-6의 "제거된 목록 재출력" 요구를 충족하지 못함).
|
|
69
69
|
export function printPurgeResult(result) {
|
|
70
70
|
const lines = ["", "제거됨:", ""];
|
|
71
71
|
lines.push(`워크플로우 (${result.workflows.length}개):`);
|
package/src/commands/status.js
CHANGED
|
@@ -26,8 +26,6 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
26
26
|
const branchesForCompare = existing.branches || { main: detectDefaultBranch(targetRoot) || "main", develop: "develop", mode: "pr-flow" };
|
|
27
27
|
const context = {
|
|
28
28
|
types: existing.types, paths: existing.paths,
|
|
29
|
-
includeNexus: existing.options.nexus === true,
|
|
30
|
-
includeSecretBackup: existing.options.secretBackup === true,
|
|
31
29
|
repoName, resolvers, branches: branchesForCompare,
|
|
32
30
|
flutterStore: flutterOptions.stores,
|
|
33
31
|
};
|
|
@@ -41,7 +39,7 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
41
39
|
branches: existing.branches,
|
|
42
40
|
options: existing.options,
|
|
43
41
|
// 사용자가 손댄 파일 = 진짜 충돌(changed) + 업스트림은 그대로인데 내가 고친 것(localOnly).
|
|
44
|
-
// baseline이 없으면 localOnly는 항상 비어 있어 종전과 동일하게
|
|
42
|
+
// baseline이 없으면 localOnly는 항상 비어 있어 종전과 동일하게 동작한다.
|
|
45
43
|
modifiedFiles: [...plan.changed, ...plan.localOnly].map((f) => f.filename),
|
|
46
44
|
// 업데이트 시 무슨 일이 일어날지 미리 보여주는 버킷들 — 판단 재료가 없어 사용자가
|
|
47
45
|
// 직접 git diff를 떠야 했던 문제를 없앤다.
|
|
@@ -71,7 +69,7 @@ export function printStatus(status) {
|
|
|
71
69
|
const semverAutoLabel = status.options.semverAuto === null ? "미설정(기본 false)" : status.options.semverAuto;
|
|
72
70
|
const copilotAiLabel = boolLabel(status.options.copilotAi ?? null);
|
|
73
71
|
const flutterLabels = status.types.includes("flutter") ? flutterOptionLabels(status.options) : "";
|
|
74
|
-
lines.push(`옵션 :
|
|
72
|
+
lines.push(`옵션 : semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${flutterLabels}`);
|
|
75
73
|
if (status.modifiedFiles.length) {
|
|
76
74
|
lines.push("", `사용자가 수정한 워크플로우 파일 (${status.modifiedFiles.length}개):`);
|
|
77
75
|
for (const f of status.modifiedFiles) lines.push(` - ${f}`);
|
|
@@ -79,7 +77,7 @@ export function printStatus(status) {
|
|
|
79
77
|
lines.push("", "모든 워크플로우 파일이 설치 시점 기본값과 동일합니다 (수정 없음).");
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
// 업데이트하면 무슨 일이
|
|
80
|
+
// 업데이트하면 무슨 일이 일어나는지. baseline이 없는 설치는 전부 0이라 출력하지 않는다.
|
|
83
81
|
const b = status.buckets || { autoUpdatable: [], localKept: [], conflicts: [], removed: [] };
|
|
84
82
|
if (b.autoUpdatable.length || b.localKept.length || b.conflicts.length || b.removed.length) {
|
|
85
83
|
lines.push("", "지금 업데이트하면:");
|
|
@@ -92,7 +90,7 @@ export function printStatus(status) {
|
|
|
92
90
|
console.log(lines.join("\n"));
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
// Flutter 타입일 때만 붙는
|
|
93
|
+
// Flutter 타입일 때만 붙는 옵션. 저장값이 없으면 그 상태에서 실제로 적용되는 동작을 함께 알린다.
|
|
96
94
|
function flutterOptionLabels(options) {
|
|
97
95
|
return [
|
|
98
96
|
` env_mode=${options.envMode ?? "미설정(dotenv 유지)"}`,
|
|
@@ -17,7 +17,7 @@ export function planUninstall(payloadRoot, targetRoot, selection) {
|
|
|
17
17
|
const removalPlan = planRemoval(payloadRoot, targetRoot);
|
|
18
18
|
return {
|
|
19
19
|
workflows: selection.workflows ? removalPlan.workflows : [],
|
|
20
|
-
// baseline은 워크플로우 해시 기록 — 워크플로우를 지우면 함께 사라져야 한다
|
|
20
|
+
// baseline은 워크플로우 해시 기록 — 워크플로우를 지우면 함께 사라져야 한다
|
|
21
21
|
baseline: selection.workflows ? removalPlan.baseline : [],
|
|
22
22
|
scripts: selection.scripts ? removalPlan.scripts : [],
|
|
23
23
|
readme: selection.readme ? hasVersionSection(targetRoot) : false,
|
|
@@ -46,7 +46,7 @@ export function runUninstall(context, payloadRoot, targetRoot, selection) {
|
|
|
46
46
|
// ── 대화형 체크리스트 흐름 ────────────────────────────────────────────
|
|
47
47
|
const ITEM_DEFS = [
|
|
48
48
|
// .github/.wizard 에는 baseline.json과 설치 로그(.wizard/logs)가 함께 들어 있다 —
|
|
49
|
-
// 워크플로우를 지우면 함께 사라지므로 라벨에
|
|
49
|
+
// 워크플로우를 지우면 함께 사라지므로 라벨에 명시한다.
|
|
50
50
|
{ key: "workflows", label: "워크플로우 (.github/workflows/PROJECT-*.yaml) + 설치 기록 (.github/.wizard)" },
|
|
51
51
|
{ key: "scripts", label: "스크립트 (.github/scripts/*.py)" },
|
|
52
52
|
{ key: "readme", label: "README.md 버전 섹션 (AUTO-VERSION-SECTION)" },
|
|
@@ -54,8 +54,7 @@ const ITEM_DEFS = [
|
|
|
54
54
|
{ key: "versionYml", label: "version.yml (버전/브랜치 설정 전체)" },
|
|
55
55
|
];
|
|
56
56
|
|
|
57
|
-
// 기본 체크 상태 —
|
|
58
|
-
// 여기서는 "안전 삭제" 2종만 기본 체크하고 나머지(readme/gitignore/versionYml)는 opt-in.
|
|
57
|
+
// 기본 체크 상태 — "안전 삭제" 2종만 기본 체크하고 나머지(readme/gitignore/versionYml)는 opt-in.
|
|
59
58
|
export const SAFE_ITEMS = ["workflows", "scripts"];
|
|
60
59
|
|
|
61
60
|
function detectAvailableItems(payloadRoot, targetRoot) {
|
package/src/context.js
CHANGED
|
@@ -4,9 +4,9 @@ export const VALID_TYPES = [
|
|
|
4
4
|
"react-native", "react-native-expo", "node", "python", "basic", "go",
|
|
5
5
|
];
|
|
6
6
|
|
|
7
|
-
// --mode 화이트리스트
|
|
8
|
-
// purge는 --help/대화형 메뉴에 노출하지 않는 숨김
|
|
9
|
-
// version/workflows(부분 설치)와 revert는 제거됐다
|
|
7
|
+
// --mode 화이트리스트 — 알 수 없는 값은 부수효과(브랜치 조회 등) 이전에 즉시 거부해야 한다.
|
|
8
|
+
// purge는 --help/대화형 메뉴에 노출하지 않는 숨김 모드이지만 검증 대상에는 포함한다.
|
|
9
|
+
// version/workflows(부분 설치)와 revert는 제거됐다 — 부분 설치는 설치 시점 baseline을
|
|
10
10
|
// 반쪽만 갱신해 업데이트 판정을 흐리고, revert는 uninstall의 부분집합이었다.
|
|
11
11
|
export const VALID_MODES = [
|
|
12
12
|
"interactive", "full",
|
|
@@ -24,11 +24,9 @@ export function createContext(overrides = {}) {
|
|
|
24
24
|
branch: "",
|
|
25
25
|
branches: null, // { main, develop, mode: "pr-flow"|"trunk-based" } — resolveBranchConfig 결과
|
|
26
26
|
paths: new Map(), // type -> path
|
|
27
|
-
includeNexus: null, // null=미설정, true/false=명시
|
|
28
|
-
includeSecretBackup: null,
|
|
29
27
|
includeSemverAuto: null, // null=미설정(다운스트림에서 true로 해석), true/false=명시
|
|
30
28
|
includeCopilotAi: null, // null=미설정(다운스트림에서 false로 해석), true/false=명시 — Copilot AI 요약 opt-in
|
|
31
|
-
// Flutter 옵션
|
|
29
|
+
// Flutter 옵션 — Flutter 타입이 없는 프로젝트에서는 전부 무시된다.
|
|
32
30
|
// 결정은 src/core/flutter-options.js의 resolveFlutterOptions가 한다.
|
|
33
31
|
envMode: "", // "dart-define" | "dotenv". ""=미결정 → 템플릿 기본값(dart-define)
|
|
34
32
|
flutterStore: null, // 스토어 배포 대상 string[] (예: ["android","ios"]). null=미결정 → 둘 다(현행 동작)
|
package/src/core/assets.js
CHANGED
|
@@ -24,7 +24,7 @@ export function readTemplateVersion() {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
// payload/workflows/common 직하위 yaml
|
|
27
|
+
// payload/workflows/common 직하위 yaml 목록.
|
|
28
28
|
export function listCommonWorkflows(payloadRoot = resolvePayloadRoot()) {
|
|
29
29
|
return listYamlFiles(join(payloadRoot, "workflows", "common"));
|
|
30
30
|
}
|
|
@@ -40,7 +40,7 @@ export function assertPayload(payloadRoot = resolvePayloadRoot()) {
|
|
|
40
40
|
return payloadRoot;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// payload/version.yml.template
|
|
43
|
+
// payload/version.yml.template 원문.
|
|
44
44
|
export function readVersionYmlTemplate(payloadRoot = resolvePayloadRoot()) {
|
|
45
45
|
const p = join(payloadRoot, "version.yml.template");
|
|
46
46
|
return exists(p) ? readText(p) : null;
|
package/src/core/baseline.js
CHANGED
package/src/core/branches.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 브랜치 구성 (DESIGN-SPEC §4 신규 질문 ①).
|
|
2
2
|
// on: push: branches: 는 YAML 정적 값 — 마법사가 릴리스/개발 브랜치를 물어(또는 플래그로 받아)
|
|
3
|
-
// {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} 플레이스홀더를 치환한다 (치환 자체는 branding.js
|
|
4
|
-
// main === develop 이면 trunk-based 모드 → RELEASE-PUBLISH 단독
|
|
3
|
+
// {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} 플레이스홀더를 치환한다 (치환 자체는 branding.js).
|
|
4
|
+
// main === develop 이면 trunk-based 모드 → RELEASE-PUBLISH 단독 설치.
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
// 기본 exec — git 명령 실행. 반환 {code, stdout, stderr}. 테스트는 mock 주입.
|
|
@@ -34,7 +34,7 @@ export function resolveBranchConfig({ mainBranch = "", developBranch = "", defau
|
|
|
34
34
|
return { main, develop, mode: main === develop ? "trunk-based" : "pr-flow" };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// 브랜치 선택 프롬프트용
|
|
37
|
+
// 브랜치 선택 프롬프트용 정렬. def를 최우선으로, 그다음 priority(main/develop)
|
|
38
38
|
// 순서로 목록 앞에 배치한다. 나머지는 remoteBranches의 원래 순서(git이 준 알파벳순)를 유지한다.
|
|
39
39
|
// 순수 함수 — remoteBranches 원본은 변경하지 않는다.
|
|
40
40
|
export function sortBranchesForSelection(remoteBranches, def, priority = ["main", "develop"]) {
|
|
@@ -24,7 +24,7 @@ export async function loadBreakingJson(payloadRoot) {
|
|
|
24
24
|
// opts:
|
|
25
25
|
// cwd - 통합 대상 루트 (기존 version.yml에서 현재 템플릿 버전 읽음)
|
|
26
26
|
// payloadRoot - 패키지 payload/ 루트 (번들 폴백용)
|
|
27
|
-
// templateVersion - 설치하려는 템플릿 버전 (.sh의 DEFAULT_VERSION 고정 버그를 실버전으로 교정
|
|
27
|
+
// templateVersion - 설치하려는 템플릿 버전 (.sh의 DEFAULT_VERSION 고정 버그를 실버전으로 교정)
|
|
28
28
|
// askYesNo - async(message, defaultYes)→bool. null이면 비대화형: 경고만 출력 후 진행
|
|
29
29
|
// loader - 테스트 주입용 (기본 loadBreakingJson)
|
|
30
30
|
export async function runBreakingCheck({ cwd, payloadRoot, templateVersion, askYesNo = null, loader = loadBreakingJson }) {
|
package/src/core/breaking.js
CHANGED
|
@@ -11,7 +11,7 @@ export function compareVersions(a, b) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
// breaking-changes.json에서 current < ver <= target 범위 항목 수집.
|
|
14
|
-
// ⚠️ .sh 버그 수정: target은 하드코딩 1.3.14가 아니라 실제 templateVersion을
|
|
14
|
+
// ⚠️ .sh 버그 수정: target은 하드코딩 1.3.14가 아니라 실제 templateVersion을 넘긴다.
|
|
15
15
|
// _ 로 시작하는 키(메타) 제외. severity critical / 그 외(warning).
|
|
16
16
|
// 버전 키의 값은 항목 객체 또는 항목 객체의 배열(같은 릴리스에 고지가 여러 건일 때).
|
|
17
17
|
export function collectBreaking(json, current, target) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// Flutter 앱 소유 파일(fastlane·ExportOptions.plist)
|
|
1
|
+
// Flutter 앱 소유 파일(fastlane·ExportOptions.plist) 설치.
|
|
2
2
|
// 워크플로우와 달리 사용자가 값을 채워 넣는 파일이라 "없을 때만 생성"한다 — 덮어쓰지 않고 baseline
|
|
3
|
-
// 3-way도 적용하지 않는다(
|
|
3
|
+
// 3-way도 적용하지 않는다(기존 사용자 파일을 덮어쓰지 않는 규약). 원본 갱신을 기존 사용자에게
|
|
4
4
|
// 전파하는 것은 범위 밖이다.
|
|
5
5
|
import { join, posix } from "node:path";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// .gitignore 보장 — 마법사 자신이 만드는 충돌 백업 부산물(*.bak, *.template.yaml)만 대상으로 한다.
|
|
2
|
-
// 마법사가 설치하는 것과 무관한 개인 개발환경 설정(IDE 등)은 마법사 책임 범위
|
|
2
|
+
// 마법사가 설치하는 것과 무관한 개인 개발환경 설정(IDE 등)은 마법사 책임 범위 밖.
|
|
3
3
|
// 배너 블록은 종료 마커(BANNER_END)로 범위가 명확히 구분되어 있어, 그 안에 사용자가 다른 줄을
|
|
4
|
-
// 끼워넣어도 REQUIRED_ENTRIES만 정확히 개별 제거하고 나머지는
|
|
4
|
+
// 끼워넣어도 REQUIRED_ENTRIES만 정확히 개별 제거하고 나머지는 보존한다.
|
|
5
5
|
// 주의(의도된 트레이드오프): 이 수정 이전 버전으로 설치되어 종료 마커가 없는 배너가 이미 있는
|
|
6
6
|
// 레포에서는, 배너 직후 REQUIRED_ENTRIES가 연속으로 이어지는 동안만 제거하는 구 방식으로
|
|
7
|
-
// 폴백한다 —
|
|
7
|
+
// 폴백한다 — 이전 결정이 "이미 설치된 레포의 .gitignore는 소급 처리하지 않고 사용자 판단에
|
|
8
8
|
// 맡긴다"고 명시하므로 별도 마이그레이션 로직을 추가하지 않는다.
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// 마법사가 설치하는 것과 무관한 개인 개발환경 항목(/.idea 등)은 마법사 책임 밖이므로 제거.
|
|
13
13
|
// 대신 마법사 자신의 충돌 처리(workflows.js backup/template 결정)가
|
|
14
14
|
// 실제로 만들어내는 부산물만 gitignore 대상으로 삼는다.
|
|
15
15
|
const REQUIRED_ENTRIES = ["*.bak", "*.template.yaml"];
|
|
@@ -66,7 +66,7 @@ export function ensureGitignore(targetRoot = ".") {
|
|
|
66
66
|
// ensureGitignore가 기존 파일에 배너 블록을 추가할 때 항상 이 정확한 시퀀스로 시작한다
|
|
67
67
|
// (빈 줄 하나 + 3줄 배너), 그리고 REQUIRED_ENTRIES 뒤에 BANNER_END로 끝난다. 배너~BANNER_END
|
|
68
68
|
// 범위 안에서 REQUIRED_ENTRIES와 일치하는 줄만 개별 제거하고, 사용자가 그 사이/뒤에 추가한
|
|
69
|
-
// 줄은 절대 건드리지
|
|
69
|
+
// 줄은 절대 건드리지 않는다.
|
|
70
70
|
const BANNER =
|
|
71
71
|
"\n" +
|
|
72
72
|
"# ====================================================================\n" +
|
|
@@ -75,7 +75,7 @@ const BANNER =
|
|
|
75
75
|
|
|
76
76
|
// REQUIRED_ENTRIES 뒤에 오는 종료 마커 — 배너 블록의 "끝"을 명확히 구분해, 그 사이에 사용자가
|
|
77
77
|
// 다른 줄을 끼워넣어도 removeAutoAddedEntriesFromGitignore가 정확한 범위 안에서 개별 제거할 수
|
|
78
|
-
// 있게
|
|
78
|
+
// 있게 한다. ensureGitignore가 기존 파일에 배너를 추가할 때만 함께 쓴다
|
|
79
79
|
// (신규 파일 생성 케이스는 NEW_FILE_CONTENT를 통째로 쓰고 종료 마커는 쓰지 않는다 —
|
|
80
80
|
// startsWith 전체 prefix 매칭이라 별도 마커가 필요 없다).
|
|
81
81
|
const BANNER_END = "# ==== project-auto-wizard: end of auto-added entries ====\n";
|
|
@@ -114,7 +114,7 @@ export function removeAutoAddedEntriesFromGitignore(targetRoot = ".") {
|
|
|
114
114
|
|
|
115
115
|
// 종료 마커가 있으면(이 수정 이후 설치분) 그 범위 안에서 REQUIRED_ENTRIES만 개별적으로
|
|
116
116
|
// 제거하고, 사용자가 그 사이에 끼워넣은 줄은 순서·연속 여부와 무관하게 그대로 보존한다
|
|
117
|
-
|
|
117
|
+
//.
|
|
118
118
|
const endIdx = content.indexOf(BANNER_END, afterBanner);
|
|
119
119
|
if (endIdx !== -1) {
|
|
120
120
|
const region = content.slice(afterBanner, endIdx).split("\n");
|
|
@@ -126,7 +126,7 @@ export function removeAutoAddedEntriesFromGitignore(targetRoot = ".") {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
// 종료 마커가 없는 구버전 설치(이 수정 이전) — 배너 직후 REQUIRED_ENTRIES가 연속으로
|
|
129
|
-
// 이어지는 동안만 제거하는 기존 방식으로 폴백한다 (
|
|
129
|
+
// 이어지는 동안만 제거하는 기존 방식으로 폴백한다 (동일한 소급 미처리 원칙).
|
|
130
130
|
const lines = content.slice(afterBanner).split("\n");
|
|
131
131
|
let consumed = 0;
|
|
132
132
|
for (const line of lines) {
|