project-auto-wizard 0.13.1 → 0.13.2
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 +1 -1
- package/package.json +1 -1
- package/payload/version.yml.template +1 -1
- package/src/cli/args.js +9 -37
- package/src/commands/full.js +3 -5
- package/src/commands/install-settings.js +4 -6
- package/src/commands/interactive.js +1 -1
- package/src/commands/status.js +4 -7
- package/src/context.js +3 -7
- package/src/core/flutter-hooks.js +97 -1
- package/src/core/flutter-options.js +5 -0
- package/src/core/types.js +19 -1
- package/src/core/version-yml.js +15 -33
- package/src/index.js +14 -33
- package/src/ui/prompts.js +3 -5
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Everything it installs is plain GitHub Actions in your own repository. No API ke
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
24
|
-
## Latest Version : v0.13.
|
|
24
|
+
## Latest Version : v0.13.2 (2026-09-30)
|
|
25
25
|
|
|
26
26
|
[Full release history](CHANGELOG.md)
|
|
27
27
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-auto-wizard",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "One command DevOps: npx wizard that installs GitHub-native Release Automation (optional GitHub Copilot AI summaries) into any project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/cli/args.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
import { VALID_TYPES, VALID_MODES } from "../context.js";
|
|
3
3
|
import { DEPLOY_STYLES, isDeployStyle, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
4
4
|
import { isValidBranchName } from "../core/branches.js";
|
|
5
|
-
import {
|
|
6
|
-
ENV_MODES, DEPLOY_MODES, STORE_PLATFORMS, NO_STORE, isEnvMode, isDeployMode, parseStoreList,
|
|
7
|
-
} from "../core/flutter-options.js";
|
|
5
|
+
import { TYPES } from "../core/types.js";
|
|
8
6
|
import { CliError } from "../core/errors.js";
|
|
9
7
|
import { normalizePath, isRepoRelativePath } from "../core/paths.js";
|
|
10
8
|
|
|
11
9
|
// 기존 import 경로(cli/args.js) 호환 — 정의는 core에 있다.
|
|
12
10
|
export { CliError, normalizePath, isRepoRelativePath };
|
|
13
11
|
|
|
12
|
+
// 타입 전용 CLI 플래그(타입 훅 cliFlags) — 플래그 이름 → { field, initial, parse }. 훅은 타입 표 순서대로 모은다.
|
|
13
|
+
export const TYPE_CLI_FLAGS = TYPES.flatMap((t) => t.hooks?.cliFlags ?? []);
|
|
14
|
+
const TYPE_CLI_FLAG_BY_NAME = new Map(TYPE_CLI_FLAGS.map((f) => [f.flag, f]));
|
|
15
|
+
|
|
14
16
|
// argv(process.argv.slice(2)) → 파싱 결과. 오류 시 throw(호출부에서 exit 1).
|
|
15
17
|
export function parseArgs(argv) {
|
|
16
18
|
const result = {
|
|
@@ -24,10 +26,8 @@ export function parseArgs(argv) {
|
|
|
24
26
|
mainBranch: "", // 릴리스 브랜치 (--main-branch). 빈값=감지된 default branch
|
|
25
27
|
developBranch: "", // 개발 브랜치 (--develop-branch). 빈값=develop
|
|
26
28
|
deployStyle: "", // 서버 배포 방식 (--deploy-style). 빈값=version.yml 저장값 → simple
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
androidDeployMode: "", // Play Store 배포 모드 (--android-deploy-mode). 빈값=저장값 → store_only
|
|
30
|
-
iosDeployMode: "", // iOS 배포 모드 (--ios-deploy-mode). 빈값=저장값 → store_only
|
|
29
|
+
// 타입 전용 플래그 필드(예: Flutter 환경변수 방식·스토어 대상·배포 모드) — 미지정 값은 타입 훅이 정한다.
|
|
30
|
+
...Object.fromEntries(TYPE_CLI_FLAGS.map((f) => [f.field, f.initial])),
|
|
31
31
|
force: false,
|
|
32
32
|
help: false,
|
|
33
33
|
showVersion: false, // -v/--version → 패키지 버전 출력 (npm 관례)
|
|
@@ -49,6 +49,8 @@ export function parseArgs(argv) {
|
|
|
49
49
|
const seenFlags = new Set(); // --semver-auto/--copilot류 상호 모순 플래그 검증용
|
|
50
50
|
while (args.length > 0) {
|
|
51
51
|
const a = args.shift();
|
|
52
|
+
const typeFlag = TYPE_CLI_FLAG_BY_NAME.get(a);
|
|
53
|
+
if (typeFlag) { result[typeFlag.field] = typeFlag.parse(args.shift()); continue; }
|
|
52
54
|
switch (a) {
|
|
53
55
|
case "-m": case "--mode":
|
|
54
56
|
result.mode = args.shift() ?? ""; break;
|
|
@@ -103,36 +105,6 @@ export function parseArgs(argv) {
|
|
|
103
105
|
}
|
|
104
106
|
result.deployStyle = v; break;
|
|
105
107
|
}
|
|
106
|
-
case "--flutter-env-mode": {
|
|
107
|
-
const v = args.shift();
|
|
108
|
-
if (!isEnvMode(v)) {
|
|
109
|
-
throw new CliError(`--flutter-env-mode 값이 올바르지 않습니다: ${v ?? "(없음)"} (${ENV_MODES.join(" | ")})`);
|
|
110
|
-
}
|
|
111
|
-
result.flutterEnvMode = v; break;
|
|
112
|
-
}
|
|
113
|
-
case "--flutter-store": {
|
|
114
|
-
const v = args.shift();
|
|
115
|
-
// 빈 문자열은 parseStoreList가 []로 보지만, 스토어를 안 고르겠다는 뜻은 명시적인 none으로만 받는다.
|
|
116
|
-
const stores = v ? parseStoreList(v) : null;
|
|
117
|
-
if (stores === null) {
|
|
118
|
-
throw new CliError(`--flutter-store 값이 올바르지 않습니다: ${v || "(없음)"} (${[STORE_PLATFORMS.join(","), ...STORE_PLATFORMS, NO_STORE].join(" | ")})`);
|
|
119
|
-
}
|
|
120
|
-
result.flutterStore = stores; break;
|
|
121
|
-
}
|
|
122
|
-
case "--android-deploy-mode": {
|
|
123
|
-
const v = args.shift();
|
|
124
|
-
if (!isDeployMode(v)) {
|
|
125
|
-
throw new CliError(`--android-deploy-mode 값이 올바르지 않습니다: ${v ?? "(없음)"} (${DEPLOY_MODES.join(" | ")})`);
|
|
126
|
-
}
|
|
127
|
-
result.androidDeployMode = v; break;
|
|
128
|
-
}
|
|
129
|
-
case "--ios-deploy-mode": {
|
|
130
|
-
const v = args.shift();
|
|
131
|
-
if (!isDeployMode(v)) {
|
|
132
|
-
throw new CliError(`--ios-deploy-mode 값이 올바르지 않습니다: ${v ?? "(없음)"} (${DEPLOY_MODES.join(" | ")})`);
|
|
133
|
-
}
|
|
134
|
-
result.iosDeployMode = v; break;
|
|
135
|
-
}
|
|
136
108
|
case "--semver-auto":
|
|
137
109
|
if (seenFlags.has("--no-semver-auto")) throw new CliError("--semver-auto와 --no-semver-auto는 동시에 지정할 수 없습니다");
|
|
138
110
|
seenFlags.add("--semver-auto"); result.includeSemverAuto = true; break;
|
package/src/commands/full.js
CHANGED
|
@@ -198,16 +198,14 @@ export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { j
|
|
|
198
198
|
return { cleanup, storeCleanup, staleCleanup };
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
// 설치 결과를 가른 선택(배포 방식·자동 승격·Copilot
|
|
201
|
+
// 설치 결과를 가른 선택(배포 방식·자동 승격·Copilot·타입 전용 옵션)을 남긴다 — 대화형에서 고른 값도
|
|
202
202
|
// 여기로 모이므로, 나중에 "왜 이렇게 설치됐나"를 로그만으로 따라갈 수 있다.
|
|
203
203
|
function logChoices(context, types) {
|
|
204
204
|
if (context.deployStyle) log.info("option", "deploy", context.deployStyle);
|
|
205
205
|
if (context.includeSemverAuto != null) log.info("option", "semver", context.includeSemverAuto ? "on" : "off");
|
|
206
206
|
if (context.includeCopilotAi != null) log.info("option", "copilot", context.includeCopilotAi ? "on" : "off");
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
log.info("option", "flutter",
|
|
210
|
-
`env=${context.envMode || "-"} stores=${stores} android=${context.androidDeployMode || "-"} ios=${context.iosDeployMode || "-"}`);
|
|
207
|
+
for (const { hook } of hooksFor(types, "logChoices")) {
|
|
208
|
+
for (const [name, value] of hook(context)) log.info("option", name, value);
|
|
211
209
|
}
|
|
212
210
|
}
|
|
213
211
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 설치 컨텍스트로 조립하는 규칙은 한 곳에 둬야 한쪽만 고쳐 두 모드의 결과가 갈라지지 않는다.
|
|
4
4
|
import { detectVersion, detectBuildNumber } from "../core/detect-fs.js";
|
|
5
5
|
import { createContext } from "../context.js";
|
|
6
|
+
import { mergeHookResults } from "../core/types.js";
|
|
6
7
|
import { isDeployStyle, DEFAULT_DEPLOY_STYLE, hasServerDeployWorkflows, effectiveDeployStyle } from "../core/deploy-style.js";
|
|
7
8
|
|
|
8
9
|
// version.yml에 저장된 배포 방식 — 없거나 알 수 없는 값이면 "".
|
|
@@ -29,21 +30,18 @@ export function resolveVersionCode({ cwd, existing, types, paths }) {
|
|
|
29
30
|
|
|
30
31
|
// 확정된 값들을 설치 컨텍스트로 조립한다.
|
|
31
32
|
// deployStyle: resolveDeployStyle 결과(고른 값). 실제 설치되는 방식으로 바꿔 기록한다.
|
|
32
|
-
//
|
|
33
|
+
// typeOptions: 타입 훅(resolveOptions)이 확정한 타입 전용 옵션 — 두 경로 모두 확정된 값. 컨텍스트 필드로의 변환도 타입 훅이 한다.
|
|
33
34
|
// releaseOptions: { includeSemverAuto, includeCopilotAi } — resolveReleaseOptions 결과.
|
|
34
35
|
// extra: 경로별로만 쓰는 필드(envValues 등).
|
|
35
36
|
export function buildInstallContext({
|
|
36
|
-
payload, existing, templateVersion, types, deployStyle,
|
|
37
|
+
payload, existing, templateVersion, types, deployStyle, typeOptions, releaseOptions, ...rest
|
|
37
38
|
}) {
|
|
38
39
|
return createContext({
|
|
39
40
|
types,
|
|
40
41
|
...releaseOptions,
|
|
41
42
|
// 안내는 고른 값으로 하되, 기록은 실제로 설치되는 방식으로 한다.
|
|
42
43
|
deployStyle: effectiveDeployStyle(payload, types, deployStyle),
|
|
43
|
-
|
|
44
|
-
flutterStore: flutterOptions.stores,
|
|
45
|
-
androidDeployMode: flutterOptions.androidDeployMode,
|
|
46
|
-
iosDeployMode: flutterOptions.iosDeployMode,
|
|
44
|
+
...mergeHookResults(types, "contextFields", typeOptions),
|
|
47
45
|
previousTemplateVersion: existing?.templateVersion || "",
|
|
48
46
|
templateVersion,
|
|
49
47
|
...rest,
|
|
@@ -317,7 +317,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
317
317
|
payload, existing, templateVersion, types,
|
|
318
318
|
// 저장값이 무중단이어도 선택한 타입에 그 방식이 없으면 단일 서버 배포가 설치된다 — 설치된 방식을 기록한다.
|
|
319
319
|
deployStyle: resolveDeployStyle({ payload, types, explicit: deployStyle, existing }),
|
|
320
|
-
flutterOptions,
|
|
320
|
+
typeOptions: flutterOptions,
|
|
321
321
|
releaseOptions: { includeSemverAuto, includeCopilotAi },
|
|
322
322
|
mode, force: true, version, versionCode, branch, branches, paths,
|
|
323
323
|
repoName, resolvers, envValues, envUseDefaults, now, today,
|
package/src/commands/status.js
CHANGED
|
@@ -5,11 +5,10 @@ import { parseExisting } from "../core/version-yml.js";
|
|
|
5
5
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
6
6
|
import { makeResolvers, detectRepoName, detectDefaultBranch } from "../core/detect-fs.js";
|
|
7
7
|
import { PATHS } from "../core/paths.js";
|
|
8
|
-
import { resolveFlutterOptions } from "../core/flutter-options.js";
|
|
9
8
|
import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
10
9
|
import { findStaleWorkflows } from "../core/removal-plan.js";
|
|
11
10
|
import { readBaseline } from "../core/baseline.js";
|
|
12
|
-
import { hooksFor } from "../core/types.js";
|
|
11
|
+
import { hooksFor, mergeHookResults } from "../core/types.js";
|
|
13
12
|
|
|
14
13
|
// payloadRoot: 패키지 payload/ 루트. targetRoot: 상태를 확인할 대상 레포.
|
|
15
14
|
export function runStatus(payloadRoot, targetRoot = ".") {
|
|
@@ -20,10 +19,8 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
20
19
|
const repoName = detectRepoName(targetRoot);
|
|
21
20
|
// 설치 때 워크플로우에 치환된 환경변수 방식·배포 모드와 스토어 선택을 비교 기준에도 똑같이 적용한다 —
|
|
22
21
|
// 그렇지 않으면 미수정 파일이 드리프트로, 선택 해제한 스토어 워크플로우가 "삭제함"으로 오탐된다.
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
const resolvers = makeResolvers(targetRoot, repoName, existing.paths, flutterOptions);
|
|
22
|
+
const typeOptions = mergeHookResults(existing.types, "resolveOptions", { existing });
|
|
23
|
+
const resolvers = makeResolvers(targetRoot, repoName, existing.paths, typeOptions);
|
|
27
24
|
// version.yml에 branches 블록이 없으면(신기능 이전 설치·수기 편집) makeSrcText(null)이
|
|
28
25
|
// {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}}를 치환하지 못해 모든 워크플로우가 드리프트로 오탐된다 —
|
|
29
26
|
// 비교용 기본값으로 폴백(실제 저장값은 아니지만 드리프트 비교 목적에는 충분).
|
|
@@ -31,7 +28,7 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
31
28
|
const context = {
|
|
32
29
|
types: existing.types, paths: existing.paths,
|
|
33
30
|
repoName, resolvers, branches: branchesForCompare,
|
|
34
|
-
|
|
31
|
+
...mergeHookResults(existing.types, "contextFields", typeOptions),
|
|
35
32
|
// 설치 때 고른 배포 방식으로 비교해야 nginx·traefik CD 수정도 드리프트로 잡힌다.
|
|
36
33
|
// 빠지면 기본값(simple)으로 걸러 설치된 무중단 CD가 비교 대상에서 사라진다.
|
|
37
34
|
deployStyle: isDeployStyle(existing.options.deployStyle) ? existing.options.deployStyle : DEFAULT_DEPLOY_STYLE,
|
package/src/context.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// 마법사 전역 상태를 하나의 객체로 명시화
|
|
2
|
-
import { TYPE_IDS } from "./core/types.js";
|
|
2
|
+
import { TYPE_IDS, allHookValues } from "./core/types.js";
|
|
3
3
|
|
|
4
4
|
// --type 화이트리스트 — 타입 레지스트리(core/types.js)의 표시 순서 그대로.
|
|
5
5
|
export const VALID_TYPES = TYPE_IDS;
|
|
@@ -26,12 +26,8 @@ export function createContext(overrides = {}) {
|
|
|
26
26
|
paths: new Map(), // type -> path
|
|
27
27
|
includeSemverAuto: null, // null=미설정(다운스트림에서 true로 해석), true/false=명시
|
|
28
28
|
includeCopilotAi: null, // null=미설정(다운스트림에서 false로 해석), true/false=명시 — Copilot AI 요약 opt-in
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
envMode: "", // "dart-define" | "dotenv". ""=미결정 → 템플릿 기본값(dart-define)
|
|
32
|
-
flutterStore: null, // 스토어 배포 대상 string[] (예: ["android","ios"]). null=미결정 → 둘 다(현행 동작)
|
|
33
|
-
androidDeployMode: "", // store_only | store_prepare | store_submit. ""=미결정 → store_only
|
|
34
|
-
iosDeployMode: "", // 위와 동일 (iOS)
|
|
29
|
+
// 타입 전용 옵션 필드 — 해당 타입이 없는 프로젝트에서는 전부 무시된다. 필드와 기본값은 타입 훅(contextDefaults)이 정한다.
|
|
30
|
+
...allHookValues("contextDefaults"),
|
|
35
31
|
templateVersion: "",
|
|
36
32
|
deployValues: new Map(), // "type.KEY" -> value
|
|
37
33
|
counters: {},
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
// Flutter 타입 훅 — types.js 레지스트리의 flutter 항목이 가리키는 구현 모음.
|
|
2
2
|
// 공통 코드는 flutter를 직접 알지 못하고 훅 이름으로만 호출한다(각 훅의 계약은 types.js 머리말 참고).
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import {
|
|
5
|
+
ENV_MODES, DEPLOY_MODES, DEFAULT_ENV_MODE, DEFAULT_DEPLOY_MODE, STORE_PLATFORMS, NO_STORE,
|
|
6
|
+
isEnvMode, isDeployMode, parseStoreList, formatStoreList, deployModeWarning,
|
|
7
|
+
storeWorkflowFilter, cleanupDeselectedStoreWorkflows, resolveFlutterOptions,
|
|
8
|
+
} from "./flutter-options.js";
|
|
4
9
|
import { planFlutterAppFiles, copyFlutterAppFiles } from "./copy/flutter-app.js";
|
|
5
10
|
import { flutterStoreChecks } from "./flutter-doctor.js";
|
|
11
|
+
import { inferInstalledStores } from "./installed-stores.js";
|
|
12
|
+
import { escapeYamlDoubleQuoted } from "./wizard-env.js";
|
|
13
|
+
import { CliError } from "./errors.js";
|
|
6
14
|
|
|
7
15
|
// 저장값이 없으면 그 상태에서 실제로 적용되는 동작을 함께 알린다.
|
|
8
16
|
function statusLabels(options) {
|
|
@@ -14,7 +22,95 @@ function statusLabels(options) {
|
|
|
14
22
|
].join("");
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
// 스토어 저장값이 없는 기존 Flutter 설치는 설치돼 있는 스토어 워크플로우로 추론한다(대화형과 같은 결론).
|
|
26
|
+
// 추론하지 않으면 미결정이 "둘 다"로 확정 저장되어, 지웠던 플랫폼의 워크플로우·fastlane 파일이 되살아난다.
|
|
27
|
+
// workflowsDir가 없으면(status 등 읽기 전용 경로) 추론하지 않는다.
|
|
28
|
+
function resolveOptions({ opts = {}, existing = null, workflowsDir = null }) {
|
|
29
|
+
const inferredStores = workflowsDir && opts.flutterStore == null && existing?.types?.includes("flutter")
|
|
30
|
+
&& existing.options?.flutterStore == null && existsSync(workflowsDir)
|
|
31
|
+
? inferInstalledStores(workflowsDir) : null;
|
|
32
|
+
return resolveFlutterOptions({
|
|
33
|
+
cli: {
|
|
34
|
+
envMode: opts.flutterEnvMode ?? "", stores: opts.flutterStore ?? inferredStores,
|
|
35
|
+
androidDeployMode: opts.androidDeployMode ?? "", iosDeployMode: opts.iosDeployMode ?? "",
|
|
36
|
+
},
|
|
37
|
+
existing,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// version.yml options 아래 Flutter 블록(6칸 들여쓰기). 값이 비었으면 워크플로우 템플릿 기본값과 같은 값으로 채운다 —
|
|
42
|
+
// 저장값과 실제 설치 내용이 어긋나지 않게. stores가 null(미결정)이면 현행 동작대로 둘 다 설치되므로 "android,ios"로 기록한다.
|
|
43
|
+
function versionOptionsBlock({ envMode, stores, androidDeployMode, iosDeployMode } = {}) {
|
|
44
|
+
const quote = (v) => `"${escapeYamlDoubleQuoted(v)}"`;
|
|
45
|
+
return [
|
|
46
|
+
` env_mode: ${quote(envMode || DEFAULT_ENV_MODE)} # ${ENV_MODES.join(" | ")} (Flutter 환경변수 주입 방식)`,
|
|
47
|
+
` flutter_store: ${quote(formatStoreList(stores ?? STORE_PLATFORMS))} # android | ios | android,ios | none (스토어 배포 대상)`,
|
|
48
|
+
` android_deploy_mode: ${quote(androidDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (Play Store 배포 모드)`,
|
|
49
|
+
` ios_deploy_mode: ${quote(iosDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (iOS 배포 모드)`,
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 값 목록 검증 플래그 하나를 만든다 — 값이 목록 밖이면 CliError.
|
|
54
|
+
const enumFlag = (flag, field, isValid, allowed) => ({
|
|
55
|
+
flag, field, initial: "",
|
|
56
|
+
parse(v) {
|
|
57
|
+
if (!isValid(v)) throw new CliError(`${flag} 값이 올바르지 않습니다: ${v ?? "(없음)"} (${allowed.join(" | ")})`);
|
|
58
|
+
return v;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
17
62
|
export const flutterHooks = {
|
|
63
|
+
resolveOptions,
|
|
64
|
+
// 확정된 옵션 → 설치 컨텍스트 필드. 기본값은 옵션이 없을 때의 "미결정" 상태다.
|
|
65
|
+
contextDefaults: {
|
|
66
|
+
envMode: "", // "dart-define" | "dotenv". ""=미결정 → 템플릿 기본값(dart-define)
|
|
67
|
+
flutterStore: null, // 스토어 배포 대상 string[] (예: ["android","ios"]). null=미결정 → 둘 다(현행 동작)
|
|
68
|
+
androidDeployMode: "", // store_only | store_prepare | store_submit. ""=미결정 → store_only
|
|
69
|
+
iosDeployMode: "", // 위와 동일 (iOS)
|
|
70
|
+
},
|
|
71
|
+
contextFields: (options) => ({
|
|
72
|
+
envMode: options.envMode,
|
|
73
|
+
flutterStore: options.stores,
|
|
74
|
+
androidDeployMode: options.androidDeployMode,
|
|
75
|
+
iosDeployMode: options.iosDeployMode,
|
|
76
|
+
}),
|
|
77
|
+
optionsFromContext: ({ envMode, flutterStore, androidDeployMode, iosDeployMode }) => ({
|
|
78
|
+
envMode, stores: flutterStore, androidDeployMode, iosDeployMode,
|
|
79
|
+
}),
|
|
80
|
+
versionOptionsBlock,
|
|
81
|
+
// version.yml 저장 키 → 파싱 결과 필드. 값은 원문 문자열로 돌려주고, 유효성 판정은 resolveOptions 몫이다.
|
|
82
|
+
savedOptionKeys: {
|
|
83
|
+
env_mode: "envMode", flutter_store: "flutterStore",
|
|
84
|
+
android_deploy_mode: "androidDeployMode", ios_deploy_mode: "iosDeployMode",
|
|
85
|
+
},
|
|
86
|
+
// CLI 플래그 — 파싱 결과 필드(opts)와 값 검증. initial은 미지정 값이다.
|
|
87
|
+
cliFlags: [
|
|
88
|
+
enumFlag("--flutter-env-mode", "flutterEnvMode", isEnvMode, ENV_MODES),
|
|
89
|
+
{
|
|
90
|
+
flag: "--flutter-store", field: "flutterStore", initial: null,
|
|
91
|
+
parse(v) {
|
|
92
|
+
// 빈 문자열은 parseStoreList가 []로 보지만, 스토어를 안 고르겠다는 뜻은 명시적인 none으로만 받는다.
|
|
93
|
+
const stores = v ? parseStoreList(v) : null;
|
|
94
|
+
if (stores === null) {
|
|
95
|
+
throw new CliError(`--flutter-store 값이 올바르지 않습니다: ${v || "(없음)"} (${[STORE_PLATFORMS.join(","), ...STORE_PLATFORMS, NO_STORE].join(" | ")})`);
|
|
96
|
+
}
|
|
97
|
+
return stores;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
enumFlag("--android-deploy-mode", "androidDeployMode", isDeployMode, DEPLOY_MODES),
|
|
101
|
+
enumFlag("--ios-deploy-mode", "iosDeployMode", isDeployMode, DEPLOY_MODES),
|
|
102
|
+
],
|
|
103
|
+
// 설치 직후 알릴 경고 — store_submit은 main push마다 심사를 자동 제출하므로 선택하지 않은 스토어에는 뜨면 안 된다.
|
|
104
|
+
installNotices: ({ stores, androidDeployMode, iosDeployMode }) => [
|
|
105
|
+
(stores === null || stores.includes("android")) && deployModeWarning(androidDeployMode),
|
|
106
|
+
(stores === null || stores.includes("ios")) && deployModeWarning(iosDeployMode),
|
|
107
|
+
],
|
|
108
|
+
// 설치 로그에 남길 선택값 [이름, 값] 목록
|
|
109
|
+
logChoices: (context) => {
|
|
110
|
+
const stores = Array.isArray(context.flutterStore) ? (context.flutterStore.join(",") || "없음") : "미결정(둘 다)";
|
|
111
|
+
return [["flutter",
|
|
112
|
+
`env=${context.envMode || "-"} stores=${stores} android=${context.androidDeployMode || "-"} ios=${context.iosDeployMode || "-"}`]];
|
|
113
|
+
},
|
|
18
114
|
// 스토어 대상이 배열일 때만 스토어 워크플로우 필터를 건다(null=미결정 → 전부 설치).
|
|
19
115
|
workflowFilter: ({ flutterStore }) => (Array.isArray(flutterStore) ? storeWorkflowFilter(flutterStore) : null),
|
|
20
116
|
// 선택 해제된 스토어 워크플로우 정리 — 대상이 미결정(null)이면 아무것도 지우지 않는다.
|
|
@@ -94,6 +94,11 @@ export function storeAppFilesFor(stores) {
|
|
|
94
94
|
return platforms.flatMap((p) => STORE_APP_FILES[p]);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// store_submit은 main push마다 심사를 제출하므로 고른 직후 한 줄로 알린다. 다른 모드는 알릴 것이 없어 빈 문자열.
|
|
98
|
+
export function deployModeWarning(mode) {
|
|
99
|
+
return mode === "store_submit" ? "store_submit을 고르면 main push마다 심사가 자동 제출됩니다." : "";
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
const validOr = (isValid, value, fallback) => (isValid(value) ? value : fallback);
|
|
98
103
|
|
|
99
104
|
// 옵션 최종 결정 — 우선순위: CLI > version.yml 저장값 > 기본값.
|
package/src/core/types.js
CHANGED
|
@@ -24,7 +24,17 @@ import { flutterHooks } from "./flutter-hooks.js";
|
|
|
24
24
|
// appFilesTag 앱 파일 복사 로그의 분류 이름
|
|
25
25
|
// statusLabels(options) status 옵션 줄에 덧붙일 문자열
|
|
26
26
|
// doctorChecks(cwd, existing, { docs }) doctor 진단 행 배열
|
|
27
|
-
//
|
|
27
|
+
// resolveOptions({ opts, existing, workflowsDir }) CLI 옵션 → 저장값 → 기본값 순으로 확정한 타입 옵션 객체
|
|
28
|
+
// (workflowsDir는 설치된 워크플로우로 추론할 때만 넘긴다)
|
|
29
|
+
// contextDefaults createContext의 미결정 기본 필드
|
|
30
|
+
// contextFields(options) / optionsFromContext(context)
|
|
31
|
+
// 옵션 객체 ↔ 설치 컨텍스트 필드 변환
|
|
32
|
+
// versionOptionsBlock(options) version.yml options 아래에 덧붙일 블록(6칸 들여쓰기)
|
|
33
|
+
// savedOptionKeys version.yml 저장 키 → parseExisting 옵션 필드
|
|
34
|
+
// cliFlags[{ flag, field, initial, parse(v) }] 타입 전용 CLI 플래그(파싱 결과 opts[field]에 담긴다)
|
|
35
|
+
// installNotices(options) 설치 직후 알릴 경고 배열(빈 값은 무시)
|
|
36
|
+
// logChoices(context) 설치 로그에 남길 선택값 [이름, 값] 배열
|
|
37
|
+
// (타입 전용 대화형 질문 흐름은 아직 interactive.js에 남아 있다.)
|
|
28
38
|
export const TYPES = [
|
|
29
39
|
{
|
|
30
40
|
id: "spring",
|
|
@@ -114,6 +124,14 @@ export function hooksFor(types, name) {
|
|
|
114
124
|
return found;
|
|
115
125
|
}
|
|
116
126
|
|
|
127
|
+
// types 중 훅 name을 가진 타입들의 반환값(객체)을 하나로 합친다 — 옵션·컨텍스트 필드처럼 타입마다 다른 키를 낼 때 쓴다.
|
|
128
|
+
export function mergeHookResults(types, name, ...args) {
|
|
129
|
+
return Object.assign({}, ...hooksFor(types, name).map(({ hook }) => hook(...args)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 모든 타입이 선언한 훅 상수(contextDefaults·savedOptionKeys 등)를 하나로 합친다 — 타입이 정해지기 전에 필요한 기본 형태용.
|
|
133
|
+
export const allHookValues = (name) => Object.assign({}, ...TYPES.map((t) => t.hooks?.[name]));
|
|
134
|
+
|
|
117
135
|
// 표시 순서 그대로의 타입 이름 목록
|
|
118
136
|
// VALID_TYPES·ALL_TYPES가 같은 배열을 공유하므로 한쪽에서 바꿔 다른 쪽이 오염되지 않게 고정한다.
|
|
119
137
|
export const TYPE_IDS = Object.freeze(TYPES.map((t) => t.id));
|
package/src/core/version-yml.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
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
|
+
import { hooksFor, mergeHookResults, allHookValues } from "./types.js";
|
|
6
4
|
|
|
7
5
|
// version.yml 파싱·생성 (전체 재생성 전략).
|
|
8
6
|
// ⚠️ YAML 재직렬화 금지 — 주석이 데이터.
|
|
@@ -37,20 +35,18 @@ export function parseExtraTopLevel(content) {
|
|
|
37
35
|
return blocks;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
android_deploy_mode: "androidDeployMode", ios_deploy_mode: "iosDeployMode",
|
|
44
|
-
};
|
|
38
|
+
// 타입 전용 옵션 키 → 반환 필드(타입 훅 savedOptionKeys). 값은 원문 문자열로 돌려주고, 유효성 판정은 타입 훅의 resolveOptions 몫이다.
|
|
39
|
+
const TYPE_OPTION_KEYS = allHookValues("savedOptionKeys");
|
|
40
|
+
const TYPE_OPTION_LINE = new RegExp(`^\\s+(${Object.keys(TYPE_OPTION_KEYS).join("|")}):\\s*(.+)`);
|
|
45
41
|
|
|
46
42
|
// metadata.template.options 상태머신 파싱.
|
|
47
43
|
// 반환: { semverAuto: bool|null, copilotAi: bool|null, deployStyle: string|null,
|
|
48
|
-
// envMode/flutterStore/androidDeployMode/iosDeployMode: string|null } — null=미기재.
|
|
44
|
+
// 타입 전용 옵션 필드(예: envMode/flutterStore/androidDeployMode/iosDeployMode): string|null } — null=미기재.
|
|
49
45
|
// 구 synology·coderabbit 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다(파싱 에러 없음).
|
|
50
46
|
export function parseTemplateOptions(content) {
|
|
51
47
|
const out = {
|
|
52
48
|
semverAuto: null, copilotAi: null, deployStyle: null,
|
|
53
|
-
|
|
49
|
+
...Object.fromEntries(Object.values(TYPE_OPTION_KEYS).map((field) => [field, null])),
|
|
54
50
|
};
|
|
55
51
|
// 값 정규화: 따옴표 제거 + 트림
|
|
56
52
|
// 인라인 주석(` # ...`)을 먼저 떼고 따옴표·공백을 정리한다. 문자열 값을 받는 키(deploy_style)는
|
|
@@ -64,8 +60,8 @@ export function parseTemplateOptions(content) {
|
|
|
64
60
|
if (inTemplate && inOptions) {
|
|
65
61
|
let m = line.match(/^\s+deploy_style:\s*(.+)/);
|
|
66
62
|
if (m) { const v = strip(m[1]); if (v) out.deployStyle = v; continue; }
|
|
67
|
-
m = line.match(
|
|
68
|
-
if (m) { const v = strip(m[2]); if (v) out[
|
|
63
|
+
m = line.match(TYPE_OPTION_LINE);
|
|
64
|
+
if (m) { const v = strip(m[2]); if (v) out[TYPE_OPTION_KEYS[m[1]]] = v; continue; }
|
|
69
65
|
m = line.match(/^\s+semver_auto:\s*(.+)/);
|
|
70
66
|
if (m) {
|
|
71
67
|
const v = strip(m[1]);
|
|
@@ -187,24 +183,11 @@ export function parseTemplateBranches(content) {
|
|
|
187
183
|
return out.main && out.develop && out.mode ? out : null;
|
|
188
184
|
}
|
|
189
185
|
|
|
190
|
-
// Flutter 옵션 블록 (전체 줄 토큰 {{FLUTTER_OPTIONS}} — Flutter 타입일 때만). options 아래 6칸 들여쓰기.
|
|
191
|
-
// 값이 비었으면 워크플로우 템플릿의 기본값과 같은 값으로 채운다 — 저장값과 실제 설치 내용이 어긋나지 않게.
|
|
192
|
-
// stores가 null(미결정)이면 현행 동작대로 둘 다 설치되므로 "android,ios"로 기록한다.
|
|
193
|
-
function buildFlutterOptionsBlock({ envMode, stores, androidDeployMode, iosDeployMode } = {}) {
|
|
194
|
-
const quote = (v) => `"${escapeYamlDoubleQuoted(v)}"`;
|
|
195
|
-
return [
|
|
196
|
-
` env_mode: ${quote(envMode || DEFAULT_ENV_MODE)} # ${ENV_MODES.join(" | ")} (Flutter 환경변수 주입 방식)`,
|
|
197
|
-
` flutter_store: ${quote(formatStoreList(stores ?? STORE_PLATFORMS))} # android | ios | android,ios | none (스토어 배포 대상)`,
|
|
198
|
-
` android_deploy_mode: ${quote(androidDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (Play Store 배포 모드)`,
|
|
199
|
-
` ios_deploy_mode: ${quote(iosDeployMode || DEFAULT_DEPLOY_MODE)} # ${DEPLOY_MODES.join(" | ")} (iOS 배포 모드)`,
|
|
200
|
-
].join("\n");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
186
|
// version.yml 전체 생성 — payload/version.yml.template 렌더링.
|
|
204
187
|
// opts: { templateText, version, types:[], paths:Map, pathMarkers?:Map,
|
|
205
188
|
// branch, branches?, versionCode, now, today, templateOptions?, deployValues?,
|
|
206
189
|
// extraTopLevel?:string[], ← 기존 version.yml의 알려지지 않은 최상위 필드 보존
|
|
207
|
-
//
|
|
190
|
+
// typeOptions?:object } ← 타입 훅(versionOptionsBlock)이 해당 타입일 때만 렌더하는 옵션 블록의 입력
|
|
208
191
|
// templateText = payload/version.yml.template 원문 (readVersionYmlTemplate — 필수)
|
|
209
192
|
// now = "YYYY-MM-DD HH:MM:SS" (UTC) — 결정성 위해 주입 / today = "YYYY-MM-DD"
|
|
210
193
|
// branches = { main, develop, mode } (resolveBranchConfig 결과. 없으면 branch 기반 기본값)
|
|
@@ -213,7 +196,7 @@ function buildFlutterOptionsBlock({ envMode, stores, androidDeployMode, iosDeplo
|
|
|
213
196
|
export function buildVersionYml({
|
|
214
197
|
templateText, version, types = [], paths = new Map(), pathMarkers = new Map(),
|
|
215
198
|
branch = "main", branches = null, versionCode = 1, now, today,
|
|
216
|
-
templateOptions = null, deployValues = new Map(), extraTopLevel = [],
|
|
199
|
+
templateOptions = null, deployValues = new Map(), extraTopLevel = [], typeOptions = {},
|
|
217
200
|
}) {
|
|
218
201
|
if (!templateText) throw new Error("version.yml.template 원문이 필요합니다 (payload/version.yml.template 누락?)");
|
|
219
202
|
const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(", ")}]` : `["basic"]`;
|
|
@@ -249,8 +232,8 @@ export function buildVersionYml({
|
|
|
249
232
|
deployBlock = rows.join("\n");
|
|
250
233
|
}
|
|
251
234
|
|
|
252
|
-
//
|
|
253
|
-
const
|
|
235
|
+
// 타입 옵션 블록 (full-line 토큰 {{TYPE_OPTIONS}} — 블록을 내는 타입이 있을 때만, 아니면 줄 제거)
|
|
236
|
+
const typeOptionsBlock = hooksFor(types, "versionOptionsBlock").map(({ hook }) => hook(typeOptions)).join("\n");
|
|
254
237
|
|
|
255
238
|
const scalars = {
|
|
256
239
|
VERSION: version, VERSION_CODE: String(versionCode),
|
|
@@ -267,7 +250,7 @@ export function buildVersionYml({
|
|
|
267
250
|
for (const line of String(templateText).split("\n")) {
|
|
268
251
|
const t = line.trim();
|
|
269
252
|
if (t === "{{PROJECT_PATHS}}") { if (pathsBlock) out.push(pathsBlock); continue; }
|
|
270
|
-
if (t === "{{
|
|
253
|
+
if (t === "{{TYPE_OPTIONS}}") { if (typeOptionsBlock) out.push(typeOptionsBlock); continue; }
|
|
271
254
|
if (t === "{{DEPLOY}}") { if (deployBlock) out.push(deployBlock); continue; }
|
|
272
255
|
if (t.startsWith("deploy_style:") && deployStyle === null) continue; // 서버 배포가 없는 타입은 기록하지 않는다
|
|
273
256
|
out.push(line.replace(/\{\{([A-Z][A-Z0-9_]*)\}\}/g, (_, name) => {
|
|
@@ -298,12 +281,11 @@ export function sameIgnoringTimestamps(a, b) {
|
|
|
298
281
|
export function renderVersionYml(context, templateText, { pathMarkers, deployValues = new Map(), extraTopLevel = [] }) {
|
|
299
282
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
300
283
|
now, today, templateVersion = "unknown", branches = null,
|
|
301
|
-
includeSemverAuto, includeCopilotAi, deployStyle
|
|
302
|
-
envMode, flutterStore, androidDeployMode, iosDeployMode } = context;
|
|
284
|
+
includeSemverAuto, includeCopilotAi, deployStyle } = context;
|
|
303
285
|
return buildVersionYml({
|
|
304
286
|
templateText, version, types, paths, pathMarkers, branch, branches, versionCode, now, today,
|
|
305
287
|
deployValues, extraTopLevel,
|
|
306
|
-
|
|
288
|
+
typeOptions: mergeHookResults(types, "optionsFromContext", context),
|
|
307
289
|
templateOptions: {
|
|
308
290
|
templateVersion,
|
|
309
291
|
includeSemverAuto: includeSemverAuto !== false,
|
package/src/index.js
CHANGED
|
@@ -6,11 +6,10 @@ import { join, dirname } from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { readFileSync, existsSync } from "node:fs";
|
|
8
8
|
import { createInterface } from "node:readline/promises";
|
|
9
|
-
import { parseArgs, parsePathsCsv, CliError } from "./cli/args.js";
|
|
9
|
+
import { parseArgs, parsePathsCsv, CliError, TYPE_CLI_FLAGS } from "./cli/args.js";
|
|
10
10
|
import { HELP_TEXT } from "./cli/help.js";
|
|
11
11
|
import { fallbackStyleTypes } from "./core/deploy-style.js";
|
|
12
|
-
import {
|
|
13
|
-
import { inferInstalledStores } from "./core/installed-stores.js";
|
|
12
|
+
import { hooksFor, mergeHookResults } from "./core/types.js";
|
|
14
13
|
import { PATHS } from "./core/paths.js";
|
|
15
14
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "./core/assets.js";
|
|
16
15
|
import { detectTypes, detectDefaultBranch, detectRepoName, makeResolvers, detectMarkers } from "./core/detect-fs.js";
|
|
@@ -108,8 +107,7 @@ async function runInner(argv, {
|
|
|
108
107
|
const ignored = [
|
|
109
108
|
[opts.types.length, "--type"], [opts.version, "--project-version"], [opts.pathsCsv, "--paths"],
|
|
110
109
|
[opts.mainBranch, "--main-branch"], [opts.developBranch, "--develop-branch"], [opts.deployStyle, "--deploy-style"],
|
|
111
|
-
|
|
112
|
-
[opts.androidDeployMode, "--android-deploy-mode"], [opts.iosDeployMode, "--ios-deploy-mode"],
|
|
110
|
+
...TYPE_CLI_FLAGS.map((f) => [opts[f.field], f.flag]),
|
|
113
111
|
].filter(([v]) => v).map(([, flag]) => flag);
|
|
114
112
|
if (ignored.length) {
|
|
115
113
|
console.error(`⚠️ 대화형 모드에서는 ${ignored.join(", ")}를 사용하지 않습니다 — 질문에서 고르거나 --mode full --force와 함께 쓰세요.`);
|
|
@@ -317,20 +315,10 @@ async function runInner(argv, {
|
|
|
317
315
|
}
|
|
318
316
|
}
|
|
319
317
|
|
|
320
|
-
//
|
|
321
|
-
// Flutter
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const workflowsDir = join(cwd, PATHS.workflowsDir);
|
|
325
|
-
const inferredStores = opts.flutterStore == null && existing?.types?.includes("flutter")
|
|
326
|
-
&& existing.options?.flutterStore == null && existsSync(workflowsDir)
|
|
327
|
-
? inferInstalledStores(workflowsDir) : null;
|
|
328
|
-
const flutterOptions = resolveFlutterOptions({
|
|
329
|
-
cli: {
|
|
330
|
-
envMode: opts.flutterEnvMode, stores: opts.flutterStore ?? inferredStores,
|
|
331
|
-
androidDeployMode: opts.androidDeployMode, iosDeployMode: opts.iosDeployMode,
|
|
332
|
-
},
|
|
333
|
-
existing,
|
|
318
|
+
// 타입 전용 옵션 — CLI 플래그 → version.yml 저장값 → 기본값을 타입 훅이 정한다. 해당 타입이 없으면 비어 있다.
|
|
319
|
+
// (Flutter는 저장값이 없는 기존 설치의 스토어를 설치된 워크플로우로 추론한다 — 대화형과 같은 결론.)
|
|
320
|
+
const typeOptions = mergeHookResults(types, "resolveOptions", {
|
|
321
|
+
opts, existing, workflowsDir: join(cwd, PATHS.workflowsDir),
|
|
334
322
|
});
|
|
335
323
|
|
|
336
324
|
// 고른 배포 방식 — 플래그 → 저장값 → 기본값. 실제 설치되는 방식으로의 정리는 컨텍스트 조립에서 한다.
|
|
@@ -338,7 +326,7 @@ async function runInner(argv, {
|
|
|
338
326
|
|
|
339
327
|
const context = buildInstallContext({
|
|
340
328
|
payload, existing, templateVersion: readTemplateVersion(), types, deployStyle: chosenDeployStyle,
|
|
341
|
-
|
|
329
|
+
typeOptions,
|
|
342
330
|
// 옵션: CLI 플래그 최우선 → version.yml 저장 옵션 → 기본값 (대화형과 같은 규칙)
|
|
343
331
|
releaseOptions: resolveReleaseOptions({ semverAuto: opts.includeSemverAuto, copilotAi: opts.includeCopilotAi }, existing),
|
|
344
332
|
mode: opts.mode, force: opts.force, version, versionCode, branch,
|
|
@@ -346,7 +334,7 @@ async function runInner(argv, {
|
|
|
346
334
|
paths,
|
|
347
335
|
repoName,
|
|
348
336
|
// @wizard ask/auto 토큰 값을 계산하는 resolver
|
|
349
|
-
resolvers: makeResolvers(cwd, repoName, paths,
|
|
337
|
+
resolvers: makeResolvers(cwd, repoName, paths, typeOptions),
|
|
350
338
|
now, today,
|
|
351
339
|
// 설치 로그용 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
352
340
|
markers: detectMarkers(cwd, types), detectWarnings,
|
|
@@ -395,18 +383,11 @@ async function runInner(argv, {
|
|
|
395
383
|
});
|
|
396
384
|
for (const n of postInstallNotices(result)) console.error(n.startsWith(" ") ? n : `⚠️ ${n}`);
|
|
397
385
|
// store_submit 배포 모드는 main push마다 심사를 자동 제출한다 — 비대화형에서도 같은 경고를 보여준다
|
|
398
|
-
// (대화형 경로는 ui/prompts.js#deployModeWarning을 선택 시점에 note로 보여준다).
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
warnings.push(prompts.deployModeWarning(flutterOptions.androidDeployMode));
|
|
404
|
-
}
|
|
405
|
-
if (types.includes("flutter") && (stores === null || stores.includes("ios"))) {
|
|
406
|
-
warnings.push(prompts.deployModeWarning(flutterOptions.iosDeployMode));
|
|
407
|
-
}
|
|
408
|
-
for (const w of warnings) {
|
|
409
|
-
if (w) console.error(`⚠️ ${w}`);
|
|
386
|
+
// (대화형 경로는 ui/prompts.js#deployModeWarning을 선택 시점에 note로 보여준다). 타입 훅이 해당 타입에만 낸다.
|
|
387
|
+
for (const { hook } of hooksFor(types, "installNotices")) {
|
|
388
|
+
for (const w of hook(typeOptions)) {
|
|
389
|
+
if (w) console.error(`⚠️ ${w}`);
|
|
390
|
+
}
|
|
410
391
|
}
|
|
411
392
|
return 0;
|
|
412
393
|
}
|
package/src/ui/prompts.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as engine from "./readline-engine.js";
|
|
6
6
|
import { DEPLOY_STYLES, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
7
7
|
import { TYPE_IDS } from "../core/types.js";
|
|
8
|
-
import { ENV_MODES, DEFAULT_ENV_MODE, STORE_PLATFORMS, DEPLOY_MODES, DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
|
|
8
|
+
import { ENV_MODES, DEFAULT_ENV_MODE, STORE_PLATFORMS, DEPLOY_MODES, DEFAULT_DEPLOY_MODE, deployModeWarning } from "../core/flutter-options.js";
|
|
9
9
|
|
|
10
10
|
export const CANCEL = engine.CANCEL;
|
|
11
11
|
|
|
@@ -188,10 +188,8 @@ export async function selectDeployMode({ platform, initialValue = DEFAULT_DEPLOY
|
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// store_submit
|
|
192
|
-
export
|
|
193
|
-
return mode === "store_submit" ? "store_submit을 고르면 main push마다 심사가 자동 제출됩니다." : "";
|
|
194
|
-
}
|
|
191
|
+
// store_submit 경고 문구 — 정의는 core에 있고, 호출부(interactive)가 note로 출력한다.
|
|
192
|
+
export { deployModeWarning };
|
|
195
193
|
|
|
196
194
|
// 브랜치 전략 선택. 종전에는 "릴리스 브랜치"/"개발 브랜치" 두 질문에
|
|
197
195
|
// 같은 이름을 입력해야만 trunk-based가 됐는데, 그 규칙이 사전에 안내되지 않아
|