projectops 4.2.5 → 4.2.7
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 +9 -10
- package/package.json +1 -1
- package/src/core/ide/adapters/claude.js +28 -1
- package/src/core/ide/adapters/codex.js +16 -0
- package/src/core/ide/adapters/cursor.js +42 -3
- package/src/core/ide/adapters/gemini.js +10 -0
- package/src/core/ide/adapters/pi-common.js +22 -1
- package/src/core/ide/adapters/pi.js +4 -1
- package/src/core/ide/legacy.js +42 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
> 이슈 등록부터 커밋, 보고서, 배포까지. 개발자는 코드만 작성하세요.
|
|
8
8
|
|
|
9
9
|
<!-- AUTO-VERSION-SECTION: DO NOT EDIT MANUALLY -->
|
|
10
|
-
## 최신 버전 : v4.2.
|
|
10
|
+
## 최신 버전 : v4.2.6 (2026-07-10)
|
|
11
11
|
|
|
12
12
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
13
13
|
|
|
@@ -20,17 +20,17 @@
|
|
|
20
20
|
이 프로젝트는 두 축으로 개발 워크플로우를 자동화합니다.
|
|
21
21
|
|
|
22
22
|
**① GitHub Actions** — main 푸시 한 번으로 버전 관리, 체인지로그, CI/CD 배포까지 자동 처리
|
|
23
|
-
**② Agent Skills** — `/
|
|
23
|
+
**② Agent Skills** — `/github`, `/commit`, `/report` 등 AI가 이슈 작성부터 커밋 메시지, 구현 보고서까지 대신 생성
|
|
24
24
|
|
|
25
25
|
| 기존 방식 | Projectops |
|
|
26
26
|
|----------|---------------------|
|
|
27
27
|
| 버전 수동 관리, 태그 직접 생성 | 릴리스(develop→main PR) 시 patch 버전 자동 증가 + 태그 생성 |
|
|
28
28
|
| 체인지로그 직접 작성 (30분+) | CodeRabbit AI가 PR마다 자동 생성 |
|
|
29
29
|
| CI/CD 처음부터 설정 | 프로젝트 타입별 워크플로우 즉시 구성 |
|
|
30
|
-
| 이슈 매번 형식 맞춰 작성 (5분+) | `/pro-
|
|
30
|
+
| 이슈 매번 형식 맞춰 작성 (5분+) | `/pro-github` 한 번에 표준 템플릿 생성 + 등록 |
|
|
31
31
|
| 커밋 메시지 이슈 URL 수동 복사 | `/pro-commit` 이슈 컨텍스트 기반 자동 완성 |
|
|
32
32
|
| PR 설명/보고서 직접 작성 | `/pro-report` git diff 분석 후 자동 생성 |
|
|
33
|
-
| 코드 리뷰·분석 매번 프롬프트 입력 |
|
|
33
|
+
| 코드 리뷰·분석 매번 프롬프트 입력 | 24종 Skills로 일관된 결과, 매번 재입력 불필요 |
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -40,7 +40,7 @@ Agent Skills가 개발 사이클 전체를 커버합니다.
|
|
|
40
40
|
|
|
41
41
|
```mermaid
|
|
42
42
|
flowchart TD
|
|
43
|
-
A([작업 시작]) --> B["/pro-
|
|
43
|
+
A([작업 시작]) --> B["/pro-github\n이슈 등록 + GitHub 자동 생성"]
|
|
44
44
|
B --> C["/pro-init-worktree\nworktree + 민감파일 자동 복사"]
|
|
45
45
|
C --> D{작업 유형}
|
|
46
46
|
|
|
@@ -154,7 +154,7 @@ bash <(curl -fsSL "https://raw.githubusercontent.com/Cassiiopeia/projectops/main
|
|
|
154
154
|
|
|
155
155
|
| 기능 | 설명 | 문서 |
|
|
156
156
|
|------|------|------|
|
|
157
|
-
| **Agent Skills** | Claude Code, Cursor, Gemini CLI, Codex CLI에서 쓰는
|
|
157
|
+
| **Agent Skills** | Claude Code, Cursor, Gemini CLI, Codex CLI에서 쓰는 24종 AI DevOps Skills | [상세](docs/SKILLS.md) |
|
|
158
158
|
| **버전 자동화** | 릴리스(develop→main PR) 시 patch 버전 자동 증가 + Git 태그 | [상세](docs/VERSION-CONTROL.md) |
|
|
159
159
|
| **AI 체인지로그** | CodeRabbit 리뷰 기반 CHANGELOG 자동 생성 | [상세](docs/CHANGELOG-AUTOMATION.md) |
|
|
160
160
|
| **PR Preview** | 댓글 한 줄로 임시 서버 배포, 닫으면 자동 삭제 | [상세](docs/PR-PREVIEW.md) |
|
|
@@ -165,18 +165,17 @@ bash <(curl -fsSL "https://raw.githubusercontent.com/Cassiiopeia/projectops/main
|
|
|
165
165
|
|
|
166
166
|
---
|
|
167
167
|
|
|
168
|
-
## Agent Skills (
|
|
168
|
+
## Agent Skills (24종)
|
|
169
169
|
|
|
170
170
|
### 🔄 개발 사이클 자동화
|
|
171
171
|
|
|
172
172
|
| 스킬 | 용도 |
|
|
173
173
|
|------|------|
|
|
174
|
-
| `/pro-issue` | 설명 한 줄 → GitHub 이슈 템플릿 자동 작성 + 등록 |
|
|
175
174
|
| `/pro-init-worktree` | Git worktree 생성 + 민감 파일 자동 복사 |
|
|
176
175
|
| `/pro-commit` | 이슈 컨텍스트 기반 커밋 메시지 자동 완성 (superpowers 준수) |
|
|
177
176
|
| `/pro-report` | git diff 분석 → 구현 보고서 생성 + GitHub 댓글 자동 포스팅 |
|
|
178
177
|
| `/pro-changelog-deploy` | develop push → main 릴리스 PR 생성 + 릴리스 노트 작성 + automerge |
|
|
179
|
-
| `/pro-github` | GitHub
|
|
178
|
+
| `/pro-github` | GitHub 전반: 이슈 생성(설명 한 줄 → 템플릿 작성+등록)/조회/수정/댓글/라벨/담당자, PR 생성/머지/조회, 레포 탐색, Actions/Secret 관리 |
|
|
180
179
|
|
|
181
180
|
### 📊 분석형 (코드 수정 없음)
|
|
182
181
|
|
|
@@ -271,7 +270,7 @@ Settings → Actions → General
|
|
|
271
270
|
|
|
272
271
|
| 문서 | 설명 |
|
|
273
272
|
|------|------|
|
|
274
|
-
| [Agent Skills 가이드](docs/SKILLS.md) |
|
|
273
|
+
| [Agent Skills 가이드](docs/SKILLS.md) | 24종 Skills 용도, 사용법, 전체 개발 사이클 흐름 |
|
|
275
274
|
| [통합 스크립트 가이드](docs/TEMPLATE-INTEGRATOR.md) | 기존 프로젝트에 템플릿 통합 |
|
|
276
275
|
| [버전 관리](docs/VERSION-CONTROL.md) | version.yml, 자동 버전 증가 |
|
|
277
276
|
| [체인지로그 자동화](docs/CHANGELOG-AUTOMATION.md) | CodeRabbit 연동, AI 문서화 |
|
package/package.json
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { existsSync, readdirSync, mkdirSync, cpSync, rmSync } from "node:fs";
|
|
6
6
|
import { compareCacheName } from "../util.js";
|
|
7
|
+
import { migrateConfigRoot } from "../legacy.js";
|
|
7
8
|
|
|
8
9
|
const MARKETPLACE = "Cassiiopeia/projectops";
|
|
9
10
|
const PLUGIN = "projectops@projectops-marketplace";
|
|
11
|
+
const LEGACY_PLUGINS = ["cassiiopeia@cassiiopeia-marketplace", "cassiiopeia"];
|
|
12
|
+
const LEGACY_MARKETPLACES = ["cassiiopeia-marketplace"];
|
|
10
13
|
|
|
11
14
|
function detect(io) {
|
|
12
15
|
if (!io.which("claude")) return { installed: false, version: null, cliMissing: true, note: "CLI 없음" };
|
|
@@ -26,10 +29,34 @@ function detect(io) {
|
|
|
26
29
|
function apply(io, ctx = {}) {
|
|
27
30
|
const st = detect(io);
|
|
28
31
|
if (st.cliMissing) { io.log(manualHint(io)); return false; }
|
|
29
|
-
|
|
32
|
+
migrateLegacy(io); // 옛 cassiiopeia 플러그인/마켓 정리
|
|
33
|
+
migrateConfigRoot(io); // 공용 config 루트 이관
|
|
34
|
+
const st2 = detect(io); // 정리 후 재감지
|
|
35
|
+
if (st2.installed) return update(io, st2.scope);
|
|
30
36
|
return install(io, "user");
|
|
31
37
|
}
|
|
32
38
|
|
|
39
|
+
// 옛 이름(cassiiopeia) 플러그인·마켓 정리. 조용히, 로그만. 실패 무해(없으면 no-op).
|
|
40
|
+
function migrateLegacy(io) {
|
|
41
|
+
const r = io.run("claude", ["plugin", "list", "--json"]);
|
|
42
|
+
let list = [];
|
|
43
|
+
try { const arr = JSON.parse(r.stdout || "[]"); list = Array.isArray(arr) ? arr : (arr.plugins || []); } catch { /* 무시 */ }
|
|
44
|
+
for (const legacy of LEGACY_PLUGINS) {
|
|
45
|
+
const hit = list.find((p) => {
|
|
46
|
+
const name = String(p.name || p.id || "");
|
|
47
|
+
return name === legacy || name.startsWith(legacy + "@");
|
|
48
|
+
});
|
|
49
|
+
if (hit) {
|
|
50
|
+
const scope = hit.scope || "user";
|
|
51
|
+
io.log(` 레거시 플러그인 정리: ${legacy} (scope: ${scope})`);
|
|
52
|
+
io.run("claude", ["plugin", "uninstall", legacy, "--scope", scope]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const mp of LEGACY_MARKETPLACES) {
|
|
56
|
+
io.run("claude", ["plugin", "marketplace", "remove", mp]); // 없으면 no-op
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
33
60
|
function install(io, scope = "user") {
|
|
34
61
|
io.log("Claude Code 마켓플레이스 등록 중...");
|
|
35
62
|
const add = io.run("claude", ["plugin", "marketplace", "add", MARKETPLACE]);
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
// marketplace 등록/업그레이드가 주 경로. native ~/.agents/skills/projectops 심링크는 감지·제거에 사용.
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { existsSync, lstatSync, rmSync } from "node:fs";
|
|
6
|
+
import { migrateConfigRoot } from "../legacy.js";
|
|
6
7
|
|
|
7
8
|
const MARKETPLACE = "Cassiiopeia/projectops";
|
|
8
9
|
const PLUGIN = "projectops";
|
|
10
|
+
const LEGACY_NATIVES = ["SUH-DEVOPS-TEMPLATE"];
|
|
11
|
+
const LEGACY_MARKETPLACES = ["SUH-DEVOPS-TEMPLATE"];
|
|
9
12
|
|
|
10
13
|
function nativeTarget(io) { return join(io.home(), ".agents/skills/projectops"); }
|
|
11
14
|
|
|
@@ -19,6 +22,8 @@ function detect(io) {
|
|
|
19
22
|
|
|
20
23
|
function apply(io) {
|
|
21
24
|
if (!io.which("codex")) { io.log(manualHint()); return false; }
|
|
25
|
+
migrateLegacy(io);
|
|
26
|
+
migrateConfigRoot(io);
|
|
22
27
|
io.log("Codex plugin marketplace 등록 중...");
|
|
23
28
|
const add = io.run("codex", ["plugin", "marketplace", "add", MARKETPLACE]);
|
|
24
29
|
io.log(add.code === 0 ? " Codex marketplace 등록 완료" : " Codex marketplace 이미 등록되어 있거나 등록 생략");
|
|
@@ -41,6 +46,17 @@ function remove(io) {
|
|
|
41
46
|
|
|
42
47
|
function manualHint() { return ` 💡 Codex CLI: codex plugin marketplace add ${MARKETPLACE}`; }
|
|
43
48
|
|
|
49
|
+
// 옛 native skills 폴더/심링크 + 옛 marketplace 정리. 실패 무해.
|
|
50
|
+
function migrateLegacy(io) {
|
|
51
|
+
for (const name of LEGACY_NATIVES) {
|
|
52
|
+
const old = join(io.home(), ".agents/skills", name);
|
|
53
|
+
if (existsSync(old) || isSymlink(old)) {
|
|
54
|
+
try { rmSync(old, { recursive: true, force: true }); io.log(` 레거시 Codex skills 정리: ${name}`); } catch { /* 무시 */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (io.which("codex")) for (const mp of LEGACY_MARKETPLACES) io.run("codex", ["plugin", "marketplace", "remove", mp]);
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
function isSymlink(p) { try { return lstatSync(p).isSymbolicLink(); } catch { return false; } }
|
|
45
61
|
|
|
46
62
|
export const codexAdapter = {
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
// 마켓플레이스 없음 → ~/.cursor/skills/ 에 skills/ 폴더를 복사하고 meta.json으로 버전 추적.
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { existsSync, readFileSync, mkdirSync, cpSync, rmSync, writeFileSync, readdirSync } from "node:fs";
|
|
6
|
+
import { migrateConfigRoot, isLegacyVersion } from "../legacy.js";
|
|
7
|
+
|
|
8
|
+
const LEGACY_NAMES = ["cassiiopeia", "suh-devops-template"];
|
|
9
|
+
const LEGACY_MAX_VERSION = "4.2.4";
|
|
6
10
|
|
|
7
11
|
function metaPath(io) { return join(io.home(), ".cursor/skills/cursor-skills-meta.json"); }
|
|
8
12
|
|
|
@@ -18,6 +22,8 @@ function detect(io) {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
function apply(io, ctx = {}) {
|
|
25
|
+
migrateLegacy(io, ctx); // 옛 이름/버전 감지 시 projectops 소유 항목 선별 삭제
|
|
26
|
+
migrateConfigRoot(io); // 공용 config 루트 이관
|
|
21
27
|
const src = resolveSkillsSrc(ctx);
|
|
22
28
|
if (!src) { io.log(" 설치할 스킬 소스를 찾지 못했습니다 (skills/ 폴더 필요)."); return false; }
|
|
23
29
|
const dest = join(io.home(), ".cursor/skills");
|
|
@@ -36,11 +42,44 @@ function apply(io, ctx = {}) {
|
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
function remove(io) {
|
|
45
|
+
function remove(io, ctx = {}) {
|
|
40
46
|
const dir = join(io.home(), ".cursor/skills");
|
|
41
47
|
if (!existsSync(join(dir, "cursor-skills-meta.json"))) { io.log(" 설치된 Cursor Skills가 없어 건너뜁니다"); return true; }
|
|
42
|
-
try {
|
|
43
|
-
|
|
48
|
+
try {
|
|
49
|
+
for (const name of ownedEntries(io, ctx)) rmSync(join(dir, name), { recursive: true, force: true });
|
|
50
|
+
// 폴더가 비면 폴더 자체 제거, 타 스킬(somansa-tools 등) 남으면 유지
|
|
51
|
+
if (existsSync(dir) && readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true });
|
|
52
|
+
io.log(` Cursor Skills 제거 완료 (projectops 소유만, ${dir}/)`);
|
|
53
|
+
return true;
|
|
54
|
+
} catch { io.log(` Cursor Skills 제거 실패 — 수동 확인: ${dir}`); return false; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// projectops가 이 폴더에 설치했다고 볼 항목만 골라낸다.
|
|
58
|
+
// 소스 skills/ 폴더명(pro-*, references, config.json.example) ∪ /^suh-/ ∪ meta 파일.
|
|
59
|
+
// 접두어 없는 폴더(analyze·gitlab 등 somansa-tools)는 제외 → 보존.
|
|
60
|
+
function ownedEntries(io, ctx) {
|
|
61
|
+
const dir = join(io.home(), ".cursor/skills");
|
|
62
|
+
if (!existsSync(dir)) return [];
|
|
63
|
+
let srcNames = new Set();
|
|
64
|
+
const src = resolveSkillsSrc(ctx);
|
|
65
|
+
if (src && existsSync(src)) srcNames = new Set(readdirSync(src));
|
|
66
|
+
const EXTRA = new Set(["cursor-skills-meta.json"]);
|
|
67
|
+
return readdirSync(dir).filter((name) => srcNames.has(name) || /^suh-/.test(name) || EXTRA.has(name));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 옛 이름/버전 meta면 projectops 소유 항목만 선별 삭제 → apply가 신규 재설치.
|
|
71
|
+
function migrateLegacy(io, ctx) {
|
|
72
|
+
const mp = metaPath(io);
|
|
73
|
+
if (!existsSync(mp)) return;
|
|
74
|
+
let meta = {};
|
|
75
|
+
try { meta = JSON.parse(readFileSync(mp, "utf8")); } catch { return; }
|
|
76
|
+
const oldName = meta.name && LEGACY_NAMES.includes(String(meta.name).toLowerCase());
|
|
77
|
+
const oldVer = isLegacyVersion(meta.version, LEGACY_MAX_VERSION);
|
|
78
|
+
if (!(oldName || oldVer)) return;
|
|
79
|
+
for (const name of ownedEntries(io, ctx)) {
|
|
80
|
+
try { rmSync(join(io.home(), ".cursor/skills", name), { recursive: true, force: true }); } catch { /* 무시 */ }
|
|
81
|
+
}
|
|
82
|
+
io.log(` 레거시 Cursor Skills 정리(선별): name=${meta.name}, v=${meta.version} → 재설치`);
|
|
44
83
|
}
|
|
45
84
|
|
|
46
85
|
function manualHint() {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Gemini CLI 어댑터 (.sh _manage_gemini_extension / _remove_gemini_section 등가).
|
|
2
2
|
// extension install/update/uninstall. 마켓플레이스 대신 git URL 확장.
|
|
3
|
+
import { migrateConfigRoot } from "../legacy.js";
|
|
4
|
+
|
|
3
5
|
const EXT = "projectops";
|
|
4
6
|
const URL = "https://github.com/Cassiiopeia/projectops";
|
|
7
|
+
const LEGACY_EXTS = ["SUH-DEVOPS-TEMPLATE"];
|
|
5
8
|
|
|
6
9
|
function detect(io) {
|
|
7
10
|
if (!io.which("gemini")) return { installed: false, version: null, cliMissing: true, note: "CLI 없음" };
|
|
@@ -11,6 +14,8 @@ function detect(io) {
|
|
|
11
14
|
|
|
12
15
|
function apply(io) {
|
|
13
16
|
if (!io.which("gemini")) { io.log(manualHint()); return false; }
|
|
17
|
+
migrateLegacy(io);
|
|
18
|
+
migrateConfigRoot(io);
|
|
14
19
|
io.log("Gemini CLI extension 업데이트 중...");
|
|
15
20
|
if (io.run("gemini", ["extensions", "update", EXT]).code === 0) { io.log(" Gemini CLI extension 업데이트 완료"); return true; }
|
|
16
21
|
io.log("Gemini CLI extension 설치 중...");
|
|
@@ -28,6 +33,11 @@ function remove(io) {
|
|
|
28
33
|
|
|
29
34
|
function manualHint() { return ` 💡 Gemini CLI: gemini extensions install ${URL}`; }
|
|
30
35
|
|
|
36
|
+
// 옛 이름 extension 정리. 없으면 실패 무시.
|
|
37
|
+
function migrateLegacy(io) {
|
|
38
|
+
for (const e of LEGACY_EXTS) io.run("gemini", ["extensions", "uninstall", e]);
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
export const geminiAdapter = {
|
|
32
42
|
id: "gemini", label: "Gemini CLI", order: 30,
|
|
33
43
|
detect, apply, remove, manualHint,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// .sh: PI_PACKAGE_URL / _pi_is_installed / _pi_clone_dir / _pi_harness_loader_path /
|
|
3
3
|
// _pi_settings_path / _pi_harness_enabled 등가.
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
6
6
|
|
|
7
7
|
export const PI_PACKAGE_URL = "https://github.com/Cassiiopeia/projectops";
|
|
8
8
|
|
|
@@ -61,3 +61,24 @@ export function harnessRemove(io) {
|
|
|
61
61
|
}
|
|
62
62
|
return { ok: true };
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
// 신·구 clone 공존 시 옛 SUH-DEVOPS-TEMPLATE clone 제거 + settings.json에서 그 loader 경로 제거.
|
|
66
|
+
// 공존일 때만 정리(옛것만 있으면 pi install이 신규를 만들 때까지 그대로 둔다). 실패 무해.
|
|
67
|
+
export function migratePiLegacy(io) {
|
|
68
|
+
const base = join(io.home(), ".pi/agent/git/github.com/Cassiiopeia");
|
|
69
|
+
const oldDir = join(base, "SUH-DEVOPS-TEMPLATE");
|
|
70
|
+
const newDir = join(base, "projectops");
|
|
71
|
+
if (!(existsSync(oldDir) && existsSync(newDir))) return;
|
|
72
|
+
const oldLoader = join(oldDir, "harness/harness-loader.ts");
|
|
73
|
+
const settings = piSettingsPath(io);
|
|
74
|
+
if (existsSync(settings)) {
|
|
75
|
+
try {
|
|
76
|
+
const s = JSON.parse(readFileSync(settings, "utf8"));
|
|
77
|
+
if (Array.isArray(s.extensions)) {
|
|
78
|
+
s.extensions = s.extensions.filter((e) => e && e !== oldLoader);
|
|
79
|
+
writeFileSync(settings, JSON.stringify(s, null, 2));
|
|
80
|
+
}
|
|
81
|
+
} catch { /* 무시 */ }
|
|
82
|
+
}
|
|
83
|
+
try { rmSync(oldDir, { recursive: true, force: true }); io.log(" 레거시 PI clone 정리: SUH-DEVOPS-TEMPLATE"); } catch { /* 무시 */ }
|
|
84
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// PI 패키지 어댑터 (.sh _manage_pi_section / _remove_pi_section 등가).
|
|
2
2
|
// pi install/update/remove. skill은 복사되지 않고 pi가 startup마다 스캔 → 설치 검증은 'pi list'.
|
|
3
|
-
import { PI_PACKAGE_URL, piInstalled, harnessEnabled, harnessRemove } from "./pi-common.js";
|
|
3
|
+
import { PI_PACKAGE_URL, piInstalled, harnessEnabled, harnessRemove, migratePiLegacy } from "./pi-common.js";
|
|
4
|
+
import { migrateConfigRoot } from "../legacy.js";
|
|
4
5
|
|
|
5
6
|
function detect(io) {
|
|
6
7
|
if (!io.which("pi")) return { installed: false, version: null, cliMissing: true, note: "CLI 없음" };
|
|
@@ -9,6 +10,8 @@ function detect(io) {
|
|
|
9
10
|
|
|
10
11
|
function apply(io) {
|
|
11
12
|
if (!io.which("pi")) { io.log(manualHint()); return false; }
|
|
13
|
+
migratePiLegacy(io);
|
|
14
|
+
migrateConfigRoot(io);
|
|
12
15
|
if (piInstalled(io)) {
|
|
13
16
|
io.log("PI 패키지 업데이트 중...");
|
|
14
17
|
if (io.run("pi", ["update", PI_PACKAGE_URL]).code !== 0) io.run("pi", ["install", PI_PACKAGE_URL]);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// IDE 어댑터 공용 레거시 유틸 — 버전 판정 + config 루트 이관.
|
|
2
|
+
// 리브랜딩(#459) 전 설치본(cassiiopeia/SUH-DEVOPS-TEMPLATE)을 신규(projectops)로 넘길 때 공용.
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { existsSync, readFileSync, mkdirSync, cpSync } from "node:fs";
|
|
5
|
+
import { compareCacheName } from "./util.js";
|
|
6
|
+
|
|
7
|
+
// version <= maxLegacy 이면 레거시. null/빈문자열은 판정 불가 → false(안전).
|
|
8
|
+
export function isLegacyVersion(version, maxLegacy) {
|
|
9
|
+
if (!version || !maxLegacy) return false;
|
|
10
|
+
return compareCacheName(String(version), String(maxLegacy)) <= 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 파일이 존재하고 JSON 파싱되며 최소 1개 키가 있으면 true.
|
|
14
|
+
export function hasNonEmptyJson(path) {
|
|
15
|
+
if (!path || !existsSync(path)) return false;
|
|
16
|
+
try {
|
|
17
|
+
const o = JSON.parse(readFileSync(path, "utf8"));
|
|
18
|
+
return o && typeof o === "object" && Object.keys(o).length > 0;
|
|
19
|
+
} catch { return false; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// config 루트 이관: 타겟(~/.projectops/config/config.json)이 비었을 때만 옛 경로에서 복사.
|
|
23
|
+
// 옛 파일은 삭제하지 않음(민감값 보존). idempotent.
|
|
24
|
+
export function migrateConfigRoot(io) {
|
|
25
|
+
const target = join(io.home(), ".projectops", "config", "config.json");
|
|
26
|
+
if (hasNonEmptyJson(target)) return { migrated: false, reason: "target-exists" };
|
|
27
|
+
const sources = [
|
|
28
|
+
join(io.home(), ".suh-template", "config", "config.json"), // 2세대 우선
|
|
29
|
+
join(io.home(), ".cassiiopeia", "config.json"), // 1세대 폴백
|
|
30
|
+
];
|
|
31
|
+
const src = sources.find(hasNonEmptyJson);
|
|
32
|
+
if (!src) return { migrated: false, reason: "no-source" };
|
|
33
|
+
try {
|
|
34
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
35
|
+
cpSync(src, target);
|
|
36
|
+
io.log(` config 마이그레이션 완료: ${src} → ~/.projectops/config/config.json`);
|
|
37
|
+
return { migrated: true, from: src };
|
|
38
|
+
} catch (e) {
|
|
39
|
+
io.log(` config 마이그레이션 실패(무시): ${e.message}`);
|
|
40
|
+
return { migrated: false, reason: "error" };
|
|
41
|
+
}
|
|
42
|
+
}
|