projectops 3.0.193 → 3.0.195

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.
Files changed (41) hide show
  1. package/README.md +1 -1
  2. package/bin/projectops.js +7 -34
  3. package/package.json +7 -2
  4. package/src/cli/args.js +84 -0
  5. package/src/cli/help.js +23 -0
  6. package/src/commands/full.js +64 -0
  7. package/src/commands/interactive.js +135 -0
  8. package/src/commands/issues.js +8 -0
  9. package/src/commands/skills.js +82 -0
  10. package/src/commands/version.js +30 -0
  11. package/src/commands/workflows.js +53 -0
  12. package/src/context.js +25 -0
  13. package/src/core/assets.js +44 -0
  14. package/src/core/breaking.js +26 -0
  15. package/src/core/copy/coderabbit.js +25 -0
  16. package/src/core/copy/gitignore.js +55 -0
  17. package/src/core/copy/readme.js +30 -0
  18. package/src/core/copy/simple.js +55 -0
  19. package/src/core/copy/util.js +22 -0
  20. package/src/core/copy/workflows.js +145 -0
  21. package/src/core/detect-fs.js +64 -0
  22. package/src/core/detect.js +62 -0
  23. package/src/core/exclusions.js +25 -0
  24. package/src/core/fsutil.js +40 -0
  25. package/src/core/ide/adapter.js +36 -0
  26. package/src/core/ide/adapters/claude.js +94 -0
  27. package/src/core/ide/adapters/codex.js +49 -0
  28. package/src/core/ide/adapters/cursor.js +76 -0
  29. package/src/core/ide/adapters/gemini.js +34 -0
  30. package/src/core/ide/adapters/pi-common.js +63 -0
  31. package/src/core/ide/adapters/pi-harness.js +48 -0
  32. package/src/core/ide/adapters/pi.js +45 -0
  33. package/src/core/ide/registry.js +27 -0
  34. package/src/core/ide/runner.js +59 -0
  35. package/src/core/ide/util.js +18 -0
  36. package/src/core/paths.js +14 -0
  37. package/src/core/version-yml.js +146 -0
  38. package/src/core/wizard-env.js +101 -0
  39. package/src/index.js +108 -0
  40. package/src/ui/prompts.js +82 -0
  41. package/src/ui/skills-prompts.js +37 -0
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  > 이슈 등록부터 커밋, 보고서, 배포까지. 개발자는 코드만 작성하세요.
8
8
 
9
9
  <!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
10
- ## 최신 버전 : v3.0.192 (2026-07-08)
10
+ ## 최신 버전 : v3.0.194 (2026-07-08)
11
11
 
12
12
  [전체 버전 기록 보기](CHANGELOG.md)
13
13
 
