project-auto-wizard 0.12.2 → 0.13.1
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 +9 -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 +482 -107
- package/payload/version.yml.template +5 -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 +26 -60
- package/src/commands/dry-run.js +59 -5
- package/src/commands/full.js +135 -55
- package/src/commands/install-settings.js +51 -0
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +135 -69
- package/src/commands/purge.js +50 -21
- package/src/commands/status.js +18 -12
- package/src/commands/uninstall.js +22 -22
- 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/app-files.js +28 -0
- 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 +133 -111
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +177 -47
- package/src/core/errors.js +3 -0
- package/src/core/flutter-doctor.js +49 -0
- package/src/core/flutter-hooks.js +29 -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-exec.js +67 -0
- package/src/core/removal-plan.js +75 -19
- package/src/core/types.js +136 -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 +128 -60
- 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/ui/env-plan.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
// @wizard env 계획 질문 UI
|
|
2
|
-
// _wf_prefill_all/_wf_prefill_interactive/wf_prompt_env_plan 등가).
|
|
3
|
-
// 실측 기준: template_integrator.sh 3059~3085(scope), 3087~3143(collect), 3152~3169(card),
|
|
4
|
-
// 3220~3280(prompt 본체 wf_prompt_env_plan).
|
|
1
|
+
// @wizard env 계획 질문 UI — ask 키 수집, 필드 카드 출력, 전부 기본값/일부만 변경 선택.
|
|
5
2
|
// io 주입식 — 테스트는 {select, multiselect, text} 스텁을 넘긴다. 기본은 readline-engine 실물.
|
|
6
3
|
import { join } from "node:path";
|
|
7
4
|
import { readFileSync } from "node:fs";
|
|
8
5
|
import { stdin, stderr } from "node:process";
|
|
9
|
-
import { PAYLOAD } from "../core/paths.js";
|
|
6
|
+
import { PAYLOAD, typeWorkflowDirs } from "../core/paths.js";
|
|
10
7
|
import { exists, listYamlFiles } from "../core/fsutil.js";
|
|
11
8
|
import { parseWizardLine, resolveToken, replaceProjectTokens } from "../core/wizard-env.js";
|
|
12
9
|
import { loadWizardPrompts, wfField, workflowDisplayName } from "../core/wizard-labels.js";
|
|
13
|
-
import { deployFilter, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
14
|
-
import { buildTypeRootFilter } from "../core/copy/workflows.js";
|
|
10
|
+
import { deployFilter, payloadWorkflowNames, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
11
|
+
import { buildTypeRootFilter, readSavedDeployValues } from "../core/copy/workflows.js";
|
|
15
12
|
import * as engine from "./readline-engine.js";
|
|
16
13
|
|
|
17
14
|
const CANCEL = engine.CANCEL;
|
|
18
15
|
|
|
19
|
-
// 진행 안내는 stderr로 출력 (
|
|
16
|
+
// 진행 안내는 stderr로 출력 (stdout 파이프 오염 방지).
|
|
20
17
|
const defaultLog = (s = "") => stderr.write(s + "\n");
|
|
21
18
|
|
|
22
|
-
// 사용처 문자열
|
|
19
|
+
// 사용처 문자열 조립.
|
|
23
20
|
// usages: [{type, workflowName}] — 타입이 여러 개면 타입만 "t1·t2", 하나면 "타입 name1·name2".
|
|
24
21
|
export function scopeString(usages = []) {
|
|
25
22
|
const types = []; const names = [];
|
|
@@ -31,18 +28,21 @@ export function scopeString(usages = []) {
|
|
|
31
28
|
return `${types.join("·")} ${names.join("·")}`.trim();
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
// ask KEY 수집
|
|
31
|
+
// ask KEY 수집 — 실제 설치되는 워크플로우와 같은 소스를 스캔한다.
|
|
35
32
|
// payloadRoot: 패키지 payload/ 루트. types: 설치 대상 타입 목록.
|
|
36
33
|
// opts:
|
|
37
|
-
// resolvers - @접두 기본값(@repo 등) 해석용 (
|
|
34
|
+
// resolvers - @접두 기본값(@repo 등) 해석용 (수집 시점에 해석)
|
|
38
35
|
// flutterStore - Flutter 스토어 대상(string[]|null). 배열이면 선택 해제된 스토어 워크플로우는 스캔에서 제외 (null=현행)
|
|
39
36
|
// prompts - wizard-labels 파싱 객체 (워크플로우 표시명용, null이면 확장자 제거 폴백)
|
|
37
|
+
// saved - Map<type, Map<key,value>>: version.yml deploy 블록 저장값. 있으면 그 값을 기본값으로 보여준다
|
|
38
|
+
// (업데이트 때 "기본값 그대로"를 고르면 설치 때 답한 값이 유지되어야 한다)
|
|
40
39
|
// 반환: { keys:[], defaults:Map<key,default>, typeDefaults:Map<"type|key",default>,
|
|
41
40
|
// usages:Map<key,[{type,workflowName}]> }
|
|
42
41
|
export function collectAsks(payloadRoot, types = [], opts = {}) {
|
|
43
|
-
const { resolvers = {}, deployStyle = "", flutterStore = null, prompts = null } = opts;
|
|
42
|
+
const { resolvers = {}, deployStyle = "", flutterStore = null, prompts = null, saved = new Map() } = opts;
|
|
44
43
|
// 설치하지 않을 배포 워크플로우의 질문까지 묻지 않는다 — 질문 수는 설치 범위를 따라간다.
|
|
45
|
-
const
|
|
44
|
+
const available = payloadWorkflowNames(payloadRoot);
|
|
45
|
+
const keepDeploy = deployFilter(deployStyle, available);
|
|
46
46
|
const baseDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
47
47
|
const keys = [];
|
|
48
48
|
const defaults = new Map();
|
|
@@ -55,19 +55,15 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
|
|
|
55
55
|
const commonDir = join(baseDir, "common");
|
|
56
56
|
if (exists(commonDir)) units.push(["common", commonDir, null]);
|
|
57
57
|
for (const type of types) {
|
|
58
|
-
const typeDir =
|
|
58
|
+
const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
|
|
59
59
|
if (!exists(typeDir)) continue;
|
|
60
|
-
// 복사 엔진과 동일한 폴더 구성: 타입 직하위 + ("배포 안 함"이
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// go/python처럼 CD가 server-deploy 없이 타입 루트에 바로 있는 타입은, "배포 안 함"일 때
|
|
64
|
-
// 타입 루트 스캔에도 keepDeploy를 걸어야 CD 전용 ask 키(예: DEPLOY_PORT)가 걸러진다.
|
|
65
|
-
// PR 프리뷰 자체의 ask 키(SSH_AUTH_METHOD 등)는 이 필터로는 걸러지지 않는다 — PR 프리뷰는
|
|
66
|
-
// 배포 방식과 무관하게 항상 설치되는 별도 축이라 의도된 잔여 범위다.
|
|
60
|
+
// 복사 엔진과 동일한 폴더 구성: 타입 직하위 + ("배포 안 함"이 아닐 때만) server-deploy.
|
|
61
|
+
// go/python·react/next처럼 서버 배포 워크플로우가 타입 루트에 바로 있는 타입도 같은 배포 방식
|
|
62
|
+
// 필터로 거른다 — "배포 안 함"이면 CD·PR 프리뷰 전용 ask 키(DEPLOY_PORT, SSH_AUTH_METHOD 등)를 묻지 않는다.
|
|
67
63
|
// Flutter는 선택 해제된 스토어 워크플로우(PLAYSTORE·TESTFLIGHT)도 같은 필터로 걸러 질문 범위가 설치 범위와 같다.
|
|
68
|
-
units.push([type, typeDir, buildTypeRootFilter(type, deployStyle, flutterStore)]);
|
|
64
|
+
units.push([type, typeDir, buildTypeRootFilter(type, deployStyle, flutterStore, available)]);
|
|
69
65
|
if (deployStyle !== NO_DEPLOY_STYLE) {
|
|
70
|
-
units.push([type,
|
|
66
|
+
units.push([type, serverDeployDir, keepDeploy]);
|
|
71
67
|
}
|
|
72
68
|
}
|
|
73
69
|
|
|
@@ -79,16 +75,19 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
|
|
|
79
75
|
if (!content.includes("@wizard")) continue;
|
|
80
76
|
const workflowName = workflowDisplayName(prompts, filename);
|
|
81
77
|
for (const line of content.split(/\r?\n/)) {
|
|
82
|
-
const p = parseWizardLine(line);
|
|
78
|
+
const p = parseWizardLine(line);
|
|
83
79
|
if (!p || p.action !== "ask") continue;
|
|
84
|
-
// 타입별 기본값: @접두면 resolver 해석, 아니면 리터럴
|
|
80
|
+
// 타입별 기본값: @접두면 resolver 해석, 아니면 리터럴
|
|
85
81
|
const rawDefault = p.arg.startsWith("@")
|
|
86
82
|
? resolveToken(p.arg.slice(1), type, resolvers)
|
|
87
83
|
: p.arg;
|
|
88
84
|
// 리터럴 기본값 안에 __PROJECT_NAME__ 등이 박혀 있으면실제 repoName으로
|
|
89
85
|
// 풀어준다 — substituteEnv()가 설치 파일에 적용하는 것과 동일한 치환이라야 마법사
|
|
90
86
|
// 화면 표시와 실제 설치 결과가 어긋나지 않는다.
|
|
91
|
-
const
|
|
87
|
+
const savedValue = saved.get(type)?.get(p.key);
|
|
88
|
+
const typeDefault = savedValue != null && savedValue !== ""
|
|
89
|
+
? savedValue
|
|
90
|
+
: replaceProjectTokens(rawDefault, resolveToken("repo", type, resolvers));
|
|
92
91
|
typeDefaults.set(`${type}|${p.key}`, typeDefault);
|
|
93
92
|
if (!defaults.has(p.key)) { keys.push(p.key); defaults.set(p.key, typeDefault); }
|
|
94
93
|
const list = usages.get(p.key) || [];
|
|
@@ -100,7 +99,7 @@ export function collectAsks(payloadRoot, types = [], opts = {}) {
|
|
|
100
99
|
return { keys, defaults, typeDefaults, usages };
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
// KEY가 처음 등장한 type
|
|
102
|
+
// KEY가 처음 등장한 type — 라벨 조회 시 타입 오버라이드 우선순위용
|
|
104
103
|
function firstTypeFor(usages, key) {
|
|
105
104
|
return usages.get(key)?.[0]?.type ?? "";
|
|
106
105
|
}
|
|
@@ -122,7 +121,7 @@ function buildAnswers(prompts, asks, values, useDefaults) {
|
|
|
122
121
|
});
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
// KEY 1개를 'label·사용처·설명·예시·기본값' 카드로
|
|
124
|
+
// KEY 1개를 'label·사용처·설명·예시·기본값' 카드로 출력.
|
|
126
125
|
// info: { default, usages } — idx/tot 있으면 "(i/t)" 진행 표시. log 주입 가능(테스트 무음화).
|
|
127
126
|
export function printFieldCard(prompts, key, info, idx = null, tot = null, log = defaultLog) {
|
|
128
127
|
const t = info.usages?.[0]?.type ?? "";
|
|
@@ -146,8 +145,25 @@ function isBooleanDefault(value) {
|
|
|
146
145
|
return value === "true" || value === "false";
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
//
|
|
150
|
-
//
|
|
148
|
+
// 형식이 정해진 ask 값 검증 — 잘못된 값은 설치는 통과하고 배포 단계에서야 실패하므로 입력 시점에 다시 묻는다.
|
|
149
|
+
// 반환: 오류 문구(문제 없으면 "").
|
|
150
|
+
export function validateAskValue(key, value) {
|
|
151
|
+
const v = String(value);
|
|
152
|
+
if (/_PORT$/.test(key)) {
|
|
153
|
+
const n = Number(v);
|
|
154
|
+
return /^\d+$/.test(v) && n >= 1 && n <= 65535 ? "" : "1~65535 사이의 숫자로 입력하세요.";
|
|
155
|
+
}
|
|
156
|
+
if (key === "SSH_AUTH_METHOD") {
|
|
157
|
+
return v === "password" || v === "key" ? "" : "password 또는 key 중 하나로 입력하세요.";
|
|
158
|
+
}
|
|
159
|
+
if (key === "JAVA_VERSION") {
|
|
160
|
+
return /^\d+(\.\d+)*$/.test(v) ? "" : "JDK 버전 숫자로 입력하세요 (예: 21, 17).";
|
|
161
|
+
}
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 지정 KEY들을 하나씩 입력받아 values에 기록.
|
|
166
|
+
// 빈 입력(Enter)/ESC → KEY 공통 기본값 유지.
|
|
151
167
|
async function promptEach(io, prompts, asks, todoKeys, values, log) {
|
|
152
168
|
const tot = todoKeys.length;
|
|
153
169
|
if (tot === 0) return;
|
|
@@ -165,8 +181,15 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
|
|
|
165
181
|
const answer = await io.confirm({ message: `↳ ${label} — 활성화할까요?`, initialValue: def === "true" });
|
|
166
182
|
input = answer === CANCEL ? def : (answer ? "true" : "false");
|
|
167
183
|
} else {
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
for (;;) {
|
|
185
|
+
input = await io.text({ message: `↳ 값 입력 (Enter=기본값 «${def}» 유지):`, defaultValue: def });
|
|
186
|
+
if (input === CANCEL || input == null) { input = def; break; }
|
|
187
|
+
input = String(input).trim();
|
|
188
|
+
if (input === "") { input = def; break; }
|
|
189
|
+
const problem = validateAskValue(key, input);
|
|
190
|
+
if (!problem) break;
|
|
191
|
+
log(` ⚠️ '${input}' — ${problem}`);
|
|
192
|
+
}
|
|
170
193
|
}
|
|
171
194
|
values.set(key, input);
|
|
172
195
|
log(` → ${label} = ${input}`);
|
|
@@ -174,15 +197,15 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
|
|
|
174
197
|
}
|
|
175
198
|
}
|
|
176
199
|
|
|
177
|
-
// 배포 env 설정
|
|
200
|
+
// 배포 env 설정 계획.
|
|
178
201
|
// 반환: { values: Map<key,value>, useDefaults: boolean, answers: [{key,label,value,isDefault,scope}] }
|
|
179
|
-
// - useDefaults=true → 호출부는 substituteEnv에 그대로 넘기면 타입별 기본값 경로
|
|
202
|
+
// - useDefaults=true → 호출부는 substituteEnv에 그대로 넘기면 타입별 기본값 경로
|
|
180
203
|
// - useDefaults=false → values에 담긴 키만 사용자 확정값으로 치환, 나머지는 기본값
|
|
181
204
|
// (⚠️ substituteEnv는 useDefaults=false일 때만 values를 참조하므로 이 플래그를 반드시 함께 전달)
|
|
182
205
|
// 인자:
|
|
183
206
|
// payloadRoot/types/resolvers/flutterStore — collectAsks와 동일 의미
|
|
184
207
|
// targetRoot — wizard-prompts.yml 1차 탐색 위치(기본 ".")
|
|
185
|
-
// force — true면 질문 없이 전부 기본값
|
|
208
|
+
// force — true면 질문 없이 전부 기본값
|
|
186
209
|
// io — {select, multiselect, text} 주입 (기본 readline-engine). 테스트 스텁 지점.
|
|
187
210
|
// log — 카드·안내 출력 함수 주입 (기본 stderr)
|
|
188
211
|
export async function promptEnvPlan({
|
|
@@ -190,13 +213,14 @@ export async function promptEnvPlan({
|
|
|
190
213
|
deployStyle = "", flutterStore = null, targetRoot = ".", repoName = "", log = defaultLog,
|
|
191
214
|
} = {}) {
|
|
192
215
|
const prompts = loadWizardPrompts(targetRoot, payloadRoot);
|
|
193
|
-
const
|
|
216
|
+
const saved = readSavedDeployValues(targetRoot);
|
|
217
|
+
const asks = collectAsks(payloadRoot, types, { resolvers, deployStyle, flutterStore, prompts, saved });
|
|
194
218
|
const defaults = asks.defaults;
|
|
195
219
|
|
|
196
|
-
// 수집 키 0개 → 질문 자체가 없음
|
|
220
|
+
// 수집 키 0개 → 질문 자체가 없음
|
|
197
221
|
if (asks.keys.length === 0) return { values: new Map(), useDefaults: true, answers: [] };
|
|
198
222
|
|
|
199
|
-
// 비대화형: force 또는 (io 미주입 && 비TTY) → 전부 기본값
|
|
223
|
+
// 비대화형: force 또는 (io 미주입 && 비TTY) → 전부 기본값
|
|
200
224
|
// io가 주입돼 있으면(테스트/상위 마법사) TTY 여부와 무관하게 대화형으로 진행한다.
|
|
201
225
|
const interactive = !force && (io != null || stdin.isTTY);
|
|
202
226
|
if (!interactive) {
|
|
@@ -206,7 +230,7 @@ export async function promptEnvPlan({
|
|
|
206
230
|
|
|
207
231
|
const ui = io ?? engine;
|
|
208
232
|
|
|
209
|
-
// 기본값 미리보기 카드 전체 출력
|
|
233
|
+
// 기본값 미리보기 카드 전체 출력
|
|
210
234
|
log("");
|
|
211
235
|
log("▶ 워크플로우 환경설정을 채웁니다");
|
|
212
236
|
log("");
|
|
@@ -227,21 +251,21 @@ export async function promptEnvPlan({
|
|
|
227
251
|
{ value: "some", label: "③ 몇 개만 골라서 바꾸기 (고른 것만 입력 · 나머지는 기본값)" },
|
|
228
252
|
],
|
|
229
253
|
});
|
|
230
|
-
// ESC/취소 → 전부 기본값
|
|
254
|
+
// ESC/취소 → 전부 기본값
|
|
231
255
|
if (choice === CANCEL || choice == null || choice === "all") {
|
|
232
256
|
const values = new Map(defaults);
|
|
233
257
|
return { values, useDefaults: true, answers: buildAnswers(prompts, asks, values, true) };
|
|
234
258
|
}
|
|
235
259
|
|
|
236
260
|
// 사용자가 확정한 키만 values에 담는다 — substituteEnv(useDefaults:false)가
|
|
237
|
-
// values에 없는 키는 타입별 기본값으로
|
|
261
|
+
// values에 없는 키는 타입별 기본값으로 채운다(기본값 위에 답한 값만 덮어쓰는 것과 같다).
|
|
238
262
|
const values = new Map();
|
|
239
263
|
if (choice === "each") {
|
|
240
264
|
await promptEach(ui, prompts, asks, asks.keys, values, log);
|
|
241
265
|
return { values, useDefaults: false, answers: buildAnswers(prompts, asks, values, false) };
|
|
242
266
|
}
|
|
243
267
|
|
|
244
|
-
// some: 바꿀 항목만 멀티선택 → 고른 것만 입력
|
|
268
|
+
// some: 바꿀 항목만 멀티선택 → 고른 것만 입력
|
|
245
269
|
const options = asks.keys.map((key) => ({
|
|
246
270
|
value: key,
|
|
247
271
|
label: `${wfField(prompts, firstTypeFor(asks.usages, key), key, "label")} (기본: ${defaults.get(key)})`,
|
|
@@ -251,12 +275,12 @@ export async function promptEnvPlan({
|
|
|
251
275
|
options,
|
|
252
276
|
initialValues: [],
|
|
253
277
|
});
|
|
254
|
-
// ESC/빈 선택 → 전부 기본값
|
|
278
|
+
// ESC/빈 선택 → 전부 기본값
|
|
255
279
|
if (selected === CANCEL || !Array.isArray(selected) || selected.length === 0) {
|
|
256
280
|
const values = new Map(defaults);
|
|
257
281
|
return { values, useDefaults: true, answers: buildAnswers(prompts, asks, values, true) };
|
|
258
282
|
}
|
|
259
|
-
// 수집 키 순서 유지 +
|
|
283
|
+
// 수집 키 순서 유지 + 수집된 키만 인정
|
|
260
284
|
const todo = asks.keys.filter((k) => selected.includes(k));
|
|
261
285
|
await promptEach(ui, prompts, asks, todo, values, log);
|
|
262
286
|
return { values, useDefaults: false, answers: buildAnswers(prompts, asks, values, false) };
|
package/src/ui/prompts.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
// 대화형 프롬프트
|
|
1
|
+
// 대화형 프롬프트 래핑.
|
|
2
2
|
// node:readline 기반 자체 엔진 사용 (@clack/prompts 는 Windows TTY에서 Enter가 멈추는 버그로 제거).
|
|
3
|
-
//
|
|
3
|
+
// ESC는 각 함수가 CANCEL 심볼을 반환 → 호출부가 기본값/머무르기로 해석한다.
|
|
4
|
+
// Ctrl+C·Ctrl+D는 엔진이 PromptAbortError로 reject → run()이 잡아 종료코드 130으로 끝낸다.
|
|
4
5
|
import * as engine from "./readline-engine.js";
|
|
5
6
|
import { DEPLOY_STYLES, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
7
|
+
import { TYPE_IDS } from "../core/types.js";
|
|
6
8
|
import { ENV_MODES, DEFAULT_ENV_MODE, STORE_PLATFORMS, DEPLOY_MODES, DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
|
|
7
9
|
|
|
8
10
|
export const CANCEL = engine.CANCEL;
|
|
@@ -36,12 +38,17 @@ export async function confirmProjectMenu() {
|
|
|
36
38
|
|
|
37
39
|
// 수정 메뉴 항목 — showFlutter=Flutter 타입일 때만 환경변수 방식/스토어 배포 대상/배포 모드 노출.
|
|
38
40
|
// 라벨·순서를 테스트할 수 있도록 순수 함수로 분리했다.
|
|
39
|
-
|
|
41
|
+
// showOptions=선택 워크플로우 토글(자동 버전 승격·Copilot) 노출 — 저장값이 있으면 처음 질문을 건너뛰므로 여기서 바꾼다.
|
|
42
|
+
export function editMenuOptions({ showFlutter = false, showOptions = false } = {}) {
|
|
40
43
|
const options = [
|
|
41
44
|
{ value: "type", label: "프로젝트 타입" },
|
|
42
45
|
{ value: "version", label: "버전" },
|
|
43
46
|
{ value: "branch", label: "기본 브랜치" },
|
|
44
47
|
];
|
|
48
|
+
if (showOptions) {
|
|
49
|
+
options.push({ value: "semverAuto", label: "자동 버전 승격" });
|
|
50
|
+
options.push({ value: "copilotAi", label: "Copilot AI 요약" });
|
|
51
|
+
}
|
|
45
52
|
if (showFlutter) {
|
|
46
53
|
options.push({ value: "envMode", label: "환경변수 방식" });
|
|
47
54
|
options.push({ value: "flutterStore", label: "스토어 배포 대상" });
|
|
@@ -52,11 +59,12 @@ export function editMenuOptions({ showFlutter = false } = {}) {
|
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
// 수정 메뉴 — 어떤 항목을 고칠지.
|
|
55
|
-
export async function editMenu({ showFlutter = false } = {}) {
|
|
56
|
-
return engine.select({ message: "어떤 항목을 수정할까요?", options: editMenuOptions({ showFlutter }) });
|
|
62
|
+
export async function editMenu({ showFlutter = false, showOptions = false } = {}) {
|
|
63
|
+
return engine.select({ message: "어떤 항목을 수정할까요?", options: editMenuOptions({ showFlutter, showOptions }) });
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
|
|
66
|
+
// 대화형 타입 선택지 — CLI 검증 목록(VALID_TYPES)과 같은 레지스트리에서 만든다.
|
|
67
|
+
export const ALL_TYPES = TYPE_IDS;
|
|
60
68
|
|
|
61
69
|
// 타입 멀티선택.
|
|
62
70
|
export async function selectTypes(current = []) {
|
|
@@ -93,20 +101,22 @@ export async function confirmTypes({ types = [], markers = null } = {}) {
|
|
|
93
101
|
// 배포 방식 선택. 서버 배포 CD 워크플로우는 서로 대체재라 하나만 쓴다.
|
|
94
102
|
// 고른 것만 설치하고 push 트리거까지 켜준다 — 종전에는 넷을 다 깔고 SIMPLE만 켜져 있어,
|
|
95
103
|
// 무중단을 원한 사람은 설치 후 YAML을 직접 고쳐야 했다.
|
|
96
|
-
// "서버 배포 안 함"은
|
|
104
|
+
// "서버 배포 안 함"은 모든 타입에서 서버 배포 워크플로우(CD·PR 프리뷰)를 제외한다 — 서버 배포를
|
|
97
105
|
// 하지 않는 프로젝트(프론트엔드 전용, 라이브러리 등)를 위한 선택지다.
|
|
98
|
-
|
|
106
|
+
// nonstop=false면(선택한 타입 어디에도 무중단 워크플로우가 없음) 고를 수 없는 무중단 선택지를 빼고 묻는다.
|
|
107
|
+
export async function selectDeployStyle({ nonstop = true } = {}) {
|
|
99
108
|
engine.note(
|
|
100
109
|
"서버 배포 워크플로우는 서로 대체재입니다 (Nginx와 Traefik을 동시에 쓰지 않습니다).\n" +
|
|
101
|
-
"고른 방식만 설치하고 자동 실행(push 트리거)까지 켭니다. PR 프리뷰는
|
|
102
|
-
"
|
|
110
|
+
"고른 방식만 설치하고 자동 실행(push 트리거)까지 켭니다. PR 프리뷰는 배포 방식과 함께 설치되고,\n" +
|
|
111
|
+
"\"서버 배포 안 함\"을 고르면 모든 타입에서 CD와 PR 프리뷰를 함께 제외합니다.\n" +
|
|
112
|
+
"무중단 배포는 spring에만 있습니다 — 다른 타입(python·go·react·next)은 단일 서버 배포로 설치됩니다.",
|
|
103
113
|
"배포 방식",
|
|
104
114
|
);
|
|
105
115
|
return engine.select({
|
|
106
116
|
message: "서버 배포는 어떤 방식으로 할까요?",
|
|
107
117
|
options: [
|
|
108
|
-
...DEPLOY_STYLES.map((s) => ({ value: s.value, label: s.label })),
|
|
109
|
-
{ value: NO_DEPLOY_STYLE, label: "서버 배포 안 함 — CD
|
|
118
|
+
...DEPLOY_STYLES.filter((s) => nonstop || s.value === "simple").map((s) => ({ value: s.value, label: s.label })),
|
|
119
|
+
{ value: NO_DEPLOY_STYLE, label: "서버 배포 안 함 — CD·PR 프리뷰 워크플로우와 배포 설정을 생성하지 않음" },
|
|
110
120
|
],
|
|
111
121
|
});
|
|
112
122
|
}
|
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
// node:readline 기반 대화형 프롬프트 엔진 (@clack/prompts 대체).
|
|
2
2
|
// WHY: @clack/prompts 1.7.0 이 Windows TTY 콘솔에서 Enter(return) 키를 처리하지 못하고
|
|
3
|
-
// 멈추는 버그가
|
|
4
|
-
//
|
|
3
|
+
// 멈추는 버그가 있다. node:readline 의 keypress 이벤트는 Windows에서 정상 동작한다.
|
|
4
|
+
// 메뉴를 직접 구현하면 외부 의존성 0 → 내부망에서도 안전.
|
|
5
5
|
//
|
|
6
|
-
// 계약:
|
|
6
|
+
// 계약: ESC는 CANCEL 심볼 반환(호출부가 "기본값/머무르기"로 해석). 각 함수 async.
|
|
7
|
+
// Ctrl+C·Ctrl+D·stdin 종료는 중단 — PromptAbortError로 reject해 어느 질문에서든 즉시 빠져나간다.
|
|
7
8
|
import { emitKeypressEvents } from "node:readline";
|
|
8
9
|
import { stdin, stdout } from "node:process";
|
|
10
|
+
import { visualWidth } from "./ansi.js";
|
|
9
11
|
|
|
10
12
|
export const CANCEL = Symbol("cancel");
|
|
11
13
|
|
|
14
|
+
// 사용자 중단(Ctrl+C 등). ESC와 달리 기본값으로 진행하면 안 되므로 반환값이 아니라 예외로 전파한다 —
|
|
15
|
+
// 호출부마다 CANCEL 해석이 달라 일부는 기본값으로 설치를 강행하거나 같은 화면을 무한히 다시 그렸다.
|
|
16
|
+
// signal: 외부 신호로 중단된 경우 그 신호명("SIGINT"·"SIGTERM") — 종료 코드를 셸 관례(128+번호)에 맞춘다.
|
|
17
|
+
export class PromptAbortError extends Error {
|
|
18
|
+
constructor(signal = null) {
|
|
19
|
+
super("사용자가 중단했습니다.");
|
|
20
|
+
this.name = "PromptAbortError";
|
|
21
|
+
this.signal = signal;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const isPromptAbort = (e) => e instanceof PromptAbortError;
|
|
25
|
+
|
|
26
|
+
// raw mode에서는 Ctrl+C가 SIGINT가 아니라 keypress로 들어온다. Ctrl+D도 raw mode에선 EOF가 아니다.
|
|
27
|
+
const isAbortKey = (key) => key.ctrl && (key.name === "c" || key.name === "d");
|
|
28
|
+
|
|
29
|
+
// 외부 신호(kill -INT·kill -TERM)는 keypress가 아니라 프로세스 신호로 온다. 리스너가 없으면 Node가 곧바로
|
|
30
|
+
// 종료해 커서 숨김과 raw mode가 복구되지 않는다 — 입력 대기 중에는 키보드 Ctrl+C와 같은 중단 경로로 돌린다.
|
|
31
|
+
const TERM_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
32
|
+
function onTermSignal(fn) {
|
|
33
|
+
const handlers = TERM_SIGNALS.map((sig) => [sig, () => fn(sig)]);
|
|
34
|
+
for (const [sig, h] of handlers) process.on(sig, h);
|
|
35
|
+
return () => { for (const [sig, h] of handlers) process.removeListener(sig, h); };
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
// ── ANSI 헬퍼 (picocolors 대체 — 의존성 0) ───────────────────────────
|
|
13
39
|
const ESC = "\x1b[";
|
|
14
40
|
const c = {
|
|
@@ -17,10 +43,27 @@ const c = {
|
|
|
17
43
|
};
|
|
18
44
|
// NO_COLOR(https://no-color.org)/비TTY 가드 — ansi.js와 동일한 규칙(존재 여부만 체크, 값 무관)이지만
|
|
19
45
|
// 의존성 0 유지를 위해 자체 구현.
|
|
20
|
-
|
|
46
|
+
// TERM=dumb은 색상뿐 아니라 커서 이동·지우기 시퀀스도 해석하지 못한다 — 그때는 다시 그리지 않고 이어서 출력한다.
|
|
47
|
+
const isDumb = () => process.env.TERM === "dumb";
|
|
48
|
+
const colorEnabled = () => process.env.NO_COLOR === undefined && !!stdout.isTTY && !isDumb();
|
|
21
49
|
const paint = (s, color, enabled = colorEnabled()) => (enabled ? `${color}${s}${c.reset}` : String(s));
|
|
22
|
-
|
|
23
|
-
|
|
50
|
+
// 커서를 숨긴 채 프로세스가 끝나면(예외·process.exit 등) 터미널 커서가 사라진 채로 남는다 — 종료 시점에 한 번 더 복구한다.
|
|
51
|
+
let cursorHidden = false;
|
|
52
|
+
let exitGuard = false;
|
|
53
|
+
const hideCursor = () => {
|
|
54
|
+
if (isDumb()) return;
|
|
55
|
+
stdout.write(`${ESC}?25l`);
|
|
56
|
+
cursorHidden = true;
|
|
57
|
+
if (!exitGuard) {
|
|
58
|
+
exitGuard = true;
|
|
59
|
+
process.once("exit", () => { if (cursorHidden) stdout.write(`${ESC}?25h`); });
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const showCursor = () => {
|
|
63
|
+
if (isDumb()) return;
|
|
64
|
+
stdout.write(`${ESC}?25h`);
|
|
65
|
+
cursorHidden = false;
|
|
66
|
+
};
|
|
24
67
|
|
|
25
68
|
// 심볼 (clack 톤 유지)
|
|
26
69
|
const S_ACTIVE = paint("●", c.green);
|
|
@@ -31,16 +74,24 @@ const S_BAR = paint("│", c.gray);
|
|
|
31
74
|
const S_Q = paint("◆", c.cyan);
|
|
32
75
|
const S_DONE = paint("◇", c.green);
|
|
33
76
|
|
|
77
|
+
// 한 줄이 터미널에서 실제로 차지하는 행 수 — 폭을 넘는 줄은 터미널이 접어서 여러 행이 된다.
|
|
78
|
+
// 논리 줄 수만큼만 올라가면 접힌 윗부분이 지워지지 않고 화면에 사본이 쌓인다.
|
|
79
|
+
export function physicalRows(line, columns = stdout.columns) {
|
|
80
|
+
if (!columns) return 1;
|
|
81
|
+
return Math.max(1, Math.ceil(visualWidth(line) / columns));
|
|
82
|
+
}
|
|
83
|
+
|
|
34
84
|
// 여러 줄 지운 뒤 커서를 블록 시작으로 되돌리는 렌더러.
|
|
35
|
-
//
|
|
85
|
+
// 직전에 그린 물리 행 수만큼 위로 올라가 지우고 새로 그린다.
|
|
36
86
|
function makeRenderer() {
|
|
37
87
|
let prevLines = 0;
|
|
38
88
|
return {
|
|
39
89
|
render(lines) {
|
|
90
|
+
if (isDumb()) { stdout.write(lines.join("\n") + "\n"); return; }
|
|
40
91
|
if (prevLines > 0) stdout.write(`${ESC}${prevLines}A`); // 위로
|
|
41
92
|
stdout.write(`${ESC}0J`); // 커서 아래 전부 지우기
|
|
42
93
|
stdout.write(lines.join("\n") + "\n");
|
|
43
|
-
prevLines = lines.
|
|
94
|
+
prevLines = lines.reduce((n, l) => n + physicalRows(l), 0);
|
|
44
95
|
},
|
|
45
96
|
reset() { prevLines = 0; },
|
|
46
97
|
};
|
|
@@ -49,7 +100,7 @@ function makeRenderer() {
|
|
|
49
100
|
// raw keypress 세션 공통 래퍼. onKey(str,key) → true 반환 시 종료.
|
|
50
101
|
// 반환값은 finalize()가 만든다. 취소 시 CANCEL.
|
|
51
102
|
function keySession(renderFn, onKey) {
|
|
52
|
-
return new Promise((resolve) => {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
53
104
|
const wasRaw = stdin.isTTY ? stdin.isRaw : false;
|
|
54
105
|
emitKeypressEvents(stdin);
|
|
55
106
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
@@ -59,21 +110,33 @@ function keySession(renderFn, onKey) {
|
|
|
59
110
|
const cleanup = () => {
|
|
60
111
|
stdin.removeListener("keypress", handler);
|
|
61
112
|
stdin.removeListener("end", onEnd);
|
|
113
|
+
offSignal();
|
|
62
114
|
if (stdin.isTTY) stdin.setRawMode(wasRaw);
|
|
63
115
|
stdin.pause();
|
|
64
116
|
showCursor();
|
|
65
117
|
};
|
|
118
|
+
const offSignal = onTermSignal((sig) => {
|
|
119
|
+
cleanup();
|
|
120
|
+
stdout.write("\n");
|
|
121
|
+
reject(new PromptAbortError(sig));
|
|
122
|
+
});
|
|
66
123
|
|
|
67
|
-
// stdin 종료(EOF
|
|
124
|
+
// stdin 종료(EOF, SSH 연결 끊김 등) — 더 이상 입력이 올 수 없으므로 중단한다.
|
|
125
|
+
// CANCEL로 돌려주면 "머무르기"로 해석하는 화면에서 다시 묻다가 영원히 대기한다.
|
|
68
126
|
const onEnd = () => {
|
|
69
127
|
cleanup();
|
|
70
|
-
|
|
128
|
+
reject(new PromptAbortError());
|
|
71
129
|
};
|
|
72
130
|
|
|
73
131
|
const handler = (str, key) => {
|
|
74
132
|
key = key || {};
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
if (isAbortKey(key)) {
|
|
134
|
+
cleanup();
|
|
135
|
+
stdout.write("\n");
|
|
136
|
+
reject(new PromptAbortError());
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (key.name === "escape") {
|
|
77
140
|
cleanup();
|
|
78
141
|
resolve(CANCEL);
|
|
79
142
|
return;
|
|
@@ -190,45 +253,69 @@ export async function multiselect({ message, options, initialValues = [], requir
|
|
|
190
253
|
// 반환: 입력 문자열(빈 입력 시 defaultValue) 또는 CANCEL.
|
|
191
254
|
export async function text({ message, defaultValue = "" }) {
|
|
192
255
|
if (!stdin.isTTY) return defaultValue;
|
|
193
|
-
return new Promise((resolve) => {
|
|
256
|
+
return new Promise((resolve, reject) => {
|
|
194
257
|
const wasRaw = stdin.isTTY ? stdin.isRaw : false;
|
|
195
258
|
emitKeypressEvents(stdin);
|
|
196
259
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
197
260
|
stdin.resume();
|
|
198
261
|
let buf = "";
|
|
199
262
|
|
|
263
|
+
// dumb 터미널은 줄을 지울 수 없으므로 프롬프트는 한 번만 쓰고 입력 글자만 이어서 출력한다.
|
|
264
|
+
const dumb = isDumb();
|
|
265
|
+
let prevRows = 0; // 직전 프롬프트가 차지한 물리 행 수 — 긴 프롬프트는 여러 행으로 접힌다
|
|
200
266
|
const prompt = () => {
|
|
201
|
-
|
|
267
|
+
if (dumb) {
|
|
268
|
+
stdout.write(`${S_Q} ${message} ${defaultValue ? `[${defaultValue}] ` : ""}`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// 커서는 접힌 마지막 행에 있으므로 첫 행까지 올라간 뒤 아래를 전부 지운다
|
|
272
|
+
stdout.write(prevRows > 1 ? `\r${ESC}${prevRows - 1}A${ESC}0J` : `\r${ESC}0J`);
|
|
202
273
|
const shown = buf.length ? buf : paint(defaultValue || "", c.dim);
|
|
203
|
-
|
|
274
|
+
const line = `${S_Q} ${paint(message, c.bold)} ${shown}`;
|
|
275
|
+
stdout.write(line);
|
|
276
|
+
prevRows = physicalRows(line);
|
|
204
277
|
};
|
|
205
278
|
|
|
206
279
|
const cleanup = () => {
|
|
207
280
|
stdin.removeListener("keypress", handler);
|
|
208
281
|
stdin.removeListener("end", onEnd);
|
|
282
|
+
offSignal();
|
|
209
283
|
if (stdin.isTTY) stdin.setRawMode(wasRaw);
|
|
210
284
|
stdin.pause();
|
|
211
285
|
stdout.write("\n");
|
|
212
286
|
};
|
|
287
|
+
const offSignal = onTermSignal((sig) => {
|
|
288
|
+
cleanup();
|
|
289
|
+
reject(new PromptAbortError(sig));
|
|
290
|
+
});
|
|
213
291
|
|
|
214
|
-
// stdin 종료(EOF
|
|
292
|
+
// stdin 종료(EOF) — 더 이상 입력이 올 수 없으므로 중단한다.
|
|
215
293
|
const onEnd = () => {
|
|
216
294
|
cleanup();
|
|
217
|
-
|
|
295
|
+
reject(new PromptAbortError());
|
|
218
296
|
};
|
|
219
297
|
|
|
220
298
|
const handler = (str, key) => {
|
|
221
299
|
key = key || {};
|
|
222
|
-
|
|
223
|
-
if (
|
|
300
|
+
if (isAbortKey(key)) { cleanup(); reject(new PromptAbortError()); return; }
|
|
301
|
+
if (key.name === "escape") { cleanup(); resolve(CANCEL); return; }
|
|
224
302
|
if (key.name === "return" || key.name === "enter") {
|
|
225
303
|
cleanup();
|
|
226
304
|
resolve(buf.length ? buf : defaultValue);
|
|
227
305
|
return;
|
|
228
306
|
}
|
|
229
|
-
if (key.name === "backspace") {
|
|
307
|
+
if (key.name === "backspace") {
|
|
308
|
+
if (dumb && buf.length) stdout.write("\b \b");
|
|
309
|
+
buf = buf.slice(0, -1);
|
|
310
|
+
if (!dumb) prompt();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
230
313
|
// 일반 문자 (제어문자 제외)
|
|
231
|
-
if (str && !key.ctrl && !key.meta && str.length === 1 && str >= " ") {
|
|
314
|
+
if (str && !key.ctrl && !key.meta && str.length === 1 && str >= " ") {
|
|
315
|
+
buf += str;
|
|
316
|
+
if (dumb) stdout.write(str); else prompt();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
232
319
|
};
|
|
233
320
|
stdin.on("keypress", handler);
|
|
234
321
|
stdin.on("end", onEnd);
|
package/src/ui/status-cards.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// 첫 화면 상태 표시 층 — 감지 로그 · 분석 카드 · 신규/업데이트 판별
|
|
2
|
-
// (Breaking Changes 박스는 core/breaking-check.js가
|
|
3
|
-
// 원본의 IDE Skills 상태는 project-auto-wizard 스코프 제외 — Agent Skills 미포함)
|
|
2
|
+
// (Breaking Changes 박스는 core/breaking-check.js가 담당)
|
|
4
3
|
import { A, paint } from "./ansi.js";
|
|
5
4
|
import { DEFAULT_DEPLOY_MODE } from "../core/flutter-options.js";
|
|
6
5
|
|
|
@@ -8,7 +7,7 @@ const GUT = paint("│", A.gray);
|
|
|
8
7
|
const HEAD = paint("◆", A.cyan);
|
|
9
8
|
const OK = paint("✓", A.green);
|
|
10
9
|
|
|
11
|
-
// 감지 로그
|
|
10
|
+
// 감지 로그 — 무엇을 근거로 어떤 타입을 감지했는지
|
|
12
11
|
// markers: Map<type, 실제 발견 파일>.
|
|
13
12
|
// warnings: 감지 도중 나온 경고. 감지 함수를 먼저 호출한 뒤 박스를 그리는 구조라 경고가
|
|
14
13
|
// 박스 위로 새어나가 앞선 질문에 대한 경고처럼 보였다 — 박스 안에서 출력한다.
|
|
@@ -28,10 +27,10 @@ export function printDetectionLog({ types = [], version = "", branch = "", marke
|
|
|
28
27
|
out(`${GUT}\n`);
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
// 프로젝트 분석 개요 카드
|
|
30
|
+
// 프로젝트 분석 개요 카드
|
|
32
31
|
export function printAnalysisCard({ mode = "", modeLabel = "", types = [], version = "", branch = "",
|
|
33
32
|
paths = new Map(), showOptional = false,
|
|
34
|
-
flutter = null, envModeDefault = "" },
|
|
33
|
+
flutter = null, envModeDefault = "", options = null },
|
|
35
34
|
out = (s) => process.stderr.write(s)) {
|
|
36
35
|
out(`${HEAD} ${paint("프로젝트 분석 결과", A.bold)}\n`);
|
|
37
36
|
const row = (icon, label, value) => out(`${GUT} ${icon} ${label.padEnd(10)} ${value}\n`);
|
|
@@ -51,6 +50,12 @@ export function printAnalysisCard({ mode = "", modeLabel = "", types = [], versi
|
|
|
51
50
|
row("🚀", "배포모드", modeParts.length ? modeParts.join(" ") : "없음");
|
|
52
51
|
}
|
|
53
52
|
}
|
|
53
|
+
// 선택 워크플로우 — 저장값이 있으면 질문 없이 쓰이므로 확정 전에 현재값을 보여준다.
|
|
54
|
+
if (options) {
|
|
55
|
+
const onOff = (v) => (v ? paint("켜짐", A.green) : paint("꺼짐", A.dim));
|
|
56
|
+
row("🔢", "자동승격", onOff(options.semverAuto));
|
|
57
|
+
row("🤖", "Copilot", onOff(options.copilotAi));
|
|
58
|
+
}
|
|
54
59
|
// 모노레포 경로 — 루트가 아닌 항목이 하나라도 있으면 표시
|
|
55
60
|
const nonRoot = [...paths.entries()].filter(([, p]) => p && p !== ".");
|
|
56
61
|
if (nonRoot.length) {
|