project-auto-wizard 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +165 -0
- package/README.ko.md +165 -0
- package/README.md +107 -286
- package/README.zh-CN.md +165 -0
- package/bin/project-auto-wizard.js +1 -2
- package/package.json +7 -3
- package/payload/config/breaking-changes.json +24 -0
- package/payload/config/wizard-prompts.yml +45 -0
- package/payload/scripts/changelog_manager.py +141 -46
- package/payload/scripts/issue_helper.py +34 -5
- package/payload/scripts/version_manager.py +397 -97
- package/payload/version.yml.template +4 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +39 -9
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +165 -38
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +14 -5
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +220 -6
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +89 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +78 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +72 -16
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +62 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +51 -13
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +75 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +52 -3
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +60 -11
- package/payload/workflows/go/PROJECT-GO-CI.yaml +10 -1
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +25 -3
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +83 -9
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +70 -6
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +148 -28
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +45 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +25 -3
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +84 -10
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +13 -3
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +47 -5
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +153 -30
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +47 -5
- package/src/cli/args.js +33 -24
- package/src/cli/help.js +16 -4
- package/src/commands/doctor.js +17 -10
- package/src/commands/dry-run.js +54 -1
- package/src/commands/full.js +127 -50
- package/src/commands/interactive-flutter.js +1 -1
- package/src/commands/interactive.js +124 -56
- package/src/commands/purge.js +59 -17
- package/src/commands/status.js +16 -1
- package/src/commands/uninstall.js +58 -13
- package/src/context.js +5 -5
- package/src/core/baseline.js +20 -5
- package/src/core/branches.js +19 -1
- package/src/core/branding.js +1 -1
- package/src/core/breaking-check.js +14 -19
- package/src/core/breaking.js +7 -3
- package/src/core/copy/gitignore.js +20 -12
- package/src/core/copy/readme.js +24 -5
- package/src/core/copy/simple.js +52 -13
- package/src/core/copy/workflows.js +90 -47
- package/src/core/deploy-style.js +83 -11
- package/src/core/detect-fs.js +127 -24
- package/src/core/detect.js +131 -47
- package/src/core/errors.js +3 -0
- package/src/core/flutter-options.js +4 -3
- package/src/core/fsutil.js +23 -6
- package/src/core/logger.js +55 -19
- package/src/core/paths-resolve.js +50 -47
- package/src/core/paths.js +26 -1
- package/src/core/release-options.js +23 -0
- package/src/core/removal-plan.js +75 -19
- package/src/core/types.js +113 -0
- package/src/core/verify.js +85 -19
- package/src/core/version-yml.js +42 -8
- package/src/core/wizard-env.js +21 -14
- package/src/core/wizard-labels.js +34 -25
- package/src/index.js +122 -49
- package/src/ui/ansi.js +3 -1
- package/src/ui/banner.js +12 -8
- package/src/ui/env-plan.js +68 -44
- package/src/ui/prompts.js +22 -12
- package/src/ui/readline-engine.js +109 -22
- package/src/ui/status-cards.js +10 -5
- package/src/ui/summary.js +71 -33
- package/payload/scripts/__pycache__/changelog_manager.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/issue_helper.cpython-314.pyc +0 -0
- package/payload/scripts/__pycache__/version_manager.cpython-314.pyc +0 -0
package/src/core/detect-fs.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// 실 파일시스템 프로젝트
|
|
1
|
+
// 실 파일시스템 프로젝트 감지.
|
|
2
2
|
// detect.js 순수 함수를 fs/git으로 구동한다.
|
|
3
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join, basename } from "node:path";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
import { detectTypesFromMarkers, detectVersionFromFiles, detectBuildNumberFromFiles, detectJdkFromFiles, resolveMarkers } from "./detect.js";
|
|
7
7
|
import { parseExisting } from "./version-yml.js";
|
|
8
|
+
import { isValidBranchName } from "./branches.js";
|
|
8
9
|
|
|
9
10
|
const hasFile = (root) => (rel) => existsSync(join(root, rel));
|
|
10
11
|
const readFile = (root) => (rel) => {
|
|
@@ -18,22 +19,101 @@ function gitOut(root, args) {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// 타입 감지 — version.yml의 project_types 최우선(source of truth), 없으면 마커 스캔.
|
|
21
|
-
|
|
22
|
+
// paths: --paths로 받은 Map<type,path>. 모노레포는 루트에 마커가 없으므로 사용자가 적은 타입을 쓴다.
|
|
23
|
+
// warn: 루트에 마커가 없어 basic으로 떨어질 때 하위 폴더에서 찾은 프로젝트를 알린다.
|
|
24
|
+
export function detectTypes(root, { paths = new Map(), warn } = {}) {
|
|
22
25
|
const vy = join(root, "version.yml");
|
|
23
26
|
if (existsSync(vy)) {
|
|
24
27
|
const { types } = parseExisting(readFileSync(vy, "utf8"));
|
|
25
28
|
if (types.length) return types; // basic 포함, 명시돼 있으면 그대로
|
|
26
29
|
}
|
|
27
|
-
|
|
30
|
+
const fromRoot = detectTypesFromMarkers({ has: hasFile(root), read: readFile(root) });
|
|
31
|
+
if (paths.size) {
|
|
32
|
+
// --paths 순서가 주 타입을 정한다. 루트 package.json의 node는 다른 타입이 있으면 빼는
|
|
33
|
+
// 마커 스캔 규칙과 맞춘다.
|
|
34
|
+
const merged = [...new Set([...paths.keys(), ...fromRoot])].filter((t) => t !== "basic");
|
|
35
|
+
const types = merged.length > 1 ? merged.filter((t) => t !== "node" || paths.has("node")) : merged;
|
|
36
|
+
return types.length ? types : ["basic"];
|
|
37
|
+
}
|
|
38
|
+
if (fromRoot.length === 1 && fromRoot[0] === "basic" && warn) {
|
|
39
|
+
const found = findSubdirProjects(root);
|
|
40
|
+
if (found.length) {
|
|
41
|
+
const firstDir = new Map();
|
|
42
|
+
for (const { dir, types } of found) for (const t of types) if (!firstDir.has(t)) firstDir.set(t, dir);
|
|
43
|
+
const list = found.map(({ dir, types }) => `${dir}(${types.join(", ")})`).join(", ");
|
|
44
|
+
const hint = [...firstDir].map(([t, d]) => `${t}=${d}`).join(",");
|
|
45
|
+
warn(`⚠️ 루트에서 프로젝트 파일을 찾지 못해 basic으로 설치합니다. 하위 폴더에서 발견: ${list}\n` +
|
|
46
|
+
` 모노레포라면 --paths "${hint}"로 다시 실행하세요.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return fromRoot;
|
|
28
50
|
}
|
|
29
51
|
|
|
30
|
-
//
|
|
52
|
+
// 루트 아래 2단계까지 프로젝트 마커가 있는 폴더를 찾는다 (모노레포 안내용).
|
|
53
|
+
// 빌드 산출물·의존성·네이티브 폴더는 오탐만 늘리므로 들어가지 않고, 찾은 폴더의 하위도 보지 않는다.
|
|
54
|
+
const SUBDIR_PRUNE = new Set(["node_modules", "build", "dist", "android", "ios", "venv", "__pycache__"]);
|
|
55
|
+
function findSubdirProjects(root, maxDepth = 2) {
|
|
56
|
+
const found = [];
|
|
57
|
+
const walk = (rel, depth) => {
|
|
58
|
+
let entries;
|
|
59
|
+
try { entries = readdirSync(join(root, rel), { withFileTypes: true }); } catch { return; }
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
if (!e.isDirectory() || e.name.startsWith(".") || SUBDIR_PRUNE.has(e.name)) continue;
|
|
62
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
63
|
+
const dir = join(root, childRel);
|
|
64
|
+
const types = detectTypesFromMarkers({ has: hasFile(dir), read: readFile(dir) });
|
|
65
|
+
if (types[0] !== "basic") found.push({ dir: childRel, types });
|
|
66
|
+
else if (depth + 1 < maxDepth) walk(childRel, depth + 1);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
walk("", 0);
|
|
70
|
+
return found.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 버전 감지 — 타입별 버전 파일을 순서대로 읽는다.
|
|
31
74
|
// hint: 폴백 경고에 붙일 해결 방법 안내 (대화형/CLI가 다르다).
|
|
32
|
-
|
|
33
|
-
|
|
75
|
+
// 모노레포(--paths)는 버전 파일이 타입 폴더 안에 있다 — 루트만 보면 0.0.1/1로 초기화된다.
|
|
76
|
+
// 주 타입 폴더 → 나머지 타입 폴더 → 루트 순으로 찾는다.
|
|
77
|
+
function projectBases(types = [], paths = null) {
|
|
78
|
+
const bases = [];
|
|
79
|
+
for (const t of types) {
|
|
80
|
+
const p = paths?.get?.(t);
|
|
81
|
+
if (p && p !== "." && !bases.includes(p)) bases.push(p);
|
|
82
|
+
}
|
|
83
|
+
return bases.length ? [...bases, "."] : ["."];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 첫 번째로 결과가 있는 기준 폴더의 값을 쓴다 — read와 list가 같은 폴더 우선순위를 따른다.
|
|
87
|
+
function firstFromBases(bases, fn) {
|
|
88
|
+
return (rel) => {
|
|
89
|
+
for (const b of bases) {
|
|
90
|
+
const c = fn(b === "." ? rel : `${b}/${rel}`);
|
|
91
|
+
if (c != null) return c;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function readFromProject(root, types = [], paths = null) {
|
|
98
|
+
return firstFromBases(projectBases(types, paths), readFile(root));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 하위 폴더 이름 목록 (React Native의 ios/<앱>/Info.plist 탐색용). 폴더가 없으면 null.
|
|
102
|
+
function listFromProject(root, types = [], paths = null) {
|
|
103
|
+
const listDirs = (rel) => {
|
|
104
|
+
try {
|
|
105
|
+
return readdirSync(join(root, rel), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
106
|
+
} catch { return null; }
|
|
107
|
+
};
|
|
108
|
+
return firstFromBases(projectBases(types, paths), listDirs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function detectVersion(root, { warn = (m) => console.error(m), hint, types = [], paths = null } = {}) {
|
|
112
|
+
const read = readFromProject(root, types, paths);
|
|
34
113
|
const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
|
|
114
|
+
const list = listFromProject(root, types, paths);
|
|
35
115
|
const gitTag = gitOut(root, ["describe", "--tags", "--abbrev=0"]);
|
|
36
|
-
return detectVersionFromFiles({ read, readJson, gitTag, warn, hint });
|
|
116
|
+
return detectVersionFromFiles({ read, readJson, list, gitTag, warn, hint, types });
|
|
37
117
|
}
|
|
38
118
|
|
|
39
119
|
// 타입별 실제 마커 파일 — 감지 로그·설치 로그가 같은 근거 파일을 인용하도록.
|
|
@@ -50,20 +130,28 @@ export function detectJdk(root, base = ".") {
|
|
|
50
130
|
}
|
|
51
131
|
|
|
52
132
|
// 빌드 번호 감지 — 신규 통합 시 pubspec.yaml/build.gradle/app.json에서 실제 빌드 번호를 읽는다.
|
|
53
|
-
export function detectBuildNumber(root, { types = [], warn = (m) => console.error(m) } = {}) {
|
|
54
|
-
const read =
|
|
133
|
+
export function detectBuildNumber(root, { types = [], paths = null, warn = (m) => console.error(m) } = {}) {
|
|
134
|
+
const read = readFromProject(root, types, paths);
|
|
55
135
|
const readJson = (rel) => { const c = read(rel); try { return c ? JSON.parse(c) : null; } catch { return null; } };
|
|
56
136
|
return detectBuildNumberFromFiles({ types, read, readJson, warn });
|
|
57
137
|
}
|
|
58
138
|
|
|
59
139
|
// 기본 브랜치 감지 — symbolic-ref → remote show → main.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
140
|
+
// 빈 원격(remote add만 하고 push 전)은 remote show가 "HEAD branch: (unknown)"을 돌려준다. 이 값이
|
|
141
|
+
// 워크플로우 트리거에 기록되면 릴리스 자동화가 조용히 멈추므로, 유효한 브랜치 이름만 인정하고
|
|
142
|
+
// 아니면 로컬 현재 브랜치(첫 push 대상) → main 순으로 폴백하며 경고한다.
|
|
143
|
+
// hint: 경고에 붙일 해결 방법 안내 (대화형/CLI가 다르다).
|
|
144
|
+
export function detectDefaultBranch(root, { warn = null, hint = "" } = {}) {
|
|
145
|
+
const b = gitOut(root, ["symbolic-ref", "refs/remotes/origin/HEAD"]).replace(/^refs\/remotes\/origin\//, "");
|
|
146
|
+
if (isValidBranchName(b)) return b;
|
|
63
147
|
const show = gitOut(root, ["remote", "show", "origin"]);
|
|
64
148
|
const m = show.match(/HEAD branch:\s*(\S+)/);
|
|
65
|
-
if (m) return m[1];
|
|
66
|
-
return "main";
|
|
149
|
+
if (m && isValidBranchName(m[1])) return m[1];
|
|
150
|
+
if (!m) return "main"; // origin 없음 — 기존 규칙 그대로
|
|
151
|
+
const local = gitOut(root, ["symbolic-ref", "--short", "HEAD"]);
|
|
152
|
+
const fallback = isValidBranchName(local) ? local : "main";
|
|
153
|
+
warn?.(`⚠️ 원격 기본 브랜치를 확인할 수 없어(빈 원격 레포 등) 릴리스 브랜치를 '${fallback}'(으)로 가정합니다.${hint ? ` ${hint}` : ""}`);
|
|
154
|
+
return fallback;
|
|
67
155
|
}
|
|
68
156
|
|
|
69
157
|
// 레포명 — git remote get-url origin 마지막 세그먼트, 실패 시 폴더명.
|
|
@@ -76,7 +164,7 @@ export function detectRepoName(root) {
|
|
|
76
164
|
return basename(root);
|
|
77
165
|
}
|
|
78
166
|
|
|
79
|
-
// Spring application*.yml 탐색
|
|
167
|
+
// Spring application*.yml 탐색
|
|
80
168
|
// find {base} -path "*/src/main/resources/application*.yml" | head -1 의 fs 재귀 구현.
|
|
81
169
|
// 반환: root 기준 상대경로 (예: "server/src/main/resources/application.yml") 또는 "".
|
|
82
170
|
//
|
|
@@ -87,9 +175,14 @@ export function detectRepoName(root) {
|
|
|
87
175
|
// 같은 디렉토리에서는 프로파일 없는 기본 파일(application.yml/.yaml)을 우선한다. 파일명 정렬만
|
|
88
176
|
// 쓰면 'application-dev.yml'이 'application.yml'보다 앞서(`-` < `.`) 프로파일 파일이 잡힌다.
|
|
89
177
|
export function findSpringAppYml(root, base = ".") {
|
|
178
|
+
return findSpringConfig(root, base, /^application(-[^/]*)?\.ya?ml$/, /^application\.ya?ml$/);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/main/resources 아래에서 pattern에 맞는 설정 파일을 찾는다. basePattern(프로파일 없는 기본 파일)이
|
|
182
|
+
// 나오면 그걸로 확정한다.
|
|
183
|
+
function findSpringConfig(root, base, pattern, basePattern) {
|
|
90
184
|
const startRel = base === "." ? "" : base;
|
|
91
185
|
const PRUNE = new Set(["node_modules", ".git", "build", ".gradle", "target", ".idea"]);
|
|
92
|
-
const APP_YML = /^application(-[^/]*)?\.ya?ml$/;
|
|
93
186
|
let hit = "";
|
|
94
187
|
let hitIsBase = false;
|
|
95
188
|
const walk = (rel, depth) => {
|
|
@@ -103,8 +196,8 @@ export function findSpringAppYml(root, base = ".") {
|
|
|
103
196
|
if (e.isDirectory()) {
|
|
104
197
|
if (PRUNE.has(e.name)) continue;
|
|
105
198
|
walk(childRel, depth + 1);
|
|
106
|
-
} else if (
|
|
107
|
-
const isBase =
|
|
199
|
+
} else if (pattern.test(e.name) && childRel.includes("src/main/resources/")) {
|
|
200
|
+
const isBase = basePattern.test(e.name);
|
|
108
201
|
// 첫 매치는 일단 채택하고, 이후 기본 파일이 나오면 그걸로 승격한다.
|
|
109
202
|
if (!hit || isBase) { hit = childRel; hitIsBase = isBase; }
|
|
110
203
|
}
|
|
@@ -114,7 +207,17 @@ export function findSpringAppYml(root, base = ".") {
|
|
|
114
207
|
return hit;
|
|
115
208
|
}
|
|
116
209
|
|
|
117
|
-
//
|
|
210
|
+
// 배포 워크플로우가 application-prod.yml을 만들 리소스 폴더.
|
|
211
|
+
// Spring Initializr 기본 산출물은 application.properties라 yml만 찾으면 빈 값이 되어
|
|
212
|
+
// __APPLICATION_YML_DIR__가 치환되지 않은 채 설치된다. yml → properties → 표준 경로 순으로 정한다.
|
|
213
|
+
export function findSpringResourcesDir(root, base = ".") {
|
|
214
|
+
const f = findSpringAppYml(root, base)
|
|
215
|
+
|| findSpringConfig(root, base, /^application(-[^/]*)?\.properties$/, /^application\.properties$/);
|
|
216
|
+
if (f) return f.split("/").slice(0, -1).join("/");
|
|
217
|
+
return base === "." ? "src/main/resources" : `${base}/src/main/resources`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// @wizard 토큰 resolver 세트 생성 — index/interactive 공용.
|
|
118
221
|
// paths: Map<type, path> (모노레포 경로).
|
|
119
222
|
// flutterOptions: resolveFlutterOptions 결과 또는 같은 필드를 가진 context. null이면 Flutter 토큰이
|
|
120
223
|
// 빈 값이라 템플릿 기본값(dart-define, store_only)이 그대로 남는다.
|
|
@@ -126,11 +229,11 @@ export function makeResolvers(root, repoName, paths, flutterOptions = null) {
|
|
|
126
229
|
// ⚠️ 빈 문자열을 돌려주면 setEnvLine이 그 줄을 건너뛰어 __JAVA_VERSION__이 그대로 남는다
|
|
127
230
|
// (같은 실패 형태). 감지 실패 시 반드시 종전 기본값 21로 폴백한다.
|
|
128
231
|
jdk: (t) => detectJdk(root, springBase(t)) || "21",
|
|
129
|
-
"spring-app-yml-dir": (t) =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
232
|
+
"spring-app-yml-dir": (t) => findSpringResourcesDir(root, springBase(t)),
|
|
233
|
+
// yml이 없는 프로젝트(properties 전용)는 리소스 폴더의 application.yml로 만든다 —
|
|
234
|
+
// properties 파일 자리에 YAML 내용을 쓰면 설정이 깨진다.
|
|
235
|
+
"spring-app-yml-path": (t) => findSpringAppYml(root, springBase(t))
|
|
236
|
+
|| `${findSpringResourcesDir(root, springBase(t))}/application.yml`,
|
|
134
237
|
"flutter-root": () => paths.get("flutter") || ".",
|
|
135
238
|
// CI changes job의 경로 필터 — 타입별 프로젝트 루트. 단일 레포·common은 "."(항상 변경됨으로 판정).
|
|
136
239
|
"project-path": (t) => paths.get(t) || ".",
|
package/src/core/detect.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
typeInfo, FALLBACK_TYPE, MARKER_DETECTED_TYPES, PACKAGE_DETECTED_TYPES, PACKAGE_FALLBACK_TYPE,
|
|
3
|
+
} from "./types.js";
|
|
4
|
+
|
|
5
|
+
// package.json 분류 — 의존성 "키"를 정확히 비교한다. 원문 부분문자열로 보면 export 스크립트나
|
|
6
|
+
// exponential-backoff가 expo로, react-native-web을 쓰는 웹앱이 react-native로, keywords의 "next"가
|
|
7
|
+
// next로 오감지된다. 입력은 package.json 원문 문자열(raw). 판정 순서는 레지스트리의 detectOrder.
|
|
4
8
|
export function classifyPackageText(raw) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
let pkg;
|
|
10
|
+
try { pkg = JSON.parse(String(raw || "")); } catch { return PACKAGE_FALLBACK_TYPE; }
|
|
11
|
+
if (!pkg || typeof pkg !== "object") return PACKAGE_FALLBACK_TYPE;
|
|
12
|
+
const deps = new Set();
|
|
13
|
+
for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
|
|
14
|
+
const d = pkg[field];
|
|
15
|
+
if (d && typeof d === "object") for (const k of Object.keys(d)) deps.add(k);
|
|
8
16
|
}
|
|
9
|
-
|
|
10
|
-
if (s.includes('"react"')) return "react";
|
|
11
|
-
return "node";
|
|
17
|
+
return PACKAGE_DETECTED_TYPES.find((t) => deps.has(t.packageDep))?.id ?? PACKAGE_FALLBACK_TYPE;
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
// 편의: 파싱된 객체를 받는 경우 원문으로 재직렬화해 위 규칙 적용
|
|
@@ -17,77 +23,152 @@ export function classifyPackageJson(pkgOrRaw) {
|
|
|
17
23
|
return classifyPackageText(raw);
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
// 마커
|
|
26
|
+
// 마커 스캔. has(relpath)=>bool 주입. node는 다른 타입 있으면 미추가.
|
|
21
27
|
// read(relpath)=>string|null 로 package.json 원문을 받아 classifyPackageText에 넘긴다.
|
|
22
28
|
export function detectTypesFromMarkers({ has, read }) {
|
|
23
29
|
const types = [];
|
|
24
|
-
if (has
|
|
25
|
-
if (has("build.gradle") || has("build.gradle.kts") || has("pom.xml")) types.push("spring");
|
|
26
|
-
if (has("pyproject.toml") || has("setup.py") || has("requirements.txt")) types.push("python");
|
|
27
|
-
if (has("go.mod")) types.push("go");
|
|
30
|
+
for (const t of MARKER_DETECTED_TYPES) if (t.markers.some(has)) types.push(t.id);
|
|
28
31
|
if (has("package.json")) {
|
|
29
32
|
const cls = classifyPackageText(read ? read("package.json") : "");
|
|
30
|
-
if (cls ===
|
|
33
|
+
if (cls === PACKAGE_FALLBACK_TYPE) { if (types.length === 0) types.push(cls); }
|
|
31
34
|
else types.push(cls);
|
|
32
35
|
}
|
|
33
|
-
return types.length ? [...new Set(types)] : [
|
|
36
|
+
return types.length ? [...new Set(types)] : [FALLBACK_TYPE];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 1.2.3-rc.1·1.2.3+7·1.2.0-SNAPSHOT 같은 prerelease/빌드 메타데이터는 x.y.z 코어만 쓴다.
|
|
40
|
+
// version.yml은 x.y.z만 받으므로 감지 실패(0.0.1)로 떨어지는 것보다 코어가 정확하다.
|
|
41
|
+
// 릴리스 시 읽기(payload/scripts/version_manager.py core_version)와 같은 규칙이어야 설치 직후 값이 유지된다.
|
|
42
|
+
function coreVersion(v) {
|
|
43
|
+
const m = String(v ?? "").trim().match(/^v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.+-]*)?$/);
|
|
44
|
+
return m ? m[1] : null;
|
|
34
45
|
}
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
// setup.py의 setup(version="x.y.z"). python_version 같은 다른 키는 단어 경계로 거른다.
|
|
48
|
+
export function versionFromSetupPy(content) {
|
|
49
|
+
if (!content) return null;
|
|
50
|
+
const m = String(content).match(/(?<![\w.])version\s*=\s*["']([^"']+)["']/);
|
|
51
|
+
return m ? coreVersion(m[1]) : null;
|
|
52
|
+
}
|
|
37
53
|
|
|
38
|
-
//
|
|
54
|
+
// React Native 앱 버전 — 릴리스 때 version_manager가 쓰는 파일(ios/<앱>/Info.plist,
|
|
55
|
+
// android/app/build.gradle)에서 읽는다. package.json version은 동기화 대상이 아니라 기준이 되면 어긋난다.
|
|
56
|
+
// $(MARKETING_VERSION) 참조나 템플릿 기본값 "1.0"처럼 x.y.z가 아니면 건너뛴다.
|
|
57
|
+
// list(relDir)=>string[]|null 로 ios 아래 앱 폴더 이름을 받는다(Pods 등 깊은 plist는 보지 않는다).
|
|
58
|
+
export function versionFromReactNative({ read, list }) {
|
|
59
|
+
for (const dir of [...(list?.("ios") || [])].sort()) {
|
|
60
|
+
const m = String(read(`ios/${dir}/Info.plist`) || "").match(/<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/);
|
|
61
|
+
const v = m && coreVersion(m[1]);
|
|
62
|
+
if (v) return v;
|
|
63
|
+
}
|
|
64
|
+
const m = String(read("android/app/build.gradle") || "").match(/versionName\s+"([^"]+)"/);
|
|
65
|
+
return m ? coreVersion(m[1]) : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 버전 감지 — 순서대로 첫 성공. read(relpath)=>string|null 주입.
|
|
39
69
|
// package.json은 이미 Node JSON.parse로 파싱을 마친 값이므로 jq 설치 여부와 무관하게 항상 사용한다.
|
|
40
70
|
// hint: 폴백 경고 뒤에 붙일 "그럼 어떻게 고치나" 한 줄. 대화형과 CLI가 서로 다른 방법을
|
|
41
71
|
// 안내해야 하므로 호출부가 정한다. 미지정 시 CLI 문구를 쓴다.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
// types: 주 타입(첫 항목)의 버전 파일을 먼저 읽는다. 릴리스 때 version_manager가 주 타입
|
|
73
|
+
// 파일과 version.yml을 비교하므로, 다른 타입 버전을 잡으면 첫 릴리스에서 버전이 뛴다.
|
|
74
|
+
export function detectVersionFromFiles({ read, readJson, list, gitTag, warn, hint, types = [] }) {
|
|
45
75
|
const grab = (content, re) => {
|
|
46
76
|
for (const line of (content || "").split("\n")) {
|
|
47
77
|
const m = line.match(re);
|
|
48
|
-
if (m
|
|
78
|
+
if (m) { const v = coreVersion(m[m.length - 1]); if (v) return v; }
|
|
49
79
|
}
|
|
50
80
|
return null;
|
|
51
81
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
// 줄 시작 앵커가 없으면 ext.kotlin_version 같은 의존성 버전 변수가 먼저 걸린다.
|
|
83
|
+
// 따옴표로 감싼 값만 본다 — 릴리스 때 동기화가 고칠 수 있는 형태가 이것뿐이다.
|
|
84
|
+
const gradleRe = /^\s*version\s*=\s*(["'])([^"'\n]*)\1/;
|
|
85
|
+
const sources = {
|
|
86
|
+
packageJson: () => coreVersion(readJson?.("package.json")?.version),
|
|
87
|
+
appJson: () => coreVersion(readJson?.("app.json")?.expo?.version),
|
|
88
|
+
// Groovy DSL과 Kotlin DSL은 같은 문법(`version = "x.y.z"`)이라 정규식을 공유한다.
|
|
89
|
+
// .kts를 빼먹으면 Kotlin DSL Spring 프로젝트가 전부 0.0.1로 초기화된다.
|
|
90
|
+
gradle: () => grab(read("build.gradle"), gradleRe),
|
|
91
|
+
gradleKts: () => grab(read("build.gradle.kts"), gradleRe),
|
|
92
|
+
pom: () => versionFromPom(read("pom.xml")),
|
|
93
|
+
pubspec: () => grab(read("pubspec.yaml"), /^version:\s*([^\s#]+)/),
|
|
94
|
+
pyproject: () => versionFromPyproject(read("pyproject.toml")),
|
|
95
|
+
setupPy: () => versionFromSetupPy(read("setup.py")),
|
|
96
|
+
reactNative: () => versionFromReactNative({ read, list }),
|
|
97
|
+
};
|
|
98
|
+
const order = [
|
|
99
|
+
...(typeInfo(types[0])?.versionSources || []),
|
|
100
|
+
"packageJson", "gradle", "gradleKts", "pom", "pubspec", "pyproject", "setupPy",
|
|
101
|
+
];
|
|
102
|
+
for (const key of new Set(order)) {
|
|
103
|
+
const v = sources[key]();
|
|
104
|
+
if (v) return v;
|
|
105
|
+
}
|
|
106
|
+
if (gitTag) { const t = coreVersion(gitTag); if (t) return t; }
|
|
62
107
|
const tail = hint ?? "--project-version으로 직접 지정하거나 version.yml을 확인하세요.";
|
|
63
108
|
warn?.(`⚠️ 버전을 자동 감지하지 못해 기본값 0.0.1을 사용합니다 — ${tail}`);
|
|
64
109
|
return "0.0.1";
|
|
65
110
|
}
|
|
66
111
|
|
|
67
|
-
//
|
|
68
|
-
//
|
|
112
|
+
// pyproject.toml의 패키지 버전. [tool.*] 등 다른 섹션의 `version =`은 도구 설정이라
|
|
113
|
+
// [project]·[tool.poetry] 섹션 안에서만 읽는다.
|
|
114
|
+
export function versionFromPyproject(content) {
|
|
115
|
+
if (!content) return null;
|
|
116
|
+
let section = "";
|
|
117
|
+
for (const line of String(content).split(/\r?\n/)) {
|
|
118
|
+
const h = line.match(/^\s*\[+\s*([^\]]+?)\s*\]+\s*(?:#.*)?$/);
|
|
119
|
+
if (h) { section = h[1]; continue; }
|
|
120
|
+
if (section !== "project" && section !== "tool.poetry") continue;
|
|
121
|
+
const m = line.match(/^\s*version\s*=\s*["']([^"']+)["']/);
|
|
122
|
+
if (m) return coreVersion(m[1]);
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Maven pom.xml의 프로젝트 버전 — <project> 바로 아래의 <version>만 본다.
|
|
128
|
+
// <parent>(스프링 부트 BOM)나 <dependencies> 안의 버전은 프로젝트 버전이 아니므로 깊이로 구분한다.
|
|
129
|
+
// 프로젝트 버전이 없으면(부모에서 상속) null — 의존성 버전을 대신 고르지 않는다.
|
|
69
130
|
export function versionFromPom(content) {
|
|
70
131
|
if (!content) return null;
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
132
|
+
const text = String(content);
|
|
133
|
+
const tokenRe = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<\?[\s\S]*?\?>|<![^>]*>|<(\/?)([A-Za-z_][\w.:-]*)[^>]*?(\/?)>/g;
|
|
134
|
+
const stack = [];
|
|
135
|
+
let start = -1;
|
|
136
|
+
let m;
|
|
137
|
+
while ((m = tokenRe.exec(text))) {
|
|
138
|
+
const name = m[2];
|
|
139
|
+
if (!name) continue;
|
|
140
|
+
if (m[1]) {
|
|
141
|
+
if (start >= 0 && stack.length === 2 && stack[1] === "version") {
|
|
142
|
+
return coreVersion(text.slice(start, m.index));
|
|
143
|
+
}
|
|
144
|
+
stack.pop();
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (m[3]) continue;
|
|
148
|
+
stack.push(name);
|
|
149
|
+
if (stack.length === 2 && name === "version") start = tokenRe.lastIndex;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
74
152
|
}
|
|
75
153
|
|
|
154
|
+
// 타입의 대표 마커 파일. 마커가 없는 타입(basic)·미지 타입은 package.json을 돌려준다.
|
|
76
155
|
export function markerForType(type) {
|
|
77
|
-
return
|
|
156
|
+
return typeInfo(type)?.markers[0] || "package.json";
|
|
78
157
|
}
|
|
79
158
|
|
|
159
|
+
// 대표 파일 외의 보조 마커 (예: Spring의 build.gradle.kts·pom.xml).
|
|
80
160
|
export function extraMarkers(type) {
|
|
81
|
-
return
|
|
161
|
+
return typeInfo(type)?.markers.slice(1) || [];
|
|
82
162
|
}
|
|
83
163
|
|
|
84
164
|
// 그 타입을 감지하는 데 실제로 쓰인 파일. markerForType은 타입당 대표 파일 하나를
|
|
85
165
|
// 고정 반환하므로, build.gradle.kts만 있는 레포에서도 "build.gradle 발견"이라고 출력돼
|
|
86
166
|
// 같은 설치 로그 안에서 경로 확정 화면과 파일명이 어긋났다. has()로 실재하는 것을 고른다.
|
|
87
|
-
// 실재하는 후보가 없으면(감지 전 화면 등) 대표 파일을 쓴다.
|
|
88
|
-
|
|
167
|
+
// 실재하는 후보가 없으면(감지 전 화면 등) 대표 파일을 쓴다. 단 "근거"로 보여줄 때는(fallback:false)
|
|
168
|
+
// 빈 문자열을 돌려준다 — 직접 고른 타입에 없는 파일을 근거로 붙이면 감지된 것처럼 보인다.
|
|
169
|
+
export function resolveMarker(type, has, { fallback = true } = {}) {
|
|
89
170
|
const candidates = [markerForType(type), ...extraMarkers(type)];
|
|
90
|
-
return candidates.find(has) ?? candidates[0];
|
|
171
|
+
return candidates.find(has) ?? (fallback ? candidates[0] : "");
|
|
91
172
|
}
|
|
92
173
|
|
|
93
174
|
// 빌드 JDK 감지 — 배포 워크플로우의 JAVA_VERSION 기본값이 21로 고정돼 있어
|
|
@@ -124,7 +205,9 @@ export function resolveMarkers(types = [], has) {
|
|
|
124
205
|
const out = new Map();
|
|
125
206
|
for (const t of types) {
|
|
126
207
|
if (t === "basic") continue;
|
|
127
|
-
|
|
208
|
+
// 실제로 있는 파일만 근거로 삼는다 — 없으면 맵에서 빠져 화면·로그가 "직접 선택"으로 다룬다.
|
|
209
|
+
const found = resolveMarker(t, has, { fallback: false });
|
|
210
|
+
if (found) out.set(t, found);
|
|
128
211
|
}
|
|
129
212
|
return out;
|
|
130
213
|
}
|
|
@@ -137,7 +220,8 @@ export function detectBuildNumberFromFiles({ types = [], read, readJson, warn })
|
|
|
137
220
|
const tryFlutter = () => {
|
|
138
221
|
const content = read("pubspec.yaml");
|
|
139
222
|
if (content == null) return null;
|
|
140
|
-
|
|
223
|
+
// 1.2.3-rc.1+4처럼 prerelease가 있어도 +N은 빌드 번호다(릴리스 시 읽기와 같은 규칙).
|
|
224
|
+
const m = content.match(/^version:\s*\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?\+(\d+)/m);
|
|
141
225
|
if (m) return parseInt(m[1], 10);
|
|
142
226
|
warn?.("⚠️ pubspec.yaml에 빌드 번호(+N)가 없어 version_code를 감지하지 못했습니다 — 기본값 1을 사용합니다. 실제 빌드 번호를 확인하세요.");
|
|
143
227
|
return null;
|
|
@@ -160,10 +244,10 @@ export function detectBuildNumberFromFiles({ types = [], read, readJson, warn })
|
|
|
160
244
|
warn?.("⚠️ app.json의 expo.android.versionCode가 없어 version_code를 감지하지 못했습니다 — 기본값 1을 사용합니다. 실제 빌드 번호를 확인하세요.");
|
|
161
245
|
return null;
|
|
162
246
|
};
|
|
247
|
+
const readers = { pubspec: tryFlutter, androidGradle: tryReactNative, expoAppJson: tryExpo };
|
|
163
248
|
for (const t of types) {
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
if (t === "react-native-expo") return tryExpo();
|
|
249
|
+
const source = typeInfo(t)?.buildNumberSource;
|
|
250
|
+
if (source) return readers[source]();
|
|
167
251
|
}
|
|
168
252
|
return null;
|
|
169
253
|
}
|
|
@@ -55,8 +55,9 @@ export function storeWorkflowFilter(stores) {
|
|
|
55
55
|
// 손대지 않은 것(baseline의 installed 해시와 동일) → 삭제
|
|
56
56
|
// 손댄 것 → .bak으로 옮긴다 (내용 보존, 트리거만 죽인다)
|
|
57
57
|
// 사용자 소유인 Fastfile·ExportOptions.plist는 여기서 다루지 않는다 (워크플로우 파일만 대상).
|
|
58
|
+
// dryRun이면 판정만 하고 파일은 건드리지 않는다 (--dry-run 미리보기용).
|
|
58
59
|
// 반환: { removed:[], backedUp:[] }
|
|
59
|
-
export function cleanupDeselectedStoreWorkflows(workflowsDir, installedFilenames, stores, baseline) {
|
|
60
|
+
export function cleanupDeselectedStoreWorkflows(workflowsDir, installedFilenames, stores, baseline, { dryRun = false } = {}) {
|
|
60
61
|
const keep = storeWorkflowFilter(stores);
|
|
61
62
|
const removed = [];
|
|
62
63
|
const backedUp = [];
|
|
@@ -69,10 +70,10 @@ export function cleanupDeselectedStoreWorkflows(workflowsDir, installedFilenames
|
|
|
69
70
|
const known = baseline?.files?.[filename]?.installed;
|
|
70
71
|
const untouched = known && sha256(readFileSync(p, "utf8")) === known;
|
|
71
72
|
if (untouched) {
|
|
72
|
-
rmSync(p, { force: true });
|
|
73
|
+
if (!dryRun) rmSync(p, { force: true });
|
|
73
74
|
removed.push(filename);
|
|
74
75
|
} else {
|
|
75
|
-
renameSync(p, `${p}.bak`);
|
|
76
|
+
if (!dryRun) renameSync(p, `${p}.bak`);
|
|
76
77
|
backedUp.push(filename);
|
|
77
78
|
}
|
|
78
79
|
}
|
package/src/core/fsutil.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// 파일시스템 공용 유틸 (LF 보존 바이트 복사). 텍스트는 그대로 복사해 원본 줄바꿈 유지.
|
|
2
2
|
import {
|
|
3
3
|
cpSync, existsSync, readFileSync, writeFileSync, mkdirSync,
|
|
4
|
-
readdirSync, rmSync,
|
|
4
|
+
readdirSync, rmSync, accessSync, constants,
|
|
5
5
|
} from "node:fs";
|
|
6
|
-
import { dirname } from "node:path";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
7
|
|
|
8
8
|
export const exists = (p) => existsSync(p);
|
|
9
9
|
export const readText = (p) => readFileSync(p, "utf8");
|
|
@@ -31,10 +31,9 @@ export function remove(p) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// 디렉토리 직하위 .yaml/.yml 파일명 목록. 하위 폴더 제외.
|
|
34
|
-
// 정렬 순서는
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// 그 결과 version.yml deploy 블록의 키 순서까지 달라진다. 확장자 그룹핑으로 바이트 등가 확보.)
|
|
34
|
+
// 정렬 순서는 고정한다: 확장자로 1차 그룹(.yaml 먼저 → .yml 나중), 각 그룹 안에서 알파벳순.
|
|
35
|
+
// (단순 .sort()는 확장자를 섞어 정렬해 기존 설치와 파일 순회 순서가 갈리고,
|
|
36
|
+
// 그 결과 version.yml deploy 블록의 키 순서까지 달라진다. 확장자 그룹핑으로 재실행 결과를 바이트 단위로 유지.)
|
|
38
37
|
export function listYamlFiles(dir) {
|
|
39
38
|
if (!existsSync(dir)) return [];
|
|
40
39
|
const names = readdirSync(dir, { withFileTypes: true })
|
|
@@ -44,3 +43,21 @@ export function listYamlFiles(dir) {
|
|
|
44
43
|
const yml = names.filter((n) => n.endsWith(".yml")).sort();
|
|
45
44
|
return [...yaml, ...yml];
|
|
46
45
|
}
|
|
46
|
+
|
|
47
|
+
// 쓰기 전에 권한을 미리 확인한다 — 중간에 EACCES로 멈추면 일부만 설치된 상태가 남고,
|
|
48
|
+
// 다음 실행에서는 기준점(baseline)이 없어 이미 쓴 파일이 전부 충돌로 분류된다.
|
|
49
|
+
// dirs: 파일을 만들 폴더(없으면 만들어질 가장 가까운 상위 폴더를 본다), files: 이미 있으면 덮어쓸 파일.
|
|
50
|
+
// 반환: 쓸 수 없는 경로(root 기준 상대경로) 목록.
|
|
51
|
+
export function findUnwritable(root, dirs = [], files = []) {
|
|
52
|
+
const writable = (p) => { try { accessSync(p, constants.W_OK); return true; } catch { return false; } };
|
|
53
|
+
const blocked = new Set();
|
|
54
|
+
for (const rel of dirs) {
|
|
55
|
+
let cur = rel;
|
|
56
|
+
while (cur !== "." && !existsSync(join(root, cur))) cur = dirname(cur);
|
|
57
|
+
if (!writable(join(root, cur))) blocked.add(cur);
|
|
58
|
+
}
|
|
59
|
+
for (const rel of files) {
|
|
60
|
+
if (existsSync(join(root, rel)) && !writable(join(root, rel))) blocked.add(rel);
|
|
61
|
+
}
|
|
62
|
+
return [...blocked];
|
|
63
|
+
}
|