project-auto-wizard 0.10.0 → 0.11.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.md +83 -4
- package/package.json +1 -1
- package/payload/config/breaking-changes.json +24 -1
- package/payload/flutter-app/android/fastlane/Fastfile.playstore +86 -0
- package/payload/flutter-app/ios/ExportOptions.plist +31 -0
- package/payload/flutter-app/ios/fastlane/Fastfile +117 -0
- 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/version.yml.template +1 -0
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +48 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +44 -19
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +28 -36
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +32 -45
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +123 -32
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +56 -41
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +61 -40
- package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -0
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +47 -0
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +47 -0
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +47 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-GITHUB-PACKAGES-PUBLISH.yml +1 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-CI.yml +47 -0
- package/payload/workflows/spring/nexus/PROJECT-SPRING-NEXUS-PUBLISH.yml +1 -0
- package/src/cli/args.js +37 -0
- package/src/cli/help.js +4 -0
- package/src/commands/doctor.js +55 -2
- package/src/commands/dry-run.js +10 -0
- package/src/commands/full.js +28 -3
- package/src/commands/interactive-flutter.js +95 -0
- package/src/commands/interactive.js +51 -5
- package/src/commands/status.js +20 -2
- package/src/context.js +6 -0
- package/src/core/breaking.js +7 -3
- package/src/core/copy/flutter-app.js +42 -0
- package/src/core/copy/workflows.js +21 -9
- package/src/core/detect-fs.js +9 -1
- package/src/core/flutter-options.js +118 -0
- package/src/core/installed-stores.js +12 -0
- package/src/core/version-yml.js +40 -5
- package/src/core/wizard-env.js +26 -4
- package/src/index.js +32 -1
- package/src/ui/env-plan.js +8 -5
- package/src/ui/prompts.js +88 -3
- package/src/ui/status-cards.js +13 -1
- package/src/ui/summary.js +24 -5
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// 동기 엔진(copyWorkflows)에 hooks.decisions로 전달한다 — 기존 시그니처·force 동작 무변경.
|
|
5
5
|
import { join, basename } from "node:path";
|
|
6
6
|
import { deployFilter, isDeployWorkflow, activateDeployTrigger, DEFAULT_DEPLOY_STYLE, NO_DEPLOY_STYLE } from "../deploy-style.js";
|
|
7
|
+
import { storeWorkflowFilter } from "../flutter-options.js";
|
|
7
8
|
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
8
9
|
import { PATHS, PAYLOAD } from "../paths.js";
|
|
9
10
|
import { exists, writeText, listYamlFiles } from "../fsutil.js";
|
|
@@ -30,6 +31,17 @@ const TRUNK_BASED_EXCLUDED = new Set([
|
|
|
30
31
|
"PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml",
|
|
31
32
|
]);
|
|
32
33
|
|
|
34
|
+
// 타입 루트 디렉토리에 거는 파일 필터 — "배포 안 함"의 CD 제외와 Flutter 스토어 대상 선택을 합성한다.
|
|
35
|
+
// copyWorkflowsForType·surveyWorkflows·planWorkflows가 같은 함수를 써야 설치·충돌 조사·status/dry-run이
|
|
36
|
+
// 서로 다른 파일 집합을 보지 않는다. flutterStore가 배열이 아니면(null=미결정, 비대화형 기본) 스토어 필터는
|
|
37
|
+
// 걸지 않는다. 필터가 없어도 항상 함수를 돌려준다 — processDir은 null을 받지 못한다.
|
|
38
|
+
export function buildTypeRootFilter(type, deployStyle, flutterStore) {
|
|
39
|
+
const filters = [];
|
|
40
|
+
if (deployStyle === NO_DEPLOY_STYLE) filters.push(deployFilter(deployStyle));
|
|
41
|
+
if (type === "flutter" && Array.isArray(flutterStore)) filters.push(storeWorkflowFilter(flutterStore));
|
|
42
|
+
return (filename) => filters.every((keep) => keep(filename));
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
// 한 파일에 env 치환을 적용해 대상 파일을 갱신 (.sh configure_workflow_env 등가).
|
|
34
46
|
// values/useDefaults: env 계획(promptEnvPlan) 결과 — 미지정이면 기본값 경로(현행 force 동작).
|
|
35
47
|
function configureEnv(targetPath, { type, projectPath = ".", repoName = "", resolvers = {}, collectAsks = null, values = new Map(), useDefaults = true }) {
|
|
@@ -143,6 +155,7 @@ function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () =>
|
|
|
143
155
|
// copy_workflows 본체 (동기 — 기존 호출부 무변경).
|
|
144
156
|
// context: { types:[], paths:Map, includeNexus, includeSecretBackup, force, repoName, resolvers,
|
|
145
157
|
// envValues?:Map<key,value>, envUseDefaults?:boolean } ← env 계획(promptEnvPlan) 결과 주입점
|
|
158
|
+
// flutterStore?:string[]|null } ← Flutter 스토어 대상 (null=필터 없음, 배열=선택된 플랫폼만)
|
|
146
159
|
// hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'>, — 진짜 충돌(changed) 결정
|
|
147
160
|
// restoreRemoved?: Set<filename> } — 사용자가 지운 파일 중 복원할 것
|
|
148
161
|
// 미지정 파일은 'skip'(현행 force 동작 100% 유지). 대화형 수집은 copyWorkflowsInteractive 참조.
|
|
@@ -270,7 +283,7 @@ function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcTe
|
|
|
270
283
|
// conflicts — 양쪽이 다 바뀐 진짜 충돌. upstreamOnly/localOnly는 자동 처리되므로 여기 없다.
|
|
271
284
|
// removed — 우리가 깔았는데 사용자가 지운 파일. 되살리기 전에 물어봐야 한다.
|
|
272
285
|
export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
273
|
-
const { types = [], paths = new Map(), includeNexus = false, repoName = "", resolvers = {} } = context;
|
|
286
|
+
const { types = [], paths = new Map(), includeNexus = false, repoName = "", resolvers = {}, flutterStore = null } = context;
|
|
274
287
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
275
288
|
const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
276
289
|
const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
|
|
@@ -297,7 +310,7 @@ export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
297
310
|
const envOpts = { type, projectPath: paths.get(type) || ".", repoName, resolvers };
|
|
298
311
|
const typeDir = join(projectTypesDir, type);
|
|
299
312
|
if (exists(typeDir)) {
|
|
300
|
-
collect(typeDir, envOpts, type, () => false, deployStyle
|
|
313
|
+
collect(typeDir, envOpts, type, () => false, buildTypeRootFilter(type, deployStyle, flutterStore));
|
|
301
314
|
}
|
|
302
315
|
const serverDeployDir = join(typeDir, "server-deploy");
|
|
303
316
|
if (exists(serverDeployDir) && !includeNexus && deployStyle !== NO_DEPLOY_STYLE) {
|
|
@@ -328,8 +341,9 @@ export async function copyWorkflowsInteractive(context, payloadRoot, targetRoot
|
|
|
328
341
|
}
|
|
329
342
|
|
|
330
343
|
function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters) {
|
|
331
|
-
const { includeNexus, deployStyle = "", envOptsFor, collectAsks = null, dirCtx } = ctx;
|
|
344
|
+
const { includeNexus, deployStyle = "", flutterStore = null, envOptsFor, collectAsks = null, dirCtx } = ctx;
|
|
332
345
|
const keepDeploy = deployFilter(deployStyle);
|
|
346
|
+
const keepTypeRoot = buildTypeRootFilter(type, deployStyle, flutterStore);
|
|
333
347
|
const { srcText, baselineTargets } = dirCtx;
|
|
334
348
|
const typeDir = join(projectTypesDir, type);
|
|
335
349
|
const envOpts = envOptsFor(type);
|
|
@@ -341,8 +355,7 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
341
355
|
// 바로 있는 타입도 있다 — "배포 안 함"일 때는 타입 루트에서도 CD 파일(SIMPLE-CICD 등)을
|
|
342
356
|
// 걸러야 한다. simple/nginx/traefik은 오늘과 동일하게 필터 없이 전부 복사한다.
|
|
343
357
|
if (exists(typeDir)) {
|
|
344
|
-
const
|
|
345
|
-
const c = processDir(typeDir, workflowsDir, envOpts, dirCtx, counters, typeRootFilter);
|
|
358
|
+
const c = processDir(typeDir, workflowsDir, envOpts, dirCtx, counters, keepTypeRoot);
|
|
346
359
|
untouched.push(...c.unchanged, ...c.localOnly);
|
|
347
360
|
}
|
|
348
361
|
|
|
@@ -374,7 +387,7 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
374
387
|
for (const filename of listYamlFiles(srcDir)) {
|
|
375
388
|
const target = join(workflowsDir, filename);
|
|
376
389
|
if (srcDir === serverDeployDir && !keepDeploy(filename)) continue; // 안 고른 배포 방식
|
|
377
|
-
if (srcDir === typeDir &&
|
|
390
|
+
if (srcDir === typeDir && !keepTypeRoot(filename)) continue; // 타입 루트: 배제된 CD·안 고른 스토어 워크플로우
|
|
378
391
|
if (!existsSync(target)) continue; // 건너뛴 파일 제외
|
|
379
392
|
if (untouched.includes(filename)) continue; // unchanged/localOnly 제외
|
|
380
393
|
configureEnv(target, { ...envOpts, collectAsks }); // env 계획 values/useDefaults 포함
|
|
@@ -386,7 +399,7 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
386
399
|
// changed뿐 아니라 newFiles/unchanged까지 전부 반환한다는 점이 listWorkflowConflicts(changed만
|
|
387
400
|
// 반환)와 다르다(읽기 전용 — 실제로 아무 파일도 쓰지 않는다).
|
|
388
401
|
export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
389
|
-
const { types = [], paths = new Map(), includeNexus = false, includeSecretBackup = false, repoName = "", resolvers = {} } = context;
|
|
402
|
+
const { types = [], paths = new Map(), includeNexus = false, includeSecretBackup = false, repoName = "", resolvers = {}, flutterStore = null } = context;
|
|
390
403
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
391
404
|
const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
392
405
|
const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
|
|
@@ -427,8 +440,7 @@ export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
427
440
|
const envOpts = { type, projectPath: paths.get(type) || ".", repoName, resolvers };
|
|
428
441
|
const typeDir = join(projectTypesDir, type);
|
|
429
442
|
if (exists(typeDir)) {
|
|
430
|
-
|
|
431
|
-
merge(classify(typeDir, workflowsDir, envOpts, srcText, baseline, typeRootFilter), type);
|
|
443
|
+
merge(classify(typeDir, workflowsDir, envOpts, srcText, baseline, buildTypeRootFilter(type, deployStyle, flutterStore)), type);
|
|
432
444
|
}
|
|
433
445
|
|
|
434
446
|
const serverDeployDir = join(typeDir, "server-deploy");
|
package/src/core/detect-fs.js
CHANGED
|
@@ -116,7 +116,9 @@ export function findSpringAppYml(root, base = ".") {
|
|
|
116
116
|
|
|
117
117
|
// 실 resolver 세트 생성 (.sh resolve_token 4종 등가) — index/interactive 공용.
|
|
118
118
|
// paths: Map<type, path> (모노레포 경로).
|
|
119
|
-
|
|
119
|
+
// flutterOptions: resolveFlutterOptions 결과 또는 같은 필드를 가진 context (이슈 #131). null이면 Flutter 토큰이
|
|
120
|
+
// 빈 값이라 템플릿 기본값(dart-define, store_only)이 그대로 남는다.
|
|
121
|
+
export function makeResolvers(root, repoName, paths, flutterOptions = null) {
|
|
120
122
|
const springBase = (t) => paths.get(t || "spring") || paths.get("spring") || ".";
|
|
121
123
|
return {
|
|
122
124
|
repo: () => repoName,
|
|
@@ -130,5 +132,11 @@ export function makeResolvers(root, repoName, paths) {
|
|
|
130
132
|
},
|
|
131
133
|
"spring-app-yml-path": (t) => findSpringAppYml(root, springBase(t)) || "",
|
|
132
134
|
"flutter-root": () => paths.get("flutter") || ".",
|
|
135
|
+
// CI changes job의 경로 필터(이슈 #131) — 타입별 프로젝트 루트. 단일 레포·common은 "."(항상 변경됨으로 판정).
|
|
136
|
+
"project-path": (t) => paths.get(t) || ".",
|
|
137
|
+
// 빈 문자열이면 setEnvLine/setFallbackLine이 줄을 건너뛰어 템플릿 기본값이 남는다.
|
|
138
|
+
"flutter-env-mode": () => flutterOptions?.envMode || "",
|
|
139
|
+
"android-deploy-mode": () => flutterOptions?.androidDeployMode || "",
|
|
140
|
+
"ios-deploy-mode": () => flutterOptions?.iosDeployMode || "",
|
|
133
141
|
};
|
|
134
142
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Flutter 옵션 (이슈 #131) — 환경변수 방식·스토어 배포 대상·배포 모드의 단일 진실.
|
|
2
|
+
// 스토어 배포 대상은 deploy-style.js(배포 방식)와 같은 구조 — 값 목록, 파일 필터, 선택 해제 정리 — 를 따른다.
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { existsSync, readFileSync, renameSync, rmSync } from "node:fs";
|
|
5
|
+
import { sha256 } from "./baseline.js";
|
|
6
|
+
|
|
7
|
+
// 환경변수 주입 방식. dart-define은 --dart-define-from-file, dotenv는 flutter_dotenv/envied용 .env 생성.
|
|
8
|
+
// 둘을 동시에 쓰는 both는 만들지 않는다 — 환경변수를 두 곳에서 관리하게 되기 때문.
|
|
9
|
+
export const ENV_MODES = ["dart-define", "dotenv"];
|
|
10
|
+
export const DEFAULT_ENV_MODE = "dart-define"; // 신규 설치 기본
|
|
11
|
+
export const LEGACY_ENV_MODE = "dotenv"; // 기존 설치(version.yml 있음, 저장값 없음)가 조용히 깨지지 않도록 보존
|
|
12
|
+
|
|
13
|
+
export const STORE_PLATFORMS = ["android", "ios"];
|
|
14
|
+
export const DEPLOY_MODES = ["store_only", "store_prepare", "store_submit"];
|
|
15
|
+
export const DEFAULT_DEPLOY_MODE = "store_only";
|
|
16
|
+
export const NO_STORE = "none";
|
|
17
|
+
|
|
18
|
+
export const isEnvMode = (v) => ENV_MODES.includes(v);
|
|
19
|
+
export const isDeployMode = (v) => DEPLOY_MODES.includes(v);
|
|
20
|
+
|
|
21
|
+
// "android,ios" | "android" | "none" | "" → 배열. 항상 STORE_PLATFORMS 순서라 직렬화가 결정적이다.
|
|
22
|
+
// 알 수 없는 토큰이 하나라도 있거나 문자열이 아니면 null — 호출부가 "값 없음/잘못된 값"으로 처리한다.
|
|
23
|
+
export function parseStoreList(csv) {
|
|
24
|
+
if (typeof csv !== "string") return null;
|
|
25
|
+
const tokens = csv.split(",").map((t) => t.trim()).filter((t) => t !== "");
|
|
26
|
+
if (tokens.length === 0 || (tokens.length === 1 && tokens[0] === NO_STORE)) return [];
|
|
27
|
+
if (!tokens.every((t) => STORE_PLATFORMS.includes(t))) return null;
|
|
28
|
+
return STORE_PLATFORMS.filter((p) => tokens.includes(p));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 저장용 직렬화 — 빈 배열은 빈 문자열이 아니라 "none"으로 적어 "선택 안 함"과 "값 없음"을 구분한다.
|
|
32
|
+
export function formatStoreList(stores) {
|
|
33
|
+
const ordered = STORE_PLATFORMS.filter((p) => stores.includes(p));
|
|
34
|
+
return ordered.length ? ordered.join(",") : NO_STORE;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 플랫폼별 스토어 워크플로우 (payload/workflows/flutter/ 기준 파일명).
|
|
38
|
+
// FIREBASE·SELFHOSTED·TEST-APK·APP-BUILD-TRIGGER·CI는 스토어와 무관해 여기 넣지 않는다 — 항상 설치된다.
|
|
39
|
+
export const STORE_WORKFLOWS = {
|
|
40
|
+
android: ["PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml"],
|
|
41
|
+
ios: ["PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml", "PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml"],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const isStoreWorkflow = (filename) => Object.values(STORE_WORKFLOWS).flat().includes(filename);
|
|
45
|
+
|
|
46
|
+
// 파일 필터 — stores가 null이면 미결정이라 현행 동작(전부 설치)이다.
|
|
47
|
+
// 배열이면 스토어 워크플로우는 선택된 플랫폼 것만 통과하고, 그 외 파일은 항상 통과한다.
|
|
48
|
+
export function storeWorkflowFilter(stores) {
|
|
49
|
+
if (stores === null || stores === undefined) return () => true;
|
|
50
|
+
const allowed = new Set(STORE_PLATFORMS.filter((p) => stores.includes(p)).flatMap((p) => STORE_WORKFLOWS[p]));
|
|
51
|
+
return (filename) => !isStoreWorkflow(filename) || allowed.has(filename);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 선택 해제한 스토어 워크플로우 정리 — cleanupOtherDeployWorkflows(deploy-style.js)와 같은 규칙이다.
|
|
55
|
+
// 손대지 않은 것(baseline의 installed 해시와 동일) → 삭제
|
|
56
|
+
// 손댄 것 → .bak으로 옮긴다 (내용 보존, 트리거만 죽인다)
|
|
57
|
+
// 사용자 소유인 Fastfile·ExportOptions.plist는 여기서 다루지 않는다 (워크플로우 파일만 대상).
|
|
58
|
+
// 반환: { removed:[], backedUp:[] }
|
|
59
|
+
export function cleanupDeselectedStoreWorkflows(workflowsDir, installedFilenames, stores, baseline) {
|
|
60
|
+
const keep = storeWorkflowFilter(stores);
|
|
61
|
+
const removed = [];
|
|
62
|
+
const backedUp = [];
|
|
63
|
+
|
|
64
|
+
for (const filename of installedFilenames) {
|
|
65
|
+
if (!isStoreWorkflow(filename) || keep(filename)) continue;
|
|
66
|
+
const p = join(workflowsDir, filename);
|
|
67
|
+
if (!existsSync(p)) continue;
|
|
68
|
+
|
|
69
|
+
const known = baseline?.files?.[filename]?.installed;
|
|
70
|
+
const untouched = known && sha256(readFileSync(p, "utf8")) === known;
|
|
71
|
+
if (untouched) {
|
|
72
|
+
rmSync(p, { force: true });
|
|
73
|
+
removed.push(filename);
|
|
74
|
+
} else {
|
|
75
|
+
renameSync(p, `${p}.bak`);
|
|
76
|
+
backedUp.push(filename);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { removed, backedUp };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 스토어 앱 파일 (payload/flutter-app/ 기준 상대경로) — 플랫폼별 묶음.
|
|
83
|
+
export const STORE_APP_FILES = {
|
|
84
|
+
android: ["android/fastlane/Fastfile.playstore"],
|
|
85
|
+
ios: ["ios/fastlane/Fastfile", "ios/ExportOptions.plist"],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// 선택된 플랫폼의 앱 파일 목록. stores가 null이면 전부 (현행 동작 = 둘 다 설치).
|
|
89
|
+
export function storeAppFilesFor(stores) {
|
|
90
|
+
const platforms = stores === null || stores === undefined
|
|
91
|
+
? STORE_PLATFORMS
|
|
92
|
+
: STORE_PLATFORMS.filter((p) => stores.includes(p));
|
|
93
|
+
return platforms.flatMap((p) => STORE_APP_FILES[p]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const validOr = (isValid, value, fallback) => (isValid(value) ? value : fallback);
|
|
97
|
+
|
|
98
|
+
// 옵션 최종 결정 — 우선순위: CLI > version.yml 저장값 > 기본값.
|
|
99
|
+
// cli { envMode:"", stores:null|string[], androidDeployMode:"", iosDeployMode:"" } (빈값/null = 미지정)
|
|
100
|
+
// existing parseExisting() 결과 또는 null (version.yml이 없으면 신규 설치)
|
|
101
|
+
//
|
|
102
|
+
// - envMode 기본값: 신규 설치와 "Flutter를 새로 추가하는" 기존 설치는 dart-define, 이미 Flutter가
|
|
103
|
+
// 설치돼 있던 프로젝트는 dotenv. 업데이트 한 번에 flutter_dotenv 프로젝트가 조용히 깨지지 않게
|
|
104
|
+
// 하려는 것이라, 판단 기준은 "이 프로젝트에 Flutter가 이미 있었는가"다(단순 existing 존재 여부가
|
|
105
|
+
// 아니다 — Spring 전용 프로젝트에 flutter 타입을 처음 추가하는 경우까지 dotenv로 묶으면 안 된다).
|
|
106
|
+
// - stores: null은 "미결정" — 비대화형은 현행 동작(둘 다 설치), 대화형은 질문한다.
|
|
107
|
+
// - 저장값은 유효할 때만 쓴다. version.yml은 사람이 고칠 수 있는데, 그 값이 워크플로우 표현식
|
|
108
|
+
// (`|| 'store_only'` 폴백 자리)에 그대로 들어가므로 목록 밖 문자열은 걸러야 한다.
|
|
109
|
+
export function resolveFlutterOptions({ cli = {}, existing = null } = {}) {
|
|
110
|
+
const saved = existing?.options ?? {};
|
|
111
|
+
const hadFlutterAlready = Array.isArray(existing?.types) && existing.types.includes("flutter");
|
|
112
|
+
return {
|
|
113
|
+
envMode: cli.envMode || validOr(isEnvMode, saved.envMode, hadFlutterAlready ? LEGACY_ENV_MODE : DEFAULT_ENV_MODE),
|
|
114
|
+
stores: cli.stores ?? parseStoreList(saved.flutterStore),
|
|
115
|
+
androidDeployMode: cli.androidDeployMode || validOr(isDeployMode, saved.androidDeployMode, DEFAULT_DEPLOY_MODE),
|
|
116
|
+
iosDeployMode: cli.iosDeployMode || validOr(isDeployMode, saved.iosDeployMode, DEFAULT_DEPLOY_MODE),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// 이미 설치된 스토어 배포 워크플로우 파일명으로 플랫폼을 추론한다 (이슈 #131).
|
|
2
|
+
// version.yml에 flutter_store 저장값이 없는 기존 설치가 대상이다 — 저장값이 없다고 "선택 없음"으로 보면
|
|
3
|
+
// 잘 쓰던 스토어 워크플로우가 선택 해제 정리 규칙에 의해 삭제된다.
|
|
4
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
5
|
+
import { STORE_PLATFORMS, STORE_WORKFLOWS } from "./flutter-options.js";
|
|
6
|
+
|
|
7
|
+
// 반환: 스토어 워크플로우가 하나라도 설치된 플랫폼 (STORE_PLATFORMS 순서).
|
|
8
|
+
export function inferInstalledStores(workflowsDir) {
|
|
9
|
+
if (!existsSync(workflowsDir)) return [];
|
|
10
|
+
const installed = new Set(readdirSync(workflowsDir));
|
|
11
|
+
return STORE_PLATFORMS.filter((platform) => STORE_WORKFLOWS[platform].some((file) => installed.has(file)));
|
|
12
|
+
}
|
package/src/core/version-yml.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { DEFAULT_DEPLOY_STYLE } from "./deploy-style.js";
|
|
2
2
|
import { escapeYamlDoubleQuoted } from "./wizard-env.js";
|
|
3
|
+
import {
|
|
4
|
+
ENV_MODES, DEPLOY_MODES, DEFAULT_ENV_MODE, DEFAULT_DEPLOY_MODE, STORE_PLATFORMS, formatStoreList,
|
|
5
|
+
} from "./flutter-options.js";
|
|
3
6
|
|
|
4
7
|
// version.yml 파싱·생성 (.sh create_version_yml 등가, 전체 재생성 전략 D4).
|
|
5
8
|
// ⚠️ YAML 재직렬화 금지 — 주석이 데이터.
|
|
@@ -34,12 +37,22 @@ export function parseExtraTopLevel(content) {
|
|
|
34
37
|
return blocks;
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
// Flutter 옵션 키(이슈 #131) → 반환 필드. 값은 원문 문자열로 돌려주고, 유효성 판정은 resolveFlutterOptions 몫이다.
|
|
41
|
+
const FLUTTER_OPTION_KEYS = {
|
|
42
|
+
env_mode: "envMode", flutter_store: "flutterStore",
|
|
43
|
+
android_deploy_mode: "androidDeployMode", ios_deploy_mode: "iosDeployMode",
|
|
44
|
+
};
|
|
45
|
+
|
|
37
46
|
// metadata.template.options 상태머신 파싱 (.sh read_template_options L2361~2416 등가).
|
|
38
|
-
// 반환: { nexus: bool|null, secretBackup: bool|null
|
|
47
|
+
// 반환: { nexus: bool|null, secretBackup: bool|null, semverAuto: bool|null, deployStyle: string|null,
|
|
48
|
+
// envMode/flutterStore/androidDeployMode/iosDeployMode: string|null } — null=미기재.
|
|
39
49
|
// 구 synology·coderabbit 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다(파싱 에러 없음).
|
|
40
50
|
// (options-ask.js가 이 함수를 import한다 — 순환 방지 위해 여기(version-yml)에 정의.)
|
|
41
51
|
export function parseTemplateOptions(content) {
|
|
42
|
-
const out = {
|
|
52
|
+
const out = {
|
|
53
|
+
nexus: null, secretBackup: null, semverAuto: null, deployStyle: null,
|
|
54
|
+
envMode: null, flutterStore: null, androidDeployMode: null, iosDeployMode: null,
|
|
55
|
+
};
|
|
43
56
|
// 값 정규화: 따옴표 제거 + 트림 (.sh tr -d '"' | tr -d "'" | xargs 등가)
|
|
44
57
|
// 인라인 주석(` # ...`)을 먼저 떼고 따옴표·공백을 정리한다. 문자열 값을 받는 키(deploy_style)는
|
|
45
58
|
// 주석을 안 떼면 "simple # simple | nginx ..." 가 통째로 값이 된다.
|
|
@@ -66,6 +79,8 @@ export function parseTemplateOptions(content) {
|
|
|
66
79
|
}
|
|
67
80
|
m = line.match(/^\s+deploy_style:\s*(.+)/);
|
|
68
81
|
if (m) { const v = strip(m[1]); if (v) out.deployStyle = v; continue; }
|
|
82
|
+
m = line.match(/^\s+(env_mode|flutter_store|android_deploy_mode|ios_deploy_mode):\s*(.+)/);
|
|
83
|
+
if (m) { const v = strip(m[2]); if (v) out[FLUTTER_OPTION_KEYS[m[1]]] = v; continue; }
|
|
69
84
|
m = line.match(/^\s+semver_auto:\s*(.+)/);
|
|
70
85
|
if (m) {
|
|
71
86
|
const v = strip(m[1]);
|
|
@@ -156,10 +171,24 @@ export function parseTemplateBranches(content) {
|
|
|
156
171
|
return out.main && out.develop && out.mode ? out : null;
|
|
157
172
|
}
|
|
158
173
|
|
|
174
|
+
// Flutter 옵션 블록 (전체 줄 토큰 {{FLUTTER_OPTIONS}} — Flutter 타입일 때만). options 아래 6칸 들여쓰기.
|
|
175
|
+
// 값이 비었으면 워크플로우 템플릿의 기본값과 같은 값으로 채운다 — 저장값과 실제 설치 내용이 어긋나지 않게.
|
|
176
|
+
// stores가 null(미결정)이면 현행 동작대로 둘 다 설치되므로 "android,ios"로 기록한다.
|
|
177
|
+
function buildFlutterOptionsBlock({ envMode, stores, androidDeployMode, iosDeployMode } = {}) {
|
|
178
|
+
const quote = (v) => `"${escapeYamlDoubleQuoted(v)}"`;
|
|
179
|
+
return [
|
|
180
|
+
` env_mode: ${quote(envMode || DEFAULT_ENV_MODE)} # ${ENV_MODES.join(" | ")} (Flutter 환경변수 주입 방식)`,
|
|
181
|
+
` flutter_store: ${quote(formatStoreList(stores ?? STORE_PLATFORMS))} # android | ios | android,ios | none (스토어 배포 대상)`,
|
|
182
|
+
` android_deploy_mode: ${quote(androidDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (Play Store 배포 모드)`,
|
|
183
|
+
` ios_deploy_mode: ${quote(iosDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (iOS 배포 모드)`,
|
|
184
|
+
].join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
159
187
|
// version.yml 전체 생성 — payload/version.yml.template 렌더링.
|
|
160
188
|
// opts: { templateText, version, types:[], paths:Map, pathMarkers?:Map,
|
|
161
189
|
// branch, branches?, versionCode, now, today, templateOptions?, deployValues?,
|
|
162
|
-
// extraTopLevel?:string[]
|
|
190
|
+
// extraTopLevel?:string[], ← 기존 version.yml의 알려지지 않은 최상위 필드 보존 (issue #20 M8)
|
|
191
|
+
// flutterOptions?:{ envMode, stores, androidDeployMode, iosDeployMode } } ← Flutter 타입일 때만 렌더 (이슈 #131)
|
|
163
192
|
// templateText = payload/version.yml.template 원문 (readVersionYmlTemplate — 필수)
|
|
164
193
|
// now = "YYYY-MM-DD HH:MM:SS" (UTC) — 결정성 위해 주입 / today = "YYYY-MM-DD"
|
|
165
194
|
// branches = { main, develop, mode } (resolveBranchConfig 결과. 없으면 branch 기반 기본값)
|
|
@@ -168,7 +197,7 @@ export function parseTemplateBranches(content) {
|
|
|
168
197
|
export function buildVersionYml({
|
|
169
198
|
templateText, version, types = [], paths = new Map(), pathMarkers = new Map(),
|
|
170
199
|
branch = "main", branches = null, versionCode = 1, now, today,
|
|
171
|
-
templateOptions = null, deployValues = new Map(), extraTopLevel = [],
|
|
200
|
+
templateOptions = null, deployValues = new Map(), extraTopLevel = [], flutterOptions = {},
|
|
172
201
|
}) {
|
|
173
202
|
if (!templateText) throw new Error("version.yml.template 원문이 필요합니다 (payload/version.yml.template 누락?)");
|
|
174
203
|
const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(", ")}]` : `["basic"]`;
|
|
@@ -204,6 +233,9 @@ export function buildVersionYml({
|
|
|
204
233
|
deployBlock = rows.join("\n");
|
|
205
234
|
}
|
|
206
235
|
|
|
236
|
+
// Flutter 옵션 블록 (full-line 토큰 {{FLUTTER_OPTIONS}} — Flutter 타입일 때만, 아니면 줄 제거)
|
|
237
|
+
const flutterBlock = types.includes("flutter") ? buildFlutterOptionsBlock(flutterOptions) : "";
|
|
238
|
+
|
|
207
239
|
const scalars = {
|
|
208
240
|
VERSION: version, VERSION_CODE: String(versionCode),
|
|
209
241
|
PROJECT_TYPES: typesJson,
|
|
@@ -219,6 +251,7 @@ export function buildVersionYml({
|
|
|
219
251
|
for (const line of String(templateText).split("\n")) {
|
|
220
252
|
const t = line.trim();
|
|
221
253
|
if (t === "{{PROJECT_PATHS}}") { if (pathsBlock) out.push(pathsBlock); continue; }
|
|
254
|
+
if (t === "{{FLUTTER_OPTIONS}}") { if (flutterBlock) out.push(flutterBlock); continue; }
|
|
222
255
|
if (t === "{{DEPLOY}}") { if (deployBlock) out.push(deployBlock); continue; }
|
|
223
256
|
out.push(line.replace(/\{\{([A-Z][A-Z0-9_]*)\}\}/g, (_, name) => {
|
|
224
257
|
if (name in scalars) return scalars[name];
|
|
@@ -240,10 +273,12 @@ export function buildVersionYml({
|
|
|
240
273
|
export function renderVersionYml(context, templateText, { pathMarkers, deployValues = new Map(), extraTopLevel = [] }) {
|
|
241
274
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
242
275
|
now, today, templateVersion = "unknown", branches = null,
|
|
243
|
-
includeNexus = false, includeSecretBackup = false, includeSemverAuto, deployStyle
|
|
276
|
+
includeNexus = false, includeSecretBackup = false, includeSemverAuto, deployStyle,
|
|
277
|
+
envMode, flutterStore, androidDeployMode, iosDeployMode } = context;
|
|
244
278
|
return buildVersionYml({
|
|
245
279
|
templateText, version, types, paths, pathMarkers, branch, branches, versionCode, now, today,
|
|
246
280
|
deployValues, extraTopLevel,
|
|
281
|
+
flutterOptions: { envMode, stores: flutterStore, androidDeployMode, iosDeployMode },
|
|
247
282
|
templateOptions: {
|
|
248
283
|
templateVersion, includeNexus, includeSecretBackup,
|
|
249
284
|
includeSemverAuto: includeSemverAuto !== false,
|
package/src/core/wizard-env.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// ⚠️ YAML 파싱/재직렬화 금지 — 라인 단위 문자열 처리 (포맷·주석 보존이 unchanged 판정 전제).
|
|
3
3
|
// 실측 기준: template_integrator.sh 3282~3360, 3003~3012.
|
|
4
4
|
|
|
5
|
-
// KEY 정규식: .sh는 [A-Z_]+ (대문자+언더스코어만). ask/auto 마커가 있는 라인만 대상.
|
|
6
|
-
|
|
5
|
+
// KEY 정규식: .sh는 [A-Z_]+ (대문자+언더스코어만). ask/auto/fallback 마커가 있는 라인만 대상.
|
|
6
|
+
// fallback(이슈 #131)은 `KEY: ${{ 런타임값 || 'literal' }}` 표현식 안의 기본 리터럴을 교체하는 마커다.
|
|
7
|
+
const MARKER_RE = /#\s*@wizard\s+(ask|auto|fallback):(.*)$/;
|
|
7
8
|
const KEY_RE = /^(\s*)([A-Z_]+):/;
|
|
8
9
|
const PATHS_ANCHOR_RE = /#\s*@wizard\s+paths-anchor/;
|
|
9
10
|
|
|
10
|
-
// 한 라인을 파싱해 {indent,key,action,arg} 반환. ask/auto 마커 없으면 null.
|
|
11
|
+
// 한 라인을 파싱해 {indent,key,action,arg} 반환. ask/auto/fallback 마커 없으면 null.
|
|
11
12
|
export function parseWizardLine(line) {
|
|
12
13
|
const marker = line.match(MARKER_RE);
|
|
13
14
|
if (!marker) return null;
|
|
@@ -45,8 +46,24 @@ export function setEnvLine(line, key, value) {
|
|
|
45
46
|
return out + cr;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// `KEY: ${{ a || b || 'literal' }} # @wizard fallback:<token>` — 표현식 안의 "마지막 홑따옴표 리터럴"만 교체하고
|
|
50
|
+
// 마커 주석을 제거한다. setEnvLine은 `KEY: "값"` 형태의 따옴표 값만 다루므로 GitHub 표현식은 처리하지 못한다.
|
|
51
|
+
// 리터럴은 `||` 체인의 맨 끝(런타임 입력·저장소 변수가 모두 비었을 때의 기본값)이라 런타임 우선순위는 바뀌지 않는다.
|
|
52
|
+
// value가 빈 문자열이면 줄을 그대로 둔다 (setEnvLine과 같은 규약 — 템플릿 기본값이 남는다).
|
|
53
|
+
const WIZARD_COMMENT_RE = /[^\S\r\n]*#[^\S\r\n]*@wizard[^\S\r\n].*$/;
|
|
54
|
+
const LAST_LITERAL_RE = /^(.*)'[^']*'([^']*)$/;
|
|
55
|
+
export function setFallbackLine(line, value) {
|
|
56
|
+
if (value === "" || value == null) return line;
|
|
57
|
+
const cr = line.endsWith("\r") ? "\r" : "";
|
|
58
|
+
const expression = (cr ? line.slice(0, -1) : line).replace(WIZARD_COMMENT_RE, "");
|
|
59
|
+
if (!LAST_LITERAL_RE.test(expression)) return line;
|
|
60
|
+
const escaped = String(value).replaceAll("'", "''"); // GitHub 표현식 문자열 리터럴의 따옴표 이스케이프
|
|
61
|
+
return expression.replace(LAST_LITERAL_RE, (_m, head, tail) => `${head}'${escaped}'${tail}`) + cr;
|
|
62
|
+
}
|
|
63
|
+
|
|
48
64
|
// resolver — .sh resolve_token 등가. 값 계산은 주입된 resolvers로 위임(순수성 유지).
|
|
49
|
-
// resolvers: { repo, "spring-app-yml-dir"(type), "spring-app-yml-path"(type), "flutter-root"
|
|
65
|
+
// resolvers: { repo, "spring-app-yml-dir"(type), "spring-app-yml-path"(type), "flutter-root",
|
|
66
|
+
// "project-path"(type), "flutter-env-mode", "android-deploy-mode", "ios-deploy-mode" }
|
|
50
67
|
export function resolveToken(name, type, resolvers = {}) {
|
|
51
68
|
const fn = resolvers[name];
|
|
52
69
|
return typeof fn === "function" ? (fn(type) ?? "") : "";
|
|
@@ -80,6 +97,11 @@ export function substituteEnv(content, opts = {}) {
|
|
|
80
97
|
for (let i = 0; i < lines.length; i++) {
|
|
81
98
|
const p = parseWizardLine(lines[i]); // 이미 \r 제거된 라인
|
|
82
99
|
if (!p) continue;
|
|
100
|
+
// fallback은 따옴표 값이 아니라 표현식 안 리터럴을 바꾸므로 ask/auto와 경로가 다르다.
|
|
101
|
+
if (p.action === "fallback") {
|
|
102
|
+
lines[i] = setFallbackLine(lines[i], resolveToken(p.arg, type, resolvers));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
83
105
|
let val = "";
|
|
84
106
|
if (p.action === "auto") {
|
|
85
107
|
val = resolveToken(p.arg, type, resolvers);
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { parseArgs, parsePathsCsv, CliError } from "./cli/args.js";
|
|
|
9
9
|
import { HELP_TEXT } from "./cli/help.js";
|
|
10
10
|
import { createContext } from "./context.js";
|
|
11
11
|
import { DEFAULT_DEPLOY_STYLE, isDeployStyle } from "./core/deploy-style.js";
|
|
12
|
+
import { resolveFlutterOptions } from "./core/flutter-options.js";
|
|
12
13
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "./core/assets.js";
|
|
13
14
|
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectBuildNumber, detectMarkers } from "./core/detect-fs.js";
|
|
14
15
|
import { parseExisting } from "./core/version-yml.js";
|
|
@@ -268,6 +269,16 @@ async function runInner(argv, {
|
|
|
268
269
|
}
|
|
269
270
|
}
|
|
270
271
|
|
|
272
|
+
// Flutter 옵션 — CLI 플래그 → version.yml 저장값 → 기본값(신규 dart-define, 기존 설치 dotenv 보존).
|
|
273
|
+
// Flutter 타입이 없는 프로젝트에서는 렌더·치환 단계가 전부 무시한다.
|
|
274
|
+
const flutterOptions = resolveFlutterOptions({
|
|
275
|
+
cli: {
|
|
276
|
+
envMode: opts.flutterEnvMode, stores: opts.flutterStore,
|
|
277
|
+
androidDeployMode: opts.androidDeployMode, iosDeployMode: opts.iosDeployMode,
|
|
278
|
+
},
|
|
279
|
+
existing,
|
|
280
|
+
});
|
|
281
|
+
|
|
271
282
|
const context = createContext({
|
|
272
283
|
mode: opts.mode, force: opts.force, types, version, versionCode, branch,
|
|
273
284
|
branches,
|
|
@@ -281,13 +292,17 @@ async function runInner(argv, {
|
|
|
281
292
|
includeSemverAuto: opts.includeSemverAuto ?? existing?.options?.semverAuto ?? (existing ? false : true),
|
|
282
293
|
repoName,
|
|
283
294
|
// 실 resolver 4종 (.sh resolve_token 등가)
|
|
284
|
-
resolvers: makeResolvers(cwd, repoName, paths),
|
|
295
|
+
resolvers: makeResolvers(cwd, repoName, paths, flutterOptions),
|
|
285
296
|
now, today,
|
|
286
297
|
// 설치 로그(#79)용 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
287
298
|
markers: detectMarkers(cwd, types), detectWarnings,
|
|
288
299
|
deployStyle: opts.deployStyle
|
|
289
300
|
|| (isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : DEFAULT_DEPLOY_STYLE),
|
|
290
301
|
previousTemplateVersion: existing?.templateVersion || "",
|
|
302
|
+
envMode: flutterOptions.envMode,
|
|
303
|
+
flutterStore: flutterOptions.stores,
|
|
304
|
+
androidDeployMode: flutterOptions.androidDeployMode,
|
|
305
|
+
iosDeployMode: flutterOptions.iosDeployMode,
|
|
291
306
|
});
|
|
292
307
|
|
|
293
308
|
context.templateVersion = readTemplateVersion();
|
|
@@ -319,7 +334,23 @@ async function runInner(argv, {
|
|
|
319
334
|
logPath: currentLogPath(),
|
|
320
335
|
legacyMdLogs: hasLegacyMdLogs(cwd),
|
|
321
336
|
cleanup: result?.cleanup ?? null,
|
|
337
|
+
storeCleanup: result?.storeCleanup ?? null,
|
|
338
|
+
flutterApp: result?.flutterApp ?? null,
|
|
322
339
|
});
|
|
340
|
+
// store_submit 배포 모드는 main push마다 심사를 자동 제출한다 — 비대화형에서도 같은 경고를 보여준다
|
|
341
|
+
// (대화형 경로는 ui/prompts.js#deployModeWarning을 선택 시점에 note로 보여준다).
|
|
342
|
+
// Flutter 타입이 아니거나 해당 스토어를 선택하지 않은 프로젝트에는 뜨면 안 된다 (fable5.1 Important #2).
|
|
343
|
+
const { stores } = flutterOptions;
|
|
344
|
+
const warnings = [];
|
|
345
|
+
if (types.includes("flutter") && (stores === null || stores.includes("android"))) {
|
|
346
|
+
warnings.push(prompts.deployModeWarning(flutterOptions.androidDeployMode));
|
|
347
|
+
}
|
|
348
|
+
if (types.includes("flutter") && (stores === null || stores.includes("ios"))) {
|
|
349
|
+
warnings.push(prompts.deployModeWarning(flutterOptions.iosDeployMode));
|
|
350
|
+
}
|
|
351
|
+
for (const w of warnings) {
|
|
352
|
+
if (w) console.error(`⚠️ ${w}`);
|
|
353
|
+
}
|
|
323
354
|
return 0;
|
|
324
355
|
}
|
|
325
356
|
|
package/src/ui/env-plan.js
CHANGED
|
@@ -11,6 +11,7 @@ import { exists, listYamlFiles } from "../core/fsutil.js";
|
|
|
11
11
|
import { parseWizardLine, resolveToken, replaceProjectTokens } from "../core/wizard-env.js";
|
|
12
12
|
import { loadWizardPrompts, wfField, workflowDisplayName } from "../core/wizard-labels.js";
|
|
13
13
|
import { deployFilter, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
14
|
+
import { buildTypeRootFilter } from "../core/copy/workflows.js";
|
|
14
15
|
import * as engine from "./readline-engine.js";
|
|
15
16
|
|
|
16
17
|
const CANCEL = engine.CANCEL;
|
|
@@ -35,11 +36,12 @@ export function scopeString(usages = []) {
|
|
|
35
36
|
// opts:
|
|
36
37
|
// resolvers - @접두 기본값(@repo 등) 해석용 (.sh는 수집 시점에 resolve_token — 동일)
|
|
37
38
|
// includeNexus - true면 server-deploy 제외 + nexus/ 포함 (복사 엔진과 스캔 범위 일치)
|
|
39
|
+
// flutterStore - Flutter 스토어 대상(string[]|null). 배열이면 선택 해제된 스토어 워크플로우는 스캔에서 제외 (null=현행)
|
|
38
40
|
// prompts - wizard-labels 파싱 객체 (워크플로우 표시명용, null이면 확장자 제거 폴백)
|
|
39
41
|
// 반환: { keys:[], defaults:Map<key,default>, typeDefaults:Map<"type|key",default>,
|
|
40
42
|
// usages:Map<key,[{type,workflowName}]> }
|
|
41
43
|
export function collectAsks(payloadRoot, types = [], opts = {}) {
|
|
42
|
-
const { resolvers = {}, includeNexus = false, includeSecretBackup = false, deployStyle = "", prompts = null } = opts;
|
|
44
|
+
const { resolvers = {}, includeNexus = false, includeSecretBackup = false, deployStyle = "", flutterStore = null, prompts = null } = opts;
|
|
43
45
|
// 설치하지 않을 배포 워크플로우의 질문까지 묻지 않는다 — 질문 수는 설치 범위를 따라간다.
|
|
44
46
|
const keepDeploy = deployFilter(deployStyle);
|
|
45
47
|
const baseDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
@@ -65,7 +67,8 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
|
|
|
65
67
|
// 타입 루트 스캔에도 keepDeploy를 걸어야 CD 전용 ask 키(예: DEPLOY_PORT)가 걸러진다.
|
|
66
68
|
// PR 프리뷰 자체의 ask 키(SSH_AUTH_METHOD 등)는 이 필터로는 걸러지지 않는다 — PR 프리뷰는
|
|
67
69
|
// 배포 방식과 무관하게 항상 설치되는 별도 축이라 의도된 잔여 범위다.
|
|
68
|
-
|
|
70
|
+
// Flutter는 선택 해제된 스토어 워크플로우(PLAYSTORE·TESTFLIGHT)도 같은 필터로 걸러 질문 범위가 설치 범위와 같다.
|
|
71
|
+
units.push([type, typeDir, buildTypeRootFilter(type, deployStyle, flutterStore)]);
|
|
69
72
|
if (includeNexus) {
|
|
70
73
|
units.push([type, join(typeDir, "nexus"), null]);
|
|
71
74
|
} else if (deployStyle !== NO_DEPLOY_STYLE) {
|
|
@@ -183,17 +186,17 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
|
|
|
183
186
|
// - useDefaults=false → values에 담긴 키만 사용자 확정값으로 치환, 나머지는 기본값
|
|
184
187
|
// (⚠️ substituteEnv는 useDefaults=false일 때만 values를 참조하므로 이 플래그를 반드시 함께 전달)
|
|
185
188
|
// 인자:
|
|
186
|
-
// payloadRoot/types/resolvers/includeNexus — collectAsks와 동일 의미
|
|
189
|
+
// payloadRoot/types/resolvers/includeNexus/flutterStore — collectAsks와 동일 의미
|
|
187
190
|
// targetRoot — wizard-prompts.yml 1차 탐색 위치(기본 ".")
|
|
188
191
|
// force — true면 질문 없이 전부 기본값 (.sh FORCE_MODE 등가)
|
|
189
192
|
// io — {select, multiselect, text} 주입 (기본 readline-engine). 테스트 스텁 지점.
|
|
190
193
|
// log — 카드·안내 출력 함수 주입 (기본 stderr)
|
|
191
194
|
export async function promptEnvPlan({
|
|
192
195
|
payloadRoot, types = [], io = null, force = false, resolvers = {},
|
|
193
|
-
includeNexus = false, includeSecretBackup = false, deployStyle = "", targetRoot = ".", repoName = "", log = defaultLog,
|
|
196
|
+
includeNexus = false, includeSecretBackup = false, deployStyle = "", flutterStore = null, targetRoot = ".", repoName = "", log = defaultLog,
|
|
194
197
|
} = {}) {
|
|
195
198
|
const prompts = loadWizardPrompts(targetRoot, payloadRoot);
|
|
196
|
-
const asks = collectAsks(payloadRoot, types, { resolvers, includeNexus, includeSecretBackup, deployStyle, prompts });
|
|
199
|
+
const asks = collectAsks(payloadRoot, types, { resolvers, includeNexus, includeSecretBackup, deployStyle, flutterStore, prompts });
|
|
197
200
|
const defaults = asks.defaults;
|
|
198
201
|
|
|
199
202
|
// 수집 키 0개 → 질문 자체가 없음 (.sh `[ ${#WF_ASK_KEYS[@]} -eq 0 ]` 등가)
|