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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// 프로젝트 타입 레지스트리 — 타입별 지식(감지 마커, 감지 순서, 버전·빌드 번호 파일)을 한 곳에 둔다.
|
|
2
|
+
// 타입 목록·마커 맵·감지 체인은 모두 이 표에서 만들어지므로, 새 타입은 여기에 한 줄을 더하는 것에서 시작한다.
|
|
3
|
+
// (릴리스 시점의 버전 파일 쓰기는 payload/scripts/version_manager.py가 따로 담당한다.)
|
|
4
|
+
//
|
|
5
|
+
// 필드:
|
|
6
|
+
// id CLI·version.yml에 쓰이는 타입 이름. 배열 순서가 곧 --help·대화형 선택지 표시 순서다.
|
|
7
|
+
// markers 그 타입의 근거 파일. [0]이 대표 파일이고, 모노레포 경로 탐색은 이 순서로 우선순위를 둔다.
|
|
8
|
+
// detectBy 자동 감지 방식 — "markers": markers 중 하나라도 있으면 감지
|
|
9
|
+
// "package": package.json 의존성 키(packageDep)로 감지
|
|
10
|
+
// "package-fallback": package.json은 있지만 위 어느 것도 아닐 때
|
|
11
|
+
// 없음: 자동 감지하지 않는다(basic은 아무것도 감지되지 않았을 때의 결과).
|
|
12
|
+
// detectOrder 같은 detectBy 안에서의 판정 순서. 앞선 규칙이 이긴다(expo 앱은 react-native 의존성도 가짐).
|
|
13
|
+
// versionSources 설치 시 버전을 읽을 파일 키(detect.js의 sources) — 주 타입일 때 이 순서로 먼저 읽는다.
|
|
14
|
+
// buildNumberSource 모바일 빌드 번호를 읽을 파일 키(detect.js의 detectBuildNumberFromFiles).
|
|
15
|
+
// singleServerCd 배포 방식 변형 없이 서버 배포 CD가 하나뿐일 때 그 워크플로우 파일명.
|
|
16
|
+
export const TYPES = [
|
|
17
|
+
{
|
|
18
|
+
id: "spring",
|
|
19
|
+
markers: ["build.gradle", "build.gradle.kts", "pom.xml"],
|
|
20
|
+
detectBy: "markers", detectOrder: 2,
|
|
21
|
+
versionSources: ["gradle", "gradleKts", "pom"],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "flutter",
|
|
25
|
+
markers: ["pubspec.yaml"],
|
|
26
|
+
detectBy: "markers", detectOrder: 1,
|
|
27
|
+
versionSources: ["pubspec"],
|
|
28
|
+
buildNumberSource: "pubspec",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "next",
|
|
32
|
+
markers: ["package.json"],
|
|
33
|
+
detectBy: "package", packageDep: "next", detectOrder: 3,
|
|
34
|
+
versionSources: ["packageJson"],
|
|
35
|
+
singleServerCd: "PROJECT-NEXT-CICD.yaml",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "react",
|
|
39
|
+
markers: ["package.json"],
|
|
40
|
+
detectBy: "package", packageDep: "react", detectOrder: 4,
|
|
41
|
+
versionSources: ["packageJson"],
|
|
42
|
+
singleServerCd: "PROJECT-REACT-CICD.yaml",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "react-native",
|
|
46
|
+
markers: ["package.json"],
|
|
47
|
+
detectBy: "package", packageDep: "react-native", detectOrder: 2,
|
|
48
|
+
// 릴리스 때 동기화하는 네이티브 파일이 기준이고, 거기서 못 읽을 때만 package.json을 본다.
|
|
49
|
+
versionSources: ["reactNative", "packageJson"],
|
|
50
|
+
buildNumberSource: "androidGradle",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
// Expo는 최신 create-expo-app 템플릿처럼 app.json 없이 app.config.ts/js만 쓸 수 있다.
|
|
54
|
+
// 구성 파일이 아예 없어도 expo 의존성이 있는 package.json이 곧 근거다.
|
|
55
|
+
id: "react-native-expo",
|
|
56
|
+
markers: ["app.json", "app.config.ts", "app.config.js", "package.json"],
|
|
57
|
+
detectBy: "package", packageDep: "expo", detectOrder: 1,
|
|
58
|
+
versionSources: ["appJson", "packageJson"],
|
|
59
|
+
buildNumberSource: "expoAppJson",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "node",
|
|
63
|
+
markers: ["package.json"],
|
|
64
|
+
detectBy: "package-fallback",
|
|
65
|
+
versionSources: ["packageJson"],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "python",
|
|
69
|
+
markers: ["pyproject.toml", "setup.py", "requirements.txt"],
|
|
70
|
+
detectBy: "markers", detectOrder: 3,
|
|
71
|
+
versionSources: ["pyproject", "setupPy"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
// 마커 파일이 없는 타입 — 아무것도 감지되지 않았을 때의 결과이자 경로가 필요 없는 타입.
|
|
75
|
+
id: "basic",
|
|
76
|
+
markers: [],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "go",
|
|
80
|
+
markers: ["go.mod"],
|
|
81
|
+
detectBy: "markers", detectOrder: 4,
|
|
82
|
+
versionSources: [],
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// 감지 결과가 하나도 없을 때의 타입
|
|
87
|
+
export const FALLBACK_TYPE = "basic";
|
|
88
|
+
|
|
89
|
+
const BY_ID = new Map(TYPES.map((t) => [t.id, t]));
|
|
90
|
+
|
|
91
|
+
// 알 수 없는 타입은 undefined — 호출부가 각자의 기본값을 정한다.
|
|
92
|
+
export const typeInfo = (id) => BY_ID.get(id);
|
|
93
|
+
|
|
94
|
+
// 표시 순서 그대로의 타입 이름 목록
|
|
95
|
+
// VALID_TYPES·ALL_TYPES가 같은 배열을 공유하므로 한쪽에서 바꿔 다른 쪽이 오염되지 않게 고정한다.
|
|
96
|
+
export const TYPE_IDS = Object.freeze(TYPES.map((t) => t.id));
|
|
97
|
+
|
|
98
|
+
const byDetectOrder = (a, b) => a.detectOrder - b.detectOrder;
|
|
99
|
+
|
|
100
|
+
// 파일 존재만으로 감지하는 타입 (감지 순서대로)
|
|
101
|
+
export const MARKER_DETECTED_TYPES = TYPES.filter((t) => t.detectBy === "markers").sort(byDetectOrder);
|
|
102
|
+
|
|
103
|
+
// package.json 의존성으로 가르는 타입 (판정 순서대로)
|
|
104
|
+
export const PACKAGE_DETECTED_TYPES = TYPES.filter((t) => t.detectBy === "package").sort(byDetectOrder);
|
|
105
|
+
|
|
106
|
+
// package.json이 있지만 알려진 프레임워크 의존성이 없을 때의 타입
|
|
107
|
+
export const PACKAGE_FALLBACK_TYPE = TYPES.find((t) => t.detectBy === "package-fallback").id;
|
|
108
|
+
|
|
109
|
+
// 모바일 빌드 번호(version_code)를 가진 타입
|
|
110
|
+
export const BUILD_NUMBER_TYPES = new Set(TYPES.filter((t) => t.buildNumberSource).map((t) => t.id));
|
|
111
|
+
|
|
112
|
+
// 배포 방식 변형 없이 서버 배포 CD가 하나뿐인 워크플로우 파일명
|
|
113
|
+
export const SINGLE_SERVER_CD_FILES = new Set(TYPES.filter((t) => t.singleServerCd).map((t) => t.singleServerCd));
|
package/src/core/verify.js
CHANGED
|
@@ -48,31 +48,97 @@ const AUTO_SECRETS = new Set(["GITHUB_TOKEN"]);
|
|
|
48
48
|
export const OPTIONAL_SECRETS = new Set(["AI_API_KEY", "WORKFLOW_PAT"]);
|
|
49
49
|
const SECRET_RE = /secrets\.([A-Z][A-Z0-9_]*)/g;
|
|
50
50
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
// 워크플로우 머리 주석의 "NAME (선택)" 표기 — 워크플로우가 비어 있어도 동작하도록 작성된 secret이다.
|
|
52
|
+
const OPTIONAL_NOTE_RE = /^\s*#\s*-?\s*([A-Z][A-Z0-9_]*)\s*\(선택/;
|
|
53
|
+
// `secrets.A || secrets.B` — 둘 중 하나만 있으면 된다.
|
|
54
|
+
const EITHER_RE = /^\s*\|\|\s*secrets\.([A-Z][A-Z0-9_]*)/;
|
|
55
|
+
// `secrets.A || '3000'`, `secrets.A || vars.A` — 기본값·대체값이 있다.
|
|
56
|
+
const FALLBACK_AFTER_RE = /^\s*\|\|/;
|
|
57
|
+
const FALLBACK_BEFORE_RE = /\|\|\s*$/;
|
|
58
|
+
export const EITHER_SEP = " 또는 ";
|
|
59
|
+
|
|
60
|
+
// 한 파일 안의 secret 참조를 필수 / 둘 중 하나 / 선택으로 가른다.
|
|
61
|
+
function classifyFile(content) {
|
|
62
|
+
const lines = content.split(/\r?\n/);
|
|
63
|
+
const noted = new Set();
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const m = line.match(OPTIONAL_NOTE_RE);
|
|
66
|
+
if (m) noted.add(m[1]);
|
|
67
|
+
}
|
|
68
|
+
const singles = new Set();
|
|
69
|
+
const pairs = [];
|
|
70
|
+
const optional = new Set();
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (isCommented(line)) continue;
|
|
73
|
+
const matches = [...line.matchAll(SECRET_RE)];
|
|
74
|
+
for (let i = 0; i < matches.length; i++) {
|
|
75
|
+
const m = matches[i];
|
|
76
|
+
const name = m[1];
|
|
77
|
+
const rest = line.slice(m.index + m[0].length);
|
|
78
|
+
const either = rest.match(EITHER_RE);
|
|
79
|
+
if (either) {
|
|
80
|
+
pairs.push([name, either[1]]);
|
|
81
|
+
i++; // 짝의 오른쪽은 이미 묶었다
|
|
82
|
+
} else if (FALLBACK_AFTER_RE.test(rest) || FALLBACK_BEFORE_RE.test(line.slice(0, m.index))) {
|
|
83
|
+
optional.add(name);
|
|
84
|
+
} else {
|
|
85
|
+
singles.add(name);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const out = { required: [], optional: [] };
|
|
90
|
+
for (const name of singles) (noted.has(name) ? out.optional : out.required).push(name);
|
|
91
|
+
for (const name of optional) out.optional.push(name);
|
|
92
|
+
for (const [a, b] of pairs) {
|
|
93
|
+
const key = `${a}${EITHER_SEP}${b}`;
|
|
94
|
+
(noted.has(a) || noted.has(b) ? out.optional : out.required).push(key);
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 설치된 워크플로우의 secret을 필수와 선택으로 나눈다.
|
|
100
|
+
// 참조한 이름을 전부 필수로 세면 기본값이 있는 것·둘 중 하나면 되는 폴백 쌍까지 "등록해야 동작합니다"에
|
|
101
|
+
// 섞여 개수가 부풀고, 처음 쓰는 사용자가 필요 없는 secret까지 등록하게 된다.
|
|
102
|
+
// 반환: { required: Map<이름|"A 또는 B", 파일명[]>, optional: Map<같은 형식> }
|
|
103
|
+
export function classifySecrets(workflowsDir, filenames = []) {
|
|
104
|
+
const required = new Map();
|
|
105
|
+
const optional = new Map();
|
|
106
|
+
const add = (map, key, filename) => {
|
|
107
|
+
if (!map.has(key)) map.set(key, []);
|
|
108
|
+
if (!map.get(key).includes(filename)) map.get(key).push(filename);
|
|
109
|
+
};
|
|
110
|
+
const skip = (key) => key.split(EITHER_SEP).some((n) => AUTO_SECRETS.has(n) || OPTIONAL_SECRETS.has(n));
|
|
59
111
|
for (const filename of filenames) {
|
|
60
112
|
const p = join(workflowsDir, filename);
|
|
61
113
|
if (!existsSync(p)) continue;
|
|
62
114
|
let content;
|
|
63
115
|
try { content = readFileSync(p, "utf8"); } catch { continue; }
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const name = m[1];
|
|
68
|
-
if (AUTO_SECRETS.has(name) || OPTIONAL_SECRETS.has(name)) continue;
|
|
69
|
-
if (!out.has(name)) out.set(name, []);
|
|
70
|
-
const users = out.get(name);
|
|
71
|
-
if (!users.includes(filename)) users.push(filename);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
116
|
+
const r = classifyFile(content);
|
|
117
|
+
for (const k of r.required) if (!skip(k)) add(required, k, filename);
|
|
118
|
+
for (const k of r.optional) if (!skip(k)) add(optional, k, filename);
|
|
74
119
|
}
|
|
75
|
-
|
|
120
|
+
// 한 파일에서라도 단독으로 필수인 이름은 필수다 — 그 이름이 든 폴백 쌍·선택 항목은 이미 충족되므로 뺀다.
|
|
121
|
+
const requiredNames = new Set([...required.keys()].filter((k) => !k.includes(EITHER_SEP)));
|
|
122
|
+
const coveredByRequired = (k) => k.split(EITHER_SEP).some((n) => requiredNames.has(n));
|
|
123
|
+
for (const k of [...required.keys()]) if (k.includes(EITHER_SEP) && coveredByRequired(k)) required.delete(k);
|
|
124
|
+
for (const k of [...optional.keys()]) if (required.has(k) || coveredByRequired(k)) optional.delete(k);
|
|
125
|
+
const sorted = (m) => new Map([...m.entries()].sort(([a], [b]) => a.localeCompare(b)));
|
|
126
|
+
return { required: sorted(required), optional: sorted(optional) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 설치된 워크플로우가 요구하는 GitHub Secret 목록.
|
|
130
|
+
// 완료 화면이 WORKFLOW_PAT와 권한만 안내하는 바람에, 배포 워크플로우가 실제로 필요로 하는
|
|
131
|
+
// SERVER_HOST·SSH_KEY 같은 값이 하나도 안내되지 않았다. 설치 직후 상태로는 배포가 돌지 않는데
|
|
132
|
+
// 그 사실이 어디에도 드러나지 않는다.
|
|
133
|
+
//
|
|
134
|
+
// 반환: Map<secretName 또는 "A 또는 B", string[] 그 secret을 쓰는 파일명>
|
|
135
|
+
export function collectRequiredSecrets(workflowsDir, filenames = []) {
|
|
136
|
+
return classifySecrets(workflowsDir, filenames).required;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 없어도 워크플로우가 도는 secret(기본값·대체값이 있거나 머리 주석에 "(선택)"으로 적힌 것).
|
|
140
|
+
export function collectOptionalSecrets(workflowsDir, filenames = []) {
|
|
141
|
+
return classifySecrets(workflowsDir, filenames).optional;
|
|
76
142
|
}
|
|
77
143
|
|
|
78
144
|
// SSH 인증 방식에 따라 둘 중 하나만 필요한 secret — 사용자가 이미 답한 값으로 목록을 좁힌다.
|
package/src/core/version-yml.js
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
ENV_MODES, DEPLOY_MODES, DEFAULT_ENV_MODE, DEFAULT_DEPLOY_MODE, STORE_PLATFORMS, formatStoreList,
|
|
5
5
|
} from "./flutter-options.js";
|
|
6
6
|
|
|
7
|
-
// version.yml 파싱·생성 (
|
|
7
|
+
// version.yml 파싱·생성 (전체 재생성 전략).
|
|
8
8
|
// ⚠️ YAML 재직렬화 금지 — 주석이 데이터.
|
|
9
9
|
// 레이아웃 단일 진실 = payload/version.yml.template (호출부가 templateText로 주입).
|
|
10
10
|
|
|
@@ -43,7 +43,7 @@ const FLUTTER_OPTION_KEYS = {
|
|
|
43
43
|
android_deploy_mode: "androidDeployMode", ios_deploy_mode: "iosDeployMode",
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
// metadata.template.options 상태머신
|
|
46
|
+
// metadata.template.options 상태머신 파싱.
|
|
47
47
|
// 반환: { semverAuto: bool|null, copilotAi: bool|null, deployStyle: string|null,
|
|
48
48
|
// envMode/flutterStore/androidDeployMode/iosDeployMode: string|null } — null=미기재.
|
|
49
49
|
// 구 synology·coderabbit 키 등 다른 키는 어느 분기에도 안 걸려 자연히 무시된다(파싱 에러 없음).
|
|
@@ -52,7 +52,7 @@ export function parseTemplateOptions(content) {
|
|
|
52
52
|
semverAuto: null, copilotAi: null, deployStyle: null,
|
|
53
53
|
envMode: null, flutterStore: null, androidDeployMode: null, iosDeployMode: null,
|
|
54
54
|
};
|
|
55
|
-
// 값 정규화: 따옴표 제거 + 트림
|
|
55
|
+
// 값 정규화: 따옴표 제거 + 트림
|
|
56
56
|
// 인라인 주석(` # ...`)을 먼저 떼고 따옴표·공백을 정리한다. 문자열 값을 받는 키(deploy_style)는
|
|
57
57
|
// 주석을 안 떼면 "simple # simple | nginx ..." 가 통째로 값이 된다.
|
|
58
58
|
const strip = (s) => String(s).replace(/\s+#.*$/, "").replace(/["']/g, "").trim();
|
|
@@ -80,16 +80,16 @@ export function parseTemplateOptions(content) {
|
|
|
80
80
|
if (v === "false") out.copilotAi = false;
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
|
-
// 들여쓰기 0~4칸의 다른 키 → options 섹션 종료
|
|
83
|
+
// 들여쓰기 0~4칸의 다른 키 → options 섹션 종료
|
|
84
84
|
if (/^\s{0,4}[a-z_]+:/.test(line)) { inOptions = false; inTemplate = false; }
|
|
85
85
|
}
|
|
86
|
-
// 최상위 키 → template 섹션 종료
|
|
86
|
+
// 최상위 키 → template 섹션 종료
|
|
87
87
|
if (inTemplate && /^[a-z_]+:/.test(line)) { inTemplate = false; inOptions = false; }
|
|
88
88
|
}
|
|
89
89
|
return out;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
// 기존 version.yml에서 값 추출 (
|
|
92
|
+
// 기존 version.yml에서 값 추출 (라인 기반, 주석 라인 오탐 방지).
|
|
93
93
|
export function parseExisting(content) {
|
|
94
94
|
const text = String(content || "");
|
|
95
95
|
const line = (re) => {
|
|
@@ -135,7 +135,31 @@ export function parseExisting(content) {
|
|
|
135
135
|
const options = parseTemplateOptions(text);
|
|
136
136
|
// metadata.template.branches — main/develop/mode (업데이트 모드 재질문 생략용)
|
|
137
137
|
const branches = parseTemplateBranches(text);
|
|
138
|
-
return {
|
|
138
|
+
return {
|
|
139
|
+
version, versionCode, types, paths, templateVersion, options, branches,
|
|
140
|
+
deploy: parseDeployBlock(text), extraTopLevel: parseExtraTopLevel(text),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// deploy 블록 파싱 — 설치 때 답한 배포 값을 다시 읽어 재실행·업데이트의 기본값으로 쓴다.
|
|
145
|
+
// 쓰기만 하고 읽지 않으면 손대지 않은 파일은 치환을 건너뛰어 블록이 사라지고, 자동 갱신은
|
|
146
|
+
// 사용자가 고른 값을 템플릿 기본값으로 되돌린다.
|
|
147
|
+
// 반환: Map<type, Map<KEY, value>> (값은 buildVersionYml이 쓴 큰따옴표 이스케이프를 푼 원문)
|
|
148
|
+
export function parseDeployBlock(content) {
|
|
149
|
+
const out = new Map();
|
|
150
|
+
let inDeploy = false;
|
|
151
|
+
let current = null;
|
|
152
|
+
for (const raw of String(content || "").split("\n")) {
|
|
153
|
+
const l = raw.replace(/\r$/, "");
|
|
154
|
+
if (/^deploy:/.test(l)) { inDeploy = true; current = null; continue; }
|
|
155
|
+
if (!inDeploy) continue;
|
|
156
|
+
if (/^\S/.test(l)) { inDeploy = false; continue; } // 다음 최상위 키 → 블록 종료
|
|
157
|
+
const t = l.match(/^ {2}([a-z][a-z-]*):\s*(?:#.*)?$/);
|
|
158
|
+
if (t) { current = new Map(); out.set(t[1], current); continue; }
|
|
159
|
+
const kv = l.match(/^ {4}([A-Za-z_][A-Za-z0-9_]*):\s*"((?:[^"\\]|\\.)*)"/);
|
|
160
|
+
if (kv && current) current.set(kv[1], kv[2].replace(/\\(["\\])/g, "$1"));
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
139
163
|
}
|
|
140
164
|
|
|
141
165
|
// metadata.template.branches 블록 파싱. 셋 다 있어야 유효 — 아니면 null.
|
|
@@ -245,6 +269,7 @@ export function buildVersionYml({
|
|
|
245
269
|
if (t === "{{PROJECT_PATHS}}") { if (pathsBlock) out.push(pathsBlock); continue; }
|
|
246
270
|
if (t === "{{FLUTTER_OPTIONS}}") { if (flutterBlock) out.push(flutterBlock); continue; }
|
|
247
271
|
if (t === "{{DEPLOY}}") { if (deployBlock) out.push(deployBlock); continue; }
|
|
272
|
+
if (t.startsWith("deploy_style:") && deployStyle === null) continue; // 서버 배포가 없는 타입은 기록하지 않는다
|
|
248
273
|
out.push(line.replace(/\{\{([A-Z][A-Z0-9_]*)\}\}/g, (_, name) => {
|
|
249
274
|
if (name in scalars) return scalars[name];
|
|
250
275
|
throw new Error(`version.yml.template에 알 수 없는 플레이스홀더: {{${name}}}`);
|
|
@@ -259,6 +284,14 @@ export function buildVersionYml({
|
|
|
259
284
|
return text.replace(/\n{3,}$/, "\n"); // 말미 과잉 빈 줄 정리
|
|
260
285
|
}
|
|
261
286
|
|
|
287
|
+
// 설치 시각 줄(metadata.last_updated 등)만 다른가 — 바뀐 게 없는 재실행이 매번 파일을 고쳐
|
|
288
|
+
// 작업트리를 dirty하게 만들지 않도록 호출부가 쓰기를 건너뛰는 데 쓴다.
|
|
289
|
+
const TIMESTAMP_LINE = /^\s*(last_updated|integration_date|integrated_date|last_update_date):/;
|
|
290
|
+
export function sameIgnoringTimestamps(a, b) {
|
|
291
|
+
const norm = (t) => String(t).replace(/\r\n/g, "\n").split("\n").filter((l) => !TIMESTAMP_LINE.test(l)).join("\n");
|
|
292
|
+
return norm(a) === norm(b);
|
|
293
|
+
}
|
|
294
|
+
|
|
262
295
|
// context 하나로 version.yml 최종형을 만든다 — 실제 설치(full)와 미리보기(dry-run)가
|
|
263
296
|
// 같은 함수를 쓰게 해서 "미리보기와 결과가 다른" 상황을 구조적으로 막는다.
|
|
264
297
|
// deployValues는 실제 설치에서만 존재한다(미리보기는 치환을 수행하지 않으므로 빈 Map).
|
|
@@ -275,7 +308,8 @@ export function renderVersionYml(context, templateText, { pathMarkers, deployVal
|
|
|
275
308
|
templateVersion,
|
|
276
309
|
includeSemverAuto: includeSemverAuto !== false,
|
|
277
310
|
includeCopilotAi: includeCopilotAi === true,
|
|
278
|
-
|
|
311
|
+
// null = 서버 배포 워크플로우가 없는 타입이라 배포 방식이 의미 없음(기록 생략)
|
|
312
|
+
deployStyle: deployStyle === null ? null : (deployStyle || DEFAULT_DEPLOY_STYLE),
|
|
279
313
|
optionsDate: today,
|
|
280
314
|
},
|
|
281
315
|
});
|
package/src/core/wizard-env.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// @wizard env 토큰 엔진
|
|
1
|
+
// @wizard env 토큰 엔진 — 워크플로우의 ask/auto/fallback 마커 치환과 unchanged 판정.
|
|
2
2
|
// ⚠️ YAML 파싱/재직렬화 금지 — 라인 단위 문자열 처리 (포맷·주석 보존이 unchanged 판정 전제).
|
|
3
|
-
// 실측 기준: template_integrator.sh 3282~3360, 3003~3012.
|
|
4
3
|
|
|
5
|
-
// KEY 정규식:
|
|
4
|
+
// KEY 정규식: env 키(대문자)에 더해 workflow_dispatch 입력의 `default:`처럼 소문자 키도 받는다.
|
|
5
|
+
// 마커가 붙은 줄만 대상이라 넓혀도 다른 줄에는 영향이 없다.
|
|
6
6
|
// fallback은 `KEY: ${{ 런타임값 || 'literal' }}` 표현식 안의 기본 리터럴을 교체하는 마커다.
|
|
7
7
|
const MARKER_RE = /#\s*@wizard\s+(ask|auto|fallback):(.*)$/;
|
|
8
|
-
const KEY_RE = /^(\s*)([A-
|
|
8
|
+
const KEY_RE = /^(\s*)([A-Za-z_]+):/;
|
|
9
9
|
const PATHS_ANCHOR_RE = /#\s*@wizard\s+paths-anchor/;
|
|
10
10
|
|
|
11
11
|
// 한 라인을 파싱해 {indent,key,action,arg} 반환. ask/auto/fallback 마커 없으면 null.
|
|
@@ -25,8 +25,8 @@ export function escapeYamlDoubleQuoted(value) {
|
|
|
25
25
|
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
//
|
|
29
|
-
// 라인 하나에 대해 수행. value가 빈문자면 (
|
|
28
|
+
// `KEY: "..."` 따옴표 안 값 치환 + 그 줄 끝 `# @wizard ...` 주석 제거.
|
|
29
|
+
// 라인 하나에 대해 수행. value가 빈문자면 치환 스킵(템플릿 기본값을 남긴다).
|
|
30
30
|
export function setEnvLine(line, key, value) {
|
|
31
31
|
if (value === "" || value == null) return line;
|
|
32
32
|
// CRLF 안전: 라인 끝 \r을 분리해 처리 후 복원 (autocrlf 프로젝트 대응)
|
|
@@ -61,7 +61,7 @@ export function setFallbackLine(line, value) {
|
|
|
61
61
|
return expression.replace(LAST_LITERAL_RE, (_m, head, tail) => `${head}'${escaped}'${tail}`) + cr;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// resolver —
|
|
64
|
+
// resolver — 값 계산은 주입된 resolvers로 위임(순수성 유지).
|
|
65
65
|
// resolvers: { repo, "spring-app-yml-dir"(type), "spring-app-yml-path"(type), "flutter-root",
|
|
66
66
|
// "project-path"(type), "flutter-env-mode", "android-deploy-mode", "ios-deploy-mode" }
|
|
67
67
|
export function resolveToken(name, type, resolvers = {}) {
|
|
@@ -77,17 +77,22 @@ export function replaceProjectTokens(text, repoName) {
|
|
|
77
77
|
return text.replaceAll("__PROJECT_NAME__", repoName).replaceAll("__APP_ARTIFACT_NAME__", repoName);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// 파일 전체
|
|
80
|
+
// 파일 전체 치환.
|
|
81
81
|
// content: 원본 워크플로우 텍스트. 반환: 치환된 텍스트.
|
|
82
82
|
// opts:
|
|
83
83
|
// type - 프로젝트 타입 (resolver·값 조회용)
|
|
84
84
|
// values - Map<key,value>: ask 키의 사용자 선택값 (없으면 기본값=arg 또는 resolver)
|
|
85
|
-
// useDefaults - true면 ask도 기본값 사용 (
|
|
85
|
+
// useDefaults - true면 ask도 기본값 사용 (unchanged 비교의 전제)
|
|
86
86
|
// resolvers - resolveToken용
|
|
87
87
|
// repoName - __PROJECT_NAME__/__APP_ARTIFACT_NAME__ 치환값
|
|
88
88
|
// projectPath - paths-anchor 치환용 ('.'이면 anchor 미변경)
|
|
89
|
+
// savedValues - Map<key,value>: version.yml deploy 블록에 저장된 이 타입의 값. ask 기본값보다 우선한다
|
|
90
|
+
// (재실행·자동 갱신이 설치 때 답한 값을 템플릿 기본값으로 되돌리지 않도록).
|
|
89
91
|
export function substituteEnv(content, opts = {}) {
|
|
90
|
-
const {
|
|
92
|
+
const {
|
|
93
|
+
type = "", values = new Map(), useDefaults = true, resolvers = {}, repoName = "", projectPath = ".",
|
|
94
|
+
collectAsks = null, savedValues = null,
|
|
95
|
+
} = opts;
|
|
91
96
|
if (!content.includes("@wizard")) return content;
|
|
92
97
|
|
|
93
98
|
// CRLF 안전: EOL을 분리해 LF 기준으로 파싱·치환하고, 원래 EOL 스타일을 복원한다.
|
|
@@ -107,20 +112,22 @@ export function substituteEnv(content, opts = {}) {
|
|
|
107
112
|
val = resolveToken(p.arg, type, resolvers);
|
|
108
113
|
} else { // ask
|
|
109
114
|
let def = p.arg.startsWith("@") ? resolveToken(p.arg.slice(1), type, resolvers) : p.arg;
|
|
115
|
+
const saved = savedValues?.get(p.key);
|
|
116
|
+
if (saved != null && saved !== "") def = saved;
|
|
110
117
|
const chosen = values.get(p.key);
|
|
111
118
|
if (chosen != null && chosen !== "" && !useDefaults) val = chosen;
|
|
112
119
|
else val = def;
|
|
113
|
-
// ask 키만 수집 (
|
|
120
|
+
// ask 키만 수집 (auto는 매번 다시 계산하므로 저장 안 함). deploy 블록용.
|
|
114
121
|
if (collectAsks) collectAsks.set(p.key, replaceProjectTokens(val, repoName));
|
|
115
122
|
}
|
|
116
123
|
lines[i] = setEnvLine(lines[i], p.key, val);
|
|
117
124
|
}
|
|
118
125
|
let out = lines.join(usesCRLF ? "\r\n" : "\n");
|
|
119
126
|
|
|
120
|
-
// 잔여 전역 토큰
|
|
127
|
+
// 잔여 전역 토큰
|
|
121
128
|
out = replaceProjectTokens(out, repoName);
|
|
122
129
|
|
|
123
|
-
// paths-anchor
|
|
130
|
+
// paths-anchor: 경로가 '.'이 아니면 주석 라인 전체를 paths 라인으로 교체
|
|
124
131
|
if (PATHS_ANCHOR_RE.test(out) && projectPath && projectPath !== ".") {
|
|
125
132
|
const eol = out.includes("\r\n") ? "\r\n" : "\n";
|
|
126
133
|
out = out.split(/\r?\n/).map((line) => {
|
|
@@ -134,7 +141,7 @@ export function substituteEnv(content, opts = {}) {
|
|
|
134
141
|
return out;
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
//
|
|
144
|
+
// unchanged 판정: 원본을 "기본값으로 가상 치환한 최종형"과 설치본을 바이트 비교.
|
|
138
145
|
export function isUnchanged(templateContent, installedContent, opts = {}) {
|
|
139
146
|
const virtual = substituteEnv(templateContent, { ...opts, useDefaults: true });
|
|
140
147
|
return virtual === installedContent;
|
|
@@ -1,7 +1,5 @@
|
|
|
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 { PAYLOAD } from "./paths.js";
|
|
@@ -9,7 +7,7 @@ import { PAYLOAD } from "./paths.js";
|
|
|
9
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,35 +51,46 @@ 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을 찾아 읽고
|
|
70
|
-
//
|
|
71
|
-
// WHY
|
|
72
|
-
//
|
|
67
|
+
// wizard-prompts.yml을 찾아 읽고 파싱.
|
|
68
|
+
// 번들본(payload/config)을 바탕으로 대상 프로젝트의 사용자 파일(커스텀 오버라이드)을 키 단위로 덮어쓴다.
|
|
69
|
+
// WHY 병합: 사용자 파일에는 바꾸고 싶은 키만 적는 것이 자연스럽다 — 파일째 대체하면 적지 않은 나머지
|
|
70
|
+
// 질문의 label/help/example과 워크플로우 표시명이 전부 사라져 KEY명만 보인다.
|
|
73
71
|
// fs 주입 가능(테스트용) — 기본 node:fs.
|
|
74
72
|
export function loadWizardPrompts(targetRoot = ".", payloadRoot = "", fs = nodeFs) {
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
const read = (p) => (fs.existsSync(p) ? parseWizardPrompts(fs.readFileSync(p, "utf8")) : null);
|
|
74
|
+
const bundled = payloadRoot ? read(join(payloadRoot, PAYLOAD.configDir, "wizard-prompts.yml")) : null;
|
|
75
|
+
const user = read(join(targetRoot, LABELS_FILE));
|
|
76
|
+
if (!bundled || !user) return user || bundled;
|
|
77
|
+
return mergeWizardPrompts(bundled, user);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 번들 + 사용자 파싱 결과 병합. 같은 키는 사용자가 적은 필드만 덮어쓴다(label만 바꾸면 help/example은 번들 유지).
|
|
81
|
+
export function mergeWizardPrompts(base, override) {
|
|
82
|
+
const fields = new Map([...base.fields].map(([k, v]) => [k, { ...v }]));
|
|
83
|
+
for (const [key, entry] of override.fields) {
|
|
84
|
+
const merged = { ...(fields.get(key) || {}) };
|
|
85
|
+
for (const [f, v] of Object.entries(entry)) if (v != null && v !== "") merged[f] = v;
|
|
86
|
+
fields.set(key, merged);
|
|
80
87
|
}
|
|
81
|
-
|
|
88
|
+
const overridden = new Set(override.workflowNames.map((w) => w.key));
|
|
89
|
+
const workflowNames = [...base.workflowNames.filter((w) => !overridden.has(w.key)), ...override.workflowNames];
|
|
90
|
+
return { fields, workflowNames };
|
|
82
91
|
}
|
|
83
92
|
|
|
84
|
-
// 필드
|
|
93
|
+
// 필드 조회. field: "label" | "help" | "example".
|
|
85
94
|
// 우선순위: "{type}.KEY" 블록 → "KEY" 블록(구형 1줄 포함) → 폴백(label이면 KEY명, 아니면 "").
|
|
86
95
|
export function wfField(prompts, type, key, field) {
|
|
87
96
|
if (prompts && prompts.fields) {
|
|
@@ -93,11 +102,11 @@ export function wfField(prompts, type, key, field) {
|
|
|
93
102
|
return field === "label" ? key : "";
|
|
94
103
|
}
|
|
95
104
|
|
|
96
|
-
// 워크플로우 파일명 → 사람이 읽는 짧은
|
|
105
|
+
// 워크플로우 파일명 → 사람이 읽는 짧은 이름.
|
|
97
106
|
// _workflow_names에서 "키가 파일명에 포함되면" 그 값 사용 — 최장 키 우선(REACT-CI vs REACT-CICD 구분).
|
|
98
|
-
// 미매칭이면 .yaml/.yml 확장자만 제거해
|
|
107
|
+
// 미매칭이면 .yaml/.yml 확장자만 제거해 반환.
|
|
99
108
|
export function workflowDisplayName(prompts, filename) {
|
|
100
|
-
const base = String(filename).split("/").pop().split("\\").pop(); // 경로 제거
|
|
109
|
+
const base = String(filename).split("/").pop().split("\\").pop(); // 경로 제거
|
|
101
110
|
let best = null; let bestLen = 0;
|
|
102
111
|
for (const { key, value } of prompts?.workflowNames ?? []) {
|
|
103
112
|
if (base.includes(key) && key.length > bestLen) { best = value; bestLen = key.length; }
|