project-auto-wizard 0.12.2 → 0.13.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.ja.md +165 -0
- package/README.ko.md +165 -0
- package/README.md +107 -286
- package/README.zh-CN.md +165 -0
- package/bin/project-auto-wizard.js +1 -2
- package/package.json +7 -3
- package/payload/config/breaking-changes.json +24 -0
- package/payload/config/wizard-prompts.yml +45 -0
- package/payload/scripts/changelog_manager.py +141 -46
- package/payload/scripts/issue_helper.py +34 -5
- package/payload/scripts/version_manager.py +397 -97
- package/payload/version.yml.template +4 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
- package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
- package/src/cli/args.js +33 -24
- package/src/cli/help.js +16 -4
- package/src/commands/doctor.js +17 -10
- package/src/commands/dry-run.js +54 -1
- package/src/commands/full.js +127 -50
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +124 -56
- package/src/commands/purge.js +59 -17
- package/src/commands/status.js +16 -1
- package/src/commands/uninstall.js +58 -13
- package/src/context.js +5 -5
- package/src/core/baseline.js +20 -5
- package/src/core/branches.js +19 -1
- package/src/core/branding.js +1 -1
- package/src/core/breaking-check.js +14 -19
- package/src/core/breaking.js +7 -3
- package/src/core/copy/gitignore.js +20 -12
- package/src/core/copy/readme.js +24 -5
- package/src/core/copy/simple.js +52 -13
- package/src/core/copy/workflows.js +90 -47
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +131 -47
- package/src/core/errors.js +3 -0
- package/src/core/flutter-options.js +4 -3
- package/src/core/fsutil.js +23 -6
- package/src/core/logger.js +55 -19
- package/src/core/paths-resolve.js +50 -47
- package/src/core/paths.js +26 -1
- package/src/core/release-options.js +23 -0
- package/src/core/removal-plan.js +75 -19
- package/src/core/types.js +113 -0
- package/src/core/verify.js +85 -19
- package/src/core/version-yml.js +42 -8
- package/src/core/wizard-env.js +21 -14
- package/src/core/wizard-labels.js +34 -25
- package/src/index.js +122 -49
- package/src/ui/ansi.js +3 -1
- package/src/ui/banner.js +12 -8
- package/src/ui/env-plan.js +68 -44
- package/src/ui/prompts.js +22 -12
- package/src/ui/readline-engine.js +109 -22
- package/src/ui/status-cards.js +10 -5
- package/src/ui/summary.js +71 -33
- 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/src/cli/help.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import { TYPE_IDS } from "../core/types.js";
|
|
2
|
+
|
|
3
|
+
// 지원 타입 목록 — 레지스트리 순서대로 한 줄 40자 안쪽에서 줄바꿈하고, 이어지는 줄은 "지원: " 뒤에 맞춘다.
|
|
4
|
+
function typeListLines(width = 40) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
for (const id of TYPE_IDS) {
|
|
7
|
+
const last = lines.length - 1;
|
|
8
|
+
if (last >= 0 && lines[last].length + 1 + id.length <= width) lines[last] += ` ${id}`;
|
|
9
|
+
else lines.push(id);
|
|
10
|
+
}
|
|
11
|
+
return lines.join(`\n${" ".repeat(33)}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
1
14
|
// --help 텍스트.
|
|
2
15
|
export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-native 릴리스 자동화 설치 마법사
|
|
3
16
|
|
|
@@ -10,8 +23,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
10
23
|
uninstall = 완전 삭제(대화형 체크리스트, --force 시 --purge-*로 opt-in)
|
|
11
24
|
status = 설치 상태·드리프트 확인(읽기 전용). doctor = 환경 진단(읽기 전용)
|
|
12
25
|
-t, --type CSV 프로젝트 타입 csv (예: spring,react,python)
|
|
13
|
-
지원:
|
|
14
|
-
react-native-expo node python basic go
|
|
26
|
+
지원: ${typeListLines()}
|
|
15
27
|
--project-version V 통합 대상의 초기 버전 (예: 1.0.0). 미지정 시 자동 감지
|
|
16
28
|
--paths "t=p,..." 타입별 프로젝트 경로 (모노레포). 예: flutter=app,react=client
|
|
17
29
|
--main-branch B 릴리스 브랜치 (기본: 감지된 default branch)
|
|
@@ -23,7 +35,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
23
35
|
--ios-deploy-mode MODE iOS 배포 모드: store_only | store_prepare | store_submit (기본: store_only)
|
|
24
36
|
--semver-auto / --no-semver-auto 커밋 타입 기반 자동 major/minor/patch 승격 (기본: 사용함)
|
|
25
37
|
--copilot / --no-copilot Copilot으로 AI 요약 생성 (기본: 사용 안 함, GitHub Copilot AI Credits 소비)
|
|
26
|
-
--force full 실행에
|
|
38
|
+
--force full 실행에 필수, uninstall은 비대화형 삭제 (모든 확인 생략, 기본값 사용)
|
|
27
39
|
--dry-run 실제 파일 변경 없이 무엇이 바뀔지만 미리 보여줌 (full/uninstall 지원)
|
|
28
40
|
--purge-readme --mode uninstall --force 시 README.md 버전 섹션도 제거
|
|
29
41
|
--purge-gitignore --mode uninstall --force 시 .gitignore 자동 추가 항목도 제거
|
|
@@ -33,7 +45,7 @@ export const HELP_TEXT = `project-auto-wizard — One command DevOps: GitHub-nat
|
|
|
33
45
|
|
|
34
46
|
예시:
|
|
35
47
|
npx project-auto-wizard --mode full --force --type spring,react
|
|
36
|
-
npx project-auto-wizard --mode
|
|
48
|
+
npx project-auto-wizard --mode full --force --type flutter --paths "flutter=app"
|
|
37
49
|
npx project-auto-wizard --mode status
|
|
38
50
|
npx project-auto-wizard --mode doctor
|
|
39
51
|
npx project-auto-wizard --mode full --force --type node --dry-run
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// doctor 명령 — 로컬 환경 진단(읽기 전용, 규칙 기반). gh CLI에 위임해 원격 상태를 점검한다.
|
|
2
|
-
// AI 진단은 포함하지 않는다(스펙 §4에서 검토 후 기각 — 복잡도 대비 이득 낮음).
|
|
3
2
|
//
|
|
4
3
|
// 출력 설계 — `flutter doctor` 패턴을 차용한다.
|
|
5
4
|
// ① 항목 라벨에 purpose("무엇을 위한 설정인지")를 병기한다. `WORKFLOW_PAT`만 보고는 그게
|
|
@@ -21,13 +20,13 @@ import { inferInstalledStores } from "../core/installed-stores.js";
|
|
|
21
20
|
|
|
22
21
|
const defaultExec = (cmd, args) => spawnSync(cmd, args, { encoding: "utf8" });
|
|
23
22
|
|
|
24
|
-
// 해결 가이드
|
|
25
|
-
// 해결 가이드 링크(README 앵커)"). README에 심은 영문 HTML 앵커를 가리킨다 — 한글 헤딩
|
|
23
|
+
// 해결 가이드 링크. 문서 사이트 페이지 안의 영문 HTML 앵커를 가리킨다 — 한글 헤딩
|
|
26
24
|
// 자동 앵커는 URL 인코딩되어 터미널에서 알아볼 수 없게 깨진다.
|
|
27
|
-
|
|
25
|
+
// 진단 출력이 한국어이므로 한국어 페이지(/ko/)로 연결한다.
|
|
26
|
+
export const DOCS_SITE_URL = "https://twin-fang.github.io/project-auto-wizard";
|
|
28
27
|
export const DOC = {
|
|
29
|
-
postInstall: `${
|
|
30
|
-
flutterStore: `${
|
|
28
|
+
postInstall: `${DOCS_SITE_URL}/ko/start/quickstart/#post-install`,
|
|
29
|
+
flutterStore: `${DOCS_SITE_URL}/ko/project-types/flutter/#flutter-store`,
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
@@ -77,7 +76,8 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
77
76
|
|
|
78
77
|
const remote = exec("git", ["-C", cwd, "remote", "get-url", "origin"]);
|
|
79
78
|
const url = remote.status === 0 ? (remote.stdout || "").trim() : "";
|
|
80
|
-
|
|
79
|
+
// 레포 이름에는 점이 올 수 있다(user.github.io, next.js) — 끝의 .git만 떼어낸다.
|
|
80
|
+
const match = url.match(/github\.com[:/]([^/]+)\/([^/]+?)(\.git)?\/?$/);
|
|
81
81
|
if (!match) {
|
|
82
82
|
add({
|
|
83
83
|
name: "GitHub 원격", purpose: "점검 대상 레포 식별", status: "WARN",
|
|
@@ -129,7 +129,7 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
129
129
|
// 파이프라인을 끝까지 이어가므로, 없는 장애를 경고로 띄우지 않는다.
|
|
130
130
|
name: "WORKFLOW_PAT secret", label: "WORKFLOW_PAT", purpose: "자동 태그·Release 발행", status: "INFO",
|
|
131
131
|
note: [
|
|
132
|
-
"secret이 없어도 폴백이 자동으로 이어받아 태그·Release
|
|
132
|
+
"secret이 없어도 폴백이 자동으로 이어받아 태그·Release 발행과 main 배포 워크플로우 실행까지 진행됩니다 — 실제 병합 후 최대 ~20초 정도 더 걸릴 뿐입니다.",
|
|
133
133
|
"속도를 더 원한다면 PAT을 등록할 수 있습니다 — 반드시 개인 계정이 아닌 조직 bot/machine 계정으로 발급하세요 (scopes: repo, workflow).",
|
|
134
134
|
"등록: 레포 Settings → Secrets and variables → Actions → New repository secret · 이름은 WORKFLOW_PAT",
|
|
135
135
|
],
|
|
@@ -160,11 +160,18 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
// 실제 설정값을 보여준다 — 켜 둔 사용자에게 "꺼져 있다"고 안내하면 현재 상태를 오해하게 된다.
|
|
164
|
+
const copilotAi = installed ? parseExisting(readFileSync(join(cwd, "version.yml"), "utf8")).options.copilotAi : null;
|
|
165
|
+
const copilotState = copilotAi === true
|
|
166
|
+
? "켜져 있습니다 (version.yml의 copilot_ai: true)."
|
|
167
|
+
: copilotAi === false
|
|
168
|
+
? "꺼져 있습니다 (version.yml의 copilot_ai: false) — 켜려면 --copilot으로 다시 설치하거나 대화형 '수정하기 > Copilot AI 요약'을 쓰세요."
|
|
169
|
+
: "기본은 꺼져 있습니다 (version.yml의 copilot_ai: false).";
|
|
163
170
|
add({
|
|
164
171
|
name: "Copilot AI 요약", label: "Copilot AI 요약", purpose: "AI 릴리스 노트 생성(선택)", status: "INFO",
|
|
165
172
|
note: [
|
|
166
|
-
|
|
167
|
-
"켜면 GitHub Copilot AI Credits가 소비됩니다 — 조직은 'Allow use of Copilot CLI billed to the organization' 정책이
|
|
173
|
+
copilotState,
|
|
174
|
+
`${copilotAi === true ? "" : "켜면 "}GitHub Copilot AI Credits가 소비됩니다 — 조직은 'Allow use of Copilot CLI billed to the organization' 정책이 필요합니다.`,
|
|
168
175
|
"꺼져 있거나 사용할 수 없으면 규칙 기반 요약으로 자동 전환되므로 그대로 두셔도 됩니다.",
|
|
169
176
|
],
|
|
170
177
|
});
|
package/src/commands/dry-run.js
CHANGED
|
@@ -9,6 +9,11 @@ import { planUninstall } from "./uninstall.js";
|
|
|
9
9
|
import { renderVersionYml, parseExisting } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
|
+
import { planScripts } from "../core/copy/simple.js";
|
|
13
|
+
import { planVersionSection } from "../core/copy/readme.js";
|
|
14
|
+
import { BASELINE_PATH, readBaseline } from "../core/baseline.js";
|
|
15
|
+
import { cleanupWorkflows } from "./full.js";
|
|
16
|
+
import { planGitignore } from "../core/copy/gitignore.js";
|
|
12
17
|
|
|
13
18
|
function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
14
19
|
const { paths = new Map() } = context;
|
|
@@ -31,12 +36,25 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
|
31
36
|
if (mode === "uninstall") {
|
|
32
37
|
return { mode, uninstall: planUninstall(payloadRoot, targetRoot, context.uninstallSelection) };
|
|
33
38
|
}
|
|
39
|
+
const workflows = planWorkflows(context, payloadRoot, targetRoot);
|
|
40
|
+
// 실제 실행과 같은 정리 판정을 파일을 건드리지 않고 돌린다 — 배포 방식 변경·스토어 해제로 지워지거나
|
|
41
|
+
// .bak으로 옮겨질 파일이 미리보기에서 빠지면 안 된다. 비대화형 실행이 새로 쓰는 파일은 신규·자동 갱신분이다.
|
|
42
|
+
const justWritten = [...workflows.newFiles, ...workflows.upstreamOnly].map((f) => f.filename);
|
|
43
|
+
const cleanup = cleanupWorkflows(context, payloadRoot, targetRoot, readBaseline(targetRoot), { justWritten, dryRun: true });
|
|
44
|
+
// .bak이 생기면 실제 실행은 .gitignore에 백업 파일 항목을 보장한다. 비대화형 충돌은 기존 유지라 .bak을 만들지 않는다.
|
|
45
|
+
const backedUp = Object.values(cleanup).some((r) => r.backedUp.length > 0);
|
|
34
46
|
return {
|
|
35
47
|
mode,
|
|
36
|
-
workflows
|
|
48
|
+
workflows,
|
|
49
|
+
cleanup,
|
|
50
|
+
gitignore: backedUp ? planGitignore(targetRoot) : null,
|
|
37
51
|
// Flutter 스토어 배포 파일(Fastfile·ExportOptions.plist) — 이미 있는 파일은 덮어쓰지 않고 유지한다.
|
|
38
52
|
flutterApp: planFlutterAppFiles(context, payloadRoot, targetRoot),
|
|
39
53
|
versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
|
|
54
|
+
// 실제 설치가 함께 바꾸는 파일 — 특히 기존 파일을 덮어쓰는 스크립트는 미리 보여줘야 한다.
|
|
55
|
+
scripts: planScripts(payloadRoot, targetRoot),
|
|
56
|
+
readme: planVersionSection(targetRoot),
|
|
57
|
+
baselineExists: existsSync(join(targetRoot, BASELINE_PATH)),
|
|
40
58
|
};
|
|
41
59
|
}
|
|
42
60
|
|
|
@@ -48,6 +66,7 @@ export function printDryRun(plan) {
|
|
|
48
66
|
for (const f of u.workflows) lines.push(` - ${f}`);
|
|
49
67
|
lines.push(`제거될 스크립트 (${u.scripts.length}개):`);
|
|
50
68
|
for (const f of u.scripts) lines.push(` - ${f}`);
|
|
69
|
+
for (const f of u.appFiles || []) lines.push(`제거될 Flutter 앱 파일: ${f}`);
|
|
51
70
|
if (u.readme) lines.push("제거될 항목: README.md 버전 섹션 (AUTO-VERSION-SECTION)");
|
|
52
71
|
if (u.gitignore) lines.push("제거될 항목: .gitignore 자동 추가 항목");
|
|
53
72
|
if (u.versionYml) lines.push("제거될 파일: version.yml");
|
|
@@ -60,6 +79,23 @@ export function printDryRun(plan) {
|
|
|
60
79
|
for (const f of w.changed) lines.push(` ~ ${f.filename} [${f.type}]`);
|
|
61
80
|
lines.push(`동일한 파일 (${w.unchanged.length}개, 변경 없음)`);
|
|
62
81
|
}
|
|
82
|
+
if (plan.cleanup) {
|
|
83
|
+
const reasons = { cleanup: "이전 배포 방식 정리", storeCleanup: "선택 해제된 스토어 워크플로우 정리", staleCleanup: "현재 버전에 없는 이전 워크플로우 정리" };
|
|
84
|
+
const removed = [];
|
|
85
|
+
const backedUp = [];
|
|
86
|
+
for (const [key, reason] of Object.entries(reasons)) {
|
|
87
|
+
for (const f of plan.cleanup[key]?.removed || []) removed.push(` - ${f} (${reason})`);
|
|
88
|
+
for (const f of plan.cleanup[key]?.backedUp || []) backedUp.push(` > ${f} → ${f}.bak (${reason}, 수정본이라 백업)`);
|
|
89
|
+
}
|
|
90
|
+
if (removed.length) lines.push(`삭제될 파일 (${removed.length}개):`, ...removed);
|
|
91
|
+
if (backedUp.length) lines.push(`.bak으로 옮겨질 파일 (${backedUp.length}개):`, ...backedUp);
|
|
92
|
+
}
|
|
93
|
+
if (plan.gitignore) {
|
|
94
|
+
const g = plan.gitignore;
|
|
95
|
+
lines.push(g.created
|
|
96
|
+
? `.gitignore: 새로 생성될 예정 (${g.added.join(", ")})`
|
|
97
|
+
: g.added.length ? `.gitignore: ${g.added.join(", ")} 추가될 예정` : ".gitignore: 변경 없음 (이미 있는 항목)");
|
|
98
|
+
}
|
|
63
99
|
if (plan.flutterApp && (plan.flutterApp.created.length || plan.flutterApp.kept.length)) {
|
|
64
100
|
const { created, kept } = plan.flutterApp;
|
|
65
101
|
lines.push(`Flutter 스토어 배포 파일 — 신규 (${created.length}개):`);
|
|
@@ -75,6 +111,23 @@ export function printDryRun(plan) {
|
|
|
75
111
|
// spring 등 deploy 블록이 있는 타입은 실제 설치 결과와 미리보기가 다를 수 있음을 안내한다.
|
|
76
112
|
lines.push(" (참고: 배포 설정 질문이 있는 타입(spring 등)은 deploy: 블록이 미리보기에 반영되지 않아 실제 설치와 다르게 보일 수 있습니다.)");
|
|
77
113
|
}
|
|
114
|
+
if (plan.scripts) {
|
|
115
|
+
const mark = { create: "+", overwrite: "~", unchanged: "=" };
|
|
116
|
+
const note = { create: "새로 생성", overwrite: "기존 파일을 새 버전으로 덮어씀 — 직접 고친 내용은 사라집니다", unchanged: "변경 없음" };
|
|
117
|
+
lines.push(`스크립트 (${PATHS.scriptsDir}/, ${plan.scripts.length}개 — 항상 새 버전으로 교체):`);
|
|
118
|
+
for (const s of plan.scripts) lines.push(` ${mark[s.action]} ${s.name} (${note[s.action]})`);
|
|
119
|
+
}
|
|
120
|
+
if (plan.readme) {
|
|
121
|
+
lines.push({
|
|
122
|
+
added: "README.md: 끝에 버전 섹션이 추가될 예정",
|
|
123
|
+
"skip-no-readme": "README.md: 파일이 없어 버전 섹션을 추가하지 않음",
|
|
124
|
+
"skip-marker": "README.md: 이미 버전 섹션이 있어 변경 없음",
|
|
125
|
+
"skip-version-line": "README.md: 이미 버전 줄이 있어 변경 없음",
|
|
126
|
+
}[plan.readme] || `README.md: ${plan.readme}`);
|
|
127
|
+
}
|
|
128
|
+
if (plan.baselineExists !== undefined) {
|
|
129
|
+
lines.push(`${BASELINE_PATH}: ${plan.baselineExists ? "갱신될 예정" : "새로 생성될 예정"} (다음 업데이트의 비교 기준)`);
|
|
130
|
+
}
|
|
78
131
|
}
|
|
79
132
|
lines.push("");
|
|
80
133
|
console.log(lines.join("\n"));
|
package/src/commands/full.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
// full 모드 오케스트레이터
|
|
1
|
+
// full 모드 오케스트레이터 — 대화형·비대화형이 확정한 context로 실제 설치를 수행한다.
|
|
2
2
|
// 복사 순서: workflows(+env 치환) → flutter 앱 파일 → version.yml → readme → scripts → gitignore(조건부)
|
|
3
3
|
// gitignore는 충돌 백업 부산물(.bak/.template.yaml)이 이번 실행에서 실제로 생겼을 때만 갱신한다.
|
|
4
|
-
// (원본의 util/issue/discussion/setup-guide/config 설치는 project-auto-wizard 스코프에서 제외 — DESIGN-SPEC §2)
|
|
5
4
|
import { join } from "node:path";
|
|
6
5
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
|
-
import { writeText } from "../core/fsutil.js";
|
|
6
|
+
import { writeText, findUnwritable } from "../core/fsutil.js";
|
|
7
|
+
import { CliError } from "../core/errors.js";
|
|
8
8
|
import { PATHS } from "../core/paths.js";
|
|
9
|
-
import { renderVersionYml, parseExisting } from "../core/version-yml.js";
|
|
9
|
+
import { renderVersionYml, parseExisting, sameIgnoringTimestamps } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
|
-
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
12
|
+
import { addVersionSectionToReadme, README_STATUS_LABEL } from "../core/copy/readme.js";
|
|
13
13
|
import { copyWorkflows, computeBaselineEntries, makeSrcText } from "../core/copy/workflows.js";
|
|
14
|
-
import { copyScripts } from "../core/copy/simple.js";
|
|
14
|
+
import { copyScripts, removeScriptBytecode } from "../core/copy/simple.js";
|
|
15
15
|
import { copyFlutterAppFiles } from "../core/copy/flutter-app.js";
|
|
16
16
|
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
17
|
-
import { readBaseline, writeBaseline } from "../core/baseline.js";
|
|
18
|
-
import { scanUnsubstituted,
|
|
19
|
-
import { cleanupOtherDeployWorkflows, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
17
|
+
import { readBaseline, writeBaseline, appFileHash } from "../core/baseline.js";
|
|
18
|
+
import { scanUnsubstituted, classifySecrets, narrowSecretsBySshAuth } from "../core/verify.js";
|
|
19
|
+
import { cleanupOtherDeployWorkflows, payloadWorkflowNames, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
20
20
|
import { cleanupDeselectedStoreWorkflows } from "../core/flutter-options.js";
|
|
21
|
+
import { findStaleWorkflows, cleanupStaleWorkflows } from "../core/removal-plan.js";
|
|
21
22
|
import { log, maskValue } from "../core/logger.js";
|
|
22
23
|
|
|
23
24
|
// context: { version, types, paths:Map, branch, versionCode,
|
|
@@ -28,17 +29,28 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
28
29
|
force = true, now, today, templateVersion = "unknown",
|
|
29
30
|
includeSemverAuto } = context;
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
+
// 아무것도 쓰기 전에 권한부터 확인한다 — 도중에 멈추면 반쯤 설치된 상태가 남는다.
|
|
33
|
+
const blocked = findUnwritable(targetRoot,
|
|
34
|
+
[".", PATHS.workflowsDir, PATHS.scriptsDir, ".github/.wizard"],
|
|
35
|
+
[PATHS.versionFile, "README.md", ".gitignore", ".github/.wizard/baseline.json"]);
|
|
36
|
+
if (blocked.length) {
|
|
37
|
+
throw new CliError(`쓰기 권한이 없어 설치를 시작하지 않았습니다 (아무 파일도 바꾸지 않았습니다):\n${blocked.map((p) => ` - ${p}`).join("\n")}\n권한을 확인한 뒤 다시 실행하세요.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// project_paths 마커 계산.
|
|
32
41
|
// 대표 마커명이 아니라 그 폴더에 실제로 있는 파일을 쓴다 — build.gradle.kts만 있는 레포의
|
|
33
42
|
// version.yml에 "# build.gradle"이라고 적히면 감지 로그와 같은 종류의 거짓말이 된다.
|
|
34
43
|
const pathMarkers = new Map();
|
|
35
44
|
for (const [t, p] of paths) {
|
|
36
45
|
const marker = existingMarkerInDir(t, join(targetRoot, p || "."));
|
|
37
46
|
pathMarkers.set(t, marker);
|
|
38
|
-
|
|
47
|
+
// 파일이 실제로 없으면 사용자가 직접 고른 타입이다 — 대표 파일명을 근거로 적으면 감지된 것처럼 보인다.
|
|
48
|
+
const found = marker && existsSync(join(targetRoot, p || ".", marker));
|
|
49
|
+
log.info("detect", "type", `${t} (근거: ${found ? marker : "직접 선택"})`);
|
|
39
50
|
}
|
|
40
51
|
log.info("detect", "version", `${version}${context.versionSource ? ` (${context.versionSource})` : ""}`);
|
|
41
52
|
log.info("detect", "branch", `${branch}${context.branches ? ` | main=${context.branches.main} develop=${context.branches.develop} mode=${context.branches.mode}` : ""}`);
|
|
53
|
+
logChoices(context, types);
|
|
42
54
|
for (const a of context.envAnswers || []) {
|
|
43
55
|
log.info("prompt", a.isDefault ? "default" : "answer", `${a.key}=${maskValue(a.key, a.value)}`);
|
|
44
56
|
}
|
|
@@ -56,19 +68,30 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
56
68
|
|
|
57
69
|
// 기존 version.yml의 알려지지 않은 최상위 필드를 재생성 시 보존한다.
|
|
58
70
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
59
|
-
const
|
|
71
|
+
const prevVy = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
72
|
+
const extraTopLevel = prevVy != null ? parseExisting(prevVy).extraTopLevel : [];
|
|
60
73
|
|
|
61
74
|
// 2. version.yml 생성 (payload/version.yml.template 렌더링 — 전체 재생성)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
// 날짜 줄만 달라지는 재실행은 다시 쓰지 않는다(멱등).
|
|
76
|
+
const vyText = renderVersionYml(context, readVersionYmlTemplate(payloadRoot), { pathMarkers, deployValues, extraTopLevel });
|
|
77
|
+
if (prevVy == null || !sameIgnoringTimestamps(prevVy, vyText)) {
|
|
78
|
+
writeText(vyPath, vyText);
|
|
79
|
+
log.info("version", "write", `version.yml (v${version}, code=${versionCode})`);
|
|
80
|
+
} else {
|
|
81
|
+
log.info("version", "skip", `version.yml (v${version}, code=${versionCode}, 변경 없음)`);
|
|
82
|
+
}
|
|
66
83
|
|
|
67
84
|
// 3. README 버전 섹션
|
|
68
|
-
addVersionSectionToReadme(version, targetRoot);
|
|
85
|
+
const readme = addVersionSectionToReadme(version, targetRoot);
|
|
86
|
+
log.info("readme", readme === "added" ? "append" : "skip", README_STATUS_LABEL[readme] || readme);
|
|
69
87
|
|
|
70
|
-
// 4. scripts (payload/scripts/*.py → .github/scripts/)
|
|
71
|
-
copyScripts(payloadRoot, targetRoot);
|
|
88
|
+
// 4. scripts (payload/scripts/*.py → .github/scripts/) — 항상 덮어쓰므로 사용자 수정이 사라진 사실도 남긴다.
|
|
89
|
+
const scripts = copyScripts(payloadRoot, targetRoot);
|
|
90
|
+
for (const { name, action } of scripts) {
|
|
91
|
+
log.info("script", action, `${PATHS.scriptsDir}/${name}${action === "overwrite" ? " (기존 내용과 달라 새 버전으로 덮어씀)" : ""}`);
|
|
92
|
+
}
|
|
93
|
+
const bytecodeRemoved = removeScriptBytecode(targetRoot);
|
|
94
|
+
for (const rel of bytecodeRemoved) log.info("script", "remove", `${rel} (예전 버전이 커밋한 바이트코드 — 삭제를 함께 커밋하세요)`);
|
|
72
95
|
|
|
73
96
|
// 5. gitignore — 워크플로우 충돌 처리가 .bak나 .template.yaml을 실제로 만든 경우에만 갱신한다.
|
|
74
97
|
// 충돌 없는 설치(대부분의 최초 설치)는 .gitignore를 전혀 건드리지 않는다.
|
|
@@ -77,35 +100,28 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
77
100
|
// 6. 이전 배포 방식 정리 — 방식을 바꿔 재설치하면 이전 CD가 남아 배포가 두 번 돈다.
|
|
78
101
|
// 옛 baseline이 살아 있는 지금이 "사용자가 손댔는가"를 판정할 수 있는 유일한 시점이다.
|
|
79
102
|
const previousBaseline = readBaseline(targetRoot);
|
|
80
|
-
const cleanup =
|
|
81
|
-
|
|
82
|
-
existsSync(join(targetRoot, PATHS.workflowsDir)) ? readdirSync(join(targetRoot, PATHS.workflowsDir)) : [],
|
|
83
|
-
context.deployStyle || DEFAULT_DEPLOY_STYLE,
|
|
84
|
-
previousBaseline);
|
|
103
|
+
const { cleanup, storeCleanup, staleCleanup } = cleanupWorkflows(context, payloadRoot, targetRoot, previousBaseline,
|
|
104
|
+
{ justWritten: wfCounters.copiedFiles || [] });
|
|
85
105
|
// 지운 파일의 기준점은 baseline에서도 빼야 다음 실행에서 "사용자가 지웠다"로 오인하지 않는다.
|
|
86
|
-
for (const
|
|
106
|
+
for (const r of [cleanup, storeCleanup, staleCleanup]) {
|
|
107
|
+
for (const f of [...r.removed, ...r.backedUp]) delete previousBaseline?.files?.[f];
|
|
108
|
+
}
|
|
87
109
|
|
|
88
110
|
for (const f of cleanup.removed || []) log.info("cleanup", "remove", `${f} (이전 배포 방식 정리)`);
|
|
89
111
|
for (const f of cleanup.backedUp || []) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
90
|
-
|
|
91
|
-
// 6-1. 선택 해제된 스토어 워크플로우 정리 — 스토어 대상을 줄여 재설치하면 이전 워크플로우가 남아
|
|
92
|
-
// main push마다 계속 도는 것을 막는다. 규칙은 6과 같다(미수정 삭제, 수정본 .bak).
|
|
93
|
-
// 선택이 미결정(null)이거나 Flutter가 없으면 현행 동작 그대로 아무것도 지우지 않는다.
|
|
94
|
-
// Fastfile·ExportOptions는 사용자 소유라 여기서 다루지 않는다.
|
|
95
|
-
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
96
|
-
const storeCleanup = Array.isArray(context.flutterStore) && types.includes("flutter")
|
|
97
|
-
? cleanupDeselectedStoreWorkflows(
|
|
98
|
-
workflowsDir,
|
|
99
|
-
existsSync(workflowsDir) ? readdirSync(workflowsDir) : [],
|
|
100
|
-
context.flutterStore,
|
|
101
|
-
previousBaseline)
|
|
102
|
-
: { removed: [], backedUp: [] };
|
|
103
|
-
for (const f of [...storeCleanup.removed, ...storeCleanup.backedUp]) delete previousBaseline?.files?.[f];
|
|
104
112
|
for (const f of storeCleanup.removed) log.info("cleanup", "remove", `${f} (선택 해제된 스토어 워크플로우 정리)`);
|
|
105
113
|
for (const f of storeCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
114
|
+
for (const f of staleCleanup.removed) log.info("cleanup", "remove", `${f} (현재 버전에 없는 이전 워크플로우 정리)`);
|
|
115
|
+
for (const f of staleCleanup.backedUp) log.info("cleanup", "backup", `${f} → ${f}.bak`);
|
|
116
|
+
const cleanupCount = (key) => [cleanup, storeCleanup, staleCleanup].reduce((n, r) => n + r[key].length, 0);
|
|
117
|
+
|
|
118
|
+
const gitignoreUpdated = gitignoreUpdated0 || cleanup.backedUp.length > 0 || storeCleanup.backedUp.length > 0
|
|
119
|
+
|| staleCleanup.backedUp.length > 0;
|
|
120
|
+
if (gitignoreUpdated) {
|
|
121
|
+
const gi = ensureGitignore(targetRoot);
|
|
122
|
+
log.info("gitignore", gi.created ? "create" : gi.added.length ? "append" : "skip",
|
|
123
|
+
gi.added.length ? `.gitignore += ${gi.added.join(", ")}` : ".gitignore (이미 있는 항목)");
|
|
124
|
+
}
|
|
109
125
|
|
|
110
126
|
// 7. baseline 기록 — 다음 업데이트에서 "누가 바꿨는지"를 가를 기준점.
|
|
111
127
|
// env 치환까지 전부 끝난 뒤에 해시해야 디스크 내용이 최종형이다. 그래서 copyWorkflows 안이
|
|
@@ -117,8 +133,11 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
117
133
|
entries: computeBaselineEntries(
|
|
118
134
|
wfCounters.baselineTargets || new Map(),
|
|
119
135
|
join(targetRoot, PATHS.workflowsDir),
|
|
120
|
-
makeSrcText(context.branches || null, context.deployStyle || "")
|
|
136
|
+
makeSrcText(context.branches || null, context.deployStyle || ""),
|
|
137
|
+
deployValues),
|
|
121
138
|
previous: previousBaseline,
|
|
139
|
+
// 새로 만든 Flutter 앱 파일만 기록한다 — 기존 사용자 파일(kept)은 완전 삭제 대상이 아니다.
|
|
140
|
+
appFiles: new Map(flutterApp.created.map((rel) => [rel, appFileHash(readFileSync(join(targetRoot, rel), "utf8"))])),
|
|
122
141
|
});
|
|
123
142
|
|
|
124
143
|
// 8. 설치 후 검증 — 디스크에 실제로 쓰인 내용을 다시 읽어 확인한다.
|
|
@@ -127,26 +146,84 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
127
146
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
128
147
|
const managed = [...(wfCounters.baselineTargets || new Map()).keys()];
|
|
129
148
|
const unresolved = scanUnsubstituted(wfDir, managed);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
// 기본값이 있거나 둘 중 하나면 되는 secret은 필수와 나눠 안내한다.
|
|
150
|
+
const secretSets = classifySecrets(wfDir, managed);
|
|
151
|
+
const secrets = narrowSecretsBySshAuth(secretSets.required, firstDeployValue(deployValues, "SSH_AUTH_METHOD"));
|
|
152
|
+
const optionalSecrets = secretSets.optional;
|
|
134
153
|
|
|
135
154
|
// 9. 요약 — 파일 끝에 결과 블록을 붙인다. tail만 봐도 결과가 보이도록.
|
|
136
155
|
for (const u of unresolved) log.warn("verify", "unresolved", `${u.filename}:${u.line} ${u.token}`);
|
|
137
156
|
for (const [name, users] of secrets) log.info("verify", "secret", `${name} ← ${users.join(", ")}`);
|
|
157
|
+
for (const [name, users] of optionalSecrets) log.info("verify", "secret-opt", `${name} (선택) ← ${users.join(", ")}`);
|
|
158
|
+
// copiedFiles에는 자동 갱신분도 들어 있다 — 빼지 않으면 같은 파일이 설치와 자동 갱신에 두 번 잡힌다.
|
|
159
|
+
const autoUpdated = new Set(wfCounters.autoUpdated || []);
|
|
138
160
|
log.summary([
|
|
139
|
-
["설치", `${(wfCounters.copiedFiles || []).length}개 파일`],
|
|
140
|
-
["자동 갱신", `${
|
|
161
|
+
["설치", `${(wfCounters.copiedFiles || []).filter((f) => !autoUpdated.has(f)).length}개 파일`],
|
|
162
|
+
["자동 갱신", `${autoUpdated.size}개 (사용자 미수정)`],
|
|
141
163
|
["유지", `${(wfCounters.keptLocal || []).length}개 (사용자 수정본)`],
|
|
142
164
|
["변경 없음", `${(wfCounters.unchangedFiles || []).length}개`],
|
|
143
165
|
["백업 교체", `${wfCounters.backupAdded || 0}개 (.bak 생성)`],
|
|
166
|
+
// 배포 방식 변경·스토어 해제·옛 워크플로우 정리도 파일을 지우거나 .bak으로 옮긴다 — 충돌 백업과 따로 센다.
|
|
167
|
+
["정리", `삭제 ${cleanupCount("removed")}개, .bak 이동 ${cleanupCount("backedUp")}개`],
|
|
144
168
|
["미치환", `${unresolved.length}건${unresolved.length ? " ← 조치 필요" : ""}`],
|
|
145
169
|
["필요 Secret", `${secrets.size}개`],
|
|
146
170
|
["결과", unresolved.length ? `주의 (미치환 ${unresolved.length}건)` : "OK"],
|
|
147
171
|
]);
|
|
148
172
|
|
|
149
|
-
return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup, storeCleanup, flutterApp };
|
|
173
|
+
return { workflows: wfCounters, gitignoreUpdated, unresolved, secrets, cleanup, storeCleanup, staleCleanup, flutterApp, readme, scripts, bytecodeRemoved, optionalSecrets };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 워크플로우 정리 3종 — 실제 설치(runFull)와 --dry-run 미리보기가 같은 판정을 쓴다.
|
|
177
|
+
// 6 배포 방식 정리 — 방식을 바꿔 재설치하면 이전 CD가 남아 배포가 두 번 돈다.
|
|
178
|
+
// 6-1 선택 해제된 스토어 워크플로우 정리 — 스토어 대상을 줄여 재설치하면 이전 워크플로우가 main push마다
|
|
179
|
+
// 계속 돈다. 선택이 미결정(null)이거나 Flutter가 없으면 아무것도 지우지 않는다.
|
|
180
|
+
// Fastfile·ExportOptions는 사용자 소유라 여기서 다루지 않는다.
|
|
181
|
+
// 6-2 payload에서 이름이 바뀌거나 빠진 옛 워크플로우 정리.
|
|
182
|
+
// 규칙은 모두 같다(미수정 삭제, 수정본 .bak). 세 대상은 파일명이 겹치지 않아 순서와 무관하게 판정이 같다.
|
|
183
|
+
// baseline은 정리 전의 것을 넘겨야 한다 — "사용자가 손댔는가"를 판정할 수 있는 유일한 기준이다.
|
|
184
|
+
// dryRun이면 판정만 하고 파일은 건드리지 않는다.
|
|
185
|
+
export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { justWritten = [], dryRun = false } = {}) {
|
|
186
|
+
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
187
|
+
const listDir = () => (existsSync(workflowsDir) ? readdirSync(workflowsDir) : []);
|
|
188
|
+
const cleanup = cleanupOtherDeployWorkflows(workflowsDir, listDir(),
|
|
189
|
+
context.deployStyle || DEFAULT_DEPLOY_STYLE, baseline,
|
|
190
|
+
{ available: payloadWorkflowNames(payloadRoot), justWritten, dryRun });
|
|
191
|
+
const storeCleanup = Array.isArray(context.flutterStore) && (context.types || []).includes("flutter")
|
|
192
|
+
? cleanupDeselectedStoreWorkflows(workflowsDir, listDir(), context.flutterStore, baseline, { dryRun })
|
|
193
|
+
: { removed: [], backedUp: [] };
|
|
194
|
+
const staleCleanup = cleanupStaleWorkflows(targetRoot, findStaleWorkflows(payloadRoot, targetRoot, baseline), baseline, { dryRun });
|
|
195
|
+
return { cleanup, storeCleanup, staleCleanup };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 설치 결과를 가른 선택(배포 방식·자동 승격·Copilot·Flutter 옵션)을 남긴다 — 대화형에서 고른 값도
|
|
199
|
+
// 여기로 모이므로, 나중에 "왜 이렇게 설치됐나"를 로그만으로 따라갈 수 있다.
|
|
200
|
+
function logChoices(context, types) {
|
|
201
|
+
if (context.deployStyle) log.info("option", "deploy", context.deployStyle);
|
|
202
|
+
if (context.includeSemverAuto != null) log.info("option", "semver", context.includeSemverAuto ? "on" : "off");
|
|
203
|
+
if (context.includeCopilotAi != null) log.info("option", "copilot", context.includeCopilotAi ? "on" : "off");
|
|
204
|
+
if (types.includes("flutter")) {
|
|
205
|
+
const stores = Array.isArray(context.flutterStore) ? (context.flutterStore.join(",") || "없음") : "미결정(둘 다)";
|
|
206
|
+
log.info("option", "flutter",
|
|
207
|
+
`env=${context.envMode || "-"} stores=${stores} android=${context.androidDeployMode || "-"} ios=${context.iosDeployMode || "-"}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 완료 요약과 별도로 알려야 하는 사실 — 비대화형 실행이 조용히 넘어가면 사용자는 업데이트를 다 받았다고 믿는다.
|
|
212
|
+
// interactive: 대화형은 충돌을 사용자가 직접 골랐으므로 충돌 안내를 빼고 정리 결과만 알린다.
|
|
213
|
+
export function postInstallNotices(result, { interactive = false } = {}) {
|
|
214
|
+
const lines = [];
|
|
215
|
+
const stale = result?.staleCleanup || { removed: [], backedUp: [] };
|
|
216
|
+
if (stale.removed.length || stale.backedUp.length) {
|
|
217
|
+
lines.push("현재 버전에 없는 이전 워크플로우를 정리했습니다 (이름이 바뀌었거나 제거된 파일):");
|
|
218
|
+
for (const f of stale.removed) lines.push(` • ${f} — 삭제 (손대지 않은 파일)`);
|
|
219
|
+
for (const f of stale.backedUp) lines.push(` • ${f} → ${f}.bak — 수정하신 내용이 있어 백업`);
|
|
220
|
+
}
|
|
221
|
+
const kept = interactive ? [] : result?.workflows?.conflictKept || [];
|
|
222
|
+
if (kept.length) {
|
|
223
|
+
lines.push(`충돌 ${kept.length}개 — 기존 파일을 유지했습니다 (업스트림 변경 미반영): ${kept.join(", ")}`);
|
|
224
|
+
lines.push(" 파일마다 고르려면 대화형(npx project-auto-wizard)으로 다시 실행하세요.");
|
|
225
|
+
}
|
|
226
|
+
return lines;
|
|
150
227
|
}
|
|
151
228
|
|
|
152
229
|
// deployValues는 Map<type, Map<key,value>> — 타입 구분 없이 첫 값만 필요할 때 쓴다.
|
|
@@ -46,7 +46,7 @@ async function askUnsetDeployModes(io, state) {
|
|
|
46
46
|
|
|
47
47
|
// 아직 정해지지 않은 옵션만 묻는다.
|
|
48
48
|
// envModeDefault — 신규 설치 dart-define / 기존 설치 dotenv (동작 보존)
|
|
49
|
-
// inferredStores — 저장값 없는 기존 설치가 이미 쓰던 스토어 (신규 설치는
|
|
49
|
+
// inferredStores — 저장값 없는 기존 설치가 이미 쓰던 스토어 (신규 설치는 CLI 기본값과 같은 전체)
|
|
50
50
|
export async function askUnsetFlutterOptions(io, state, { envModeDefault, inferredStores }) {
|
|
51
51
|
const next = { ...state };
|
|
52
52
|
if (!next.envMode) {
|