project-auto-wizard 0.10.0 → 0.12.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 +104 -18
- package/package.json +2 -2
- 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 +83 -30
- package/payload/version.yml.template +5 -0
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +30 -7
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +36 -8
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +25 -1
- 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 +44 -0
- package/src/cli/help.js +6 -1
- package/src/commands/doctor.js +59 -5
- 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 +60 -5
- package/src/commands/status.js +21 -2
- package/src/context.js +7 -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 +1 -1
- package/src/core/version-yml.js +50 -6
- package/src/core/wizard-env.js +26 -4
- package/src/index.js +34 -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 +28 -9
package/src/cli/help.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// --help 텍스트.
|
|
2
|
-
export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-native
|
|
2
|
+
export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-native 릴리스 자동화 설치 마법사
|
|
3
3
|
|
|
4
4
|
사용법:
|
|
5
5
|
npx project-auto-wizard [옵션]
|
|
@@ -17,9 +17,14 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
17
17
|
--main-branch B 릴리스 브랜치 (기본: 감지된 default branch)
|
|
18
18
|
--develop-branch B 개발 브랜치 (기본: develop). 릴리스 브랜치와 같으면 trunk-based 모드
|
|
19
19
|
--deploy-style STYLE 서버 배포 방식: simple | nginx | traefik | none (기본: simple)
|
|
20
|
+
--flutter-env-mode MODE Flutter 환경변수 방식: dart-define | dotenv (기본: 신규 설치 dart-define, 기존 설치는 저장값·dotenv 유지)
|
|
21
|
+
--flutter-store CSV Flutter 스토어 배포 대상: android,ios | android | ios | none (기본: 둘 다 설치)
|
|
22
|
+
--android-deploy-mode MODE Play Store 배포 모드: store_only | store_prepare | store_submit (기본: store_only)
|
|
23
|
+
--ios-deploy-mode MODE iOS 배포 모드: store_only | store_prepare | store_submit (기본: store_only)
|
|
20
24
|
--nexus / --no-nexus 라이브러리 publish 워크플로우 포함/제외 (Nexus + GitHub Packages)
|
|
21
25
|
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
22
26
|
--semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
|
|
27
|
+
--copilot / --no-copilot Copilot으로 AI 요약 생성 (기본: 사용 안 함, GitHub Copilot AI Credits 소비)
|
|
23
28
|
--force full 실행에 필수 (모든 확인 생략, 비대화형 기본값 사용)
|
|
24
29
|
--dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/uninstall 지원)
|
|
25
30
|
--purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
|
package/src/commands/doctor.js
CHANGED
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
// ④ GitHub 설정 화면에 실제로 표시되는 문자열("Read and write permissions" 등)은 번역하지
|
|
12
12
|
// 않는다. 번역하면 설명은 읽히지만 정작 화면에서 그 항목을 찾지 못한다.
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
-
import { existsSync } from "node:fs";
|
|
15
|
-
import { join } from "node:path";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join, posix } from "node:path";
|
|
16
16
|
import { A, paint, colorEnabled, visualWidth } from "../ui/ansi.js";
|
|
17
|
+
import { PATHS } from "../core/paths.js";
|
|
18
|
+
import { parseExisting } from "../core/version-yml.js";
|
|
19
|
+
import { STORE_PLATFORMS, STORE_APP_FILES, parseStoreList, storeAppFilesFor } from "../core/flutter-options.js";
|
|
20
|
+
import { inferInstalledStores } from "../core/installed-stores.js";
|
|
17
21
|
|
|
18
22
|
const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
19
23
|
|
|
@@ -23,6 +27,7 @@ const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
|
23
27
|
const REPO_URL = "https://github.com/Twin-Fang/project-auto-wizard";
|
|
24
28
|
export const DOC = {
|
|
25
29
|
postInstall: `${REPO_URL}#post-install`,
|
|
30
|
+
flutterStore: `${REPO_URL}#flutter-store`,
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
@@ -39,6 +44,9 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
39
44
|
note: ["이 폴더엔 아직 설치되지 않았습니다 — 지금 설치하면 됩니다."],
|
|
40
45
|
});
|
|
41
46
|
|
|
47
|
+
// Flutter 스토어 배포 파일 점검 (이슈 #131) — 로컬 파일만 보므로 gh 조회 결과와 무관하게 수행한다.
|
|
48
|
+
if (installed) for (const item of flutterStoreChecks(cwd)) add(item);
|
|
49
|
+
|
|
42
50
|
const ghVersion = exec("gh", ["--version"]);
|
|
43
51
|
if (ghVersion.error || ghVersion.status !== 0) {
|
|
44
52
|
add({
|
|
@@ -153,16 +161,62 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
add({
|
|
156
|
-
name: "
|
|
164
|
+
name: "Copilot AI 요약", label: "Copilot AI 요약", purpose: "AI 릴리스 노트 생성(선택)", status: "INFO",
|
|
157
165
|
note: [
|
|
158
|
-
"
|
|
159
|
-
"
|
|
166
|
+
"기본은 꺼져 있습니다 (version.yml의 copilot_ai: false).",
|
|
167
|
+
"켜면 GitHub Copilot AI Credits가 소비됩니다 — 조직은 'Allow use of Copilot CLI billed to the organization' 정책이 필요합니다.",
|
|
168
|
+
"꺼져 있거나 사용할 수 없으면 규칙 기반 요약으로 자동 전환되므로 그대로 두셔도 됩니다.",
|
|
160
169
|
],
|
|
161
170
|
});
|
|
162
171
|
|
|
163
172
|
return results;
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
const PLATFORM_ROW_NAME = { android: "Flutter Android 배포 파일", ios: "Flutter iOS 배포 파일" };
|
|
176
|
+
const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g; // 감지 규칙은 ExportOptions.plist 템플릿·IOS-TESTFLIGHT 검증과 동일
|
|
177
|
+
const EXPORT_OPTIONS_REL = STORE_APP_FILES.ios.find((rel) => rel.endsWith("ExportOptions.plist"));
|
|
178
|
+
|
|
179
|
+
// Flutter 스토어 배포 진단 — 선택한 플랫폼의 필수 파일과 ExportOptions.plist 플레이스홀더 (이슈 #131).
|
|
180
|
+
// 스토어 시크릿 등록 여부는 이번 범위 밖이다. 반환: doctor 결과 행 배열.
|
|
181
|
+
function flutterStoreChecks(cwd) {
|
|
182
|
+
const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
|
|
183
|
+
if (!existing.types.includes("flutter")) return [];
|
|
184
|
+
const saved = existing.options.flutterStore == null ? null : parseStoreList(existing.options.flutterStore);
|
|
185
|
+
// 저장값 없는 기존 설치는 설치된 스토어 워크플로우로 추론한다 (interactive와 같은 규칙).
|
|
186
|
+
const stores = saved ?? inferInstalledStores(join(cwd, PATHS.workflowsDir));
|
|
187
|
+
const flutterRoot = existing.paths.get("flutter") || ".";
|
|
188
|
+
const rows = [];
|
|
189
|
+
|
|
190
|
+
for (const platform of STORE_PLATFORMS.filter((p) => stores.includes(p))) {
|
|
191
|
+
const files = storeAppFilesFor([platform]).map((rel) => posix.join(flutterRoot, rel));
|
|
192
|
+
const missing = files.filter((file) => !existsSync(join(cwd, file)));
|
|
193
|
+
const head = { name: PLATFORM_ROW_NAME[platform], purpose: "fastlane 스토어 업로드에 필요한 파일" };
|
|
194
|
+
rows.push(missing.length
|
|
195
|
+
? {
|
|
196
|
+
...head, status: "WARN", value: `없는 파일: ${missing.join(", ")}`,
|
|
197
|
+
impact: ["이 파일이 없으면 스토어 배포 워크플로우가 fastlane 단계에서 실패합니다."],
|
|
198
|
+
actions: ["마법사를 다시 실행하면 없는 파일만 새로 만들어 줍니다 (이미 있는 파일은 덮어쓰지 않음)"],
|
|
199
|
+
doc: DOC.flutterStore,
|
|
200
|
+
}
|
|
201
|
+
: { ...head, status: "OK", value: `${files.length}개 있음` });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const plistPath = posix.join(flutterRoot, EXPORT_OPTIONS_REL);
|
|
205
|
+
if (stores.includes("ios") && existsSync(join(cwd, plistPath))) {
|
|
206
|
+
const head = { name: "ExportOptions.plist", purpose: "iOS 서명·내보내기 설정" };
|
|
207
|
+
const left = [...new Set(readFileSync(join(cwd, plistPath), "utf8").match(PLACEHOLDER_RE) ?? [])];
|
|
208
|
+
rows.push(left.length
|
|
209
|
+
? {
|
|
210
|
+
...head, status: "WARN", value: `채워지지 않은 값: ${left.join(", ")}`,
|
|
211
|
+
impact: ["채우지 않으면 IOS-TESTFLIGHT 워크플로우가 ExportOptions.plist 검증 단계에서 중단됩니다."],
|
|
212
|
+
actions: [`${plistPath} 의 플레이스홀더를 실제 값(Team ID · 번들 ID · 프로비저닝 프로파일 이름)으로 바꾸세요`],
|
|
213
|
+
doc: DOC.flutterStore,
|
|
214
|
+
}
|
|
215
|
+
: { ...head, status: "OK", value: "플레이스홀더 없음" });
|
|
216
|
+
}
|
|
217
|
+
return rows;
|
|
218
|
+
}
|
|
219
|
+
|
|
166
220
|
const asLines = (v) => (Array.isArray(v) ? v : v ? [String(v)] : []);
|
|
167
221
|
const headOf = (r) => `${r.label || r.name}${r.purpose ? ` — ${r.purpose}` : ""}`;
|
|
168
222
|
|
package/src/commands/dry-run.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { PATHS } from "../core/paths.js";
|
|
6
6
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
7
|
+
import { planFlutterAppFiles } from "../core/copy/flutter-app.js";
|
|
7
8
|
import { planUninstall } from "./uninstall.js";
|
|
8
9
|
import { renderVersionYml, parseExisting } from "../core/version-yml.js";
|
|
9
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
@@ -33,6 +34,8 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
|
33
34
|
return {
|
|
34
35
|
mode,
|
|
35
36
|
workflows: planWorkflows(context, payloadRoot, targetRoot),
|
|
37
|
+
// Flutter 스토어 배포 파일(Fastfile·ExportOptions.plist) — 이미 있는 파일은 덮어쓰지 않고 유지한다.
|
|
38
|
+
flutterApp: planFlutterAppFiles(context, payloadRoot, targetRoot),
|
|
36
39
|
versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
|
|
37
40
|
};
|
|
38
41
|
}
|
|
@@ -57,6 +60,13 @@ export function printDryRun(plan) {
|
|
|
57
60
|
for (const f of w.changed) lines.push(` ~ ${f.filename} [${f.type}]`);
|
|
58
61
|
lines.push(`동일한 파일 (${w.unchanged.length}개, 변경 없음)`);
|
|
59
62
|
}
|
|
63
|
+
if (plan.flutterApp && (plan.flutterApp.created.length || plan.flutterApp.kept.length)) {
|
|
64
|
+
const { created, kept } = plan.flutterApp;
|
|
65
|
+
lines.push(`Flutter 스토어 배포 파일 — 신규 (${created.length}개):`);
|
|
66
|
+
for (const f of created) lines.push(` + ${f}`);
|
|
67
|
+
lines.push(`Flutter 스토어 배포 파일 — 기존 파일 유지 (${kept.length}개, 덮어쓰지 않음):`);
|
|
68
|
+
for (const f of kept) lines.push(` = ${f} (기존 파일 유지)`);
|
|
69
|
+
}
|
|
60
70
|
if (plan.versionYml) {
|
|
61
71
|
lines.push(plan.versionYml.existed
|
|
62
72
|
? (plan.versionYml.changed ? "version.yml: 갱신될 예정" : "version.yml: 변경 없음")
|
package/src/commands/full.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// full 모드 오케스트레이터 (.sh execute_integration full case 등가).
|
|
2
|
-
// 복사 순서: workflows(+env 치환) → version.yml → readme → scripts → gitignore(조건부)
|
|
2
|
+
// 복사 순서: workflows(+env 치환) → flutter 앱 파일 → version.yml → readme → scripts → gitignore(조건부)
|
|
3
3
|
// gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다 — issue #7.
|
|
4
4
|
// (원본의 util/issue/discussion/setup-guide/config 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -12,10 +12,12 @@ import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
|
12
12
|
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
13
13
|
import { copyWorkflows, computeBaselineEntries, makeSrcText } from "../core/copy/workflows.js";
|
|
14
14
|
import { copyScripts } from "../core/copy/simple.js";
|
|
15
|
+
import { copyFlutterAppFiles } from "../core/copy/flutter-app.js";
|
|
15
16
|
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
16
17
|
import { readBaseline, writeBaseline } from "../core/baseline.js";
|
|
17
18
|
import { scanUnsubstituted, collectRequiredSecrets, narrowSecretsBySshAuth } from "../core/verify.js";
|
|
18
19
|
import { cleanupOtherDeployWorkflows, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
20
|
+
import { cleanupDeselectedStoreWorkflows } from "../core/flutter-options.js";
|
|
19
21
|
import { log, maskValue } from "../core/logger.js";
|
|
20
22
|
|
|
21
23
|
// context: { version, types, paths:Map, branch, versionCode, includeNexus, includeSecretBackup,
|
|
@@ -47,6 +49,12 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
47
49
|
const wfCounters = copyWorkflows(context, payloadRoot, targetRoot, hooks);
|
|
48
50
|
const deployValues = wfCounters.deployValues || new Map(); // Map<type, Map<key,value>>
|
|
49
51
|
|
|
52
|
+
// 1-1. Flutter 앱 파일(fastlane·ExportOptions) — 사용자가 값을 채워 쓰는 파일이라 없을 때만 만든다.
|
|
53
|
+
// 워크플로우 복사가 끝난 뒤에 실행한다(워크플로우가 이 파일들을 전제로 돈다).
|
|
54
|
+
const flutterApp = copyFlutterAppFiles(context, payloadRoot, targetRoot);
|
|
55
|
+
for (const f of flutterApp.created) log.info("flutter-app", "create", f);
|
|
56
|
+
for (const f of flutterApp.kept) log.info("flutter-app", "keep", `${f} (기존 파일 유지)`);
|
|
57
|
+
|
|
50
58
|
// 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다 (issue #20 M8).
|
|
51
59
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
52
60
|
const extraTopLevel = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")).extraTopLevel : [];
|
|
@@ -80,7 +88,24 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
80
88
|
|
|
81
89
|
for (const f of cleanup.removed || []) log.info("cleanup", "remove", `${f} (이전 배포 방식 정리)`);
|
|
82
90
|
for (const f of cleanup.backedUp || []) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
83
|
-
|
|
91
|
+
|
|
92
|
+
// 6-1. 선택 해제된 스토어 워크플로우 정리 — 스토어 대상을 줄여 재설치하면 이전 워크플로우가 남아
|
|
93
|
+
// main push마다 계속 도는 것을 막는다. 규칙은 6과 같다(미수정 삭제, 수정본 .bak).
|
|
94
|
+
// 선택이 미결정(null)이거나 Flutter가 없으면 현행 동작 그대로 아무것도 지우지 않는다.
|
|
95
|
+
// Fastfile·ExportOptions는 사용자 소유라 여기서 다루지 않는다.
|
|
96
|
+
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
97
|
+
const storeCleanup = Array.isArray(context.flutterStore) && types.includes("flutter")
|
|
98
|
+
? cleanupDeselectedStoreWorkflows(
|
|
99
|
+
workflowsDir,
|
|
100
|
+
existsSync(workflowsDir) ? readdirSync(workflowsDir) : [],
|
|
101
|
+
context.flutterStore,
|
|
102
|
+
previousBaseline)
|
|
103
|
+
: { removed: [], backedUp: [] };
|
|
104
|
+
for (const f of [...storeCleanup.removed, ...storeCleanup.backedUp]) delete previousBaseline?.files?.[f];
|
|
105
|
+
for (const f of storeCleanup.removed) log.info("cleanup", "remove", `${f} (선택 해제된 스토어 워크플로우 정리)`);
|
|
106
|
+
for (const f of storeCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
107
|
+
|
|
108
|
+
const gitignoreUpdated = gitignoreUpdated0 || cleanup.backedUp.length > 0 || storeCleanup.backedUp.length > 0;
|
|
84
109
|
if (gitignoreUpdated) ensureGitignore(targetRoot);
|
|
85
110
|
|
|
86
111
|
// 7. baseline 기록 (issue #69) — 다음 업데이트에서 "누가 바꿨는지"를 가를 기준점.
|
|
@@ -122,7 +147,7 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
122
147
|
["결과", unresolved.length ? `주의 (미치환 ${unresolved.length}건)` : "OK"],
|
|
123
148
|
]);
|
|
124
149
|
|
|
125
|
-
return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup };
|
|
150
|
+
return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup, storeCleanup, flutterApp };
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
// deployValues는 Map<type, Map<key,value>> — 타입 구분 없이 첫 값만 필요할 때 쓴다.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// 대화형 마법사의 Flutter 옵션 질문 (이슈 #131) — 환경변수 방식 · 스토어 배포 대상 · 배포 모드.
|
|
2
|
+
// 상태는 { envMode, stores, androidDeployMode, iosDeployMode } 불변 객체로 주고받는다.
|
|
3
|
+
// envMode·*DeployMode의 ""와 stores의 null = 미결정 (stores의 빈 배열 = "스토어 배포 안 함"으로 이미 결정됨).
|
|
4
|
+
// 이미 결정된 값은 다시 묻지 않는다 (version.yml 저장값 재질문 생략 규약). ESC(취소)는 항상 기본값:
|
|
5
|
+
// 처음 묻는 질문이면 기본값, 수정 중이면 현재값을 유지한다.
|
|
6
|
+
import {
|
|
7
|
+
isEnvMode, isDeployMode, parseStoreList, STORE_PLATFORMS, DEFAULT_DEPLOY_MODE,
|
|
8
|
+
} from "../core/flutter-options.js";
|
|
9
|
+
import { deployModeWarning } from "../ui/prompts.js";
|
|
10
|
+
|
|
11
|
+
export const FLUTTER_EDIT_ITEMS = new Set(["envMode", "flutterStore", "deployMode"]);
|
|
12
|
+
|
|
13
|
+
const DEPLOY_MODE_KEY = { android: "androidDeployMode", ios: "iosDeployMode" };
|
|
14
|
+
|
|
15
|
+
// STORE_PLATFORMS 순서로 정렬하고 모르는 값은 버린다 — 플랫폼별 질문 순서를 고정한다.
|
|
16
|
+
const normalizeStores = (picked) => STORE_PLATFORMS.filter((platform) => picked.includes(platform));
|
|
17
|
+
|
|
18
|
+
// 기존 version.yml의 저장값을 상태로 옮긴다. 잘못된 값은 저장이 없는 것으로 보고 다시 묻는다.
|
|
19
|
+
export function savedFlutterState(existing) {
|
|
20
|
+
const options = existing?.options ?? {};
|
|
21
|
+
return {
|
|
22
|
+
envMode: isEnvMode(options.envMode) ? options.envMode : "",
|
|
23
|
+
stores: options.flutterStore == null ? null : parseStoreList(options.flutterStore),
|
|
24
|
+
androidDeployMode: isDeployMode(options.androidDeployMode) ? options.androidDeployMode : "",
|
|
25
|
+
iosDeployMode: isDeployMode(options.iosDeployMode) ? options.iosDeployMode : "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function askDeployMode(io, platform, initialValue) {
|
|
30
|
+
const picked = await io.selectDeployMode({ platform, initialValue });
|
|
31
|
+
const mode = isDeployMode(picked) ? picked : initialValue; // ESC = 기본값
|
|
32
|
+
const warning = deployModeWarning(mode);
|
|
33
|
+
if (warning) io.note?.(warning, "배포 모드");
|
|
34
|
+
return mode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 고른 플랫폼 중 배포 모드가 아직 없는 것만 묻는다.
|
|
38
|
+
async function askUnsetDeployModes(io, state) {
|
|
39
|
+
const next = { ...state };
|
|
40
|
+
for (const platform of state.stores ?? []) {
|
|
41
|
+
const key = DEPLOY_MODE_KEY[platform];
|
|
42
|
+
if (!next[key]) next[key] = await askDeployMode(io, platform, DEFAULT_DEPLOY_MODE);
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 아직 정해지지 않은 옵션만 묻는다.
|
|
48
|
+
// envModeDefault — 신규 설치 dart-define / 기존 설치 dotenv (동작 보존)
|
|
49
|
+
// inferredStores — 저장값 없는 기존 설치가 이미 쓰던 스토어 (신규 설치는 [])
|
|
50
|
+
export async function askUnsetFlutterOptions(io, state, { envModeDefault, inferredStores }) {
|
|
51
|
+
const next = { ...state };
|
|
52
|
+
if (!next.envMode) {
|
|
53
|
+
const picked = await io.selectEnvMode({ initialValue: envModeDefault });
|
|
54
|
+
next.envMode = isEnvMode(picked) ? picked : envModeDefault; // ESC = 기본값
|
|
55
|
+
}
|
|
56
|
+
if (next.stores === null) {
|
|
57
|
+
const picked = await io.selectFlutterStores({ initialValues: inferredStores });
|
|
58
|
+
next.stores = Array.isArray(picked) ? normalizeStores(picked) : inferredStores; // ESC = 초기 선택
|
|
59
|
+
}
|
|
60
|
+
return askUnsetDeployModes(io, next);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 수정하기 메뉴에서 고른 항목 하나를 다시 묻는다. 현재값이 초기 선택이고 ESC는 현재값 유지.
|
|
64
|
+
export async function editFlutterOption(io, what, state, envModeDefault) {
|
|
65
|
+
if (what === "envMode") {
|
|
66
|
+
const picked = await io.selectEnvMode({ initialValue: state.envMode || envModeDefault });
|
|
67
|
+
return isEnvMode(picked) ? { ...state, envMode: picked } : state;
|
|
68
|
+
}
|
|
69
|
+
if (what === "flutterStore") {
|
|
70
|
+
const picked = await io.selectFlutterStores({ initialValues: state.stores ?? [] });
|
|
71
|
+
if (!Array.isArray(picked)) return state;
|
|
72
|
+
const stores = normalizeStores(picked);
|
|
73
|
+
// 해제된 플랫폼의 배포 모드는 초기화한다 — 그대로 두면 재선택 시 옛 값이 남아 다시 묻지 않는다.
|
|
74
|
+
const reset = { ...state, stores };
|
|
75
|
+
for (const platform of STORE_PLATFORMS) {
|
|
76
|
+
if (!stores.includes(platform)) reset[DEPLOY_MODE_KEY[platform]] = "";
|
|
77
|
+
}
|
|
78
|
+
// 새로 추가되거나 방금 초기화된 플랫폼만 배포 모드를 묻는다 — 이미 정한 플랫폼의 모드는 그대로 둔다.
|
|
79
|
+
return askUnsetDeployModes(io, reset);
|
|
80
|
+
}
|
|
81
|
+
if (what === "deployMode") {
|
|
82
|
+
const stores = state.stores ?? [];
|
|
83
|
+
if (!stores.length) {
|
|
84
|
+
io.note?.("스토어 배포 대상을 먼저 선택하세요 ('스토어 배포 대상' 항목).", "배포 모드");
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
const next = { ...state };
|
|
88
|
+
for (const platform of stores) {
|
|
89
|
+
const key = DEPLOY_MODE_KEY[platform];
|
|
90
|
+
next[key] = await askDeployMode(io, platform, state[key] || DEFAULT_DEPLOY_MODE);
|
|
91
|
+
}
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
return state;
|
|
95
|
+
}
|
|
@@ -15,6 +15,10 @@ import { promptEnvPlan } from "../ui/env-plan.js";
|
|
|
15
15
|
import { surveyWorkflows } from "../core/copy/workflows.js";
|
|
16
16
|
import { createContext, VALID_TYPES } from "../context.js";
|
|
17
17
|
import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
18
|
+
import { PATHS } from "../core/paths.js";
|
|
19
|
+
import { resolveFlutterOptions, DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
|
|
20
|
+
import { inferInstalledStores } from "../core/installed-stores.js";
|
|
21
|
+
import { savedFlutterState, askUnsetFlutterOptions, editFlutterOption, FLUTTER_EDIT_ITEMS } from "./interactive-flutter.js";
|
|
18
22
|
import { runFull } from "./full.js";
|
|
19
23
|
import { runUninstallFlow } from "./uninstall.js";
|
|
20
24
|
import * as prompts from "../ui/prompts.js";
|
|
@@ -84,11 +88,27 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
84
88
|
let includeNexus = existing?.options?.nexus ?? false;
|
|
85
89
|
let includeSecretBackup = existing?.options?.secretBackup ?? false;
|
|
86
90
|
let includeSemverAuto = existing?.options?.semverAuto ?? null;
|
|
91
|
+
let includeCopilotAi = existing?.options?.copilotAi ?? null;
|
|
87
92
|
// 서버 배포 방식 — 저장값(version.yml)이 있으면 재질문하지 않는다 (nexus/secret_backup과 같은 규약).
|
|
88
93
|
let deployStyle = isDeployStyle(existing?.options?.deployStyle) ? existing.options.deployStyle : "";
|
|
89
94
|
const showOptional = mode === "full";
|
|
90
95
|
const realTty = process.stdout.isTTY === true;
|
|
91
96
|
|
|
97
|
+
// Flutter 옵션 (이슈 #131) — 저장값이 있으면 재질문하지 않는다 (deploy_style과 같은 규약).
|
|
98
|
+
// 저장값 없는 기존 설치는 동작 보존을 위해 dotenv를 초기 선택으로, 스토어는 설치된 워크플로우로 추론한다.
|
|
99
|
+
// 환경변수 기본값 규칙(신규=dart-define, 기존 설치·저장값 없음=dotenv)은 resolveFlutterOptions가 단일 진실이다.
|
|
100
|
+
let flutter = savedFlutterState(existing);
|
|
101
|
+
const flutterAsk = {
|
|
102
|
+
envModeDefault: resolveFlutterOptions({
|
|
103
|
+
cli: { envMode: "", stores: null, androidDeployMode: "", iosDeployMode: "" }, existing,
|
|
104
|
+
}).envMode,
|
|
105
|
+
inferredStores: existing && flutter.stores === null ? inferInstalledStores(join(cwd, PATHS.workflowsDir)) : [],
|
|
106
|
+
};
|
|
107
|
+
// 이미 정해진 값은 건너뛰므로 여러 번 불러도 같은 질문이 반복되지 않는다.
|
|
108
|
+
const askFlutterOptions = async () => {
|
|
109
|
+
if (types.includes("flutter")) flutter = await askUnsetFlutterOptions(io, flutter, flutterAsk);
|
|
110
|
+
};
|
|
111
|
+
|
|
92
112
|
// 층2 — 감지 로그 (#446). markers = 실제로 존재를 확인한 파일 (이슈 #77).
|
|
93
113
|
let markers = detectMarkers(cwd, types);
|
|
94
114
|
io.detectionLog?.({ types, version, branch, markers, warnings: detectWarnings });
|
|
@@ -125,17 +145,27 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
125
145
|
deployStyle = isDeployStyle(picked) ? picked : DEFAULT_DEPLOY_STYLE; // ESC = 기본값
|
|
126
146
|
}
|
|
127
147
|
|
|
148
|
+
// Flutter 옵션 (이슈 #131) — 환경변수 방식 → 스토어 배포 대상 → 플랫폼별 배포 모드.
|
|
149
|
+
await askFlutterOptions();
|
|
150
|
+
|
|
128
151
|
// 신규 질문 — 자동 semver 승격 (기본 ON). 저장값 있으면 재질문 생략.
|
|
129
152
|
// version.yml을 쓰지 않는 workflows 모드에서는 답변이 무의미하므로 full에서만 질문한다.
|
|
130
153
|
if (mode === "full" && includeSemverAuto === null) {
|
|
131
154
|
const y2 = await io.askYesNo("자동 버전 승격을 사용하시겠습니까? (커밋 타입에 따라 major/minor/patch 자동 결정)", true);
|
|
132
155
|
includeSemverAuto = y2 === true;
|
|
133
156
|
}
|
|
157
|
+
|
|
158
|
+
// Copilot AI 요약 — AI Credits를 소비하므로 opt-in(기본 No). 저장값 있으면 재질문 생략.
|
|
159
|
+
if (mode === "full" && includeCopilotAi === null) {
|
|
160
|
+
const y3 = await io.askYesNo("Copilot으로 AI 요약을 생성하시겠습니까? (GitHub Copilot AI Credits가 소비되며, 사용할 수 없으면 자동으로 규칙 기반 요약으로 전환됩니다)", false);
|
|
161
|
+
includeCopilotAi = y3 === true;
|
|
162
|
+
}
|
|
134
163
|
}
|
|
135
164
|
// 질문이 실제로 나온 경우(위 full 모드 질문) 답변을 그대로 존중.
|
|
136
165
|
// 질문이 안 나온 경우(version/workflows 모드) — 기존 설치는 안전하게 false로 폴백,
|
|
137
166
|
// 완전 신규 설치만 true(기존 설계) 유지 — CLI 경로(index.js)와 동일한 안전 정책.
|
|
138
167
|
includeSemverAuto = includeSemverAuto === null ? (existing ? false : true) : includeSemverAuto !== false;
|
|
168
|
+
includeCopilotAi = includeCopilotAi === true;
|
|
139
169
|
|
|
140
170
|
// 확인/수정 루프 — ESC는 '머무르기' (.sh L1877~1881: 명시적 '아니오'만 종료)
|
|
141
171
|
let paths = new Map();
|
|
@@ -143,9 +173,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
143
173
|
while (!confirmed) {
|
|
144
174
|
// 층3 — 프로젝트 분석 개요 카드 (#446). 스텁엔 없음 → note 폴백.
|
|
145
175
|
if (io.analysisCard) {
|
|
146
|
-
io.analysisCard({ mode, modeLabel: modeLabel(mode), types, version, branch, includeNexus, includeSecretBackup, showOptional, paths });
|
|
176
|
+
io.analysisCard({ mode, modeLabel: modeLabel(mode), types, version, branch, includeNexus, includeSecretBackup, showOptional, paths, flutter, envModeDefault: flutterAsk.envModeDefault });
|
|
147
177
|
} else {
|
|
148
|
-
io.note?.(summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional }), "프로젝트 분석 결과");
|
|
178
|
+
io.note?.(summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional, flutter, envModeDefault: flutterAsk.envModeDefault }), "프로젝트 분석 결과");
|
|
149
179
|
}
|
|
150
180
|
const choice = await io.confirmProjectMenu();
|
|
151
181
|
if (choice === "cancel") { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
|
|
@@ -154,7 +184,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
154
184
|
// edit 루프
|
|
155
185
|
let editing = true;
|
|
156
186
|
while (editing) {
|
|
157
|
-
const what = await io.editMenu({ showOptional });
|
|
187
|
+
const what = await io.editMenu({ showOptional, showFlutter: showOptional && types.includes("flutter") });
|
|
158
188
|
if (isCancel(what) || what === "done") { editing = false; break; }
|
|
159
189
|
if (what === "type") {
|
|
160
190
|
const t = await io.selectTypes(types);
|
|
@@ -180,10 +210,22 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
180
210
|
} else if (what === "secret") {
|
|
181
211
|
const y = await io.askYesNo("Secret 백업 워크플로우를 포함할까요?", includeSecretBackup);
|
|
182
212
|
if (!isCancel(y)) includeSecretBackup = y === true;
|
|
213
|
+
} else if (FLUTTER_EDIT_ITEMS.has(what)) {
|
|
214
|
+
flutter = await editFlutterOption(io, what, flutter, flutterAsk.envModeDefault);
|
|
183
215
|
}
|
|
184
216
|
}
|
|
185
217
|
}
|
|
186
218
|
|
|
219
|
+
// 편집 루프에서 뒤늦게 flutter 타입이 추가된 경우에도 옵션을 확정한다 — 이미 정해진 값은 다시 묻지 않는다.
|
|
220
|
+
if (showOptional) await askFlutterOptions();
|
|
221
|
+
// 질문이 나오지 않은 경우(비 full 모드 등)도 동작 보존 기본값으로 채워 워크플로우 치환이 어긋나지 않게 한다.
|
|
222
|
+
const flutterOptions = {
|
|
223
|
+
envMode: flutter.envMode || flutterAsk.envModeDefault,
|
|
224
|
+
stores: flutter.stores,
|
|
225
|
+
androidDeployMode: flutter.androidDeployMode || DEFAULT_DEPLOY_MODE,
|
|
226
|
+
iosDeployMode: flutter.iosDeployMode || DEFAULT_DEPLOY_MODE,
|
|
227
|
+
};
|
|
228
|
+
|
|
187
229
|
const versionCode = existing?.versionCode ?? detectBuildNumber(cwd, { types }) ?? 1; // 기존 빌드번호 보존, 신규 통합 시 프로젝트 파일에서 감지 (이슈 #41)
|
|
188
230
|
|
|
189
231
|
// 신규 질문 ① — 브랜치 설정 (DESIGN-SPEC §4). full/workflows만 질문, version은 기본값 기록.
|
|
@@ -226,12 +268,13 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
226
268
|
}
|
|
227
269
|
|
|
228
270
|
// @wizard env 계획 질문 (.sh wf_prompt_env_plan L3220 — full/workflows만)
|
|
229
|
-
const resolvers = makeResolvers(cwd, repoName, paths);
|
|
271
|
+
const resolvers = makeResolvers(cwd, repoName, paths, flutterOptions);
|
|
230
272
|
let envValues = new Map(), envUseDefaults = true, envAnswers = [];
|
|
231
273
|
if (showOptional) {
|
|
232
274
|
const plan = await promptEnvPlan({
|
|
233
275
|
payloadRoot: payload, types, io: io.engineIo ?? null, force: false,
|
|
234
276
|
resolvers, includeNexus, includeSecretBackup, deployStyle, targetRoot: cwd, repoName,
|
|
277
|
+
flutterStore: flutterOptions.stores, // 선택 해제된 스토어 워크플로우의 ask 질문은 묻지 않는다 (D2 env-plan)
|
|
235
278
|
});
|
|
236
279
|
envValues = plan.values;
|
|
237
280
|
envUseDefaults = plan.useDefaults;
|
|
@@ -243,10 +286,13 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
243
286
|
mode, force: true, types, version, versionCode, branch, branches, paths,
|
|
244
287
|
includeNexus, includeSecretBackup,
|
|
245
288
|
includeSemverAuto,
|
|
289
|
+
includeCopilotAi,
|
|
246
290
|
repoName, templateVersion, resolvers, envValues, envUseDefaults, now, today,
|
|
247
291
|
// 설치 로그(#79)·완료 요약(#80)이 쓰는 부가 문맥 — 설치 동작 자체는 바꾸지 않는다.
|
|
248
292
|
markers, envAnswers, detectWarnings,
|
|
249
293
|
deployStyle: deployStyle || DEFAULT_DEPLOY_STYLE,
|
|
294
|
+
envMode: flutterOptions.envMode, flutterStore: flutterOptions.stores,
|
|
295
|
+
androidDeployMode: flutterOptions.androidDeployMode, iosDeployMode: flutterOptions.iosDeployMode,
|
|
250
296
|
previousTemplateVersion: existing?.templateVersion || "",
|
|
251
297
|
});
|
|
252
298
|
ctx.templateVersion = templateVersion;
|
|
@@ -309,6 +355,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
309
355
|
logPath: currentLogPath(),
|
|
310
356
|
legacyMdLogs: hasLegacyMdLogs(cwd),
|
|
311
357
|
cleanup: result?.cleanup ?? null,
|
|
358
|
+
storeCleanup: result?.storeCleanup ?? null,
|
|
359
|
+
flutterApp: result?.flutterApp ?? null,
|
|
312
360
|
});
|
|
313
361
|
io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
|
|
314
362
|
return 0;
|
|
@@ -335,7 +383,7 @@ export async function pickBranch(io, message, def, remoteBranches, isCancel) {
|
|
|
335
383
|
return isCancel(v) || !v ? def : v;
|
|
336
384
|
}
|
|
337
385
|
|
|
338
|
-
function summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional }) {
|
|
386
|
+
function summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional, flutter, envModeDefault }) {
|
|
339
387
|
const lines = [
|
|
340
388
|
`통합 모드 : ${modeLabel(mode)}`,
|
|
341
389
|
`프로젝트 타입 : ${types.join(", ")}${types.length > 1 ? " (멀티)" : ""}`,
|
|
@@ -345,6 +393,13 @@ function summarize({ mode, types, version, branch, includeNexus, includeSecretBa
|
|
|
345
393
|
if (showOptional) {
|
|
346
394
|
lines.push(`Nexus publish : ${includeNexus ? "포함" : "제외"}`);
|
|
347
395
|
lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
|
|
396
|
+
if (types.includes("flutter")) {
|
|
397
|
+
const stores = flutter.stores ?? [];
|
|
398
|
+
const modeParts = stores.map((p) => `${p}=${(p === "android" ? flutter.androidDeployMode : flutter.iosDeployMode) || DEFAULT_DEPLOY_MODE}`);
|
|
399
|
+
lines.push(`환경변수 방식 : ${flutter.envMode || envModeDefault}`);
|
|
400
|
+
lines.push(`스토어 배포 대상 : ${stores.length ? stores.join(", ") : "없음"}`);
|
|
401
|
+
lines.push(`배포 모드 : ${modeParts.length ? modeParts.join(" ") : "없음"}`);
|
|
402
|
+
}
|
|
348
403
|
}
|
|
349
404
|
return lines.join("\n");
|
|
350
405
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -5,6 +5,7 @@ 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";
|
|
8
9
|
|
|
9
10
|
// payloadRoot: 패키지 payload/ 루트. targetRoot: 상태를 확인할 대상 레포.
|
|
10
11
|
export function runStatus(payloadRoot, targetRoot = ".") {
|
|
@@ -13,7 +14,12 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
13
14
|
|
|
14
15
|
const existing = parseExisting(readFileSync(vyPath, "utf8"));
|
|
15
16
|
const repoName = detectRepoName(targetRoot);
|
|
16
|
-
|
|
17
|
+
// 설치 때 워크플로우에 치환된 환경변수 방식·배포 모드와 스토어 선택을 비교 기준에도 똑같이 적용한다 —
|
|
18
|
+
// 그렇지 않으면 미수정 파일이 드리프트로, 선택 해제한 스토어 워크플로우가 "삭제함"으로 오탐된다.
|
|
19
|
+
const flutterOptions = resolveFlutterOptions({
|
|
20
|
+
cli: { envMode: "", stores: null, androidDeployMode: "", iosDeployMode: "" }, existing,
|
|
21
|
+
});
|
|
22
|
+
const resolvers = makeResolvers(targetRoot, repoName, existing.paths, flutterOptions);
|
|
17
23
|
// version.yml에 branches 블록이 없으면(신기능 이전 설치·수기 편집) makeSrcText(null)이
|
|
18
24
|
// {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}}를 치환하지 못해 모든 워크플로우가 드리프트로 오탐된다 —
|
|
19
25
|
// 비교용 기본값으로 폴백(실제 저장값은 아니지만 드리프트 비교 목적에는 충분).
|
|
@@ -23,6 +29,7 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
23
29
|
includeNexus: existing.options.nexus === true,
|
|
24
30
|
includeSecretBackup: existing.options.secretBackup === true,
|
|
25
31
|
repoName, resolvers, branches: branchesForCompare,
|
|
32
|
+
flutterStore: flutterOptions.stores,
|
|
26
33
|
};
|
|
27
34
|
const plan = planWorkflows(context, payloadRoot, targetRoot);
|
|
28
35
|
|
|
@@ -62,7 +69,9 @@ export function printStatus(status) {
|
|
|
62
69
|
}
|
|
63
70
|
const boolLabel = (v) => (v === null ? "미설정(기본 false)" : v);
|
|
64
71
|
const semverAutoLabel = status.options.semverAuto === null ? "미설정(기본 false)" : status.options.semverAuto;
|
|
65
|
-
|
|
72
|
+
const copilotAiLabel = boolLabel(status.options.copilotAi ?? null);
|
|
73
|
+
const flutterLabels = status.types.includes("flutter") ? flutterOptionLabels(status.options) : "";
|
|
74
|
+
lines.push(`옵션 : nexus=${boolLabel(status.options.nexus)} secret_backup=${boolLabel(status.options.secretBackup)} semver_auto=${semverAutoLabel} copilot_ai=${copilotAiLabel}${flutterLabels}`);
|
|
66
75
|
if (status.modifiedFiles.length) {
|
|
67
76
|
lines.push("", `사용자가 수정한 워크플로우 파일 (${status.modifiedFiles.length}개):`);
|
|
68
77
|
for (const f of status.modifiedFiles) lines.push(` - ${f}`);
|
|
@@ -82,3 +91,13 @@ export function printStatus(status) {
|
|
|
82
91
|
lines.push("");
|
|
83
92
|
console.log(lines.join("\n"));
|
|
84
93
|
}
|
|
94
|
+
|
|
95
|
+
// Flutter 타입일 때만 붙는 옵션 (이슈 #131). 저장값이 없으면 그 상태에서 실제로 적용되는 동작을 함께 알린다.
|
|
96
|
+
function flutterOptionLabels(options) {
|
|
97
|
+
return [
|
|
98
|
+
` env_mode=${options.envMode ?? "미설정(dotenv 유지)"}`,
|
|
99
|
+
` flutter_store=${options.flutterStore ?? "미설정(둘 다 설치)"}`,
|
|
100
|
+
` android_deploy_mode=${options.androidDeployMode ?? "미설정(store_only)"}`,
|
|
101
|
+
` ios_deploy_mode=${options.iosDeployMode ?? "미설정(store_only)"}`,
|
|
102
|
+
].join("");
|
|
103
|
+
}
|
package/src/context.js
CHANGED
|
@@ -27,6 +27,13 @@ export function createContext(overrides = {}) {
|
|
|
27
27
|
includeNexus: null, // null=미설정, true/false=명시
|
|
28
28
|
includeSecretBackup: null,
|
|
29
29
|
includeSemverAuto: null, // null=미설정(다운스트림에서 true로 해석), true/false=명시
|
|
30
|
+
includeCopilotAi: null, // null=미설정(다운스트림에서 false로 해석), true/false=명시 — Copilot AI 요약 opt-in
|
|
31
|
+
// Flutter 옵션 (이슈 #131) — Flutter 타입이 없는 프로젝트에서는 전부 무시된다.
|
|
32
|
+
// 결정은 src/core/flutter-options.js의 resolveFlutterOptions가 한다.
|
|
33
|
+
envMode: "", // "dart-define" | "dotenv". ""=미결정 → 템플릿 기본값(dart-define)
|
|
34
|
+
flutterStore: null, // 스토어 배포 대상 string[] (예: ["android","ios"]). null=미결정 → 둘 다(현행 동작)
|
|
35
|
+
androidDeployMode: "", // store_only | store_prepare | store_submit. ""=미결정 → store_only
|
|
36
|
+
iosDeployMode: "", // 위와 동일 (iOS)
|
|
30
37
|
templateVersion: "",
|
|
31
38
|
deployValues: new Map(), // "type.KEY" -> value
|
|
32
39
|
counters: {},
|
package/src/core/breaking.js
CHANGED
|
@@ -13,13 +13,17 @@ export function compareVersions(a, b) {
|
|
|
13
13
|
// breaking-changes.json에서 current < ver <= target 범위 항목 수집.
|
|
14
14
|
// ⚠️ .sh 버그 수정: target은 하드코딩 1.3.14가 아니라 실제 templateVersion을 넘긴다 (D2).
|
|
15
15
|
// _ 로 시작하는 키(메타) 제외. severity critical / 그 외(warning).
|
|
16
|
+
// 버전 키의 값은 항목 객체 또는 항목 객체의 배열(같은 릴리스에 고지가 여러 건일 때).
|
|
16
17
|
export function collectBreaking(json, current, target) {
|
|
17
18
|
const critical = [], warnings = [];
|
|
18
|
-
for (const [ver,
|
|
19
|
+
for (const [ver, value] of Object.entries(json || {})) {
|
|
19
20
|
if (ver.startsWith("_")) continue;
|
|
20
21
|
if (compareVersions(current, ver) < 0 && compareVersions(ver, target) <= 0) {
|
|
21
|
-
|
|
22
|
-
(entry
|
|
22
|
+
// 한 릴리스에 고지가 여러 건이면 배열로 등록한다 — JSON 키(버전)는 유일해서 객체로는 한 건만 담긴다.
|
|
23
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
24
|
+
const rec = { version: ver, ...entry };
|
|
25
|
+
(entry?.severity === "critical" ? critical : warnings).push(rec);
|
|
26
|
+
}
|
|
23
27
|
}
|
|
24
28
|
}
|
|
25
29
|
return { critical, warnings };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Flutter 앱 소유 파일(fastlane·ExportOptions.plist) 설치 (이슈 #131).
|
|
2
|
+
// 워크플로우와 달리 사용자가 값을 채워 넣는 파일이라 "없을 때만 생성"한다 — 덮어쓰지 않고 baseline
|
|
3
|
+
// 3-way도 적용하지 않는다(copyWorkflows의 secret-backup 경로와 같은 선례). 원본 갱신을 기존 사용자에게
|
|
4
|
+
// 전파하는 것은 범위 밖이다.
|
|
5
|
+
import { join, posix } from "node:path";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { copyFileSync } from "../fsutil.js";
|
|
8
|
+
import { storeAppFilesFor } from "../flutter-options.js";
|
|
9
|
+
|
|
10
|
+
const FLUTTER_APP_DIR = "flutter-app"; // payload/flutter-app/
|
|
11
|
+
|
|
12
|
+
// 이번 실행에서 다룰 파일의 (보고용 상대경로, 원본, 목적지) 목록.
|
|
13
|
+
// 대상은 Flutter 타입이 있을 때만이고, 선택된 플랫폼(context.flutterStore, null이면 둘 다)의 파일로 한정한다.
|
|
14
|
+
function flutterAppTargets(context, payloadRoot, targetRoot) {
|
|
15
|
+
const { types = [], paths = new Map(), flutterStore = null } = context;
|
|
16
|
+
if (!types.includes("flutter")) return [];
|
|
17
|
+
const flutterRoot = paths.get("flutter") || ".";
|
|
18
|
+
return storeAppFilesFor(flutterStore).map((rel) => ({
|
|
19
|
+
reported: posix.join(flutterRoot, rel),
|
|
20
|
+
src: join(payloadRoot, FLUTTER_APP_DIR, rel),
|
|
21
|
+
dst: join(targetRoot, flutterRoot, rel),
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 읽기 전용 — status/dry-run이 쓴다. 원본 payload는 읽지 않는다.
|
|
26
|
+
export function planFlutterAppFiles(context, payloadRoot, targetRoot = ".") {
|
|
27
|
+
const result = { created: [], kept: [] };
|
|
28
|
+
for (const { reported, dst } of flutterAppTargets(context, payloadRoot, targetRoot)) {
|
|
29
|
+
(existsSync(dst) ? result.kept : result.created).push(reported);
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function copyFlutterAppFiles(context, payloadRoot, targetRoot = ".") {
|
|
35
|
+
const result = { created: [], kept: [] };
|
|
36
|
+
for (const { reported, src, dst } of flutterAppTargets(context, payloadRoot, targetRoot)) {
|
|
37
|
+
if (existsSync(dst)) { result.kept.push(reported); continue; }
|
|
38
|
+
copyFileSync(src, dst); // 부모 디렉토리 생성 + 바이트 그대로(CRLF 보존)
|
|
39
|
+
result.created.push(reported);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|