project-auto-wizard 0.9.1 → 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/scripts/changelog_manager.py +2 -2
- package/payload/scripts/version_manager.py +2 -2
- package/payload/version.yml.template +1 -0
- package/payload/workflows/common/secret-backup/PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml +12 -12
- 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 +33 -46
- package/payload/workflows/flutter/{PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml → PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml} +38 -38
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +123 -32
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +57 -42
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +61 -40
- package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -0
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +40 -40
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +47 -0
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +47 -0
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +40 -40
- 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/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +39 -39
- 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/verify.js +2 -2
- 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
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 ]` 등가)
|
package/src/ui/prompts.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 취소(ESC/Ctrl+C)는 각 함수가 CANCEL 심볼을 반환 → 호출부가 정상 종료(exit 0) 처리.
|
|
4
4
|
import * as engine from "./readline-engine.js";
|
|
5
5
|
import { DEPLOY_STYLES, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
6
|
+
import { ENV_MODES, DEFAULT_ENV_MODE, STORE_PLATFORMS, DEPLOY_MODES, DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
|
|
6
7
|
|
|
7
8
|
export const CANCEL = engine.CANCEL;
|
|
8
9
|
|
|
@@ -33,8 +34,10 @@ export async function confirmProjectMenu() {
|
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
// 수정 메뉴 —
|
|
37
|
-
|
|
37
|
+
// 수정 메뉴 항목 — showOptional=full/workflows에서만 nexus/secret 노출,
|
|
38
|
+
// showFlutter=Flutter 타입일 때만 환경변수 방식/스토어 배포 대상/배포 모드 노출 (이슈 #131).
|
|
39
|
+
// 라벨·순서를 테스트할 수 있도록 순수 함수로 분리했다.
|
|
40
|
+
export function editMenuOptions({ showOptional = false, showFlutter = false } = {}) {
|
|
38
41
|
const options = [
|
|
39
42
|
{ value: "type", label: "프로젝트 타입" },
|
|
40
43
|
{ value: "version", label: "버전" },
|
|
@@ -44,8 +47,18 @@ export async function editMenu({ showOptional = false } = {}) {
|
|
|
44
47
|
options.push({ value: "nexus", label: "Nexus publish 포함 여부" });
|
|
45
48
|
options.push({ value: "secret", label: "Secret 백업 포함 여부" });
|
|
46
49
|
}
|
|
50
|
+
if (showFlutter) {
|
|
51
|
+
options.push({ value: "envMode", label: "환경변수 방식" });
|
|
52
|
+
options.push({ value: "flutterStore", label: "스토어 배포 대상" });
|
|
53
|
+
options.push({ value: "deployMode", label: "배포 모드" });
|
|
54
|
+
}
|
|
47
55
|
options.push({ value: "done", label: "모두 맞음, 계속" });
|
|
48
|
-
return
|
|
56
|
+
return options;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 수정 메뉴 — 어떤 항목을 고칠지.
|
|
60
|
+
export async function editMenu({ showOptional = false, showFlutter = false } = {}) {
|
|
61
|
+
return engine.select({ message: "어떤 항목을 수정할까요?", options: editMenuOptions({ showOptional, showFlutter }) });
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
const ALL_TYPES = ["spring", "flutter", "next", "react", "react-native", "react-native-expo", "node", "python", "basic", "go"];
|
|
@@ -103,6 +116,78 @@ export async function selectDeployStyle() {
|
|
|
103
116
|
});
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
// ── Flutter 옵션 (이슈 #131) ─────────────────────────────────────────
|
|
120
|
+
// 프로젝트 타입에 flutter가 포함된 경우에만 interactive.js가 묻는다. 세 함수 모두 취소(ESC) 시 CANCEL을
|
|
121
|
+
// 그대로 돌려주고, "ESC = 기본값" 처리는 호출부가 한다 (selectDeployStyle과 같은 규약).
|
|
122
|
+
const ENV_MODE_LABELS = {
|
|
123
|
+
"dart-define": "dart-define (신규 설치 기본) — 시크릿을 --dart-define-from-file로 빌드에 전달, 프로젝트에 .env를 만들지 않음",
|
|
124
|
+
dotenv: "dotenv — Flutter 루트에 .env를 만들어 flutter_dotenv·envied가 읽음 (기존 설치 유지값)",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const STORE_LABELS = {
|
|
128
|
+
android: "Android — Google Play Store (fastlane)",
|
|
129
|
+
ios: "iOS — TestFlight / App Store Connect (fastlane)",
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const DEPLOY_MODE_LABELS = {
|
|
133
|
+
android: {
|
|
134
|
+
store_only: "store_only — internal 트랙에 업로드 (기본)",
|
|
135
|
+
store_prepare: "store_prepare — production 트랙에 draft로 업로드 (Play Console에서 직접 출시)",
|
|
136
|
+
store_submit: "store_submit — production 심사 제출까지",
|
|
137
|
+
},
|
|
138
|
+
ios: {
|
|
139
|
+
store_only: "store_only — TestFlight 업로드 (기본)",
|
|
140
|
+
store_prepare: "store_prepare — 앱 버전·메타데이터 준비까지 (심사 제출 안 함)",
|
|
141
|
+
store_submit: "store_submit — 심사 제출까지",
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const PLATFORM_TITLES = { android: "Android (Play Store)", ios: "iOS (TestFlight)" };
|
|
146
|
+
|
|
147
|
+
// 환경변수 방식 — 시크릿 ENV_FILE(.env 형식)을 Flutter 빌드에 넘기는 방법.
|
|
148
|
+
export async function selectEnvMode({ initialValue = DEFAULT_ENV_MODE } = {}) {
|
|
149
|
+
engine.note(
|
|
150
|
+
"시크릿 ENV_FILE(.env 형식)을 Flutter 빌드에 넘기는 방식입니다.\n" +
|
|
151
|
+
"dart-define은 String.fromEnvironment로 읽고, dotenv는 flutter_dotenv·envied가 .env 파일을 읽습니다.\n" +
|
|
152
|
+
"나중에 확인 화면의 '수정하기 > 환경변수 방식'에서 바꿀 수 있습니다.",
|
|
153
|
+
"환경변수 방식",
|
|
154
|
+
);
|
|
155
|
+
return engine.select({
|
|
156
|
+
message: "Flutter 환경변수는 어떤 방식으로 넘길까요?",
|
|
157
|
+
options: ENV_MODES.map((value) => ({ value, label: ENV_MODE_LABELS[value] })),
|
|
158
|
+
initialIndex: Math.max(0, ENV_MODES.indexOf(initialValue)),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 스토어 배포 대상 — 고른 플랫폼의 워크플로우와 fastlane 템플릿만 설치한다. 아무것도 안 고르면 스토어 배포 없이 설치.
|
|
163
|
+
export async function selectFlutterStores({ initialValues = [] } = {}) {
|
|
164
|
+
engine.note(
|
|
165
|
+
"고른 플랫폼의 스토어 배포 워크플로우와 fastlane 파일(Fastfile 등)만 설치합니다.\n" +
|
|
166
|
+
"아무것도 고르지 않으면 스토어 배포 없이 설치합니다 (Firebase·Selfhosted·Test APK·CI는 항상 설치).",
|
|
167
|
+
"스토어 배포 대상",
|
|
168
|
+
);
|
|
169
|
+
return engine.multiselect({
|
|
170
|
+
message: "스토어에 배포할 플랫폼을 선택하세요 (Space 토글, Enter 확정)",
|
|
171
|
+
options: STORE_PLATFORMS.map((value) => ({ value, label: STORE_LABELS[value] })),
|
|
172
|
+
initialValues,
|
|
173
|
+
required: false,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 배포 모드 — 플랫폼별로 한 번씩 묻는다. 런타임의 저장소 변수와 workflow_dispatch 입력이 항상 이 값보다 우선한다.
|
|
178
|
+
export async function selectDeployMode({ platform, initialValue = DEFAULT_DEPLOY_MODE }) {
|
|
179
|
+
return engine.select({
|
|
180
|
+
message: `${PLATFORM_TITLES[platform]} 배포 모드를 선택하세요`,
|
|
181
|
+
options: DEPLOY_MODES.map((value) => ({ value, label: DEPLOY_MODE_LABELS[platform][value] })),
|
|
182
|
+
initialIndex: Math.max(0, DEPLOY_MODES.indexOf(initialValue)),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// store_submit은 main push마다 심사를 제출하므로 고른 직후 한 줄로 알린다. 호출부(interactive)가 note로 출력한다.
|
|
187
|
+
export function deployModeWarning(mode) {
|
|
188
|
+
return mode === "store_submit" ? "store_submit을 고르면 main push마다 심사가 자동 제출됩니다." : "";
|
|
189
|
+
}
|
|
190
|
+
|
|
106
191
|
// 브랜치 전략 선택 (이슈 #93). 종전에는 "릴리스 브랜치"/"개발 브랜치" 두 질문에
|
|
107
192
|
// 같은 이름을 입력해야만 trunk-based가 됐는데, 그 규칙이 사전에 안내되지 않아
|
|
108
193
|
// 의도치 않게 pr-flow로 흘러갔다. 전략을 먼저 명시적으로 고르게 해 이를 없앤다.
|
package/src/ui/status-cards.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (층5의 Breaking Changes 박스는 core/breaking-check.js가 담당.
|
|
3
3
|
// 원본의 층4 IDE Skills 상태는 project-auto-wizard 스코프 제외 — Agent Skills 미포함)
|
|
4
4
|
import { A, paint } from "./ansi.js";
|
|
5
|
+
import { DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
|
|
5
6
|
|
|
6
7
|
const GUT = paint("│", A.gray);
|
|
7
8
|
const HEAD = paint("◆", A.cyan);
|
|
@@ -29,7 +30,8 @@ export function printDetectionLog({ types = [], version = "", branch = "", marke
|
|
|
29
30
|
|
|
30
31
|
// 층3 — 프로젝트 분석 개요 카드 (.ps1 Print-ProjectAnalysis 등가+)
|
|
31
32
|
export function printAnalysisCard({ mode = "", modeLabel = "", types = [], version = "", branch = "",
|
|
32
|
-
includeNexus = null, includeSecretBackup = null, paths = new Map(), showOptional = false
|
|
33
|
+
includeNexus = null, includeSecretBackup = null, paths = new Map(), showOptional = false,
|
|
34
|
+
flutter = null, envModeDefault = "" },
|
|
33
35
|
out = (s) => process.stderr.write(s)) {
|
|
34
36
|
out(`${HEAD} ${paint("프로젝트 분석 결과", A.bold)}\n`);
|
|
35
37
|
const row = (icon, label, value) => out(`${GUT} ${icon} ${label.padEnd(10)} ${value}\n`);
|
|
@@ -40,6 +42,16 @@ export function printAnalysisCard({ mode = "", modeLabel = "", types = [], versi
|
|
|
40
42
|
if (showOptional) {
|
|
41
43
|
row("📦", "Nexus", includeNexus === true ? paint("포함", A.green) : paint("제외", A.dim));
|
|
42
44
|
row("🔐", "Secret백업", includeSecretBackup === true ? paint("포함", A.green) : paint("제외", A.dim));
|
|
45
|
+
// Flutter 옵션 (이슈 #131 fable5.1 리뷰 Important #1) — 확정 직전 화면에서도 선택값을 보여준다.
|
|
46
|
+
if (flutter && types.includes("flutter")) {
|
|
47
|
+
const stores = flutter.stores ?? [];
|
|
48
|
+
const modeParts = stores.map(
|
|
49
|
+
(p) => `${p}=${(p === "android" ? flutter.androidDeployMode : flutter.iosDeployMode) || DEFAULT_DEPLOY_MODE}`,
|
|
50
|
+
);
|
|
51
|
+
row("⚙️", "환경변수", flutter.envMode || envModeDefault);
|
|
52
|
+
row("🏬", "스토어", stores.length ? stores.join(", ") : "없음");
|
|
53
|
+
row("🚀", "배포모드", modeParts.length ? modeParts.join(" ") : "없음");
|
|
54
|
+
}
|
|
43
55
|
}
|
|
44
56
|
// 모노레포 경로 — 루트가 아닌 항목이 하나라도 있으면 표시
|
|
45
57
|
const nonRoot = [...paths.entries()].filter(([, p]) => p && p !== ".");
|
package/src/ui/summary.js
CHANGED
|
@@ -8,7 +8,9 @@ const SEPARATOR = "────────────────────
|
|
|
8
8
|
export function printSummary(ctx) {
|
|
9
9
|
const { mode, types = [], version = "", versionCode = null, copiedFiles = [], branches = null, gitignoreUpdated = false,
|
|
10
10
|
// 설치 후 검증·기록 (#79, #80, #81)
|
|
11
|
-
answers = [], unresolved = [], secrets = new Map(), logPath = "", legacyMdLogs = false, cleanup = null
|
|
11
|
+
answers = [], unresolved = [], secrets = new Map(), logPath = "", legacyMdLogs = false, cleanup = null,
|
|
12
|
+
// Flutter 스토어 배포 (이슈 #131) — 앱 파일 생성/유지와 스토어 선택 해제 정리 결과
|
|
13
|
+
flutterApp = null, storeCleanup = null } = ctx || {};
|
|
12
14
|
const err = (s = "") => process.stderr.write(`${s}\n`);
|
|
13
15
|
// 색상은 ansi.js의 공용 가드로 통일 (NO_COLOR + stderr TTY 여부)
|
|
14
16
|
const enabled = colorEnabled(process.stderr);
|
|
@@ -105,10 +107,18 @@ export function printSummary(ctx) {
|
|
|
105
107
|
err("");
|
|
106
108
|
}
|
|
107
109
|
// 배포 방식을 바꿔 재설치한 경우, 이전 CD를 어떻게 처리했는지 알린다 (#80).
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
printCleanup(err, enabled, "이전 배포 방식 정리", cleanup);
|
|
111
|
+
// 스토어 배포 대상을 해제한 경우도 같은 규칙으로 정리한 결과를 알린다 (#131).
|
|
112
|
+
printCleanup(err, enabled, "선택 해제한 스토어 배포 정리", storeCleanup);
|
|
113
|
+
// Fastfile·ExportOptions.plist는 사용자 소유라 없을 때만 만든다 — 만든 것과 그대로 둔 것을 나눠 보여준다 (#131).
|
|
114
|
+
const { created: appCreated = [], kept: appKept = [] } = flutterApp || {};
|
|
115
|
+
if (appCreated.length || appKept.length) {
|
|
116
|
+
err(" 📱 Flutter 스토어 배포 파일:");
|
|
117
|
+
for (const f of appCreated) err(` • ${f} ${paint("새로 생성", A.dim, enabled)}`);
|
|
118
|
+
for (const f of appKept) err(` • ${f} ${paint("기존 파일 유지", A.dim, enabled)}`);
|
|
119
|
+
if (appCreated.some((f) => f.endsWith("ExportOptions.plist"))) {
|
|
120
|
+
err(" → ExportOptions.plist의 __TEAM_ID__ · __BUNDLE_ID__ · __PROVISIONING_PROFILE_NAME__ 을 실제 값으로 채워야 iOS 배포가 동작합니다");
|
|
121
|
+
}
|
|
112
122
|
err("");
|
|
113
123
|
}
|
|
114
124
|
if (logPath) {
|
|
@@ -175,3 +185,12 @@ export function printSummary(ctx) {
|
|
|
175
185
|
err(paint("📖 워크플로우 구성과 릴리스 흐름은 README를 참고하세요.", A.cyan, enabled));
|
|
176
186
|
err("");
|
|
177
187
|
}
|
|
188
|
+
|
|
189
|
+
// 삭제·백업 정리 결과 한 블록 — 이전 배포 방식 정리와 스토어 선택 해제 정리가 같은 형식을 쓴다.
|
|
190
|
+
function printCleanup(err, enabled, title, cleanup) {
|
|
191
|
+
if (!cleanup?.removed?.length && !cleanup?.backedUp?.length) return;
|
|
192
|
+
err(` 🧹 ${title}:`);
|
|
193
|
+
for (const f of cleanup.removed || []) err(` • ${f} ${paint("삭제 (손대지 않은 파일)", A.dim, enabled)}`);
|
|
194
|
+
for (const f of cleanup.backedUp || []) err(` • ${f} → ${f}.bak ${paint("수정하신 내용이 있어 백업", A.dim, enabled)}`);
|
|
195
|
+
err("");
|
|
196
|
+
}
|