projectops 4.2.4 → 4.2.6
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 +40 -40
- 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.5 (2026-07-10)
|
|
11
11
|
|
|
12
12
|
[전체 버전 기록 보기](CHANGELOG.md)
|
|
13
13
|
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
| 버전 수동 관리, 태그 직접 생성 | 릴리스(develop→main PR) 시 patch 버전 자동 증가 + 태그 생성 |
|
|
28
28
|
| 체인지로그 직접 작성 (30분+) | CodeRabbit AI가 PR마다 자동 생성 |
|
|
29
29
|
| CI/CD 처음부터 설정 | 프로젝트 타입별 워크플로우 즉시 구성 |
|
|
30
|
-
| 이슈 매번 형식 맞춰 작성 (5분+) | `/
|
|
31
|
-
| 커밋 메시지 이슈 URL 수동 복사 | `/
|
|
32
|
-
| PR 설명/보고서 직접 작성 | `/
|
|
30
|
+
| 이슈 매번 형식 맞춰 작성 (5분+) | `/pro-issue` 한 번에 표준 템플릿 생성 |
|
|
31
|
+
| 커밋 메시지 이슈 URL 수동 복사 | `/pro-commit` 이슈 컨텍스트 기반 자동 완성 |
|
|
32
|
+
| PR 설명/보고서 직접 작성 | `/pro-report` git diff 분석 후 자동 생성 |
|
|
33
33
|
| 코드 리뷰·분석 매번 프롬프트 입력 | 25종 Skills로 일관된 결과, 매번 재입력 불필요 |
|
|
34
34
|
|
|
35
35
|
---
|
|
@@ -40,23 +40,23 @@ Agent Skills가 개발 사이클 전체를 커버합니다.
|
|
|
40
40
|
|
|
41
41
|
```mermaid
|
|
42
42
|
flowchart TD
|
|
43
|
-
A([작업 시작]) --> B["/
|
|
44
|
-
B --> C["/
|
|
43
|
+
A([작업 시작]) --> B["/pro-issue\n이슈 등록 + GitHub 자동 생성"]
|
|
44
|
+
B --> C["/pro-init-worktree\nworktree + 민감파일 자동 복사"]
|
|
45
45
|
C --> D{작업 유형}
|
|
46
46
|
|
|
47
|
-
D -->|새 기능| E1["/
|
|
48
|
-
D -->|버그| E2["/
|
|
49
|
-
D -->|리팩토링| E3["/
|
|
47
|
+
D -->|새 기능| E1["/pro-plan\n전략 수립"]
|
|
48
|
+
D -->|버그| E2["/pro-troubleshoot\n원인 분석"]
|
|
49
|
+
D -->|리팩토링| E3["/pro-refactor-analyze\nSmell 탐지"]
|
|
50
50
|
|
|
51
|
-
E1 --> F["/
|
|
51
|
+
E1 --> F["/pro-implement\n구현"]
|
|
52
52
|
E2 --> F
|
|
53
53
|
E3 --> F
|
|
54
|
-
F --> G["/
|
|
55
|
-
G --> H["/
|
|
56
|
-
H --> I["/
|
|
57
|
-
I --> J["/
|
|
54
|
+
F --> G["/pro-test\n테스트"]
|
|
55
|
+
G --> H["/pro-review\n셀프 리뷰"]
|
|
56
|
+
H --> I["/pro-commit\n이슈 연동 커밋 자동 완성"]
|
|
57
|
+
I --> J["/pro-report\n구현 보고서 + GitHub 댓글"]
|
|
58
58
|
J --> K([PR 등록])
|
|
59
|
-
K --> L["/
|
|
59
|
+
K --> L["/pro-changelog-deploy\ndevelop push → main 릴리스 PR + automerge"]
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
> Skills 전체 목록 및 상세 사용법: **[docs/SKILLS.md](docs/SKILLS.md)**
|
|
@@ -171,46 +171,46 @@ bash <(curl -fsSL "https://raw.githubusercontent.com/Cassiiopeia/projectops/main
|
|
|
171
171
|
|
|
172
172
|
| 스킬 | 용도 |
|
|
173
173
|
|------|------|
|
|
174
|
-
| `/
|
|
175
|
-
| `/
|
|
176
|
-
| `/
|
|
177
|
-
| `/
|
|
178
|
-
| `/
|
|
179
|
-
| `/
|
|
174
|
+
| `/pro-issue` | 설명 한 줄 → GitHub 이슈 템플릿 자동 작성 + 등록 |
|
|
175
|
+
| `/pro-init-worktree` | Git worktree 생성 + 민감 파일 자동 복사 |
|
|
176
|
+
| `/pro-commit` | 이슈 컨텍스트 기반 커밋 메시지 자동 완성 (superpowers 준수) |
|
|
177
|
+
| `/pro-report` | git diff 분석 → 구현 보고서 생성 + GitHub 댓글 자동 포스팅 |
|
|
178
|
+
| `/pro-changelog-deploy` | develop push → main 릴리스 PR 생성 + 릴리스 노트 작성 + automerge |
|
|
179
|
+
| `/pro-github` | GitHub 이슈/PR/댓글 독립 조회 및 관리 |
|
|
180
180
|
|
|
181
181
|
### 📊 분석형 (코드 수정 없음)
|
|
182
182
|
|
|
183
183
|
| 스킬 | 용도 |
|
|
184
184
|
|------|------|
|
|
185
|
-
| `/
|
|
186
|
-
| `/
|
|
187
|
-
| `/
|
|
188
|
-
| `/
|
|
189
|
-
| `/
|
|
190
|
-
| `/
|
|
185
|
+
| `/pro-analyze` | 구현 전 현재 코드 상태 분석 및 영향 범위 평가 |
|
|
186
|
+
| `/pro-plan` | 요구사항 명확화 + 2가지 이상 접근 방식 비교로 전략 수립 |
|
|
187
|
+
| `/pro-design-analyze` | 아키텍처/API/DB/UI 설계 분석 (구현 X) |
|
|
188
|
+
| `/pro-refactor-analyze` | Code Smell 탐지 + Before/After 기반 리팩토링 계획 |
|
|
189
|
+
| `/pro-review` | 보안/성능/버그/품질 6관점 리뷰, Critical/Major/Minor 분류 |
|
|
190
|
+
| `/pro-troubleshoot` | 가설-검증 방식 근본 원인 분석, Quick Fix/Root Fix 제시 |
|
|
191
191
|
|
|
192
192
|
### 🔧 구현형 (실제 코드 작성)
|
|
193
193
|
|
|
194
194
|
| 스킬 | 용도 |
|
|
195
195
|
|------|------|
|
|
196
|
-
| `/
|
|
197
|
-
| `/
|
|
198
|
-
| `/
|
|
199
|
-
| `/
|
|
200
|
-
| `/
|
|
201
|
-
| `/
|
|
196
|
+
| `/pro-implement` | 계획/분석 결과 기반 코드 구현 (기존 스타일 100% 준수) |
|
|
197
|
+
| `/pro-design` | 아키텍처/API/DB/UI 설계 + 구현까지 |
|
|
198
|
+
| `/pro-refactor` | Extract Method, DRY 등 리팩토링 기법 단계별 적용 |
|
|
199
|
+
| `/pro-test` | AAA 패턴 단위/통합/E2E 테스트 코드 작성 |
|
|
200
|
+
| `/pro-figma` | Figma CSS → React/RN/Flutter 반응형 코드 변환 |
|
|
201
|
+
| `/pro-build` | 프로젝트 빌드 실행, 에러 분석, 최적화 제안 |
|
|
202
202
|
|
|
203
203
|
### 📝 문서/산출물 생성형
|
|
204
204
|
|
|
205
205
|
| 스킬 | 용도 |
|
|
206
206
|
|------|------|
|
|
207
|
-
| `/
|
|
208
|
-
| `/
|
|
209
|
-
| `/
|
|
210
|
-
| `/
|
|
211
|
-
| `/
|
|
212
|
-
| `/
|
|
213
|
-
| `/
|
|
207
|
+
| `/pro-document` | 코드 주석/README/API 문서 작성 |
|
|
208
|
+
| `/pro-testcase` | 이슈 분석 → QA 체크리스트 생성 |
|
|
209
|
+
| `/pro-ppt` | 트러블슈팅/구현 사례 → 5섹션 발표자료 |
|
|
210
|
+
| `/pro-spring-test` | Spring Boot 테스트 샘플 코드 생성 |
|
|
211
|
+
| `/pro-synology-expose` | Synology NAS 외부 도메인 노출 설정 가이드 |
|
|
212
|
+
| `/pro-ssh` | 원격 서버 SSH 접속·명령 실행 (AWS EC2, 시놀로지 NAS, Linux 등 범용) |
|
|
213
|
+
| `/pro-skill-creator` | Skill 생성/리뷰/개선 (CREATE·REVIEW·IMPROVE 3모드) |
|
|
214
214
|
|
|
215
215
|
---
|
|
216
216
|
|
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
|
+
}
|