package/bin/projectops.js CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
- // projectops 스텁 CLI — 이름 선점 및 배포 파이프라인 검증용 (SP1)
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 pkg = JSON.parse(
15
- readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"),
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 args = process.argv.slice(2);
19
- if (args.includes("--version") || args.includes("-v")) {
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.193",
3
+ "version": "3.0.195",
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
  }
@@ -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
+ }
@@ -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,135 @@
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 { runSkills } from "./skills.js";
14
+ import * as prompts from "../ui/prompts.js";
15
+
16
+ const CANCEL = prompts.CANCEL;
17
+
18
+ // io 기본값 = 실제 prompts. 테스트는 스텁 io 주입.
19
+ // skills = runSkills 주입 지점(테스트가 실제 IDE CLI를 안 건드리게). 기본은 실제 runSkills.
20
+ export async function runInteractive(baseCtx, { cwd = process.cwd(), source = { type: "git" }, clock, io = prompts, skills = runSkills } = {}) {
21
+ io.intro?.("projectops — 대화형 통합 마법사");
22
+
23
+ // 1) 모드 선택
24
+ const mode = await io.selectMode();
25
+ if (mode === CANCEL || mode == null) { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
26
+
27
+ const tempDir = join(cwd, PATHS.tempDir);
28
+ try {
29
+ acquireTemplate({ tempDir, source });
30
+ const templateVersion = readTemplateVersion(tempDir);
31
+
32
+ // skills 모드 — IDE 스킬 설치 (템플릿 통합 없음). 대화형으로 실행.
33
+ if (mode === "skills") {
34
+ await skills({ templateVersion, tempDir, interactive: true });
35
+ io.outro?.("AI 스킬 설치를 마쳤습니다.");
36
+ return 0;
37
+ }
38
+
39
+ // issues 모드는 정보 수집 없이 바로 실행
40
+ if (mode === "issues") {
41
+ const ctx = createContext({ ...baseCtx, mode, force: true });
42
+ runIssues(ctx, tempDir, cwd);
43
+ io.outro?.("이슈·PR 템플릿을 설치했습니다.");
44
+ return 0;
45
+ }
46
+
47
+ // full/version/workflows — 감지
48
+ let types = detectTypes(cwd);
49
+ let version = detectVersion(cwd);
50
+ let branch = detectDefaultBranch(cwd);
51
+ const repoName = detectRepoName(cwd);
52
+ let includeNexus = false, includeSecretBackup = false;
53
+ const showOptional = mode === "full" || mode === "workflows";
54
+
55
+ // 확인/수정 루프
56
+ let confirmed = false;
57
+ while (!confirmed) {
58
+ io.note?.(summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional }), "프로젝트 분석 결과");
59
+ const choice = await io.confirmProjectMenu();
60
+ if (choice === CANCEL || choice === "cancel") { io.cancelMessage?.("설치를 취소했습니다."); return 0; }
61
+ if (choice === "continue") { confirmed = true; break; }
62
+ // edit 루프
63
+ let editing = true;
64
+ while (editing) {
65
+ const what = await io.editMenu({ showOptional });
66
+ if (what === CANCEL || what === "done") { editing = false; break; }
67
+ if (what === "type") {
68
+ const t = await io.selectTypes(types);
69
+ if (t !== CANCEL && Array.isArray(t) && t.length) types = t;
70
+ } else if (what === "version") {
71
+ const v = await io.askText("새 버전 (예: 1.0.0)", version);
72
+ if (v !== CANCEL) version = v;
73
+ } else if (what === "branch") {
74
+ const b = await io.askText("기본 브랜치", branch);
75
+ if (b !== CANCEL) branch = b;
76
+ } else if (what === "nexus") {
77
+ const y = await io.askYesNo("Nexus publish 워크플로우를 포함할까요?", includeNexus);
78
+ if (y !== CANCEL) includeNexus = y;
79
+ } else if (what === "secret") {
80
+ const y = await io.askYesNo("Secret 백업 워크플로우를 포함할까요?", includeSecretBackup);
81
+ if (y !== CANCEL) includeSecretBackup = y;
82
+ }
83
+ }
84
+ }
85
+
86
+ // 경로: 미지정 타입은 루트(basic 제외)
87
+ const paths = new Map();
88
+ for (const t of types) if (t !== "basic") paths.set(t, ".");
89
+
90
+ const { now, today } = clock || utcNow();
91
+ const ctx = createContext({
92
+ mode, force: true, types, version, branch, paths, includeNexus, includeSecretBackup, repoName, templateVersion,
93
+ resolvers: {
94
+ repo: () => repoName, "spring-app-yml-dir": () => "", "spring-app-yml-path": () => "",
95
+ "flutter-root": () => paths.get("flutter") || ".",
96
+ },
97
+ now, today,
98
+ });
99
+ ctx.templateVersion = templateVersion;
100
+
101
+ if (mode === "full") runFull(ctx, tempDir, cwd);
102
+ else if (mode === "version") runVersion(ctx, tempDir, cwd);
103
+ else if (mode === "workflows") runWorkflows(ctx, tempDir, cwd);
104
+
105
+ io.outro?.(`통합 완료 — ${mode} 모드로 설치했습니다.`);
106
+ return 0;
107
+ } finally {
108
+ remove(tempDir);
109
+ }
110
+ }
111
+
112
+ function summarize({ mode, types, version, branch, includeNexus, includeSecretBackup, showOptional }) {
113
+ const lines = [
114
+ `통합 모드 : ${modeLabel(mode)}`,
115
+ `프로젝트 타입 : ${types.join(", ")}${types.length > 1 ? " (멀티)" : ""}`,
116
+ `버전 : ${version}`,
117
+ `기본 브랜치 : ${branch}`,
118
+ ];
119
+ if (showOptional) {
120
+ lines.push(`Nexus publish : ${includeNexus ? "포함" : "제외"}`);
121
+ lines.push(`Secret 백업 : ${includeSecretBackup ? "포함" : "제외"}`);
122
+ }
123
+ return lines.join("\n");
124
+ }
125
+
126
+ function modeLabel(m) {
127
+ return { full: "전체 설치", version: "버전 관리만", workflows: "워크플로우만", issues: "이슈·PR 템플릿만", skills: "AI 스킬만" }[m] || m;
128
+ }
129
+
130
+ function utcNow(date = new Date()) {
131
+ const p = (n) => String(n).padStart(2, "0");
132
+ const d = `${date.getUTCFullYear()}-${p(date.getUTCMonth() + 1)}-${p(date.getUTCDate())}`;
133
+ const t = `${p(date.getUTCHours())}:${p(date.getUTCMinutes())}:${p(date.getUTCSeconds())}`;
134
+ return { now: `${d} ${t}`, today: d };
135
+ }
@@ -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,82 @@
1
+ // IDE Skills 설치 오케스트레이터 (.sh offer_ide_tools_install 등가).
2
+ // 레지스트리 기반 — 개별 IDE를 이름으로 알지 않고 ADAPTERS 배열만 순회한다.
3
+ // 새 IDE는 adapters/ 에 파일 추가 + registry 등록만으로 여기 수정 없이 동작한다.
4
+ import { ADAPTERS } from "../core/ide/registry.js";
5
+ import { versionTag } from "../core/ide/util.js";
6
+ import { defaultIo } from "../core/ide/runner.js";
7
+ import * as skillsPrompts from "../ui/skills-prompts.js";
8
+
9
+ // 상태 수집 → [{adapter, status}]. optional 어댑터는 감지 불가(cliMissing)면 목록에서 제외.
10
+ export function collectStatuses(io) {
11
+ const rows = [];
12
+ for (const a of ADAPTERS) {
13
+ const status = safe(() => a.detect(io), { installed: false, version: null, cliMissing: true });
14
+ if (a.optional && status.cliMissing) continue; // harness 등 조건부 항목은 불가 시 숨김
15
+ rows.push({ adapter: a, status });
16
+ }
17
+ return rows;
18
+ }
19
+
20
+ // 상태 표시 문자열 목록 (표시용, 순수).
21
+ export function formatStatuses(rows, templateVersion) {
22
+ return rows.map(({ adapter, status }) => {
23
+ const label = adapter.label.padEnd(12);
24
+ if (status.cliMissing) return `${label}: ${status.note || "skill 미설치 (CLI 없음)"}`;
25
+ if (status.installed) {
26
+ const v = status.version ? ` (v${status.version})` : "";
27
+ return `${label}: skill 설치됨${v}${versionTag(status.version, templateVersion)}`;
28
+ }
29
+ return `${label}: ${status.note || "skill 미설치"}`;
30
+ });
31
+ }
32
+
33
+ // 대화형/비대화형 공통 진입. opts: {io, templateVersion, sourceSkillsDir, tempDir, interactive, ui}
34
+ export async function runSkills(opts = {}) {
35
+ const io = opts.io || defaultIo();
36
+ const ui = opts.ui || skillsPrompts;
37
+ const ctx = { templateVersion: opts.templateVersion || "", sourceSkillsDir: opts.sourceSkillsDir, tempDir: opts.tempDir };
38
+
39
+ const rows = collectStatuses(io);
40
+ io.log("");
41
+ io.log("── IDE Skills 현재 상태 ──");
42
+ for (const line of formatStatuses(rows, ctx.templateVersion)) io.log(line);
43
+ io.log("");
44
+
45
+ // 비대화형: 감지된(=관리 가능) 모든 어댑터에 apply(설치/업데이트) 순차 실행.
46
+ if (!opts.interactive) {
47
+ for (const { adapter } of rows) safe(() => adapter.apply(io, ctx), false);
48
+ return 0;
49
+ }
50
+
51
+ // 대화형: 동작 선택 → IDE 멀티셀렉트 → 실행.
52
+ const action = await ui.selectAction();
53
+ if (action == null || action === skillsPrompts.CANCEL || action === "skip") {
54
+ io.log("IDE Skills는 변경하지 않고 넘어갑니다.");
55
+ return 0;
56
+ }
57
+
58
+ const choices = rows.map(({ adapter, status }) => ({
59
+ id: adapter.id, label: adapter.label,
60
+ disabled: action === "apply" && status.cliMissing, // 설치는 CLI 있어야, 제거는 무조건 후보
61
+ }));
62
+ const preselect = rows
63
+ .filter(({ status }) => action === "apply" ? !status.cliMissing : true)
64
+ .map(({ adapter }) => adapter.id);
65
+
66
+ const targets = await ui.selectTargets(choices, preselect, action);
67
+ if (targets == null || targets === skillsPrompts.CANCEL || !targets.length) {
68
+ io.log("선택한 IDE가 없어 건너뜁니다 (원할 때 다시 실행하세요).");
69
+ return 0;
70
+ }
71
+
72
+ const selected = new Set(targets);
73
+ for (const { adapter } of rows) {
74
+ if (!selected.has(adapter.id)) continue;
75
+ io.log("");
76
+ io.log(`[ ${adapter.label} ${action === "apply" ? "설치/업데이트" : "제거"} ]`);
77
+ safe(() => (action === "apply" ? adapter.apply(io, ctx) : adapter.remove(io, ctx)), false);
78
+ }
79
+ return 0;
80
+ }
81
+
82
+ function safe(fn, fallback) { try { return fn(); } catch { return fallback; } }
@@ -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
+ }