projectops 4.28.0 → 4.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/args.js +51 -2
- package/src/cli/errors.js +40 -0
- package/src/commands/full.js +6 -2
- package/src/commands/interactive.js +1 -0
- package/src/commands/version.js +8 -2
- package/src/commands/workflows.js +7 -4
- package/src/core/copy/workflows.js +5 -0
- package/src/core/detect-fs.js +2 -0
- package/src/core/version-yml.js +44 -12
- package/src/index.js +29 -3
- package/src/ui/summary.js +22 -3
package/README.md
CHANGED
package/package.json
CHANGED
package/src/cli/args.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
// CLI 인자 파싱 (.sh top-level while-case 등가) — template_integrator.sh 842~920.
|
|
2
|
+
import { existsSync, statSync, realpathSync } from "node:fs";
|
|
3
|
+
import { join, relative, isAbsolute } from "node:path";
|
|
2
4
|
import { VALID_TYPES } from "../context.js";
|
|
3
5
|
|
|
4
6
|
export const DEPLOY_TARGETS = ["docker-ssh", "vercel", "none"];
|
|
5
7
|
export const PUBLISH_TARGETS = ["nexus", "npm", "github-packages"];
|
|
6
8
|
export const INTENT_VALUES = ["app", "library", "both", "none", "manual"];
|
|
9
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
10
|
+
const BRANCH_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
11
|
+
// 허용 실행 모드 (#665) — 오타가 조용히 "복사 0건 성공"으로 끝나지 않게 한다.
|
|
12
|
+
export const MODE_VALUES = ["full", "version", "workflows", "issues", "skills", "doctor", "interactive"];
|
|
7
13
|
|
|
8
14
|
// argv(process.argv.slice(2)) → 파싱 결과. 오류 시 throw(호출부에서 exit 1).
|
|
9
15
|
export function parseArgs(argv) {
|
|
@@ -28,12 +34,28 @@ export function parseArgs(argv) {
|
|
|
28
34
|
const a = args.shift();
|
|
29
35
|
switch (a) {
|
|
30
36
|
case "-m": case "--mode":
|
|
31
|
-
|
|
37
|
+
{
|
|
38
|
+
const v = (args.shift() ?? "").trim();
|
|
39
|
+
if (!MODE_VALUES.includes(v)) {
|
|
40
|
+
throw new CliError(`--mode 값은 ${MODE_VALUES.join(" | ")} 중 하나여야 합니다: '${v}'`);
|
|
41
|
+
}
|
|
42
|
+
result.mode = v;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
32
45
|
case "-v": case "--version":
|
|
33
46
|
// npm 관례: -v/--version 은 패키지 버전 출력. (초기 버전 지정은 --project-version)
|
|
34
47
|
result.showVersion = true; break;
|
|
35
48
|
case "--project-version":
|
|
36
|
-
|
|
49
|
+
{
|
|
50
|
+
// version.yml·셸 스크립트가 그대로 읽는 값이라 x.y.z 외에는 받지 않는다 (#676)
|
|
51
|
+
// 'v1.0.0'은 사용자가 흔히 쓰는 표기라 거부하지 않고 접두사만 떼어 받는다
|
|
52
|
+
const v = (args.shift() ?? "").trim().replace(/^[vV](?=\d)/, "");
|
|
53
|
+
if (!SEMVER_RE.test(v)) {
|
|
54
|
+
throw new CliError(`--project-version 값은 x.y.z (예: 1.0.0) 형식이어야 합니다: '${v}'`);
|
|
55
|
+
}
|
|
56
|
+
result.version = v;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
37
59
|
case "-t": case "--type": {
|
|
38
60
|
const csv = args.shift() ?? "";
|
|
39
61
|
const seen = new Set();
|
|
@@ -79,6 +101,10 @@ export function parseArgs(argv) {
|
|
|
79
101
|
case "--deploy-branch": {
|
|
80
102
|
// 릴리스 PR head 브랜치 (#456). default_branch와 별개.
|
|
81
103
|
const v = (args.shift() ?? "").trim();
|
|
104
|
+
// 워크플로우 yaml·셸에 그대로 삽입되므로 안전한 브랜치명만 허용 (#676)
|
|
105
|
+
if (v && (!BRANCH_RE.test(v) || v.includes("..") || v.endsWith("/") || v.endsWith(".lock"))) {
|
|
106
|
+
throw new CliError(`--deploy-branch 값은 영문·숫자·. _ - / 로 이루어진 브랜치명이어야 합니다: '${v}'`);
|
|
107
|
+
}
|
|
82
108
|
if (v) result.deployBranch = v;
|
|
83
109
|
break;
|
|
84
110
|
}
|
|
@@ -132,6 +158,29 @@ export function normalizePath(p) {
|
|
|
132
158
|
return s === "" ? "." : s;
|
|
133
159
|
}
|
|
134
160
|
|
|
161
|
+
// --paths 맵 검증 (#674) — 저장소 밖 경로는 version_manager가 그 파일을 실제로 수정하므로 가장 위험하다.
|
|
162
|
+
// · 절대경로·`..` 세그먼트 거부 (저장소 루트 기준 상대경로만)
|
|
163
|
+
// · --type 으로 고르지 않은 타입 키 거부
|
|
164
|
+
// · 존재하지 않는 폴더 거부 (심볼릭 링크로 밖을 가리키는 경우도 realpath 로 막는다)
|
|
165
|
+
export function validatePathsMap(map, { root, types }) {
|
|
166
|
+
for (const [type, p] of map) {
|
|
167
|
+
if (!types.includes(type)) {
|
|
168
|
+
throw new CliError(`--paths 에 선택하지 않은 타입이 있습니다: '${type}' (선택한 타입: ${types.join(",") || "없음"})`);
|
|
169
|
+
}
|
|
170
|
+
if (p.startsWith("/") || /^[A-Za-z]:/.test(p) || p.split("/").includes("..")) {
|
|
171
|
+
throw new CliError(`--paths ${type}=${p} : 저장소 루트 기준 상대 경로여야 합니다 (절대경로, '..' 불가)`);
|
|
172
|
+
}
|
|
173
|
+
const abs = join(root, p);
|
|
174
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory()) {
|
|
175
|
+
throw new CliError(`--paths ${type}=${p} : 존재하지 않는 폴더입니다`);
|
|
176
|
+
}
|
|
177
|
+
const rel = relative(realpathSync(root), realpathSync(abs));
|
|
178
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
179
|
+
throw new CliError(`--paths ${type}=${p} : 저장소 밖을 가리킵니다`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
135
184
|
// "flutter=app,react=client" → Map<type, normalizedPath>. 타입 검증(무효 → throw).
|
|
136
185
|
export function parsePathsCsv(csv) {
|
|
137
186
|
const map = new Map();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// 예기치 못한 예외를 사용자가 읽을 수 있는 한 줄 오류 + 조치 안내로 바꾼다 (#672).
|
|
2
|
+
// 스택 트레이스는 사용자가 고칠 수 있는 정보가 아니므로 DEBUG 환경변수가 있을 때만 노출한다.
|
|
3
|
+
|
|
4
|
+
// err → { message, hint }. 분류 근거는 Node 가 던지는 err.code / 메시지 형식이다.
|
|
5
|
+
export function describeError(err) {
|
|
6
|
+
const msg = String(err?.message || err);
|
|
7
|
+
const code = err?.code;
|
|
8
|
+
|
|
9
|
+
// PATH 에 git 이 없음 — spawn 계열이 ENOENT 를 던진다
|
|
10
|
+
if (code === "ENOENT" && /\bgit\b/.test(`${err?.path ?? ""} ${err?.syscall ?? ""} ${msg}`)) {
|
|
11
|
+
return { message: "git 을 실행할 수 없습니다.", hint: "git 이 설치되어 있고 PATH 에 있는지 확인하세요." };
|
|
12
|
+
}
|
|
13
|
+
// 템플릿 내려받기 실패 — execFileSync 가 "Command failed: git clone ..." 를 던진다
|
|
14
|
+
if (/Command failed: git clone/.test(msg)) {
|
|
15
|
+
return {
|
|
16
|
+
message: "템플릿을 내려받지 못했습니다.",
|
|
17
|
+
hint: "네트워크·프록시 설정과 GitHub 접근 가능 여부를 확인한 뒤 다시 실행하세요.",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// 권한/읽기 전용 파일시스템
|
|
21
|
+
if (code === "EACCES" || code === "EPERM" || code === "EROFS") {
|
|
22
|
+
const where = err?.path ? ` (${err.path})` : "";
|
|
23
|
+
return {
|
|
24
|
+
message: `파일을 쓸 수 없습니다${where}.`,
|
|
25
|
+
hint: "프로젝트 폴더와 .github 폴더의 쓰기 권한(읽기 전용 여부)을 확인하세요.",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (code === "ENOSPC") {
|
|
29
|
+
return { message: "디스크 공간이 부족합니다.", hint: "공간을 확보한 뒤 다시 실행하세요." };
|
|
30
|
+
}
|
|
31
|
+
return { message: `예기치 못한 오류: ${msg}`, hint: "자세한 내용은 DEBUG=1 로 다시 실행하면 볼 수 있습니다." };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 오류를 stderr 로 출력한다. 호출부는 비0 종료코드를 돌려주면 된다.
|
|
35
|
+
export function reportFatal(err, { env = process.env, write = (s) => process.stderr.write(s) } = {}) {
|
|
36
|
+
const { message, hint } = describeError(err);
|
|
37
|
+
write(`❌ ${message}\n`);
|
|
38
|
+
if (hint) write(` ${hint}\n`);
|
|
39
|
+
if (env.DEBUG) write(`\n${err?.stack || err}\n`);
|
|
40
|
+
}
|
package/src/commands/full.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// 복사 순서: version.yml → readme → workflows → (deploy블록) → scripts → config →
|
|
3
3
|
// util(타입별) → issue → discussion → coderabbit → gitignore → setup_guide → (옵션저장)
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
6
|
import { writeText } from "../core/fsutil.js";
|
|
6
7
|
import { PATHS } from "../core/paths.js";
|
|
7
|
-
import { buildVersionYml } from "../core/version-yml.js";
|
|
8
|
+
import { buildVersionYml, mergeDeployValues } from "../core/version-yml.js";
|
|
8
9
|
import { markerForType } from "../core/detect.js";
|
|
9
10
|
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
10
11
|
import { copyWorkflows } from "../core/copy/workflows.js";
|
|
@@ -36,7 +37,10 @@ export function runFull(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
36
37
|
const step = (name, fn, d) => (hooks.trace ? hooks.trace.step(name, fn, d) : fn());
|
|
37
38
|
const wfCounters = step("copy-workflows", () => copyWorkflows(context, tempDir, targetRoot, hooks),
|
|
38
39
|
{ types, deploy: deployTarget, publish: publishTargets });
|
|
39
|
-
|
|
40
|
+
// 기존 version.yml 의 deploy 값은 보존한다 (#670) — 전체 재생성이라 읽지 않으면 사용자 수정이 사라진다.
|
|
41
|
+
const vyFile = join(targetRoot, PATHS.versionFile);
|
|
42
|
+
const deployValues = mergeDeployValues(existsSync(vyFile) ? readFileSync(vyFile, "utf8") : "",
|
|
43
|
+
wfCounters.deployValues || new Map()); // Map<type, Map<key,value>
|
|
40
44
|
|
|
41
45
|
// 1. version.yml 생성 (전체 재생성 — metadata → deploy → template 순, .sh 최종형과 동일)
|
|
42
46
|
step("write-version-yml", () => writeText(join(targetRoot, PATHS.versionFile),
|
|
@@ -408,6 +408,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = {
|
|
|
408
408
|
mode, types, version, deployBranch, deployBranchReady, migrationGuidePath,
|
|
409
409
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
410
410
|
skippedConflicts: result?.workflows?.skippedConflicts ?? [], // #654 병합 안내
|
|
411
|
+
replacedBak: result?.workflows?.replacedBak ?? [], // #673 기준점 없이 교체한 파일 안내
|
|
411
412
|
verification: result?.verification, // #549 설치 검증 결과
|
|
412
413
|
aiPrSummary, codeReviewCoderabbit, // #569 — 고른 것만 안내
|
|
413
414
|
logDir: files ? MIGRATION_DIR : null, // #561 기록 위치 안내
|
package/src/commands/version.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// version 모드 (.sh execute_integration version case 등가) — template_integrator.sh 4517~4530.
|
|
2
2
|
// 순서: version.yml → readme → scripts → config → gitignore → setup_guide.
|
|
3
3
|
// (워크플로우 미복사 → deploy 블록 없음. util·issue·coderabbit도 없음.)
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { writeText } from "../core/fsutil.js";
|
|
6
7
|
import { PATHS } from "../core/paths.js";
|
|
7
|
-
import { buildVersionYml } from "../core/version-yml.js";
|
|
8
|
+
import { buildVersionYml, mergeDeployValues } from "../core/version-yml.js";
|
|
8
9
|
import { markerForType } from "../core/detect.js";
|
|
9
10
|
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
10
11
|
import { copyScripts, copyConfigFolder, copySetupGuide } from "../core/copy/simple.js";
|
|
@@ -19,9 +20,14 @@ export function runVersion(context, tempDir, targetRoot = ".") {
|
|
|
19
20
|
const pathMarkers = new Map();
|
|
20
21
|
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
// version.yml 은 전체 재생성이라, 읽지 않으면 이전 통합이 남긴 deploy 값(사용자 수정 포함)이 사라진다 (#670)
|
|
24
|
+
const vyFile = join(targetRoot, PATHS.versionFile);
|
|
25
|
+
const deployValues = mergeDeployValues(existsSync(vyFile) ? readFileSync(vyFile, "utf8") : "", new Map());
|
|
26
|
+
|
|
27
|
+
writeText(vyFile,
|
|
23
28
|
buildVersionYml({
|
|
24
29
|
version, types, paths, pathMarkers, branch, deployBranch, versionCode, now, today,
|
|
30
|
+
deployValues,
|
|
25
31
|
// mode(#502): version 모드가 기존 full 통합 기록을 "version"으로 강등하지 않도록
|
|
26
32
|
// 호출부가 recordMode로 기존 값을 넘긴다 (full이 우세 — 업데이트 재실행 범위 축소 방지).
|
|
27
33
|
templateOptions: { templateVersion, deployTarget, publishTargets, includeSecretBackup, optionsDate: today,
|
|
@@ -7,7 +7,7 @@ import { PATHS } from "../core/paths.js";
|
|
|
7
7
|
import { copyWorkflows } from "../core/copy/workflows.js";
|
|
8
8
|
import { copyScripts, copyConfigFolder, copySetupGuide } from "../core/copy/simple.js";
|
|
9
9
|
import { copyUtilModules } from "../core/copy/util.js";
|
|
10
|
-
import { convertLegacySingularType } from "../core/version-yml.js";
|
|
10
|
+
import { convertLegacySingularType, mergeDeployValues } from "../core/version-yml.js";
|
|
11
11
|
import { verifyInstall } from "../core/verify.js";
|
|
12
12
|
|
|
13
13
|
export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
@@ -25,7 +25,8 @@ export function runWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
25
25
|
|
|
26
26
|
// update_version_yml_deploy: 기존 version.yml이 있고 ask 값이 있을 때만 deploy 블록 갱신
|
|
27
27
|
if (existsSync(vy) && wf.deployValues && wf.deployValues.size) {
|
|
28
|
-
|
|
28
|
+
const cur = readFileSync(vy, "utf8");
|
|
29
|
+
writeFileSync(vy, upsertDeployBlock(cur, mergeDeployValues(cur, wf.deployValues))); // 기존 값 우선 (#670)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
copyScripts(tempDir, targetRoot);
|
|
@@ -62,8 +63,10 @@ export function upsertDeployBlock(content, deployValues) {
|
|
|
62
63
|
for (const line of lines) {
|
|
63
64
|
if (/^deploy:/.test(line)) { inDeploy = true; continue; }
|
|
64
65
|
if (inDeploy) {
|
|
65
|
-
|
|
66
|
-
inDeploy = false;
|
|
66
|
+
// 4.28.0 이 만든 깨진 구조: deploy 아래 2칸 `template:` 은 deploy 내용이 아니라 metadata 의 자식이다 (#670)
|
|
67
|
+
if (/^ {2}template:/.test(line)) inDeploy = false;
|
|
68
|
+
else if (/^\s/.test(line) || line === "") continue; // 들여쓰기/빈줄 = deploy 내부
|
|
69
|
+
else inDeploy = false;
|
|
67
70
|
}
|
|
68
71
|
out.push(line);
|
|
69
72
|
}
|
|
@@ -81,6 +81,7 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
81
81
|
if (!exists(projectTypesDir)) throw new Error("템플릿 저장소 구조 오류 — project-types 폴더를 찾지 못했습니다.");
|
|
82
82
|
|
|
83
83
|
const counters = { copied: 0, skipped: 0, templateAdded: 0, optionalCopied: 0, copiedFiles: [],
|
|
84
|
+
replacedBak: [], // #673 — 기준점이 없어 수정 여부를 모른 채 .bak 백업 후 교체한 파일명
|
|
84
85
|
skippedConflicts: [] }; // #654 — 유지한 사용자 수정본별 { filename, incoming, added, removed }
|
|
85
86
|
const deployValues = new Map(); // Map<type, Map<key,value>> — deploy 블록용 ask 값
|
|
86
87
|
counters.deployValues = deployValues;
|
|
@@ -152,6 +153,7 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
152
153
|
counters.copied++;
|
|
153
154
|
counters.copiedFiles.push(filename);
|
|
154
155
|
trace?.event("copy", "replaced-bak", filename, { group: "common", reason: "baseline-absent" });
|
|
156
|
+
counters.replacedBak.push(filename); // #673 — 수정 가능성이 있는 파일을 교체했음을 화면에 알린다
|
|
155
157
|
continue;
|
|
156
158
|
}
|
|
157
159
|
// 사용자가 손댄 것이 확인된 파일 — 결정에 따라 처리(미지정이면 유지).
|
|
@@ -184,6 +186,7 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
184
186
|
counters.copied++;
|
|
185
187
|
counters.copiedFiles.push(filename);
|
|
186
188
|
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: "common-deploy" });
|
|
189
|
+
if (backedUp) counters.replacedBak.push(filename); // #673
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
|
|
@@ -207,6 +210,7 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
207
210
|
counters.copied++;
|
|
208
211
|
counters.copiedFiles.push(filename);
|
|
209
212
|
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: "pr-summary" });
|
|
213
|
+
if (backedUp) counters.replacedBak.push(filename); // #673
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
216
|
|
|
@@ -407,6 +411,7 @@ function copyWorkflowsForType(type, projectTypesDir, workflowsDir, ctx, counters
|
|
|
407
411
|
counters.copied++;
|
|
408
412
|
counters.copiedFiles.push(filename);
|
|
409
413
|
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: `${type}/publish/${target}` });
|
|
414
|
+
if (backedUp) counters.replacedBak.push(filename); // #673
|
|
410
415
|
}
|
|
411
416
|
}
|
|
412
417
|
|
package/src/core/detect-fs.js
CHANGED
|
@@ -21,6 +21,8 @@ function gitOut(root, args) {
|
|
|
21
21
|
const SCAN_PRUNE = new Set([
|
|
22
22
|
"node_modules", ".git", "build", "dist", ".dart_tool",
|
|
23
23
|
"android", "ios", ".gradle", "venv", ".venv", "__pycache__",
|
|
24
|
+
// 템플릿이 스스로 설치하는 .github/scripts/*.py 가 python 으로 오탐되지 않게 한다 (#671)
|
|
25
|
+
".github",
|
|
24
26
|
]);
|
|
25
27
|
export function listScanFiles(root, maxDepth = 3) {
|
|
26
28
|
const files = [];
|
package/src/core/version-yml.js
CHANGED
|
@@ -267,6 +267,37 @@ export function parseExisting(content) {
|
|
|
267
267
|
return { version, versionCode, types, paths, templateVersion, templateMode, options, defaultBranch, deployBranch };
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
// 기존 version.yml 의 deploy 블록 → Map<type, Map<key,value>> (#670 — 재실행 시 사용자 수정값 보존용).
|
|
271
|
+
// 4.28.0 이 만든 깨진 구조(deploy 아래에 2칸 들여쓴 template 이 딸려 있음)도 읽을 수 있게,
|
|
272
|
+
// 2칸 키 `template` 은 타입이 아니라 블록 종료로 본다.
|
|
273
|
+
export function parseDeployBlock(content) {
|
|
274
|
+
const out = new Map();
|
|
275
|
+
let inDeploy = false;
|
|
276
|
+
let type = null;
|
|
277
|
+
for (const line of String(content || "").split(/\r?\n/)) {
|
|
278
|
+
if (/^deploy:/.test(line)) { inDeploy = true; type = null; continue; }
|
|
279
|
+
if (!inDeploy) continue;
|
|
280
|
+
if (line === "" || /^\s*#/.test(line)) continue;
|
|
281
|
+
if (/^\S/.test(line) || /^ {2}template:/.test(line)) { inDeploy = false; continue; }
|
|
282
|
+
const t = line.match(/^ {2}([A-Za-z0-9_-]+):\s*(#.*)?$/);
|
|
283
|
+
if (t) { type = t[1]; if (!out.has(type)) out.set(type, new Map()); continue; }
|
|
284
|
+
const kv = line.match(/^ {4}([A-Za-z0-9_.-]+):\s*"([^"]*)"/);
|
|
285
|
+
if (kv && type) out.get(type).set(kv[1], kv[2]);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 재실행 시 기존 deploy 값 우선 병합 (#670) — 사용자가 version.yml 에서 직접 고친 값이 템플릿 기본값에
|
|
291
|
+
// 덮이지 않게 한다. 기존 값이 이기고, 이번에 새로 묻는 키만 추가된다.
|
|
292
|
+
export function mergeDeployValues(existingContent, fresh) {
|
|
293
|
+
const merged = parseDeployBlock(existingContent);
|
|
294
|
+
for (const [t, kv] of fresh) {
|
|
295
|
+
if (!merged.has(t)) merged.set(t, new Map());
|
|
296
|
+
for (const [k, v] of kv) if (!merged.get(t).has(k)) merged.get(t).set(k, v);
|
|
297
|
+
}
|
|
298
|
+
return merged;
|
|
299
|
+
}
|
|
300
|
+
|
|
270
301
|
// version.yml 전체 생성 (.sh create_version_yml + save_template_options 신규 케이스 등가).
|
|
271
302
|
// opts: { version, types:[], paths:Map, pathMarkers?:Map, branch, versionCode, now, today, templateOptions? }
|
|
272
303
|
// primary 타입은 별도 키 없이 project_types[0]이다 (v4.1.0 SSOT — 단수 project_type 키 제거).
|
|
@@ -302,18 +333,6 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
302
333
|
out += ` integrated_from: "projectops"\n`;
|
|
303
334
|
out += ` integration_date: "${today}"\n`;
|
|
304
335
|
|
|
305
|
-
// deploy 블록 (.sh update_version_yml_deploy). deployValues: Map<type, Map<key,value>>.
|
|
306
|
-
// WF ask 값이 있는 타입만. metadata 뒤, template 앞. (앞에 빈 줄 1개)
|
|
307
|
-
const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t) && deployValues.get(t).size > 0);
|
|
308
|
-
if (deployTypes.length) {
|
|
309
|
-
out += `\n`;
|
|
310
|
-
out += `deploy: # 마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)\n`;
|
|
311
|
-
for (const t of deployTypes) {
|
|
312
|
-
out += ` ${t}:\n`;
|
|
313
|
-
for (const [k, v] of deployValues.get(t)) out += ` ${k}: "${v}"\n`;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
336
|
// template 옵션 블록 (.sh save_template_options 신규 추가 케이스). templateOptions 지정 시.
|
|
318
337
|
if (templateOptions) {
|
|
319
338
|
const { templateVersion = "unknown", deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, optionsDate = today,
|
|
@@ -347,5 +366,18 @@ export function buildVersionYml({ version, types = [], paths = new Map(), pathMa
|
|
|
347
366
|
out += ` provider: "${changelogProvider}"\n`;
|
|
348
367
|
out += ` base_url: "${changelogBaseUrl}"\n`;
|
|
349
368
|
}
|
|
369
|
+
|
|
370
|
+
// deploy 블록 (.sh update_version_yml_deploy). deployValues: Map<type, Map<key,value>>.
|
|
371
|
+
// WF ask 값이 있는 타입만. 반드시 template 뒤의 별도 최상위 키로 둔다 — metadata 와 template
|
|
372
|
+
// 사이에 끼면 template 이 deploy 의 자식이 되어 metadata.template.options 구조가 깨진다 (#670).
|
|
373
|
+
const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t) && deployValues.get(t).size > 0);
|
|
374
|
+
if (deployTypes.length) {
|
|
375
|
+
out += `\n`;
|
|
376
|
+
out += `deploy: # 마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)\n`;
|
|
377
|
+
for (const t of deployTypes) {
|
|
378
|
+
out += ` ${t}:\n`;
|
|
379
|
+
for (const [k, v] of deployValues.get(t)) out += ` ${k}: "${v}"\n`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
350
382
|
return out;
|
|
351
383
|
}
|
package/src/index.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
-
import { parseArgs, parsePathsCsv, CliError } from "./cli/args.js";
|
|
6
|
+
import { parseArgs, parsePathsCsv, validatePathsMap, CliError } from "./cli/args.js";
|
|
7
7
|
import { HELP_TEXT } from "./cli/help.js";
|
|
8
|
+
import { reportFatal } from "./cli/errors.js";
|
|
8
9
|
import { createContext } from "./context.js";
|
|
9
10
|
import { PATHS } from "./core/paths.js";
|
|
10
11
|
import { remove } from "./core/fsutil.js";
|
|
@@ -49,7 +50,18 @@ function utcNow(date = new Date()) {
|
|
|
49
50
|
// run(argv, opts) → exitCode. opts: { cwd, source?, clock? }
|
|
50
51
|
// source: acquireTemplate용 (기본 git clone). 테스트는 {type:'local', path} 주입.
|
|
51
52
|
// clock: {now, today} 주입 (기본 현재 UTC).
|
|
52
|
-
export async function run(argv,
|
|
53
|
+
export async function run(argv, options = {}) {
|
|
54
|
+
try {
|
|
55
|
+
return await runCore(argv, options);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
// 네트워크·git·권한 같은 환경 문제는 스택 트레이스 대신 한 줄 원인 + 조치로 알린다 (#672).
|
|
58
|
+
// 실행 기록(trace)은 runCore 의 finally 에서 이미 닫혔다.
|
|
59
|
+
reportFatal(err);
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runCore(argv, { cwd = process.cwd(), source = { type: "git" }, clock } = {}) {
|
|
53
65
|
let opts;
|
|
54
66
|
try {
|
|
55
67
|
opts = parseArgs(argv);
|
|
@@ -130,8 +142,21 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
130
142
|
const repoName = detectRepoName(cwd);
|
|
131
143
|
trace.event("detect", "repo", repoName || "(미상)", { defaultBranch: branch, versionCode });
|
|
132
144
|
// 경로 확정 (.sh resolve_project_paths 비대화형 경로 — --paths 우선 → 저장값 → 후보 1개 자동 → 루트 폴백)
|
|
145
|
+
let cliPaths;
|
|
146
|
+
try {
|
|
147
|
+
cliPaths = parsePathsCsv(opts.pathsCsv);
|
|
148
|
+
validatePathsMap(cliPaths, { root: cwd, types }); // #674 — 파일을 쓰기 전에 거른다
|
|
149
|
+
} catch (e) {
|
|
150
|
+
if (e instanceof CliError) {
|
|
151
|
+
console.error(e.message);
|
|
152
|
+
trace.mirrorStop();
|
|
153
|
+
disarmSignals();
|
|
154
|
+
return 1;
|
|
155
|
+
}
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
133
158
|
const paths = await resolveProjectPaths({
|
|
134
|
-
root: cwd, types, paths:
|
|
159
|
+
root: cwd, types, paths: cliPaths,
|
|
135
160
|
existingPaths: existing?.paths ?? new Map(), force: true, tty: false, io: {},
|
|
136
161
|
});
|
|
137
162
|
|
|
@@ -322,6 +347,7 @@ export async function run(argv, { cwd = process.cwd(), source = { type: "git" },
|
|
|
322
347
|
mode: opts.mode, types, version, deployBranch: context.deployBranch, migrationGuidePath,
|
|
323
348
|
counters: { workflows: result?.workflows?.copied ?? 0, workflowFiles: result?.workflows?.copiedFiles ?? [], utilModules: 0 },
|
|
324
349
|
skippedConflicts: result?.workflows?.skippedConflicts ?? [], // #654 병합 안내
|
|
350
|
+
replacedBak: result?.workflows?.replacedBak ?? [], // #673 기준점 없이 교체한 파일 안내
|
|
325
351
|
verification: result?.verification, // #549 설치 후 검증 결과 (full/workflows 모드에서만 존재)
|
|
326
352
|
// #569 — 고른 것만 안내하려면 선택값이 필요하다
|
|
327
353
|
aiPrSummary: context.aiPrSummary, codeReviewCoderabbit: context.codeReviewCoderabbit,
|
package/src/ui/summary.js
CHANGED
|
@@ -18,6 +18,8 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
18
18
|
const CYAN = isTty ? "\x1b[0;36m" : "";
|
|
19
19
|
const NC = isTty ? "\x1b[0m" : "";
|
|
20
20
|
const utilModulesCopied = counters.utilModules ?? 0;
|
|
21
|
+
// README.md 가 없으면 버전 섹션 추가를 건너뛰므로 화면에도 추가·자동 갱신으로 표시하지 않는다 (#677)
|
|
22
|
+
const hasReadme = existsSync(join(targetRoot, "README.md"));
|
|
21
23
|
|
|
22
24
|
err("");
|
|
23
25
|
err(SEPARATOR);
|
|
@@ -32,7 +34,7 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
32
34
|
switch (mode) {
|
|
33
35
|
case "full":
|
|
34
36
|
err(" ✅ 버전 관리 시스템 (version.yml)");
|
|
35
|
-
err(" ✅ README.md 자동 버전 업데이트");
|
|
37
|
+
if (hasReadme) err(" ✅ README.md 자동 버전 업데이트");
|
|
36
38
|
err(" ✅ GitHub Actions 워크플로우");
|
|
37
39
|
if (utilModulesCopied > 0) err(` ✅ 유틸리티 모듈 (${utilModulesCopied} 개)`);
|
|
38
40
|
err(" ✅ 이슈/PR/Discussion 템플릿");
|
|
@@ -42,7 +44,7 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
42
44
|
break;
|
|
43
45
|
case "version":
|
|
44
46
|
err(" ✅ 버전 관리 시스템 (version.yml)");
|
|
45
|
-
err(" ✅ README.md 자동 버전 업데이트");
|
|
47
|
+
if (hasReadme) err(" ✅ README.md 자동 버전 업데이트");
|
|
46
48
|
err(" ✅ .gitignore 필수 항목");
|
|
47
49
|
err(" ✅ 템플릿 설정 가이드 (SETUP-GUIDE.md)");
|
|
48
50
|
break;
|
|
@@ -72,7 +74,7 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
72
74
|
err("");
|
|
73
75
|
err("추가된 파일:");
|
|
74
76
|
err(` 📄 version.yml (버전: ${version}, 타입: ${types.join(",")})`);
|
|
75
|
-
err(" 📝 README.md (버전 섹션 추가)");
|
|
77
|
+
err(hasReadme ? " 📝 README.md (버전 섹션 추가)" : " 📝 README.md 없음 — 버전 섹션 추가를 건너뜀");
|
|
76
78
|
err("");
|
|
77
79
|
err("추가된 워크플로우:");
|
|
78
80
|
|
|
@@ -110,6 +112,16 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
110
112
|
}
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
// #673 — 업데이트 기준점이 없어 수정 여부를 구분하지 못하고 교체한 파일: 원본이 .bak 으로만 남으므로
|
|
116
|
+
// 사용자가 자기 수정이 사라졌는지 git diff 없이도 알 수 있어야 한다.
|
|
117
|
+
const replaced = ctx?.replacedBak ?? [];
|
|
118
|
+
if (replaced.length > 0) {
|
|
119
|
+
err("");
|
|
120
|
+
err(` ${YELLOW}⚠️ 수정 여부를 확인할 수 없어 ${replaced.length}개 워크플로를 새 템플릿으로 교체하고 원본을 .bak 으로 백업했습니다${NC}`);
|
|
121
|
+
for (const f of replaced) err(` 💾 .github/workflows/${f}.bak`);
|
|
122
|
+
err(" 직접 수정한 내용이 있었다면 .bak 에서 옮겨 오세요. (업데이트 기준점은 이번 실행으로 새로 기록됩니다)");
|
|
123
|
+
}
|
|
124
|
+
|
|
113
125
|
err("");
|
|
114
126
|
err(" 🔧 .github/scripts/");
|
|
115
127
|
err(" ├─ version_manager.sh");
|
|
@@ -237,6 +249,13 @@ export function printSummary(ctx, targetRoot = ".") {
|
|
|
237
249
|
err("");
|
|
238
250
|
step++;
|
|
239
251
|
}
|
|
252
|
+
// #668 — 라벨 동기화는 issue-labels.yml 이 바뀐 push 에서만 도는데, 설치 직후 첫 push 에서
|
|
253
|
+
// 실행 기록이 없는 경우가 실측됐다. 라벨이 없으면 상태 라벨 자동화가 조용히 무시되므로 1회 수동 실행을 안내한다.
|
|
254
|
+
err(` ${step}️⃣ (권장) 템플릿 라벨 만들기`);
|
|
255
|
+
err(" → Actions 탭 > PROJECT-SYNC-GITHUB-LABELS > Run workflow 를 한 번 실행하세요.");
|
|
256
|
+
err(" → 안 하면 작업전·작업중 같은 상태 라벨이 레포에 없어 라벨 자동화가 무시됩니다.");
|
|
257
|
+
err("");
|
|
258
|
+
step++;
|
|
240
259
|
err(SEPARATOR);
|
|
241
260
|
err("");
|
|
242
261
|
err(`${CYAN}📖 자세한 설정 방법은 다음 파일을 참고하세요:${NC}`);
|