projectops 4.2.5 → 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 +1 -1
- 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
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
|
+
}
|