projectops 4.59.1 → 4.60.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 +2 -2
- package/package.json +1 -1
- package/src/cli/args.js +2 -2
- package/src/commands/doctor.js +3 -1
- package/src/commands/full.js +9 -11
- package/src/commands/interactive.js +13 -13
- package/src/commands/issues.js +1 -1
- package/src/commands/skills.js +1 -1
- package/src/commands/version.js +5 -6
- package/src/commands/workflows.js +3 -3
- package/src/context.js +2 -2
- package/src/core/agent-guide.js +2 -1
- package/src/core/assets.js +3 -3
- package/src/core/breaking-check.js +6 -6
- package/src/core/breaking.js +2 -2
- package/src/core/copy/coderabbit.js +1 -1
- package/src/core/copy/gitignore.js +3 -3
- package/src/core/copy/readme.js +2 -3
- package/src/core/copy/simple.js +4 -2
- package/src/core/copy/util.js +2 -2
- package/src/core/copy/workflows.js +9 -13
- package/src/core/detect-fs.js +6 -6
- package/src/core/detect.js +3 -3
- package/src/core/exclusions.js +1 -1
- package/src/core/fsutil.js +2 -4
- package/src/core/ide/adapters/claude.js +3 -3
- package/src/core/ide/adapters/codex.js +3 -3
- package/src/core/ide/adapters/cursor.js +3 -3
- package/src/core/ide/adapters/gemini.js +4 -3
- package/src/core/ide/adapters/pi-common.js +8 -7
- package/src/core/ide/adapters/pi-harness.js +4 -4
- package/src/core/ide/adapters/pi.js +1 -1
- package/src/core/ide/runner.js +2 -2
- package/src/core/options-ask.js +7 -9
- package/src/core/options-schema.js +5 -5
- package/src/core/paths-resolve.js +27 -28
- package/src/core/paths.js +7 -3
- package/src/core/resolve-options.js +14 -0
- package/src/core/run-trace.js +1 -1
- package/src/core/version-yml.js +44 -25
- package/src/core/wizard-env.js +11 -12
- package/src/core/wizard-labels.js +15 -17
- package/src/i18n/ide.en.js +1 -0
- package/src/i18n/ide.ko.js +1 -0
- package/src/index.js +8 -8
- package/src/ui/banner.js +2 -1
- package/src/ui/env-plan.js +22 -25
- package/src/ui/prompts.js +1 -1
- package/src/ui/readline-engine.js +1 -1
- package/src/ui/skills-prompts.js +1 -1
- package/src/ui/summary.js +3 -3
package/src/core/version-yml.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// version.yml 파싱·생성 (
|
|
2
|
-
// ⚠️ YAML 재직렬화 금지 — 주석이 데이터.
|
|
3
|
-
// 실측 기준: template_integrator.sh 2184~2354.
|
|
1
|
+
// version.yml 파싱·생성 (전체 재생성 전략 D4).
|
|
2
|
+
// ⚠️ YAML 재직렬화 금지 — 주석이 데이터. 템플릿 문자열로 바이트 단위 고정.
|
|
4
3
|
import { execFileSync } from "node:child_process";
|
|
5
4
|
import { readFileSync } from "node:fs";
|
|
6
5
|
import { DEPLOY_TARGETS, LABEL_STYLES } from "./constants.js";
|
|
@@ -118,6 +117,7 @@ const COMMENTS = {
|
|
|
118
117
|
excluded: "workflow files the update must never install (safe to edit by hand)",
|
|
119
118
|
storeLocales: "store release-note languages, first is the default (missing = one text for every store language)",
|
|
120
119
|
storeLocalesStore: "same, only for this store when its languages differ from store_locales (optional)",
|
|
120
|
+
legacyDeployBranch: "old name of development_branch, kept equal for older skills",
|
|
121
121
|
},
|
|
122
122
|
ko: {
|
|
123
123
|
versionCode: "app build number",
|
|
@@ -135,6 +135,7 @@ const COMMENTS = {
|
|
|
135
135
|
excluded: "업데이트가 절대 설치하지 않을 워크플로우 파일 (직접 수정 가능)",
|
|
136
136
|
storeLocales: "스토어 릴리스 노트 언어 목록, 첫 항목이 기본 언어 (미기재면 모든 스토어 언어에 같은 문구)",
|
|
137
137
|
storeLocalesStore: "이 스토어만 언어 구성이 store_locales 와 다를 때 쓰는 선택 키",
|
|
138
|
+
legacyDeployBranch: "development_branch 의 구 이름 — 구버전 스킬용으로 같은 값 유지",
|
|
138
139
|
},
|
|
139
140
|
};
|
|
140
141
|
const commentsFor = (lang) => COMMENTS[lang === "ko" ? "ko" : "en"];
|
|
@@ -151,7 +152,7 @@ export function resolveUpdatedBy(existingContent = "", cwd = ".") {
|
|
|
151
152
|
return m && m[1] ? m[1] : "template_integrator";
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
// metadata.template.options 상태머신
|
|
155
|
+
// metadata.template.options 상태머신 파싱.
|
|
155
156
|
// 반환(#439 배포/publish 축): { deploy: string|null, publish: string[]|null, secretBackup: bool|null }
|
|
156
157
|
// deploy: 'docker-ssh'|'vercel'|'none', publish: ['nexus','npm','github-packages'] 부분집합. null=미기재.
|
|
157
158
|
// 구 키(nexus/npm_publish/synology)는 신 축으로 자동 마이그레이션해 읽는다 (v4.2.0 이전 파일 호환):
|
|
@@ -164,16 +165,12 @@ export function parseTemplateOptions(content) {
|
|
|
164
165
|
const out = { deploy: null, publish: null, secretBackup: null,
|
|
165
166
|
changelogProvider: null, changelogBaseUrl: null, codeReviewCoderabbit: null, aiPrSummary: null, projectsSync: null,
|
|
166
167
|
deployBranch: null, intent: null, semverAuto: null, appRelease: null, labelStyle: null, closeOnRelease: null, language: null, excludedWorkflows: null, storeLocales: null, storeLocalesIos: null, storeLocalesPlay: null };
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
if (line.startsWith("#")) continue;
|
|
170
|
-
const m = line.match(/^\s+deploy_branch:\s*"([^"]*)"/);
|
|
171
|
-
if (m) { out.deployBranch = m[1]; break; }
|
|
172
|
-
}
|
|
168
|
+
// 개발 브랜치는 metadata 직속(#456) — template.options 밖이라 별도로 스캔한다.
|
|
169
|
+
out.deployBranch = readDevelopmentBranch(content);
|
|
173
170
|
let legacyNexus = null;
|
|
174
171
|
let legacyNpm = null;
|
|
175
172
|
let legacySynology = null;
|
|
176
|
-
// 값 정규화: 따옴표 제거 + 트림
|
|
173
|
+
// 값 정규화: 따옴표 제거 + 트림
|
|
177
174
|
const strip = (s) => String(s).replace(/["']/g, "").trim();
|
|
178
175
|
let inTemplate = false;
|
|
179
176
|
let inOptions = false;
|
|
@@ -303,10 +300,10 @@ export function parseTemplateOptions(content) {
|
|
|
303
300
|
if (v === "false") legacySynology = false;
|
|
304
301
|
continue;
|
|
305
302
|
}
|
|
306
|
-
// 들여쓰기 0~4칸의 다른 키 → options 섹션 종료
|
|
303
|
+
// 들여쓰기 0~4칸의 다른 키 → options 섹션 종료
|
|
307
304
|
if (/^\s{0,4}[a-z_]+:/.test(line)) { inOptions = false; inTemplate = false; }
|
|
308
305
|
}
|
|
309
|
-
// 최상위 키 → template 섹션 종료
|
|
306
|
+
// 최상위 키 → template 섹션 종료
|
|
310
307
|
if (inTemplate && /^[a-z_]+:/.test(line)) { inTemplate = false; inOptions = false; }
|
|
311
308
|
}
|
|
312
309
|
// ── 구 키 마이그레이션 — 신 publish 키가 없을 때만 ──
|
|
@@ -362,7 +359,23 @@ export function convertLegacySingularType(content) {
|
|
|
362
359
|
return lines.join("\n");
|
|
363
360
|
}
|
|
364
361
|
|
|
365
|
-
//
|
|
362
|
+
// 개발(릴리스 소스) 브랜치 = 릴리스 PR head (#915)
|
|
363
|
+
// 키 2개: development_branch(정본) > deploy_branch(구 이름 — 이것만 읽는 구버전 스킬이 있어 함께 기록)
|
|
364
|
+
// 반환: 브랜치명, 둘 다 없으면 null
|
|
365
|
+
export const DEVELOPMENT_BRANCH_KEYS = ["development_branch", "deploy_branch"];
|
|
366
|
+
export function readDevelopmentBranch(content) {
|
|
367
|
+
const lines = String(content || "").split("\n").filter((l) => !l.startsWith("#"));
|
|
368
|
+
for (const key of DEVELOPMENT_BRANCH_KEYS) {
|
|
369
|
+
const re = new RegExp(`^\\s+${key}:\\s*"([^"]*)"`);
|
|
370
|
+
for (const l of lines) {
|
|
371
|
+
const m = l.match(re);
|
|
372
|
+
if (m) return m[1];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 기존 version.yml에서 값 추출 (라인 정규식, 주석 라인 오탐 방지).
|
|
366
379
|
export function parseExisting(content) {
|
|
367
380
|
const text = String(content || "");
|
|
368
381
|
const line = (re) => {
|
|
@@ -407,9 +420,9 @@ export function parseExisting(content) {
|
|
|
407
420
|
if (/^\S/.test(l)) break;
|
|
408
421
|
}
|
|
409
422
|
}
|
|
410
|
-
// 브랜치 정보 (#456) — default_branch(레포 기본)와
|
|
423
|
+
// 브랜치 정보 (#456) — default_branch(레포 기본)와 개발 브랜치(릴리스 PR head)는 별개.
|
|
411
424
|
const defaultBranch = line(/^\s*default_branch:\s*"([^"]*)"/);
|
|
412
|
-
const deployBranch =
|
|
425
|
+
const deployBranch = readDevelopmentBranch(text);
|
|
413
426
|
// 선택 워크플로우 옵션 (metadata.template.options — nexus/secret_backup)
|
|
414
427
|
const options = parseTemplateOptions(text);
|
|
415
428
|
return { version, versionCode, types, paths, templateVersion, templateMode, options, defaultBranch, deployBranch };
|
|
@@ -422,9 +435,11 @@ export function parseExisting(content) {
|
|
|
422
435
|
// project_type(단수)는 v4.1.0 에서 없앴다 — 남기면 version_manager 가 거부한다. 그래서 보존하지 않고 버린다.
|
|
423
436
|
const OWNED_TOP = new Set(["version", "version_code", "project_types", "project_type", "project_paths", "metadata", "deploy"]);
|
|
424
437
|
// metadata.template.options 아래에서 생성기가 직접 쓰는 키. 여기에 없는 것만 보존한다.
|
|
438
|
+
// buildVersionYml 이 쓰는 6칸 키와 같아야 한다 — 빠지면 재실행마다 그 키가 두 번 쓰인다 (대조: test/version-yml-preserve.test.js).
|
|
425
439
|
// nexus · npm_publish · synology 는 옛 키다 — 파서가 신형(publish · secret_backup)으로 바꿔 쓰므로 그대로 두면 이중 기록이 된다.
|
|
426
|
-
const
|
|
427
|
-
"language", "label_style", "app_release", "excluded_workflows", "store_locales", "
|
|
440
|
+
export const OWNED_OPTION_KEYS = new Set(["intent", "deploy", "publish", "secret_backup", "semver_auto", "projects_sync", "close_on_release",
|
|
441
|
+
"language", "label_style", "app_release", "excluded_workflows", "store_locales", "store_locales_ios", "store_locales_play",
|
|
442
|
+
"code_review", "changelog",
|
|
428
443
|
"nexus", "npm_publish", "synology"]);
|
|
429
444
|
|
|
430
445
|
// indent 칸으로 시작하는 키 한 줄과 그 자식(더 깊은 들여쓰기·빈 줄·그 아래의 주석)을 한 덩어리로 자른다.
|
|
@@ -476,7 +491,7 @@ export function extractCustomYml(content) {
|
|
|
476
491
|
if (tp >= 0) {
|
|
477
492
|
const tpEnd = end(tp, 2);
|
|
478
493
|
const op = find(tp + 1, tpEnd, /^ {4}options:/);
|
|
479
|
-
if (op >= 0) optionBlocks = keyBlocks(lines.slice(op + 1, end(op, 4)), 6, (k) =>
|
|
494
|
+
if (op >= 0) optionBlocks = keyBlocks(lines.slice(op + 1, end(op, 4)), 6, (k) => OWNED_OPTION_KEYS.has(k));
|
|
480
495
|
}
|
|
481
496
|
}
|
|
482
497
|
return { optionBlocks, topBlocks };
|
|
@@ -526,7 +541,7 @@ export function mergeDeployValues(existingContent, fresh) {
|
|
|
526
541
|
return merged;
|
|
527
542
|
}
|
|
528
543
|
|
|
529
|
-
// version.yml 전체 생성 (
|
|
544
|
+
// version.yml 전체 생성 (metadata > template 옵션 > deploy > 사용자 블록).
|
|
530
545
|
// opts: { version, types:[], paths:Map, pathMarkers?:Map, branch, versionCode, now, today, templateOptions? }
|
|
531
546
|
// primary 타입은 별도 키 없이 project_types[0]이다 (v4.1.0 SSOT — 단수 project_type 키 제거).
|
|
532
547
|
// now = "YYYY-MM-DD HH:MM:SS" (UTC) — 결정성 위해 주입
|
|
@@ -557,13 +572,17 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
557
572
|
out += ` last_updated: "${now}"\n`;
|
|
558
573
|
out += ` last_updated_by: "${String(updatedBy || "template_integrator").replace(/"/g, "")}"\n`;
|
|
559
574
|
out += ` default_branch: "${branch}"\n`;
|
|
560
|
-
//
|
|
561
|
-
// 지정된 경우에만 출력 — 미지정이면 스킬이 develop로
|
|
562
|
-
|
|
575
|
+
// 개발(릴리스 소스) 브랜치 = 릴리스 PR head (#456). default_branch(레포 기본)와 별개 개념.
|
|
576
|
+
// 지정된 경우에만 출력 — 미지정이면 스킬이 develop로 폴백.
|
|
577
|
+
// deploy_branch 병기: 그 키만 읽는 구버전 스킬(changelog_cli)이 develop로 폴백하지 않게 (#915)
|
|
578
|
+
if (deployBranch) {
|
|
579
|
+
out += ` development_branch: "${deployBranch}"\n`;
|
|
580
|
+
out += ` deploy_branch: "${deployBranch}" # ${C.legacyDeployBranch}\n`;
|
|
581
|
+
}
|
|
563
582
|
out += ` integrated_from: "projectops"\n`;
|
|
564
583
|
out += ` integration_date: "${today}"\n`;
|
|
565
584
|
|
|
566
|
-
// template 옵션
|
|
585
|
+
// template 옵션 블록. templateOptions 지정 시.
|
|
567
586
|
if (templateOptions) {
|
|
568
587
|
const { templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, optionsDate = today,
|
|
569
588
|
changelogProvider = "commit", changelogBaseUrl = "", codeReviewCoderabbit = true, aiPrSummary = true, intent = null, mode = null,
|
|
@@ -613,7 +632,7 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
613
632
|
for (const b of customOptionBlocks) out += `${b}\n`;
|
|
614
633
|
}
|
|
615
634
|
|
|
616
|
-
// deploy
|
|
635
|
+
// deploy 블록. deployValues: Map<type, Map<key,value>>.
|
|
617
636
|
// WF ask 값이 있는 타입만. 반드시 template 뒤의 별도 최상위 키로 둔다 — metadata 와 template
|
|
618
637
|
// 사이에 끼면 template 이 deploy 의 자식이 되어 metadata.template.options 구조가 깨진다 (#670).
|
|
619
638
|
const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t) && deployValues.get(t).size > 0);
|
package/src/core/wizard-env.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// @wizard env 토큰
|
|
1
|
+
// @wizard env 토큰 엔진: 값 치환, 잔여 토큰 치환, 미변경 판정.
|
|
2
2
|
// ⚠️ YAML 파싱/재직렬화 금지 — 라인 단위 문자열 처리 (포맷·주석 보존이 unchanged 판정 전제).
|
|
3
|
-
// 실측 기준: template_integrator.sh 3282~3360, 3003~3012.
|
|
4
3
|
import { substituteBranches } from "./branch-sub.js";
|
|
5
4
|
|
|
6
|
-
// KEY 정규식:
|
|
5
|
+
// KEY 정규식: [A-Z_]+ (대문자+언더스코어만). ask/auto 마커가 있는 라인만 대상.
|
|
7
6
|
const MARKER_RE = /#\s*@wizard\s+(ask|auto):(.*)$/;
|
|
8
7
|
const KEY_RE = /^(\s*)([A-Z_]+):/;
|
|
9
8
|
const PATHS_ANCHOR_RE = /#\s*@wizard\s+paths-anchor/;
|
|
@@ -17,8 +16,8 @@ export function parseWizardLine(line) {
|
|
|
17
16
|
return { indent: km[1], key: km[2], action: marker[1], arg: marker[2].trim() };
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
//
|
|
21
|
-
// 라인 하나에 대해 수행. value가 빈문자면
|
|
19
|
+
// `KEY: "..."` 따옴표 안 값 치환 + 그 줄 끝 `# @wizard ...` 주석 제거.
|
|
20
|
+
// 라인 하나에 대해 수행. value가 빈문자면 치환 스킵.
|
|
22
21
|
export function setEnvLine(line, key, value) {
|
|
23
22
|
if (value === "" || value == null) return line;
|
|
24
23
|
// CRLF 안전: 라인 끝 \r을 분리해 처리 후 복원 (autocrlf 프로젝트 대응)
|
|
@@ -54,21 +53,21 @@ export function isPlaceholderDefault(value) {
|
|
|
54
53
|
return /(^|\.)example\.(com|org|net)$/i.test(String(value || "").trim());
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
// resolver —
|
|
56
|
+
// resolver — 값 계산은 주입된 resolvers로 위임(순수성 유지).
|
|
58
57
|
// resolvers: { repo, "spring-app-yml-dir"(type), "spring-app-yml-path"(type), "flutter-root" }
|
|
59
58
|
export function resolveToken(name, type, resolvers = {}) {
|
|
60
59
|
const fn = resolvers[name];
|
|
61
60
|
return typeof fn === "function" ? (fn(type) ?? "") : "";
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
// 잔여 전역 토큰 치환
|
|
63
|
+
// 잔여 전역 토큰 치환 — 파일 본문·수집값(#489)·카드 표시가 전부 같은 규칙을 쓴다.
|
|
65
64
|
// 파일에 실제 써지는 값과 version.yml deploy 블록 기억값이 어긋나지 않게 하는 단일 지점.
|
|
66
65
|
export function resolveGlobalTokens(s, repoName = "") {
|
|
67
66
|
if (typeof s !== "string" || (!s.includes("__PROJECT_NAME__") && !s.includes("__APP_ARTIFACT_NAME__"))) return s;
|
|
68
67
|
return s.replaceAll("__PROJECT_NAME__", repoName).replaceAll("__APP_ARTIFACT_NAME__", repoName);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
// 파일 전체
|
|
70
|
+
// 파일 전체 치환.
|
|
72
71
|
// content: 원본 워크플로우 텍스트. 반환: 치환된 텍스트.
|
|
73
72
|
// opts:
|
|
74
73
|
// type - 프로젝트 타입 (resolver·값 조회용)
|
|
@@ -98,7 +97,7 @@ export function substituteEnv(content, opts = {}) {
|
|
|
98
97
|
if (chosen != null && chosen !== "" && !useDefaults) val = chosen;
|
|
99
98
|
else if (isPlaceholderDefault(def)) continue; // 예시값은 쓰지 않는다 — 토큰·마커 그대로 (#810)
|
|
100
99
|
else val = def;
|
|
101
|
-
// ask 키만 수집 (
|
|
100
|
+
// ask 키만 수집 (auto는 저장 안 함). deploy 블록용.
|
|
102
101
|
// #489 — 파일 본문은 아래 전역 토큰 치환을 거치므로 수집값도 동일 치환해
|
|
103
102
|
// version.yml deploy 블록이 설치본과 항상 바이트 일치하게 한다.
|
|
104
103
|
if (collectAsks) collectAsks.set(p.key, resolveGlobalTokens(val, repoName));
|
|
@@ -112,10 +111,10 @@ export function substituteEnv(content, opts = {}) {
|
|
|
112
111
|
}
|
|
113
112
|
let out = lines.join(usesCRLF ? "\r\n" : "\n");
|
|
114
113
|
|
|
115
|
-
// 잔여 전역 토큰
|
|
114
|
+
// 잔여 전역 토큰
|
|
116
115
|
out = resolveGlobalTokens(out, repoName);
|
|
117
116
|
|
|
118
|
-
// paths-anchor
|
|
117
|
+
// paths-anchor: 경로가 '.'이 아니면 주석 라인 전체를 paths 라인으로 교체
|
|
119
118
|
if (PATHS_ANCHOR_RE.test(out) && projectPath && projectPath !== ".") {
|
|
120
119
|
const eol = out.includes("\r\n") ? "\r\n" : "\n";
|
|
121
120
|
out = out.split(/\r?\n/).map((line) => {
|
|
@@ -129,7 +128,7 @@ export function substituteEnv(content, opts = {}) {
|
|
|
129
128
|
return out;
|
|
130
129
|
}
|
|
131
130
|
|
|
132
|
-
//
|
|
131
|
+
// 미변경 판정: 원본을 "기본값으로 가상 치환한 최종형"과 설치본을 바이트 비교.
|
|
133
132
|
export function isUnchanged(templateContent, installedContent, opts = {}) {
|
|
134
133
|
// 브랜치 치환(#477)도 가상 비교에 포함 — 치환 설치본이 "변경됨"으로 오판되어 매 업데이트마다
|
|
135
134
|
// 재복사(.bak churn)되는 것을 막는다. opts.branches 미지정/표준값이면 no-op.
|
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
// wizard-prompts.yml 라벨 메타
|
|
2
|
-
//
|
|
3
|
-
// 2934~2960(_wf_read_field), 2962~2970(wf_field).
|
|
4
|
-
// ⚠️ YAML 라이브러리 금지 — .sh와 동일하게 라인 기반 파싱만 한다(외부 의존성 0 + 포맷 관용성 동일).
|
|
1
|
+
// wizard-prompts.yml 라벨 메타 파서: 파일 위치, 필드 조회, 워크플로우 표시 이름.
|
|
2
|
+
// ⚠️ YAML 라이브러리 금지 — 라인 기반 파싱만 한다(외부 의존성 0 + 포맷 관용성).
|
|
5
3
|
import { join } from "node:path";
|
|
6
4
|
import * as nodeFs from "node:fs";
|
|
7
5
|
import { PATHS } from "./paths.js";
|
|
8
6
|
|
|
9
|
-
// wizard-prompts.yml 위치 (레포 루트 기준 상대경로
|
|
7
|
+
// wizard-prompts.yml 위치 (레포 루트 기준 상대경로)
|
|
10
8
|
export const LABELS_FILE = ".github/config/wizard-prompts.yml";
|
|
11
9
|
|
|
12
|
-
// 따옴표 감싸진 값이면 벗기고 trim ("값" → 값).
|
|
10
|
+
// 따옴표 감싸진 값이면 벗기고 trim ("값" → 값).
|
|
13
11
|
function unquote(s) {
|
|
14
12
|
const t = s.trim();
|
|
15
13
|
if (t.startsWith('"') && t.endsWith('"') && t.length >= 2) return t.slice(1, -1);
|
|
@@ -19,7 +17,7 @@ function unquote(s) {
|
|
|
19
17
|
// wizard-prompts.yml 텍스트 → 파싱 객체 (순수 함수 — 테스트 직접 사용 가능).
|
|
20
18
|
// 반환: { fields: Map<조회키, {label?,help?,example?}>, workflowNames: [{key,value}] }
|
|
21
19
|
// - 조회키: "PROJECT_NAME" 또는 "flutter.APP_ARTIFACT_NAME" (dotted 타입 오버라이드)
|
|
22
|
-
// - 구형 1줄(KEY: "라벨")은 fields의 label로 흡수 (
|
|
20
|
+
// - 구형 1줄(KEY: "라벨")은 fields의 label로 흡수 (label만 의미)
|
|
23
21
|
export function parseWizardPrompts(text) {
|
|
24
22
|
const fields = new Map();
|
|
25
23
|
const workflowNames = [];
|
|
@@ -28,10 +26,10 @@ export function parseWizardPrompts(text) {
|
|
|
28
26
|
|
|
29
27
|
for (const raw of String(text).split(/\r?\n/)) {
|
|
30
28
|
const line = raw.replace(/\r$/, "");
|
|
31
|
-
if (!line.trim() || line.trim().startsWith("#")) continue; // 빈 줄·주석은 블록을 끊지 않음
|
|
29
|
+
if (!line.trim() || line.trim().startsWith("#")) continue; // 빈 줄·주석은 블록을 끊지 않음
|
|
32
30
|
|
|
33
31
|
if (!/^\s/.test(line)) {
|
|
34
|
-
// 최상위 키 라인 — 이전 블록 종료
|
|
32
|
+
// 최상위 키 라인 — 이전 블록 종료
|
|
35
33
|
current = null; inWfNames = false;
|
|
36
34
|
const m = line.match(/^([A-Za-z_][A-Za-z0-9_.\-]*):(.*)$/);
|
|
37
35
|
if (!m) continue;
|
|
@@ -40,7 +38,7 @@ export function parseWizardPrompts(text) {
|
|
|
40
38
|
if (key === "_workflow_names") { inWfNames = true; continue; }
|
|
41
39
|
const entry = fields.get(key) || {};
|
|
42
40
|
if (rest) {
|
|
43
|
-
// 구형 1줄: KEY: "라벨" (
|
|
41
|
+
// 구형 1줄: KEY: "라벨" (label로만 사용)
|
|
44
42
|
const q = rest.match(/^"([^"]*)"\s*$/);
|
|
45
43
|
if (q) entry.label = q[1];
|
|
46
44
|
}
|
|
@@ -53,20 +51,20 @@ export function parseWizardPrompts(text) {
|
|
|
53
51
|
const m = line.match(/^\s+([A-Za-z_][A-Za-z0-9_.\-]*):\s*(.*)$/);
|
|
54
52
|
if (!m) continue;
|
|
55
53
|
if (inWfNames) {
|
|
56
|
-
// " KEY: "값"" 형식만 인정
|
|
54
|
+
// " KEY: "값"" 형식만 인정
|
|
57
55
|
const q = m[2].match(/^"(.*)"\s*$/);
|
|
58
56
|
if (q) workflowNames.push({ key: m[1], value: q[1] });
|
|
59
57
|
continue;
|
|
60
58
|
}
|
|
61
59
|
if (current && (m[1] === "label" || m[1] === "help" || m[1] === "example")) {
|
|
62
|
-
// 블록 내 첫 등장만 채택
|
|
60
|
+
// 블록 내 첫 등장만 채택
|
|
63
61
|
if (current[m[1]] == null) current[m[1]] = unquote(m[2]);
|
|
64
62
|
}
|
|
65
63
|
}
|
|
66
64
|
return { fields, workflowNames };
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
// wizard-prompts.yml을 찾아 읽고
|
|
67
|
+
// wizard-prompts.yml을 찾아 읽고 파싱.
|
|
70
68
|
// 우선순위: 대상 프로젝트(targetRoot) → 다운로드 원본(tempDir) 폴백 → null.
|
|
71
69
|
// WHY 폴백: 신규 통합에서는 copy_config_folder가 configure_workflow_env보다 늦게 실행되어
|
|
72
70
|
// 대상에 아직 파일이 없다 — 폴백 없으면 label/help/example이 전부 빈값(KEY명만 출력)이 된다.
|
|
@@ -80,7 +78,7 @@ export function loadWizardPrompts(targetRoot = ".", tempDir = "", fs = nodeFs) {
|
|
|
80
78
|
return null;
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
// 필드
|
|
81
|
+
// 필드 조회. field: "label" | "help" | "example".
|
|
84
82
|
// 우선순위: "{type}.KEY" 블록 → "KEY" 블록(구형 1줄 포함) → 폴백(label이면 KEY명, 아니면 "").
|
|
85
83
|
export function wfField(prompts, type, key, field) {
|
|
86
84
|
if (prompts && prompts.fields) {
|
|
@@ -92,11 +90,11 @@ export function wfField(prompts, type, key, field) {
|
|
|
92
90
|
return field === "label" ? key : "";
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
// 워크플로우 파일명 → 사람이 읽는 짧은
|
|
93
|
+
// 워크플로우 파일명 → 사람이 읽는 짧은 이름.
|
|
96
94
|
// _workflow_names에서 "키가 파일명에 포함되면" 그 값 사용 — 최장 키 우선(REACT-CI vs REACT-CICD 구분).
|
|
97
|
-
// 미매칭이면 .yaml/.yml 확장자만 제거해
|
|
95
|
+
// 미매칭이면 .yaml/.yml 확장자만 제거해 반환.
|
|
98
96
|
export function workflowDisplayName(prompts, filename) {
|
|
99
|
-
const base = String(filename).split("/").pop().split("\\").pop(); // 경로 제거
|
|
97
|
+
const base = String(filename).split("/").pop().split("\\").pop(); // 경로 제거
|
|
100
98
|
let best = null; let bestLen = 0;
|
|
101
99
|
for (const { key, value } of prompts?.workflowNames ?? []) {
|
|
102
100
|
if (base.includes(key) && key.length > bestLen) { best = value; bestLen = key.length; }
|
package/src/i18n/ide.en.js
CHANGED
|
@@ -65,6 +65,7 @@ export default {
|
|
|
65
65
|
"ide.harness.already": " Persona Harness: already enabled (kept)",
|
|
66
66
|
"ide.harness.enabledDone": " Persona Harness enabled. Restart PI to apply.",
|
|
67
67
|
"ide.harness.noSettings": " PI settings.json is missing. Run PI once and try again.",
|
|
68
|
+
"ide.harness.invalidSettings": " PI settings.json could not be parsed, so it was left untouched. Fix the JSON in {file} and try again.",
|
|
68
69
|
"ide.harness.needPackage": " The harness loader is missing. Install or update the PI package first.",
|
|
69
70
|
"ide.harness.removeNone": " Persona Harness is not enabled, skipping",
|
|
70
71
|
"ide.harness.keepSkill": " Leaving the PI skill as is and only unregistering the harness.",
|
package/src/i18n/ide.ko.js
CHANGED
|
@@ -65,6 +65,7 @@ export default {
|
|
|
65
65
|
"ide.harness.already": " Persona Harness: 이미 활성화됨 (유지)",
|
|
66
66
|
"ide.harness.enabledDone": " Persona Harness 활성화 완료 — PI 재시작 후 적용됩니다.",
|
|
67
67
|
"ide.harness.noSettings": " PI settings.json이 없습니다 — PI를 한 번 실행한 뒤 다시 시도하세요.",
|
|
68
|
+
"ide.harness.invalidSettings": " PI settings.json을 해석할 수 없어 건드리지 않았습니다 — {file} 의 JSON 문법을 고친 뒤 다시 시도하세요.",
|
|
68
69
|
"ide.harness.needPackage": " harness loader가 없습니다 — 먼저 PI 패키지를 설치/업데이트하세요.",
|
|
69
70
|
"ide.harness.removeNone": " Persona Harness가 활성화돼 있지 않아 건너뜁니다",
|
|
70
71
|
"ide.harness.keepSkill": " PI skill은 그대로 두고 harness 등록만 해제합니다.",
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// projectops CLI 진입 파이프라인 (
|
|
1
|
+
// projectops CLI 진입 파이프라인 (인자 > 감지 > 모드별 통합 > 요약).
|
|
2
2
|
// 감지 → 다운로드 → 모드 라우팅 → 통합 실행 → 정리. 비대화형(--force) 우선.
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -130,7 +130,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
130
130
|
disarmSignals = trace.armSignals({ targetRoot: cwd, now: "" });
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
// 기존 version.yml 로드 — version/version_code/project_paths 보존의 단일 진실
|
|
133
|
+
// 기존 version.yml 로드 — version/version_code/project_paths 보존의 단일 진실
|
|
134
134
|
const vyPath = join(cwd, "version.yml");
|
|
135
135
|
const existing = existsSync(vyPath) ? parseExisting(readFileSync(vyPath, "utf8")) : null;
|
|
136
136
|
trace.event("detect", "existing-install", existing ? "found" : "none", {
|
|
@@ -150,11 +150,11 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
150
150
|
source: existing?.version ? "version.yml(existing value kept)"
|
|
151
151
|
: (opts.version ? "cli-flag(--project-version)" : "project file detection"),
|
|
152
152
|
});
|
|
153
|
-
const versionCode = existing?.versionCode ?? 1; // 기존 빌드번호 보존
|
|
153
|
+
const versionCode = existing?.versionCode ?? 1; // 기존 빌드번호 보존
|
|
154
154
|
const branch = detectDefaultBranch(cwd);
|
|
155
155
|
const repoName = detectRepoName(cwd);
|
|
156
156
|
trace.event("detect", "repo", repoName || "(unknown)", { defaultBranch: branch, versionCode });
|
|
157
|
-
// 경로 확정 (
|
|
157
|
+
// 경로 확정 (비대화형 — --paths 우선 → 저장값 → 후보 1개 자동 → 루트 폴백)
|
|
158
158
|
let cliPaths;
|
|
159
159
|
try {
|
|
160
160
|
cliPaths = parsePathsCsv(opts.pathsCsv);
|
|
@@ -221,7 +221,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
221
221
|
mode: opts.mode, force: true, types, version, versionCode, branch, paths,
|
|
222
222
|
...resolved,
|
|
223
223
|
repoName,
|
|
224
|
-
// 실 resolver 4종 (
|
|
224
|
+
// 실 resolver 4종 (spring-app-yml 포함)
|
|
225
225
|
resolvers: makeResolvers(cwd, repoName, paths),
|
|
226
226
|
now, today,
|
|
227
227
|
});
|
|
@@ -263,7 +263,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
263
263
|
// 비대화형 축약 배너 (#446 확정 — 1줄, 로그 오염 최소)
|
|
264
264
|
printBannerCompact({ version: context.templateVersion, mode: opts.mode });
|
|
265
265
|
|
|
266
|
-
// Breaking Changes 게이트 (
|
|
266
|
+
// Breaking Changes 게이트 (비대화형은 경고 후 진행)
|
|
267
267
|
const proceed = await trace.stepAsync("breaking-check", () => runBreakingCheck({
|
|
268
268
|
cwd, tempDir, templateVersion: context.templateVersion,
|
|
269
269
|
onItems: (items) => { breakingReport = items; },
|
|
@@ -301,7 +301,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
301
301
|
case "workflows": result = trace.step("install-workflows", () => runWorkflows(context, tempDir, cwd, { trace })); break;
|
|
302
302
|
case "issues": result = trace.step("install-issues", () => runIssues(context, tempDir, cwd)); break;
|
|
303
303
|
default:
|
|
304
|
-
// 알 수 없는 모드 →
|
|
304
|
+
// 알 수 없는 모드 → 복사 0건, 에러 아님
|
|
305
305
|
break;
|
|
306
306
|
}
|
|
307
307
|
// 마이그레이션 기록 (#493/#494) — Layer 2/3 트레이스 파일 + Layer 1 가이드 엔트리
|
|
@@ -323,7 +323,7 @@ async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, cl
|
|
|
323
323
|
}).guidePath;
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
-
// 완료 요약 (
|
|
326
|
+
// 완료 요약 (CLI 모드에서도 출력)
|
|
327
327
|
printSummary({
|
|
328
328
|
mode: opts.mode, types, version, deployBranch: context.deployBranch, migrationGuidePath,
|
|
329
329
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
package/src/ui/banner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Start screen banner (layer 1). Classic boxed title, kept from the original wizard.
|
|
2
2
|
import { A, paint, visualWidth } from "./ansi.js";
|
|
3
3
|
import { t, getLang, getLangSource } from "../i18n/index.js";
|
|
4
|
+
import { TEMPLATE_REPO_WEB_URL } from "../core/paths.js";
|
|
4
5
|
|
|
5
6
|
const INNER = 56; // inner width of the box
|
|
6
7
|
|
|
@@ -22,7 +23,7 @@ export function printBanner({ version, modeLabel }, out = (s) => process.stdout.
|
|
|
22
23
|
out(` 🐵 Author : Cassiiopeia\n`);
|
|
23
24
|
out(` 🪐 Mode : ${modeLabel}\n`);
|
|
24
25
|
out(` 🌐 Language: ${getLang()} (${t(`cli.langSource.${getLangSource()}`)})\n`);
|
|
25
|
-
out(` 📦 Repo : ${paint(
|
|
26
|
+
out(` 📦 Repo : ${paint(TEMPLATE_REPO_WEB_URL.replace("https://", ""), A.dim)}\n`);
|
|
26
27
|
out("\n");
|
|
27
28
|
}
|
|
28
29
|
|
package/src/ui/env-plan.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
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 KEY 수집, 필드 카드, 기본값·개별 입력.
|
|
5
2
|
// io 주입식 — 테스트는 {select, multiselect, text} 스텁을 넘긴다. 기본은 readline-engine 실물.
|
|
6
3
|
import { join } from "node:path";
|
|
7
4
|
import { readFileSync } from "node:fs";
|
|
@@ -15,10 +12,10 @@ import { t } from "../i18n/index.js";
|
|
|
15
12
|
|
|
16
13
|
const CANCEL = engine.CANCEL;
|
|
17
14
|
|
|
18
|
-
// 진행 안내는 stderr로 출력 (
|
|
15
|
+
// 진행 안내는 stderr로 출력 (stdout 파이프 오염 방지).
|
|
19
16
|
const defaultLog = (s = "") => stderr.write(s + "\n");
|
|
20
17
|
|
|
21
|
-
// 사용처 문자열
|
|
18
|
+
// 사용처 문자열 조립.
|
|
22
19
|
// usages: [{type, workflowName}] — 타입이 여러 개면 타입만 "t1·t2", 하나면 "타입 name1·name2".
|
|
23
20
|
export function scopeString(usages = []) {
|
|
24
21
|
const types = []; const names = [];
|
|
@@ -30,10 +27,10 @@ export function scopeString(usages = []) {
|
|
|
30
27
|
return `${types.join("·")} ${names.join("·")}`.trim();
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
// ask KEY 수집
|
|
30
|
+
// ask KEY 수집 — 실제 설치되는 워크플로우와 같은 소스를 스캔한다.
|
|
34
31
|
// tempDir: 다운로드 원본 루트. types: 설치 대상 타입 목록.
|
|
35
32
|
// opts:
|
|
36
|
-
// resolvers - @접두 기본값(@repo 등) 해석용 (
|
|
33
|
+
// resolvers - @접두 기본값(@repo 등) 해석용 (수집 시점에 해석)
|
|
37
34
|
// deployTarget - #439 배포 축: docker-ssh(기본)일 때만 server-deploy 스캔 (복사 엔진과 일치)
|
|
38
35
|
// publishTargets - #439 publish 축: 선택된 타겟의 publish/<target>/ 스캔
|
|
39
36
|
// prompts - wizard-labels 파싱 객체 (워크플로우 표시명용, null이면 확장자 제거 폴백)
|
|
@@ -62,9 +59,9 @@ export function collectAsks(tempDir, types = [], opts = {}) {
|
|
|
62
59
|
if (!content.includes("@wizard")) continue;
|
|
63
60
|
const workflowName = workflowDisplayName(prompts, filename);
|
|
64
61
|
for (const line of content.split(/\r?\n/)) {
|
|
65
|
-
const p = parseWizardLine(line); // KEY 정규식 [A-Z_]+
|
|
62
|
+
const p = parseWizardLine(line); // KEY 정규식 [A-Z_]+
|
|
66
63
|
if (!p || p.action !== "ask") continue;
|
|
67
|
-
// 타입별 기본값: @접두면 resolver 해석, 아니면 리터럴
|
|
64
|
+
// 타입별 기본값: @접두면 resolver 해석, 아니면 리터럴
|
|
68
65
|
// #489 — __PROJECT_NAME__ 등 전역 토큰은 카드 표시 전에 미리 해석한다.
|
|
69
66
|
// 설치본(파일 복사 시 전역 치환)과 동일한 값을 보여주지 않으면
|
|
70
67
|
// 사용자가 "리터럴이 그대로 박히나?" 오해하게 된다.
|
|
@@ -84,12 +81,12 @@ export function collectAsks(tempDir, types = [], opts = {}) {
|
|
|
84
81
|
return { keys, defaults, typeDefaults, usages };
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
// KEY가 처음 등장한 type (
|
|
84
|
+
// KEY가 처음 등장한 type (라벨 조회 시 타입 오버라이드 우선순위용)
|
|
88
85
|
function firstTypeFor(usages, key) {
|
|
89
86
|
return usages.get(key)?.[0]?.type ?? "";
|
|
90
87
|
}
|
|
91
88
|
|
|
92
|
-
// KEY 1개를 'label·사용처·설명·예시·기본값' 카드로
|
|
89
|
+
// KEY 1개를 'label·사용처·설명·예시·기본값' 카드로 출력.
|
|
93
90
|
// info: { default, usages } — idx/tot 있으면 "(i/t)" 진행 표시. log 주입 가능(테스트 무음화).
|
|
94
91
|
export function printFieldCard(prompts, key, info, idx = null, tot = null, log = defaultLog) {
|
|
95
92
|
const ty = info.usages?.[0]?.type ?? "";
|
|
@@ -107,8 +104,8 @@ export function printFieldCard(prompts, key, info, idx = null, tot = null, log =
|
|
|
107
104
|
log("");
|
|
108
105
|
}
|
|
109
106
|
|
|
110
|
-
// 지정 KEY들을 하나씩 입력받아 values에
|
|
111
|
-
// 빈 입력(Enter)/ESC → KEY 공통 기본값
|
|
107
|
+
// 지정 KEY들을 하나씩 입력받아 values에 기록.
|
|
108
|
+
// 빈 입력(Enter)/ESC → KEY 공통 기본값 유지.
|
|
112
109
|
async function promptEach(io, prompts, asks, todoKeys, values, log) {
|
|
113
110
|
const tot = todoKeys.length;
|
|
114
111
|
if (tot === 0) return;
|
|
@@ -129,15 +126,15 @@ async function promptEach(io, prompts, asks, todoKeys, values, log) {
|
|
|
129
126
|
}
|
|
130
127
|
}
|
|
131
128
|
|
|
132
|
-
// 배포 env 설정
|
|
129
|
+
// 배포 env 설정 계획.
|
|
133
130
|
// 반환: { values: Map<key,value>, useDefaults: boolean }
|
|
134
|
-
// - useDefaults=true → 호출부는 substituteEnv에 그대로 넘기면 타입별 기본값 경로
|
|
131
|
+
// - useDefaults=true → 호출부는 substituteEnv에 그대로 넘기면 타입별 기본값 경로
|
|
135
132
|
// - useDefaults=false → values에 담긴 키만 사용자 확정값으로 치환, 나머지는 기본값
|
|
136
133
|
// (⚠️ substituteEnv는 useDefaults=false일 때만 values를 참조하므로 이 플래그를 반드시 함께 전달)
|
|
137
134
|
// 인자:
|
|
138
135
|
// tempDir/types/resolvers/deployTarget/publishTargets — collectAsks와 동일 의미
|
|
139
136
|
// targetRoot — wizard-prompts.yml 1차 탐색 위치(기본 ".")
|
|
140
|
-
// force — true면 질문 없이 전부 기본값
|
|
137
|
+
// force — true면 질문 없이 전부 기본값
|
|
141
138
|
// io — {select, multiselect, text} 주입 (기본 readline-engine). 테스트 스텁 지점.
|
|
142
139
|
// log — 카드·안내 출력 함수 주입 (기본 stderr)
|
|
143
140
|
export async function promptEnvPlan({
|
|
@@ -148,17 +145,17 @@ export async function promptEnvPlan({
|
|
|
148
145
|
const asks = collectAsks(tempDir, types, { resolvers, deployTarget, publishTargets, prompts, repoName });
|
|
149
146
|
const defaults = asks.defaults;
|
|
150
147
|
|
|
151
|
-
// 수집 키 0개 → 질문 자체가 없음
|
|
148
|
+
// 수집 키 0개 → 질문 자체가 없음
|
|
152
149
|
if (asks.keys.length === 0) return { values: new Map(), useDefaults: true };
|
|
153
150
|
|
|
154
|
-
// 비대화형: force 또는 (io 미주입 && 비TTY) → 전부 기본값
|
|
151
|
+
// 비대화형: force 또는 (io 미주입 && 비TTY) → 전부 기본값
|
|
155
152
|
// io가 주입돼 있으면(테스트/상위 마법사) TTY 여부와 무관하게 대화형으로 진행한다.
|
|
156
153
|
const interactive = !force && (io != null || stdin.isTTY);
|
|
157
154
|
if (!interactive) return { values: new Map(defaults), useDefaults: true };
|
|
158
155
|
|
|
159
156
|
const ui = io ?? engine;
|
|
160
157
|
|
|
161
|
-
// 기본값 미리보기 카드 전체 출력
|
|
158
|
+
// 기본값 미리보기 카드 전체 출력
|
|
162
159
|
log("");
|
|
163
160
|
log(t("envPlan.title"));
|
|
164
161
|
log("");
|
|
@@ -179,20 +176,20 @@ export async function promptEnvPlan({
|
|
|
179
176
|
{ value: "some", label: t("envPlan.optSome") },
|
|
180
177
|
],
|
|
181
178
|
});
|
|
182
|
-
// ESC/취소 → 전부 기본값
|
|
179
|
+
// ESC/취소 → 전부 기본값
|
|
183
180
|
if (choice === CANCEL || choice == null || choice === "all") {
|
|
184
181
|
return { values: new Map(defaults), useDefaults: true };
|
|
185
182
|
}
|
|
186
183
|
|
|
187
184
|
// 사용자가 확정한 키만 values에 담는다 — substituteEnv(useDefaults:false)가
|
|
188
|
-
// values에 없는 키는 타입별 기본값으로
|
|
185
|
+
// values에 없는 키는 타입별 기본값으로 채운다 (기본값 위에 입력값 덮어쓰기).
|
|
189
186
|
const values = new Map();
|
|
190
187
|
if (choice === "each") {
|
|
191
188
|
await promptEach(ui, prompts, asks, asks.keys, values, log);
|
|
192
189
|
return { values, useDefaults: false };
|
|
193
190
|
}
|
|
194
191
|
|
|
195
|
-
// some: 바꿀 항목만 멀티선택 → 고른 것만 입력
|
|
192
|
+
// some: 바꿀 항목만 멀티선택 → 고른 것만 입력
|
|
196
193
|
const options = asks.keys.map((key) => ({
|
|
197
194
|
value: key,
|
|
198
195
|
label: `${wfField(prompts, firstTypeFor(asks.usages, key), key, "label")} ${t("envPlan.defaultTag", { value: defaults.get(key) })}`,
|
|
@@ -202,11 +199,11 @@ export async function promptEnvPlan({
|
|
|
202
199
|
options,
|
|
203
200
|
initialValues: [],
|
|
204
201
|
});
|
|
205
|
-
// ESC/빈 선택 → 전부 기본값
|
|
202
|
+
// ESC/빈 선택 → 전부 기본값
|
|
206
203
|
if (selected === CANCEL || !Array.isArray(selected) || selected.length === 0) {
|
|
207
204
|
return { values: new Map(defaults), useDefaults: true };
|
|
208
205
|
}
|
|
209
|
-
// 수집 키 순서 유지 +
|
|
206
|
+
// 수집 키 순서 유지 + 수집된 ask KEY만 인정
|
|
210
207
|
const todo = asks.keys.filter((k) => selected.includes(k));
|
|
211
208
|
await promptEach(ui, prompts, asks, todo, values, log);
|
|
212
209
|
return { values, useDefaults: false };
|
package/src/ui/prompts.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 대화형 프롬프트 래핑 (
|
|
1
|
+
// 대화형 프롬프트 래핑 (메뉴·선택·질문).
|
|
2
2
|
// node:readline 기반 자체 엔진 사용 (@clack/prompts 는 Windows TTY에서 Enter가 멈추는 버그로 제거).
|
|
3
3
|
// 취소(ESC/Ctrl+C)는 각 함수가 CANCEL 심볼을 반환 → 호출부가 정상 종료(exit 0) 처리.
|
|
4
4
|
import * as engine from "./readline-engine.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// node:readline 기반 대화형 프롬프트 엔진 (@clack/prompts 대체).
|
|
2
2
|
// WHY: @clack/prompts 1.7.0 이 Windows TTY 콘솔에서 Enter(return) 키를 처리하지 못하고
|
|
3
3
|
// 멈추는 버그가 있다(실측 확정). node:readline 의 keypress 이벤트는 Windows에서 정상 동작한다.
|
|
4
|
-
//
|
|
4
|
+
// 외부 의존성 0 → 내부망에서도 안전.
|
|
5
5
|
//
|
|
6
6
|
// 계약: 취소(ESC/Ctrl+C)는 CANCEL 심볼 반환. 각 함수 async.
|
|
7
7
|
import { emitKeypressEvents } from "node:readline";
|
package/src/ui/skills-prompts.js
CHANGED