projectops 3.0.193 → 3.0.194
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/bin/projectops.js +7 -34
- package/package.json +7 -2
- package/src/cli/args.js +84 -0
- package/src/cli/help.js +23 -0
- package/src/commands/full.js +64 -0
- package/src/commands/interactive.js +132 -0
- package/src/commands/issues.js +8 -0
- package/src/commands/version.js +30 -0
- package/src/commands/workflows.js +53 -0
- package/src/context.js +25 -0
- package/src/core/assets.js +44 -0
- package/src/core/breaking.js +26 -0
- package/src/core/copy/coderabbit.js +25 -0
- package/src/core/copy/gitignore.js +55 -0
- package/src/core/copy/readme.js +30 -0
- package/src/core/copy/simple.js +55 -0
- package/src/core/copy/util.js +22 -0
- package/src/core/copy/workflows.js +145 -0
- package/src/core/detect-fs.js +64 -0
- package/src/core/detect.js +62 -0
- package/src/core/exclusions.js +25 -0
- package/src/core/fsutil.js +40 -0
- package/src/core/paths.js +14 -0
- package/src/core/version-yml.js +146 -0
- package/src/core/wizard-env.js +101 -0
- package/src/index.js +99 -0
- package/src/ui/prompts.js +82 -0
package/README.md
CHANGED
package/bin/projectops.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// projectops
|
|
3
|
-
// 마법사 본체(SP2)가 이식되기 전까지 기존 template_integrator 안내를 제공한다.
|
|
4
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
// projectops CLI 엔트리 — argv를 src/index.js의 run()에 넘긴다.
|
|
5
3
|
import { fileURLToPath } from "node:url";
|
|
6
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
7
6
|
|
|
8
7
|
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
9
8
|
if (nodeMajor < 18) {
|
|
@@ -11,35 +10,9 @@ if (nodeMajor < 18) {
|
|
|
11
10
|
process.exit(1);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
);
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const indexPath = join(here, "..", "src", "index.js");
|
|
15
|
+
const { run } = await import(pathToFileURL(indexPath).href);
|
|
17
16
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
console.log(pkg.version);
|
|
21
|
-
process.exit(0);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const RESET = "\x1b[0m";
|
|
25
|
-
const CYAN = "\x1b[36m";
|
|
26
|
-
const GREEN = "\x1b[32m";
|
|
27
|
-
const YELLOW = "\x1b[33m";
|
|
28
|
-
const DIM = "\x1b[2m";
|
|
29
|
-
|
|
30
|
-
console.log(`
|
|
31
|
-
${CYAN}=========================================================
|
|
32
|
-
ProjectOps v${pkg.version}
|
|
33
|
-
완전 자동화 GitHub 프로젝트 관리 템플릿 통합 CLI
|
|
34
|
-
=========================================================${RESET}
|
|
35
|
-
|
|
36
|
-
${YELLOW}npx 마법사는 준비 중입니다.${RESET} 지금은 아래 기존 방식으로 통합하세요.
|
|
37
|
-
|
|
38
|
-
${GREEN}macOS / Linux:${RESET}
|
|
39
|
-
bash <(curl -fsSL "https://raw.githubusercontent.com/Cassiiopeia/projectops/main/template_integrator.sh")
|
|
40
|
-
|
|
41
|
-
${GREEN}Windows (PowerShell):${RESET}
|
|
42
|
-
$wc=New-Object Net.WebClient;$wc.Encoding=[Text.Encoding]::UTF8;iex $wc.DownloadString("https://raw.githubusercontent.com/Cassiiopeia/projectops/main/template_integrator.ps1")
|
|
43
|
-
|
|
44
|
-
${DIM}문서: https://github.com/Cassiiopeia/projectops${RESET}
|
|
45
|
-
`);
|
|
17
|
+
const code = await run(process.argv.slice(2), { cwd: process.cwd() });
|
|
18
|
+
process.exit(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectops",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.194",
|
|
4
4
|
"description": "ProjectOps — 완전 자동화 GitHub 프로젝트 관리 템플릿 통합 CLI (구 SUH-DEVOPS-TEMPLATE 마법사)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"devops",
|
|
@@ -31,11 +31,16 @@
|
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
|
-
"bin/"
|
|
34
|
+
"bin/",
|
|
35
|
+
"src/"
|
|
35
36
|
],
|
|
36
37
|
"pi": {
|
|
37
38
|
"skills": [
|
|
38
39
|
"./skills"
|
|
39
40
|
]
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@clack/prompts": "^1.7.0",
|
|
44
|
+
"picocolors": "^1.1.1"
|
|
40
45
|
}
|
|
41
46
|
}
|
package/src/cli/args.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// CLI 인자 파싱 (.sh top-level while-case 등가) — template_integrator.sh 842~920.
|
|
2
|
+
import { VALID_TYPES } from "../context.js";
|
|
3
|
+
|
|
4
|
+
// argv(process.argv.slice(2)) → 파싱 결과. 오류 시 throw(호출부에서 exit 1).
|
|
5
|
+
export function parseArgs(argv) {
|
|
6
|
+
const result = {
|
|
7
|
+
mode: "interactive",
|
|
8
|
+
version: "",
|
|
9
|
+
types: [],
|
|
10
|
+
primaryType: "",
|
|
11
|
+
includeNexus: null, // null=미설정
|
|
12
|
+
includeSecretBackup: null,
|
|
13
|
+
pathsCsv: "", // "flutter=app,react=client" 원문 (정규화는 resolve 단계)
|
|
14
|
+
force: false,
|
|
15
|
+
help: false,
|
|
16
|
+
};
|
|
17
|
+
const args = [...argv];
|
|
18
|
+
while (args.length > 0) {
|
|
19
|
+
const a = args.shift();
|
|
20
|
+
switch (a) {
|
|
21
|
+
case "-m": case "--mode":
|
|
22
|
+
result.mode = args.shift() ?? ""; break;
|
|
23
|
+
case "-v": case "--version":
|
|
24
|
+
result.version = args.shift() ?? ""; break;
|
|
25
|
+
case "-t": case "--type": {
|
|
26
|
+
const csv = args.shift() ?? "";
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const types = [];
|
|
29
|
+
for (let t of csv.split(",")) {
|
|
30
|
+
t = t.replace(/\s/g, "");
|
|
31
|
+
if (t === "") continue;
|
|
32
|
+
if (seen.has(t)) continue; // dedup
|
|
33
|
+
if (!VALID_TYPES.includes(t)) {
|
|
34
|
+
throw new CliError(`지원하지 않는 타입: '${t}'\n지원 타입: ${VALID_TYPES.join(" ")}`);
|
|
35
|
+
}
|
|
36
|
+
seen.add(t);
|
|
37
|
+
types.push(t);
|
|
38
|
+
}
|
|
39
|
+
if (types.length === 0) throw new CliError("--type 인자가 비어 있습니다");
|
|
40
|
+
result.types = types;
|
|
41
|
+
result.primaryType = types[0];
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "--force": result.force = true; break;
|
|
45
|
+
case "--nexus": result.includeNexus = true; break;
|
|
46
|
+
case "--no-nexus": result.includeNexus = false; break;
|
|
47
|
+
case "--secret-backup": result.includeSecretBackup = true; break;
|
|
48
|
+
case "--no-secret-backup": result.includeSecretBackup = false; break;
|
|
49
|
+
case "--paths": result.pathsCsv = args.shift() ?? ""; break;
|
|
50
|
+
case "-h": case "--help": result.help = true; break;
|
|
51
|
+
default:
|
|
52
|
+
throw new CliError(`알 수 없는 옵션: ${a}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class CliError extends Error {}
|
|
59
|
+
|
|
60
|
+
// 경로 정규화 (.sh resolve_project_paths §3.4): 앞뒤 공백·\→/·끝 /·앞 ./ 제거, 빈값→"."
|
|
61
|
+
export function normalizePath(p) {
|
|
62
|
+
let s = String(p).trim();
|
|
63
|
+
s = s.replace(/\\/g, "/");
|
|
64
|
+
s = s.replace(/\/+$/, ""); // 끝 /
|
|
65
|
+
s = s.replace(/^\.\//, ""); // 앞 ./
|
|
66
|
+
return s === "" ? "." : s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// "flutter=app,react=client" → Map<type, normalizedPath>. 타입 검증(무효 → throw).
|
|
70
|
+
export function parsePathsCsv(csv) {
|
|
71
|
+
const map = new Map();
|
|
72
|
+
if (!csv) return map;
|
|
73
|
+
for (const pair of csv.split(",")) {
|
|
74
|
+
if (pair.trim() === "") continue;
|
|
75
|
+
const eq = pair.indexOf("=");
|
|
76
|
+
const type = (eq >= 0 ? pair.slice(0, eq) : pair).trim();
|
|
77
|
+
const rawPath = eq >= 0 ? pair.slice(eq + 1) : "";
|
|
78
|
+
if (!VALID_TYPES.includes(type)) {
|
|
79
|
+
throw new CliError(`--paths에 지원하지 않는 타입: '${type}'`);
|
|
80
|
+
}
|
|
81
|
+
map.set(type, normalizePath(rawPath));
|
|
82
|
+
}
|
|
83
|
+
return map;
|
|
84
|
+
}
|
package/src/cli/help.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// --help 텍스트 (.sh show_help 요약 이식).
|
|
2
|
+
export const HELP_TEXT = `projectops — GitHub 프로젝트 자동화 템플릿 통합 CLI
|
|
3
|
+
|
|
4
|
+
사용법:
|
|
5
|
+
npx projectops [옵션]
|
|
6
|
+
|
|
7
|
+
옵션:
|
|
8
|
+
-m, --mode MODE 통합 모드 (full | version | workflows | issues | skills)
|
|
9
|
+
기본: interactive (대화형)
|
|
10
|
+
-t, --type CSV 프로젝트 타입 csv (예: spring,react,python)
|
|
11
|
+
지원: spring flutter next react react-native
|
|
12
|
+
react-native-expo node python basic
|
|
13
|
+
-v, --version VERSION 초기 버전 (예: 1.0.0). 미지정 시 자동 감지
|
|
14
|
+
--paths "t=p,..." 타입별 프로젝트 경로 (모노레포). 예: flutter=app,react=client
|
|
15
|
+
--nexus / --no-nexus Nexus 라이브러리 publish 워크플로우 포함/제외
|
|
16
|
+
--secret-backup / --no-secret-backup Secret 백업 워크플로우 포함/제외
|
|
17
|
+
--force 모든 확인 생략, 비대화형 기본값 사용
|
|
18
|
+
-h, --help 이 도움말 표시
|
|
19
|
+
|
|
20
|
+
예시:
|
|
21
|
+
npx projectops --mode full --force --type spring,react
|
|
22
|
+
npx projectops --mode workflows --type flutter --paths "flutter=app"
|
|
23
|
+
`;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// full 모드 오케스트레이터 (.sh execute_integration full case 등가) — template_integrator.sh 4502~4514.
|
|
2
|
+
// 복사 순서: version.yml → readme → workflows → (deploy블록) → scripts → config →
|
|
3
|
+
// util(타입별) → issue → discussion → coderabbit → gitignore → setup_guide → (옵션저장)
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { writeText } from "../core/fsutil.js";
|
|
6
|
+
import { PATHS } from "../core/paths.js";
|
|
7
|
+
import { buildVersionYml } from "../core/version-yml.js";
|
|
8
|
+
import { markerForType } from "../core/detect.js";
|
|
9
|
+
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
10
|
+
import { copyWorkflows } from "../core/copy/workflows.js";
|
|
11
|
+
import {
|
|
12
|
+
copyScripts, copyConfigFolder, copyIssueTemplates,
|
|
13
|
+
copyDiscussionTemplates, copySetupGuide,
|
|
14
|
+
} from "../core/copy/simple.js";
|
|
15
|
+
import { copyUtilModules } from "../core/copy/util.js";
|
|
16
|
+
import { copyCoderabbit } from "../core/copy/coderabbit.js";
|
|
17
|
+
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
18
|
+
|
|
19
|
+
// context: { version, types, paths:Map, branch, versionCode, includeNexus, includeSecretBackup,
|
|
20
|
+
// force, repoName, resolvers, now, today }
|
|
21
|
+
// tempDir: 획득된 템플릿. targetRoot: 통합 대상.
|
|
22
|
+
export function runFull(context, tempDir, targetRoot = ".") {
|
|
23
|
+
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
24
|
+
force = true, now, today, templateVersion = "unknown",
|
|
25
|
+
includeNexus = false, includeSecretBackup = false } = context;
|
|
26
|
+
|
|
27
|
+
// project_paths 마커 계산 (.sh existing_marker_in_dir 등가 — 대표 마커명)
|
|
28
|
+
const pathMarkers = new Map();
|
|
29
|
+
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
30
|
+
|
|
31
|
+
// 3. 워크플로우 복사 (+ env 치환) — deploy 블록에 쓸 ask 값을 수집한다.
|
|
32
|
+
// (.sh는 copy_workflows 후 update_version_yml_deploy로 deploy 블록을 붙인다.)
|
|
33
|
+
const wfCounters = copyWorkflows(context, tempDir, targetRoot);
|
|
34
|
+
const deployValues = wfCounters.deployValues || new Map(); // Map<type, Map<key,value>>
|
|
35
|
+
|
|
36
|
+
// 1. version.yml 생성 (전체 재생성 — metadata → deploy → template 순, .sh 최종형과 동일)
|
|
37
|
+
writeText(join(targetRoot, PATHS.versionFile),
|
|
38
|
+
buildVersionYml({
|
|
39
|
+
version, types, paths, pathMarkers, branch, versionCode, now, today,
|
|
40
|
+
deployValues,
|
|
41
|
+
templateOptions: { templateVersion, includeNexus, includeSecretBackup, optionsDate: today },
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
// 2. README 버전 섹션
|
|
45
|
+
addVersionSectionToReadme(version, targetRoot);
|
|
46
|
+
|
|
47
|
+
// 5. scripts / config
|
|
48
|
+
copyScripts(tempDir, targetRoot);
|
|
49
|
+
copyConfigFolder(tempDir, targetRoot);
|
|
50
|
+
|
|
51
|
+
// 6. util (타입별)
|
|
52
|
+
for (const t of types) copyUtilModules(tempDir, t, { force }, targetRoot);
|
|
53
|
+
|
|
54
|
+
// 7. issue / discussion 템플릿
|
|
55
|
+
copyIssueTemplates(tempDir, targetRoot);
|
|
56
|
+
copyDiscussionTemplates(tempDir, targetRoot);
|
|
57
|
+
|
|
58
|
+
// 8. coderabbit / gitignore / setup guide
|
|
59
|
+
copyCoderabbit(tempDir, { force }, targetRoot);
|
|
60
|
+
ensureGitignore(targetRoot);
|
|
61
|
+
copySetupGuide(tempDir, targetRoot);
|
|
62
|
+
|
|
63
|
+
return { workflows: wfCounters };
|
|
64
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// 대화형 마법사 (.sh interactive_mode 등가) — template_integrator.sh 4262~4411.
|
|
2
|
+
// io 주입으로 테스트 가능. 실제 실행은 src/ui/prompts.js 함수를 io로 넘긴다.
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { PATHS } from "../core/paths.js";
|
|
5
|
+
import { remove } from "../core/fsutil.js";
|
|
6
|
+
import { acquireTemplate, readTemplateVersion } from "../core/assets.js";
|
|
7
|
+
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName } from "../core/detect-fs.js";
|
|
8
|
+
import { createContext, VALID_TYPES } from "../context.js";
|
|
9
|
+
import { runFull } from "./full.js";
|
|
10
|
+
import { runVersion } from "./version.js";
|
|
11
|
+
import { runWorkflows } from "./workflows.js";
|
|
12
|
+
import { runIssues } from "./issues.js";
|
|
13
|
+
import * as prompts from "../ui/prompts.js";
|
|
14
|
+
|
|
15
|
+
const CANCEL = prompts.CANCEL;
|
|
16
|
+
|
|
17
|
+
// io 기본값 = 실제 prompts. 테스트는 스텁 io 주입.
|
|
18
|
+
export async function runInteractive(baseCtx, { cwd = process.cwd(), source = { type: "git" }, clock, io = prompts } = {}) {
|
|
19
|
+
io.intro?.("projectops — 대화형 통합 마법사");
|
|
20
|
+
|
|
21
|
+
// 1) 모드 선택
|
|
22
|
+
const mode = await io.selectMode();
|
|
23
|
+
if (mode === CANCEL || mode == null) { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
|
|
24
|
+
|
|
25
|
+
// skills는 SP2-D 예정
|
|
26
|
+
if (mode === "skills") {
|
|
27
|
+
io.note?.("AI 스킬 설치는 아직 준비 중입니다 (곧 제공). 기존 template_integrator를 사용하세요.", "안내");
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const tempDir = join(cwd, PATHS.tempDir);
|
|
32
|
+
try {
|
|
33
|
+
acquireTemplate({ tempDir, source });
|
|
34
|
+
const templateVersion = readTemplateVersion(tempDir);
|
|
35
|
+
|
|
36
|
+
// issues 모드는 정보 수집 없이 바로 실행
|
|
37
|
+
if (mode === "issues") {
|
|
38
|
+
const ctx = createContext({ ...baseCtx, mode, force: true });
|
|
39
|
+
runIssues(ctx, tempDir, cwd);
|
|
40
|
+
io.outro?.("이슈·PR 템플릿을 설치했습니다.");
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// full/version/workflows — 감지
|
|
45
|
+
let types = detectTypes(cwd);
|
|
46
|
+
let version = detectVersion(cwd);
|
|
47
|
+
let branch = detectDefaultBranch(cwd);
|
|
48
|
+
const repoName = detectRepoName(cwd);
|
|
49
|
+
let includeNexus = false, includeSecretBackup = false;
|
|
50
|
+
const showOptional = mode === "full" || mode === "workflows";
|
|
51
|
+
|
|
52
|
+
// 확인/수정 루프
|
|
53
|
+
let confirmed = false;
|
|
54
|
+
while (!confirmed) {
|
|
55
|
+
io.note?.(summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional }), "프로젝트 분석 결과");
|
|
56
|
+
const choice = await io.confirmProjectMenu();
|
|
57
|
+
if (choice === CANCEL || choice === "cancel") { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
|
|
58
|
+
if (choice === "continue") { confirmed = true; break; }
|
|
59
|
+
// edit 루프
|
|
60
|
+
let editing = true;
|
|
61
|
+
while (editing) {
|
|
62
|
+
const what = await io.editMenu({ showOptional });
|
|
63
|
+
if (what === CANCEL || what === "done") { editing = false; break; }
|
|
64
|
+
if (what === "type") {
|
|
65
|
+
const t = await io.selectTypes(types);
|
|
66
|
+
if (t !== CANCEL && Array.isArray(t) && t.length) types = t;
|
|
67
|
+
} else if (what === "version") {
|
|
68
|
+
const v = await io.askText("새 버전 (예: 1.0.0)", version);
|
|
69
|
+
if (v !== CANCEL) version = v;
|
|
70
|
+
} else if (what === "branch") {
|
|
71
|
+
const b = await io.askText("기본 브랜치", branch);
|
|
72
|
+
if (b !== CANCEL) branch = b;
|
|
73
|
+
} else if (what === "nexus") {
|
|
74
|
+
const y = await io.askYesNo("Nexus publish 워크플로우를 포함할까요?", includeNexus);
|
|
75
|
+
if (y !== CANCEL) includeNexus = y;
|
|
76
|
+
} else if (what === "secret") {
|
|
77
|
+
const y = await io.askYesNo("Secret 백업 워크플로우를 포함할까요?", includeSecretBackup);
|
|
78
|
+
if (y !== CANCEL) includeSecretBackup = y;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 경로: 미지정 타입은 루트(basic 제외)
|
|
84
|
+
const paths = new Map();
|
|
85
|
+
for (const t of types) if (t !== "basic") paths.set(t, ".");
|
|
86
|
+
|
|
87
|
+
const { now, today } = clock || utcNow();
|
|
88
|
+
const ctx = createContext({
|
|
89
|
+
mode, force: true, types, version, branch, paths, includeNexus, includeSecretBackup, repoName, templateVersion,
|
|
90
|
+
resolvers: {
|
|
91
|
+
repo: () => repoName, "spring-app-yml-dir": () => "", "spring-app-yml-path": () => "",
|
|
92
|
+
"flutter-root": () => paths.get("flutter") || ".",
|
|
93
|
+
},
|
|
94
|
+
now, today,
|
|
95
|
+
});
|
|
96
|
+
ctx.templateVersion = templateVersion;
|
|
97
|
+
|
|
98
|
+
if (mode === "full") runFull(ctx, tempDir, cwd);
|
|
99
|
+
else if (mode === "version") runVersion(ctx, tempDir, cwd);
|
|
100
|
+
else if (mode === "workflows") runWorkflows(ctx, tempDir, cwd);
|
|
101
|
+
|
|
102
|
+
io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
|
|
103
|
+
return 0;
|
|
104
|
+
} finally {
|
|
105
|
+
remove(tempDir);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional }) {
|
|
110
|
+
const lines = [
|
|
111
|
+
`통합 모드 : ${modeLabel(mode)}`,
|
|
112
|
+
`프로젝트 타입 : ${types.join(", ")}${types.length > 1 ? " (멀티)" : ""}`,
|
|
113
|
+
`버전 : ${version}`,
|
|
114
|
+
`기본 브랜치 : ${branch}`,
|
|
115
|
+
];
|
|
116
|
+
if (showOptional) {
|
|
117
|
+
lines.push(`Nexus publish : ${includeNexus ? "포함" : "제외"}`);
|
|
118
|
+
lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
|
|
119
|
+
}
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function modeLabel(m) {
|
|
124
|
+
return { full: "전체 설치", version: "버전 관리만", workflows: "워크플로우만", issues: "이슈·PR 템플릿만", skills: "AI 스킬만" }[m] || m;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function utcNow(date = new Date()) {
|
|
128
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
129
|
+
const d = `${date.getUTCFullYear()}-${p(date.getUTCMonth() + 1)}-${p(date.getUTCDate())}`;
|
|
130
|
+
const t = `${p(date.getUTCHours())}:${p(date.getUTCMinutes())}:${p(date.getUTCSeconds())}`;
|
|
131
|
+
return { now: `${d} ${t}`, today: d };
|
|
132
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// issues 모드 (.sh execute_integration issues case 등가) — template_integrator.sh 4532~4535.
|
|
2
|
+
// 이슈/PR 템플릿 + Discussion 템플릿만 복사.
|
|
3
|
+
import { copyIssueTemplates, copyDiscussionTemplates } from "../core/copy/simple.js";
|
|
4
|
+
|
|
5
|
+
export function runIssues(context, tempDir, targetRoot = ".") {
|
|
6
|
+
copyIssueTemplates(tempDir, targetRoot);
|
|
7
|
+
copyDiscussionTemplates(tempDir, targetRoot);
|
|
8
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// version 모드 (.sh execute_integration version case 등가) — template_integrator.sh 4517~4530.
|
|
2
|
+
// 순서: version.yml → readme → scripts → config → gitignore → setup_guide.
|
|
3
|
+
// (워크플로우 미복사 → deploy 블록 없음. util·issue·coderabbit도 없음.)
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { writeText } from "../core/fsutil.js";
|
|
6
|
+
import { PATHS } from "../core/paths.js";
|
|
7
|
+
import { buildVersionYml } from "../core/version-yml.js";
|
|
8
|
+
import { markerForType } from "../core/detect.js";
|
|
9
|
+
import { addVersionSectionToReadme } from "../core/copy/readme.js";
|
|
10
|
+
import { copyScripts, copyConfigFolder, copySetupGuide } from "../core/copy/simple.js";
|
|
11
|
+
import { ensureGitignore } from "../core/copy/gitignore.js";
|
|
12
|
+
|
|
13
|
+
export function runVersion(context, tempDir, targetRoot = ".") {
|
|
14
|
+
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
15
|
+
now, today, templateVersion = "unknown", includeNexus = false, includeSecretBackup = false } = context;
|
|
16
|
+
|
|
17
|
+
const pathMarkers = new Map();
|
|
18
|
+
for (const [t] of paths) pathMarkers.set(t, markerForType(t));
|
|
19
|
+
|
|
20
|
+
writeText(join(targetRoot, PATHS.versionFile),
|
|
21
|
+
buildVersionYml({
|
|
22
|
+
version, types, paths, pathMarkers, branch, versionCode, now, today,
|
|
23
|
+
templateOptions: { templateVersion, includeNexus, includeSecretBackup, optionsDate: today },
|
|
24
|
+
}));
|
|
25
|
+
addVersionSectionToReadme(version, targetRoot);
|
|
26
|
+
copyScripts(tempDir, targetRoot);
|
|
27
|
+
copyConfigFolder(tempDir, targetRoot);
|
|
28
|
+
ensureGitignore(targetRoot);
|
|
29
|
+
copySetupGuide(tempDir, targetRoot);
|
|
30
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// workflows 모드 (.sh execute_integration workflows case 등가) — template_integrator.sh 4523~4531.
|
|
2
|
+
// 순서: copy_workflows → update_version_yml_deploy(version.yml 있을 때만) → scripts → config → util(타입별) → setup_guide.
|
|
3
|
+
// (version.yml 생성 안 함 — 기존 version.yml이 있을 때만 deploy 블록 추가.)
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { PATHS } from "../core/paths.js";
|
|
7
|
+
import { copyWorkflows } from "../core/copy/workflows.js";
|
|
8
|
+
import { copyScripts, copyConfigFolder, copySetupGuide } from "../core/copy/simple.js";
|
|
9
|
+
import { copyUtilModules } from "../core/copy/util.js";
|
|
10
|
+
|
|
11
|
+
export function runWorkflows(context, tempDir, targetRoot = ".") {
|
|
12
|
+
const { types = [], force = true } = context;
|
|
13
|
+
const wf = copyWorkflows(context, tempDir, targetRoot);
|
|
14
|
+
|
|
15
|
+
// update_version_yml_deploy: 기존 version.yml이 있고 ask 값이 있을 때만 deploy 블록 갱신
|
|
16
|
+
const vy = join(targetRoot, PATHS.versionFile);
|
|
17
|
+
if (existsSync(vy) && wf.deployValues && wf.deployValues.size) {
|
|
18
|
+
writeFileSync(vy, upsertDeployBlock(readFileSync(vy, "utf8"), wf.deployValues));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
copyScripts(tempDir, targetRoot);
|
|
22
|
+
copyConfigFolder(tempDir, targetRoot);
|
|
23
|
+
for (const t of types) copyUtilModules(tempDir, t, { force }, targetRoot);
|
|
24
|
+
copySetupGuide(tempDir, targetRoot);
|
|
25
|
+
return { workflows: wf };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 기존 version.yml에서 deploy: 블록을 제거하고 새로 append (.sh update_version_yml_deploy 멱등).
|
|
29
|
+
export function upsertDeployBlock(content, deployValues) {
|
|
30
|
+
// 기존 deploy: 블록 제거 (deploy: 라인 ~ 다음 최상위 키 전까지)
|
|
31
|
+
const lines = content.split(/\r?\n/);
|
|
32
|
+
const out = [];
|
|
33
|
+
let inDeploy = false;
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
if (/^deploy:/.test(line)) { inDeploy = true; continue; }
|
|
36
|
+
if (inDeploy) {
|
|
37
|
+
if (/^\s/.test(line) || line === "") continue; // 들여쓰기/빈줄 = deploy 내부
|
|
38
|
+
inDeploy = false;
|
|
39
|
+
}
|
|
40
|
+
out.push(line);
|
|
41
|
+
}
|
|
42
|
+
let text = out.join("\n").replace(/\n+$/, "\n");
|
|
43
|
+
// 새 deploy 블록 append
|
|
44
|
+
const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t)?.size);
|
|
45
|
+
if (deployTypes.length) {
|
|
46
|
+
text += `\ndeploy: # 마법사가 기억하는 배포 설정 (비민감 / 직접 수정 가능)\n`;
|
|
47
|
+
for (const t of deployTypes) {
|
|
48
|
+
text += ` ${t}:\n`;
|
|
49
|
+
for (const [k, v] of deployValues.get(t)) text += ` ${k}: "${v}"\n`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return text;
|
|
53
|
+
}
|
package/src/context.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// 마법사 전역 상태를 하나의 객체로 명시화 (bash 전역 변수군 대체)
|
|
2
|
+
export const VALID_TYPES = [
|
|
3
|
+
"spring", "flutter", "next", "react",
|
|
4
|
+
"react-native", "react-native-expo", "node", "python", "basic",
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_VERSION = "1.3.14"; // .sh DEFAULT_VERSION (배너 폴백용 — breaking 비교엔 안 씀)
|
|
8
|
+
|
|
9
|
+
export function createContext(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
mode: "interactive",
|
|
12
|
+
force: false,
|
|
13
|
+
types: [],
|
|
14
|
+
version: "",
|
|
15
|
+
branch: "",
|
|
16
|
+
paths: new Map(), // type -> path
|
|
17
|
+
includeNexus: null, // null=미설정, true/false=명시
|
|
18
|
+
includeSecretBackup: null,
|
|
19
|
+
templateVersion: "",
|
|
20
|
+
tempDir: "",
|
|
21
|
+
deployValues: new Map(), // "type.KEY" -> value
|
|
22
|
+
counters: {},
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 템플릿 자산 획득 + 제외 적용 (.sh download_template 등가) — template_integrator.sh 2064~2141.
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { exists, readText, copyDirSync, remove } from "./fsutil.js";
|
|
5
|
+
import { DOCS_TO_REMOVE, PLUGIN_ITEMS_TO_REMOVE } from "./exclusions.js";
|
|
6
|
+
import { DEFAULT_VERSION } from "../context.js";
|
|
7
|
+
import { TEMPLATE_REPO } from "./paths.js";
|
|
8
|
+
|
|
9
|
+
// 템플릿을 tempDir로 획득한다.
|
|
10
|
+
// source:
|
|
11
|
+
// {type:'git', repo?} → git clone --depth 1 (기본 repo=TEMPLATE_REPO)
|
|
12
|
+
// {type:'local', path} → 로컬 트리 복사 (네트워크 없는 등가 검증용)
|
|
13
|
+
// 이미 tempDir/.github 있으면 스킵(.sh 중복 호출 방지). 획득 후 applyExclusions 자동 호출.
|
|
14
|
+
export function acquireTemplate({ tempDir, source = { type: "git" } }) {
|
|
15
|
+
if (exists(join(tempDir, ".github"))) {
|
|
16
|
+
applyExclusions(tempDir);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
remove(tempDir);
|
|
20
|
+
if (source.type === "local") {
|
|
21
|
+
copyDirSync(source.path, tempDir);
|
|
22
|
+
} else {
|
|
23
|
+
const repo = source.repo || TEMPLATE_REPO;
|
|
24
|
+
execFileSync("git", ["clone", "--depth", "1", "--quiet", repo, tempDir], { stdio: "ignore" });
|
|
25
|
+
}
|
|
26
|
+
applyExclusions(tempDir);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// DOCS_TO_REMOVE + PLUGIN_ITEMS_TO_REMOVE 삭제. skills/ 는 보존(.sh 주석).
|
|
30
|
+
export function applyExclusions(tempDir) {
|
|
31
|
+
for (const doc of DOCS_TO_REMOVE) remove(join(tempDir, doc));
|
|
32
|
+
for (const item of PLUGIN_ITEMS_TO_REMOVE) remove(join(tempDir, item));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// tempDir/version.yml 의 ^version: 값. 없으면 DEFAULT_VERSION.
|
|
36
|
+
export function readTemplateVersion(tempDir) {
|
|
37
|
+
const p = join(tempDir, "version.yml");
|
|
38
|
+
if (!exists(p)) return DEFAULT_VERSION;
|
|
39
|
+
for (const line of readText(p).split("\n")) {
|
|
40
|
+
const m = line.match(/^version:\s*["']?([^"'\s]+)["']?/);
|
|
41
|
+
if (m) return m[1];
|
|
42
|
+
}
|
|
43
|
+
return DEFAULT_VERSION;
|
|
44
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// .sh compare_versions 등가: v 접두 제거, 3자리 숫자 비교, 누락 자리=0
|
|
2
|
+
export function compareVersions(a, b) {
|
|
3
|
+
const parse = (v) => String(v).replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
|
|
4
|
+
const pa = parse(a), pb = parse(b);
|
|
5
|
+
for (let i = 0; i < 3; i++) {
|
|
6
|
+
const x = pa[i] ?? 0, y = pb[i] ?? 0;
|
|
7
|
+
if (x > y) return 1;
|
|
8
|
+
if (x < y) return -1;
|
|
9
|
+
}
|
|
10
|
+
return 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// breaking-changes.json에서 current < ver <= target 범위 항목 수집.
|
|
14
|
+
// ⚠️ .sh 버그 수정: target은 하드코딩 1.3.14가 아니라 실제 templateVersion을 넘긴다 (D2).
|
|
15
|
+
// _ 로 시작하는 키(메타) 제외. severity critical / 그 외(warning).
|
|
16
|
+
export function collectBreaking(json, current, target) {
|
|
17
|
+
const critical = [], warnings = [];
|
|
18
|
+
for (const [ver, entry] of Object.entries(json || {})) {
|
|
19
|
+
if (ver.startsWith("_")) continue;
|
|
20
|
+
if (compareVersions(current, ver) < 0 && compareVersions(ver, target) <= 0) {
|
|
21
|
+
const rec = { version: ver, ...entry };
|
|
22
|
+
(entry?.severity === "critical" ? critical : warnings).push(rec);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { critical, warnings };
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// .coderabbit.yaml 복사 (.sh copy_coderabbit_config 등가, force 경로) — template_integrator.sh 3938~3990.
|
|
2
|
+
// SP2-B는 force/비대화형 경로만 구현. 대화형 덮어쓰기/건너뛰기 메뉴는 SP2-C.
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { exists, copyFileSync } from "../fsutil.js";
|
|
5
|
+
|
|
6
|
+
// 반환: 'skip-no-src' | 'copied-new' | 'overwritten-backup' | 'skip-non-tty'
|
|
7
|
+
// opts: { force, tty } — SP2-B 검증은 force:true 경로.
|
|
8
|
+
export function copyCoderabbit(tempDir, { force = false, tty = false } = {}, targetRoot = ".") {
|
|
9
|
+
const src = join(tempDir, ".coderabbit.yaml");
|
|
10
|
+
if (!exists(src)) return "skip-no-src";
|
|
11
|
+
const dst = join(targetRoot, ".coderabbit.yaml");
|
|
12
|
+
|
|
13
|
+
if (exists(dst)) {
|
|
14
|
+
if (force) {
|
|
15
|
+
copyFileSync(dst, dst + ".bak"); // 백업 후 덮어쓰기
|
|
16
|
+
copyFileSync(src, dst);
|
|
17
|
+
return "overwritten-backup";
|
|
18
|
+
}
|
|
19
|
+
if (!tty) return "skip-non-tty"; // 비TTY & !force → 유지
|
|
20
|
+
// 대화형 메뉴는 SP2-C — 여기서는 force가 아니면 유지
|
|
21
|
+
return "skip-non-tty";
|
|
22
|
+
}
|
|
23
|
+
copyFileSync(src, dst); // 신규
|
|
24
|
+
return "copied-new";
|
|
25
|
+
}
|