projectops 4.31.5 → 4.33.1
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 +144 -145
- package/package.json +2 -2
- package/src/cli/args.js +65 -29
- package/src/cli/errors.js +9 -7
- package/src/cli/help.js +51 -3
- package/src/commands/doctor.js +80 -78
- package/src/commands/full.js +17 -3
- package/src/commands/interactive.js +78 -50
- package/src/commands/issues.js +8 -1
- package/src/commands/skills.js +8 -7
- package/src/commands/version.js +4 -2
- package/src/commands/workflows.js +3 -1
- package/src/context.js +4 -0
- package/src/core/breaking-check.js +6 -5
- package/src/core/copy/simple.js +27 -1
- package/src/core/copy/workflows.js +10 -5
- package/src/core/exclusions.js +8 -0
- package/src/core/ide/adapters/claude.js +18 -17
- package/src/core/ide/adapters/codex.js +12 -11
- package/src/core/ide/adapters/cursor.js +11 -10
- package/src/core/ide/adapters/gemini.js +11 -10
- package/src/core/ide/adapters/pi-common.js +2 -1
- package/src/core/ide/adapters/pi-harness.js +13 -16
- package/src/core/ide/adapters/pi.js +12 -11
- package/src/core/label-style.js +78 -0
- package/src/core/migration-guide.js +38 -38
- package/src/core/migrations/index.js +19 -15
- package/src/core/migrations/registry.js +70 -55
- package/src/core/options-ask.js +65 -64
- package/src/core/orphan-workflows.js +2 -1
- package/src/core/paths-resolve.js +31 -30
- package/src/core/repo-language.js +57 -0
- package/src/core/run-trace.js +10 -0
- package/src/core/version-yml.js +68 -36
- package/src/i18n/core.en.js +31 -0
- package/src/i18n/core.ko.js +31 -0
- package/src/i18n/doctor.en.js +89 -0
- package/src/i18n/doctor.ko.js +89 -0
- package/src/i18n/en.js +142 -0
- package/src/i18n/ide.en.js +102 -0
- package/src/i18n/ide.ko.js +102 -0
- package/src/i18n/index.js +73 -0
- package/src/i18n/ko.js +135 -0
- package/src/i18n/migrate.en.js +66 -0
- package/src/i18n/migrate.ko.js +66 -0
- package/src/i18n/options.en.js +104 -0
- package/src/i18n/options.ko.js +104 -0
- package/src/i18n/summary.en.js +70 -0
- package/src/i18n/summary.ko.js +70 -0
- package/src/index.js +46 -28
- package/src/ui/banner.js +8 -6
- package/src/ui/env-plan.js +19 -18
- package/src/ui/prompts.js +25 -24
- package/src/ui/readline-engine.js +18 -10
- package/src/ui/skills-prompts.js +8 -7
- package/src/ui/status-cards.js +38 -38
- package/src/ui/summary.js +103 -111
|
@@ -71,7 +71,7 @@ function classify(srcDir, workflowsDir, envOpts, baseline = null) {
|
|
|
71
71
|
// hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'> } — 기존 파일(changed) 충돌 결정.
|
|
72
72
|
// 반환: {copied, skipped, templateAdded, optionalCopied, copiedFiles[]} — copiedFiles는 실제 복사·교체된 파일명 (#473 요약용)
|
|
73
73
|
export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
74
|
-
const { types = [], paths = new Map(), deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true, branch = "", deployBranch = "" } = context;
|
|
74
|
+
const { types = [], paths = new Map(), deployTarget = "docker-ssh", publishTargets = [], includeSecretBackup = false, aiPrSummary = true, projectsSync = true, repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true, branch = "", deployBranch = "" } = context;
|
|
75
75
|
const decisions = hooks.decisions instanceof Map ? hooks.decisions : new Map();
|
|
76
76
|
const trace = hooks.trace ?? null; // #494 — 실행 트레이스 (null-safe: 미주입이면 전 이벤트 no-op)
|
|
77
77
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
@@ -193,14 +193,18 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
193
193
|
// (4.7) common/pr-summary — AI 변경 요약 (#566). 선택했을 때만 복사한다.
|
|
194
194
|
// 종전에는 common 본체에 있어 무조건 복사된 뒤 런타임에 스스로 빠졌고, 그 판단이
|
|
195
195
|
// ".coderabbit.yaml 존재"라 파일만 있고 앱이 없는 저장소에서는 아무도 요약하지 않았다.
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
// (4.8) common/projects-sync — Projects 보드 동기화 (#716). 이것도 선택했을 때만 복사한다.
|
|
197
|
+
// Secret(_GITHUB_PAT_TOKEN)과 PROJECT_URL 변수를 등록해야 동작하는데, Projects 를 안 쓰는 레포가
|
|
198
|
+
// 많아 설치만 되고 설정되지 않은 채 남았다. 같은 폴더 방식이라 게이트와 고아 감지(orphan-workflows.js)를 함께 둔다.
|
|
199
|
+
for (const [groupName, groupEnabled] of [["pr-summary", aiPrSummary], ["projects-sync", projectsSync]]) {
|
|
200
|
+
const prSummaryDir = join(commonDir, groupName);
|
|
201
|
+
if (exists(prSummaryDir) && groupEnabled) {
|
|
198
202
|
for (const filename of listYamlFiles(prSummaryDir)) {
|
|
199
203
|
const src = join(prSummaryDir, filename);
|
|
200
204
|
const dst = join(workflowsDir, filename);
|
|
201
205
|
if (existsSync(dst) && isUnchanged(readFileSync(src, "utf8"), readFileSync(dst, "utf8"), envOptsFor("common"))) {
|
|
202
206
|
counters.skipped++;
|
|
203
|
-
trace?.event("copy", "skipped-unchanged", filename, { group:
|
|
207
|
+
trace?.event("copy", "skipped-unchanged", filename, { group: groupName });
|
|
204
208
|
continue;
|
|
205
209
|
}
|
|
206
210
|
const backedUp = existsSync(dst);
|
|
@@ -209,10 +213,11 @@ export function copyWorkflows(context, tempDir, targetRoot = ".", hooks = {}) {
|
|
|
209
213
|
counters.optionalCopied++;
|
|
210
214
|
counters.copied++;
|
|
211
215
|
counters.copiedFiles.push(filename);
|
|
212
|
-
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group:
|
|
216
|
+
trace?.event("copy", backedUp ? "replaced-bak" : "copied", filename, { group: groupName });
|
|
213
217
|
if (backedUp) counters.replacedBak.push(filename); // #673
|
|
214
218
|
}
|
|
215
219
|
}
|
|
220
|
+
}
|
|
216
221
|
|
|
217
222
|
// (5) common/secret-backup — 있으면 무조건 스킵/신규만 복사
|
|
218
223
|
const secretDir = join(commonDir, "secret-backup");
|
package/src/core/exclusions.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export const DOCS_TO_REMOVE = [
|
|
6
6
|
"CONTRIBUTING.md",
|
|
7
7
|
"SECURITY.md",
|
|
8
|
+
"ARCHITECTURE.md",
|
|
8
9
|
"CLAUDE.md",
|
|
9
10
|
"AGENTS.md",
|
|
10
11
|
"GEMINI.md",
|
|
@@ -14,6 +15,10 @@ export const DOCS_TO_REMOVE = [
|
|
|
14
15
|
"pr_body.md",
|
|
15
16
|
];
|
|
16
17
|
|
|
18
|
+
// ⚠️ `.github/i18n`(한국어 템플릿 오버레이, #769)은 여기에 넣지 않는다. 이 목록은 내려받은 템플릿 폴더에서
|
|
19
|
+
// 항목을 지우는데, 오버레이는 language: ko 설치 때 그 폴더에서 읽어 와야 한다. 사용자 레포로는
|
|
20
|
+
// copyIssueTemplates 가 ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE 만 골라 복사하므로 새지 않는다.
|
|
21
|
+
// ("Use this template" 경로는 template_initializer.py 가 지운다.)
|
|
17
22
|
export const PLUGIN_ITEMS_TO_REMOVE = [
|
|
18
23
|
".claude-plugin",
|
|
19
24
|
".codex-plugin",
|
|
@@ -25,8 +30,11 @@ export const PLUGIN_ITEMS_TO_REMOVE = [
|
|
|
25
30
|
"bin",
|
|
26
31
|
"src",
|
|
27
32
|
"site",
|
|
33
|
+
".github/CODEOWNERS",
|
|
28
34
|
".github/workflows/PROJECT-TEMPLATE-PLUGIN-VERSION-SYNC.yaml",
|
|
29
35
|
".github/workflows/PROJECT-TEMPLATE-NPM-PUBLISH.yaml",
|
|
30
36
|
".github/workflows/PROJECT-TEMPLATE-DOCS-DEPLOY.yaml",
|
|
37
|
+
".github/workflows/PROJECT-TEMPLATE-SYNC-COMMUNITY-LABELS.yaml",
|
|
38
|
+
".github/config/community-labels.yml",
|
|
31
39
|
];
|
|
32
40
|
// ⚠️ skills/ 는 제외하지 않는다 (Cursor 설치 소스로 보존)
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { existsSync, readdirSync, mkdirSync, cpSync, rmSync } from "node:fs";
|
|
6
6
|
import { compareCacheName } from "../util.js";
|
|
7
7
|
import { migrateConfigRoot } from "../legacy.js";
|
|
8
|
+
import { t } from "../../../i18n/index.js";
|
|
8
9
|
|
|
9
10
|
const MARKETPLACE = "Cassiiopeia/projectops";
|
|
10
11
|
const PLUGIN = "projectops@projectops-marketplace";
|
|
@@ -12,7 +13,7 @@ const LEGACY_PLUGINS = ["cassiiopeia@cassiiopeia-marketplace", "cassiiopeia"];
|
|
|
12
13
|
const LEGACY_MARKETPLACES = ["cassiiopeia-marketplace"];
|
|
13
14
|
|
|
14
15
|
function detect(io) {
|
|
15
|
-
if (!io.which("claude")) return { installed: false, version: null, cliMissing: true, note: "
|
|
16
|
+
if (!io.which("claude")) return { installed: false, version: null, cliMissing: true, note: t("ide.cliMissing") };
|
|
16
17
|
const r = io.run("claude", ["plugin", "list", "--json"]);
|
|
17
18
|
let scope = "", version = null;
|
|
18
19
|
try {
|
|
@@ -48,7 +49,7 @@ function migrateLegacy(io) {
|
|
|
48
49
|
});
|
|
49
50
|
if (hit) {
|
|
50
51
|
const scope = hit.scope || "user";
|
|
51
|
-
io.log(
|
|
52
|
+
io.log(t("ide.claude.legacy", { name: legacy, scope }));
|
|
52
53
|
io.run("claude", ["plugin", "uninstall", legacy, "--scope", scope]);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
@@ -58,39 +59,39 @@ function migrateLegacy(io) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function install(io, scope = "user") {
|
|
61
|
-
io.log("
|
|
62
|
+
io.log(t("ide.claude.marketAdding"));
|
|
62
63
|
const add = io.run("claude", ["plugin", "marketplace", "add", MARKETPLACE]);
|
|
63
|
-
io.log(add.code === 0 ? "
|
|
64
|
-
io.log(
|
|
64
|
+
io.log(t(add.code === 0 ? "ide.claude.marketAdded" : "ide.claude.marketSkipped"));
|
|
65
|
+
io.log(t("ide.claude.installing", { scope }));
|
|
65
66
|
const ins = io.run("claude", ["plugin", "install", PLUGIN, "--scope", scope]);
|
|
66
|
-
if (ins.code === 0) { io.log(
|
|
67
|
-
io.log(
|
|
67
|
+
if (ins.code === 0) { io.log(t("ide.claude.installed", { scope })); return true; }
|
|
68
|
+
io.log(t("ide.claude.installFailed", { plugin: PLUGIN, scope }));
|
|
68
69
|
return false;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
function update(io, scope) {
|
|
72
73
|
const cacheRoot = join(io.home(), ".claude/plugins/cache/projectops-marketplace/projectops");
|
|
73
74
|
const oldCache = latestCacheDir(cacheRoot);
|
|
74
|
-
io.log("
|
|
75
|
+
io.log(t("ide.claude.updating"));
|
|
75
76
|
const up = io.run("claude", ["plugin", "update", PLUGIN, "--scope", scope]);
|
|
76
|
-
if (up.code !== 0) { io.log(
|
|
77
|
-
io.log(
|
|
77
|
+
if (up.code !== 0) { io.log(t("ide.claude.updateFailed", { plugin: PLUGIN, scope })); return false; }
|
|
78
|
+
io.log(t("ide.claude.updated", { scope }));
|
|
78
79
|
migrateConfig(io, oldCache, latestCacheDir(cacheRoot));
|
|
79
80
|
return true;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function remove(io) {
|
|
83
84
|
const st = detect(io);
|
|
84
|
-
if (st.cliMissing || !st.installed) { io.log("
|
|
85
|
-
io.log(
|
|
85
|
+
if (st.cliMissing || !st.installed) { io.log(t("ide.claude.removeNone")); return true; }
|
|
86
|
+
io.log(t("ide.claude.removeTarget", { plugin: PLUGIN, scope: st.scope }));
|
|
86
87
|
const un = io.run("claude", ["plugin", "uninstall", PLUGIN, "--scope", st.scope]);
|
|
87
|
-
if (un.code === 0) { io.log("
|
|
88
|
-
io.log(
|
|
88
|
+
if (un.code === 0) { io.log(t("ide.claude.removed")); removePluginData(io); return true; }
|
|
89
|
+
io.log(t("ide.claude.removeFailed", { plugin: PLUGIN, scope: st.scope }));
|
|
89
90
|
return false;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
function manualHint() {
|
|
93
|
-
return
|
|
94
|
+
return t("ide.claude.manual", { market: MARKETPLACE, plugin: PLUGIN });
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
// ── 내부 헬퍼 ──
|
|
@@ -107,12 +108,12 @@ function migrateConfig(io, oldCache, newCache) {
|
|
|
107
108
|
mkdirSync(newCfg, { recursive: true });
|
|
108
109
|
let copied = 0;
|
|
109
110
|
for (const f of readdirSync(oldCfg)) if (f.endsWith(".json")) { cpSync(join(oldCfg, f), join(newCfg, f)); copied++; }
|
|
110
|
-
if (copied) io.log("
|
|
111
|
+
if (copied) io.log(t("ide.claude.configMigrated"));
|
|
111
112
|
} catch { /* 무해 */ }
|
|
112
113
|
}
|
|
113
114
|
function removePluginData(io) {
|
|
114
115
|
const dataDir = join(io.home(), ".claude/plugins/data", PLUGIN);
|
|
115
|
-
if (existsSync(dataDir)) { try { rmSync(dataDir, { recursive: true, force: true }); io.log(
|
|
116
|
+
if (existsSync(dataDir)) { try { rmSync(dataDir, { recursive: true, force: true }); io.log(t("ide.claude.dataRemoved")); } catch { /* 무해 */ } }
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
export const claudeAdapter = {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { existsSync, lstatSync, rmSync } from "node:fs";
|
|
6
6
|
import { migrateConfigRoot } from "../legacy.js";
|
|
7
|
+
import { t } from "../../../i18n/index.js";
|
|
7
8
|
|
|
8
9
|
const MARKETPLACE = "Cassiiopeia/projectops";
|
|
9
10
|
const PLUGIN = "projectops";
|
|
@@ -16,20 +17,20 @@ function detect(io) {
|
|
|
16
17
|
const tgt = nativeTarget(io);
|
|
17
18
|
const nativeInstalled = existsSync(tgt);
|
|
18
19
|
if (nativeInstalled) return { installed: true, version: null, cliMissing: false, note: "native skills" };
|
|
19
|
-
if (!io.which("codex")) return { installed: false, version: null, cliMissing: true, note: "
|
|
20
|
-
return { installed: false, version: null, cliMissing: false, note: "
|
|
20
|
+
if (!io.which("codex")) return { installed: false, version: null, cliMissing: true, note: t("ide.cliMissing") };
|
|
21
|
+
return { installed: false, version: null, cliMissing: false, note: t("ide.installable") };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
function apply(io) {
|
|
24
25
|
if (!io.which("codex")) { io.log(manualHint()); return false; }
|
|
25
26
|
migrateLegacy(io);
|
|
26
27
|
migrateConfigRoot(io);
|
|
27
|
-
io.log("
|
|
28
|
+
io.log(t("ide.codex.marketAdding"));
|
|
28
29
|
const add = io.run("codex", ["plugin", "marketplace", "add", MARKETPLACE]);
|
|
29
|
-
io.log(add.code === 0 ? "
|
|
30
|
-
io.log("
|
|
31
|
-
if (io.run("codex", ["plugin", "marketplace", "upgrade", PLUGIN]).code === 0) { io.log(
|
|
32
|
-
io.log(
|
|
30
|
+
io.log(t(add.code === 0 ? "ide.codex.marketAdded" : "ide.codex.marketSkipped"));
|
|
31
|
+
io.log(t("ide.codex.marketUpgrading"));
|
|
32
|
+
if (io.run("codex", ["plugin", "marketplace", "upgrade", PLUGIN]).code === 0) { io.log(t("ide.codex.marketDone")); return true; }
|
|
33
|
+
io.log(t("ide.codex.marketError", { market: MARKETPLACE }));
|
|
33
34
|
return false;
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -37,10 +38,10 @@ function remove(io) {
|
|
|
37
38
|
const tgt = nativeTarget(io);
|
|
38
39
|
let removed = false;
|
|
39
40
|
if (existsSync(tgt) || isSymlink(tgt)) {
|
|
40
|
-
try { rmSync(tgt, { recursive: true, force: true }); io.log(
|
|
41
|
+
try { rmSync(tgt, { recursive: true, force: true }); io.log(t("ide.codex.skillsRemoved", { path: tgt })); removed = true; } catch { /* 무해 */ }
|
|
41
42
|
}
|
|
42
|
-
if (!removed) io.log("
|
|
43
|
-
if (io.which("codex")) io.log(
|
|
43
|
+
if (!removed) io.log(t("ide.codex.removeNone"));
|
|
44
|
+
if (io.which("codex")) io.log(t("ide.codex.unregisterManual", { plugin: PLUGIN }));
|
|
44
45
|
return true;
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -51,7 +52,7 @@ function migrateLegacy(io) {
|
|
|
51
52
|
for (const name of LEGACY_NATIVES) {
|
|
52
53
|
const old = join(io.home(), ".agents/skills", name);
|
|
53
54
|
if (existsSync(old) || isSymlink(old)) {
|
|
54
|
-
try { rmSync(old, { recursive: true, force: true }); io.log(
|
|
55
|
+
try { rmSync(old, { recursive: true, force: true }); io.log(t("ide.codex.legacy", { name })); } catch { /* 무시 */ }
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
if (io.which("codex")) for (const mp of LEGACY_MARKETPLACES) io.run("codex", ["plugin", "marketplace", "remove", mp]);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { existsSync, readFileSync, mkdirSync, cpSync, rmSync, writeFileSync, readdirSync } from "node:fs";
|
|
6
6
|
import { migrateConfigRoot, isLegacyVersion } from "../legacy.js";
|
|
7
|
+
import { t } from "../../../i18n/index.js";
|
|
7
8
|
|
|
8
9
|
const LEGACY_NAMES = ["cassiiopeia", "suh-devops-template"];
|
|
9
10
|
const LEGACY_MAX_VERSION = "4.2.4";
|
|
@@ -30,9 +31,9 @@ function apply(io, ctx = {}) {
|
|
|
30
31
|
migrateLegacy(io, ctx); // 옛 이름/버전 감지 시 projectops 소유 항목 선별 삭제
|
|
31
32
|
migrateConfigRoot(io); // 공용 config 루트 이관
|
|
32
33
|
const src = resolveSkillsSrc(ctx);
|
|
33
|
-
if (!src) { io.log("
|
|
34
|
+
if (!src) { io.log(t("ide.cursor.noSource")); return false; }
|
|
34
35
|
const dest = join(io.home(), ".cursor/skills");
|
|
35
|
-
io.log("
|
|
36
|
+
io.log(t("ide.cursor.copying"));
|
|
36
37
|
try {
|
|
37
38
|
mkdirSync(dest, { recursive: true });
|
|
38
39
|
for (const e of readdirSync(src, { withFileTypes: true })) {
|
|
@@ -52,28 +53,28 @@ function apply(io, ctx = {}) {
|
|
|
52
53
|
if (!/^(pro|suh)-/.test(name)) continue;
|
|
53
54
|
try {
|
|
54
55
|
rmSync(join(dest, name), { recursive: true, force: true });
|
|
55
|
-
io.log(
|
|
56
|
+
io.log(t("ide.cursor.obsolete", { name }));
|
|
56
57
|
} catch { /* 무시 — 지우지 못해도 설치는 계속한다 */ }
|
|
57
58
|
}
|
|
58
59
|
writeMeta(io, dest, ctx.templateVersion);
|
|
59
|
-
io.log(
|
|
60
|
+
io.log(t("ide.cursor.installed", { dest, version: ctx.templateVersion || "unknown" }));
|
|
60
61
|
return true;
|
|
61
62
|
} catch {
|
|
62
|
-
io.log("
|
|
63
|
+
io.log(t("ide.cursor.copyFailed"));
|
|
63
64
|
return false;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
function remove(io, ctx = {}) {
|
|
68
69
|
const dir = join(io.home(), ".cursor/skills");
|
|
69
|
-
if (!existsSync(join(dir, "cursor-skills-meta.json"))) { io.log("
|
|
70
|
+
if (!existsSync(join(dir, "cursor-skills-meta.json"))) { io.log(t("ide.cursor.removeNone")); return true; }
|
|
70
71
|
try {
|
|
71
72
|
for (const name of ownedEntries(io, ctx)) rmSync(join(dir, name), { recursive: true, force: true });
|
|
72
73
|
// 폴더가 비면 폴더 자체 제거, 타 스킬(somansa-tools 등) 남으면 유지
|
|
73
74
|
if (existsSync(dir) && readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true });
|
|
74
|
-
io.log(
|
|
75
|
+
io.log(t("ide.cursor.removed", { dir }));
|
|
75
76
|
return true;
|
|
76
|
-
} catch { io.log(
|
|
77
|
+
} catch { io.log(t("ide.cursor.removeFailed", { dir })); return false; }
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
// projectops가 이 폴더에 설치했다고 볼 항목만 골라낸다.
|
|
@@ -103,11 +104,11 @@ function migrateLegacy(io, ctx) {
|
|
|
103
104
|
for (const name of ownedEntries(io, ctx)) {
|
|
104
105
|
try { rmSync(join(io.home(), ".cursor/skills", name), { recursive: true, force: true }); } catch { /* 무시 */ }
|
|
105
106
|
}
|
|
106
|
-
io.log(
|
|
107
|
+
io.log(t("ide.cursor.legacy", { name: meta.name, version: meta.version }));
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
function manualHint() {
|
|
110
|
-
return "
|
|
111
|
+
return t("ide.cursor.manual");
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
// ── 헬퍼 ──
|
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
// Gemini CLI 어댑터 (.sh _manage_gemini_extension / _remove_gemini_section 등가).
|
|
2
2
|
// extension install/update/uninstall. 마켓플레이스 대신 git URL 확장.
|
|
3
3
|
import { migrateConfigRoot } from "../legacy.js";
|
|
4
|
+
import { t } from "../../../i18n/index.js";
|
|
4
5
|
|
|
5
6
|
const EXT = "projectops";
|
|
6
7
|
const URL = "https://github.com/Cassiiopeia/projectops";
|
|
7
8
|
const LEGACY_EXTS = ["SUH-DEVOPS-TEMPLATE"];
|
|
8
9
|
|
|
9
10
|
function detect(io) {
|
|
10
|
-
if (!io.which("gemini")) return { installed: false, version: null, cliMissing: true, note: "
|
|
11
|
+
if (!io.which("gemini")) return { installed: false, version: null, cliMissing: true, note: t("ide.cliMissing") };
|
|
11
12
|
// .sh는 gemini 설치 상태를 정밀 조회하지 않고 "설치 가능"으로만 표기 → installed 미상.
|
|
12
|
-
return { installed: false, version: null, cliMissing: false, note: "
|
|
13
|
+
return { installed: false, version: null, cliMissing: false, note: t("ide.installable") };
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
function apply(io) {
|
|
16
17
|
if (!io.which("gemini")) { io.log(manualHint()); return false; }
|
|
17
18
|
migrateLegacy(io);
|
|
18
19
|
migrateConfigRoot(io);
|
|
19
|
-
io.log("
|
|
20
|
-
if (io.run("gemini", ["extensions", "update", EXT]).code === 0) { io.log("
|
|
21
|
-
io.log("
|
|
22
|
-
if (io.run("gemini", ["extensions", "install", URL]).code === 0) { io.log("
|
|
23
|
-
io.log(
|
|
20
|
+
io.log(t("ide.gemini.updating"));
|
|
21
|
+
if (io.run("gemini", ["extensions", "update", EXT]).code === 0) { io.log(t("ide.gemini.updated")); return true; }
|
|
22
|
+
io.log(t("ide.gemini.installing"));
|
|
23
|
+
if (io.run("gemini", ["extensions", "install", URL]).code === 0) { io.log(t("ide.gemini.installed")); return true; }
|
|
24
|
+
io.log(t("ide.gemini.error", { url: URL }));
|
|
24
25
|
return false;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function remove(io) {
|
|
28
|
-
if (!io.which("gemini")) { io.log("
|
|
29
|
-
if (io.run("gemini", ["extensions", "uninstall", EXT]).code === 0) { io.log("
|
|
30
|
-
io.log(
|
|
29
|
+
if (!io.which("gemini")) { io.log(t("ide.gemini.notFound")); return true; }
|
|
30
|
+
if (io.run("gemini", ["extensions", "uninstall", EXT]).code === 0) { io.log(t("ide.gemini.removed")); return true; }
|
|
31
|
+
io.log(t("ide.gemini.removeFailed", { ext: EXT }));
|
|
31
32
|
return true; // 미설치 제거는 실패로 보지 않음
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// _pi_settings_path / _pi_harness_enabled 등가.
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs";
|
|
6
|
+
import { t } from "../../../i18n/index.js";
|
|
6
7
|
|
|
7
8
|
export const PI_PACKAGE_URL = "https://github.com/Cassiiopeia/projectops";
|
|
8
9
|
|
|
@@ -80,5 +81,5 @@ export function migratePiLegacy(io) {
|
|
|
80
81
|
}
|
|
81
82
|
} catch { /* 무시 */ }
|
|
82
83
|
}
|
|
83
|
-
try { rmSync(oldDir, { recursive: true, force: true }); io.log("
|
|
84
|
+
try { rmSync(oldDir, { recursive: true, force: true }); io.log(t("ide.pi.legacyClone")); } catch { /* 무시 */ }
|
|
84
85
|
}
|
|
@@ -3,43 +3,40 @@
|
|
|
3
3
|
// PI skill과 독립 — skill은 그대로 두고 harness 등록(settings.json extensions)만 켜고/끈다.
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { harnessEnabled, harnessAdd, harnessRemove, harnessLoaderPath } from "./pi-common.js";
|
|
6
|
+
import { t } from "../../../i18n/index.js";
|
|
6
7
|
|
|
7
8
|
// 이 어댑터는 "설치 가능" 여부가 pi 존재 + loader 파일 존재에 달림.
|
|
8
9
|
function available(io) { return !!io.which("pi") && existsSync(harnessLoaderPath(io)); }
|
|
9
10
|
|
|
10
11
|
function detect(io) {
|
|
11
|
-
if (!io.which("pi")) return { installed: false, version: null, cliMissing: true, note: "
|
|
12
|
-
if (!existsSync(harnessLoaderPath(io))) return { installed: false, version: null, cliMissing: true, note: "
|
|
13
|
-
return { installed: harnessEnabled(io), version: null, cliMissing: false, note: harnessEnabled(io) ? "
|
|
12
|
+
if (!io.which("pi")) return { installed: false, version: null, cliMissing: true, note: t("ide.harness.noPi") };
|
|
13
|
+
if (!existsSync(harnessLoaderPath(io))) return { installed: false, version: null, cliMissing: true, note: t("ide.harness.noLoader") };
|
|
14
|
+
return { installed: harnessEnabled(io), version: null, cliMissing: false, note: t(harnessEnabled(io) ? "ide.harness.enabled" : "ide.harness.disabled") };
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
// apply = harness 활성화 (skill은 안 건드림).
|
|
17
18
|
function apply(io) {
|
|
18
|
-
if (!available(io)) { io.log("
|
|
19
|
-
if (harnessEnabled(io)) { io.log(
|
|
19
|
+
if (!available(io)) { io.log(t("ide.harness.needPi")); return false; }
|
|
20
|
+
if (harnessEnabled(io)) { io.log(t("ide.harness.already")); return true; }
|
|
20
21
|
const r = harnessAdd(io);
|
|
21
|
-
if (r.ok) { io.log("
|
|
22
|
-
io.log(r.reason === "no-settings" ? "
|
|
23
|
-
: " harness loader가 없습니다 — 먼저 PI 패키지를 설치/업데이트하세요.");
|
|
22
|
+
if (r.ok) { io.log(t("ide.harness.enabledDone")); return true; }
|
|
23
|
+
io.log(t(r.reason === "no-settings" ? "ide.harness.noSettings" : "ide.harness.needPackage"));
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// remove = harness만 해제 (PI skill 보존).
|
|
28
28
|
function remove(io) {
|
|
29
|
-
if (!harnessEnabled(io)) { io.log("
|
|
30
|
-
io.log("
|
|
29
|
+
if (!harnessEnabled(io)) { io.log(t("ide.harness.removeNone")); return true; }
|
|
30
|
+
io.log(t("ide.harness.keepSkill"));
|
|
31
31
|
harnessRemove(io);
|
|
32
|
-
io.log("
|
|
32
|
+
io.log(t("ide.harness.removed"));
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function manualHint() { return "
|
|
36
|
+
function manualHint() { return t("ide.harness.manual"); }
|
|
37
37
|
|
|
38
38
|
// harness 설명 (프롬프트에서 재사용).
|
|
39
|
-
export const
|
|
40
|
-
"Persona Harness는 PI가 대화를 시작할 때마다 '전문가 페르소나'와 'SDLC 워크플로우'를",
|
|
41
|
-
"시스템 프롬프트에 자동 주입하는 기능입니다 (skill과 독립적으로 켜고/끌 수 있음).",
|
|
42
|
-
].join("\n");
|
|
39
|
+
export const harnessDesc = () => t("ide.harness.desc");
|
|
43
40
|
|
|
44
41
|
export const piHarnessAdapter = {
|
|
45
42
|
id: "pi-harness", label: "PI Persona Harness", order: 60,
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// pi install/update/remove. skill은 복사되지 않고 pi가 startup마다 스캔 → 설치 검증은 'pi list'.
|
|
3
3
|
import { PI_PACKAGE_URL, piInstalled, harnessEnabled, harnessRemove, migratePiLegacy } from "./pi-common.js";
|
|
4
4
|
import { migrateConfigRoot } from "../legacy.js";
|
|
5
|
+
import { t } from "../../../i18n/index.js";
|
|
5
6
|
|
|
6
7
|
function detect(io) {
|
|
7
|
-
if (!io.which("pi")) return { installed: false, version: null, cliMissing: true, note: "
|
|
8
|
+
if (!io.which("pi")) return { installed: false, version: null, cliMissing: true, note: t("ide.cliMissing") };
|
|
8
9
|
return { installed: piInstalled(io), version: null, cliMissing: false };
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -13,30 +14,30 @@ function apply(io) {
|
|
|
13
14
|
migratePiLegacy(io);
|
|
14
15
|
migrateConfigRoot(io);
|
|
15
16
|
if (piInstalled(io)) {
|
|
16
|
-
io.log("
|
|
17
|
+
io.log(t("ide.pi.updating"));
|
|
17
18
|
if (io.run("pi", ["update", PI_PACKAGE_URL]).code !== 0) io.run("pi", ["install", PI_PACKAGE_URL]);
|
|
18
19
|
} else {
|
|
19
|
-
io.log("
|
|
20
|
+
io.log(t("ide.pi.installing"));
|
|
20
21
|
io.run("pi", ["install", PI_PACKAGE_URL]);
|
|
21
22
|
}
|
|
22
23
|
if (piInstalled(io)) {
|
|
23
|
-
io.log("
|
|
24
|
-
io.log("
|
|
24
|
+
io.log(t("ide.pi.done"));
|
|
25
|
+
io.log(t("ide.pi.afterInstall"));
|
|
25
26
|
return true;
|
|
26
27
|
}
|
|
27
|
-
io.log(
|
|
28
|
+
io.log(t("ide.pi.failed", { url: PI_PACKAGE_URL }));
|
|
28
29
|
return false;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function remove(io) {
|
|
32
|
-
if (!io.which("pi")) { io.log("
|
|
33
|
-
if (!piInstalled(io)) { io.log("
|
|
33
|
+
if (!io.which("pi")) { io.log(t("ide.pi.notFound")); return true; }
|
|
34
|
+
if (!piInstalled(io)) { io.log(t("ide.pi.removeNone")); return true; }
|
|
34
35
|
io.log(` pi remove ${PI_PACKAGE_URL}`);
|
|
35
36
|
io.run("pi", ["remove", PI_PACKAGE_URL]);
|
|
36
|
-
if (piInstalled(io)) io.log("
|
|
37
|
-
else io.log("
|
|
37
|
+
if (piInstalled(io)) io.log(t("ide.pi.stillThere"));
|
|
38
|
+
else io.log(t("ide.pi.removed"));
|
|
38
39
|
// package 클론이 사라지면 harness loader 경로가 허공을 가리킴 → 함께 해제
|
|
39
|
-
if (harnessEnabled(io)) { io.log("
|
|
40
|
+
if (harnessEnabled(io)) { io.log(t("ide.pi.harnessAlso")); harnessRemove(io); }
|
|
40
41
|
return true;
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// 상태 라벨 표기(#776): 영문 표준("en")과 기존 한글("ko").
|
|
2
|
+
// 템플릿 원본은 영문 하나다. 한글 모드는 복사 직후 이름만 되돌려 만든다 —
|
|
3
|
+
// 한글 사본을 따로 두면 두 벌이 어긋난다.
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync } from "node:fs";
|
|
6
|
+
|
|
7
|
+
export const LABEL_STYLES = ["en", "ko"];
|
|
8
|
+
|
|
9
|
+
// [영문 표준, 한글 기존]
|
|
10
|
+
export const LABEL_PAIRS = [
|
|
11
|
+
["status: todo", "작업전"],
|
|
12
|
+
["status: in progress", "작업중"],
|
|
13
|
+
["status: needs review", "담당자확인"],
|
|
14
|
+
["status: feedback", "피드백"],
|
|
15
|
+
["status: done", "작업완료"],
|
|
16
|
+
["status: on hold", "보류"],
|
|
17
|
+
["status: cancelled", "취소"],
|
|
18
|
+
["priority: urgent", "긴급"],
|
|
19
|
+
["documentation", "문서"],
|
|
20
|
+
];
|
|
21
|
+
const EN_TO_KO = new Map(LABEL_PAIRS);
|
|
22
|
+
|
|
23
|
+
// version.yml 에 저장값이 없을 때의 결정. 신규 설치는 영문, 이미 설치된 레포는 한글 유지.
|
|
24
|
+
// 이미 쓰는 레포의 라벨 이름이 업데이트만으로 바뀌면 Projects 보드 매핑이 조용히 멈추기 때문이다.
|
|
25
|
+
export function resolveLabelStyle({ flag, stored, existing }) {
|
|
26
|
+
if (LABEL_STYLES.includes(flag)) return flag;
|
|
27
|
+
if (LABEL_STYLES.includes(stored)) return stored;
|
|
28
|
+
return existing ? "ko" : "en";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rewrite(file, fn) {
|
|
32
|
+
if (!existsSync(file)) return false;
|
|
33
|
+
const before = readFileSync(file, "utf8");
|
|
34
|
+
const after = fn(before);
|
|
35
|
+
if (after === before) return false;
|
|
36
|
+
writeFileSync(file, after);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const quoted = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
|
|
42
|
+
// 한글 모드: 복사된 파일의 라벨 이름을 한글로 되돌린다. 영문 모드는 아무것도 하지 않는다.
|
|
43
|
+
// 반환: 바뀐 파일의 상대경로 목록.
|
|
44
|
+
export function applyLabelStyle(targetRoot, style) {
|
|
45
|
+
if (style !== "ko") return [];
|
|
46
|
+
const changed = [];
|
|
47
|
+
const gh = join(targetRoot, ".github");
|
|
48
|
+
|
|
49
|
+
// 라벨 정의: name 줄만 바꾸고, 이름 변경용 from_name 줄은 지운다(이미 한글이라 필요 없다).
|
|
50
|
+
const labelsFile = join(gh, "config", "issue-labels.yml");
|
|
51
|
+
if (rewrite(labelsFile, (txt) => {
|
|
52
|
+
let out = txt.replace(/^\s*from_name:.*\n/gm, "");
|
|
53
|
+
for (const [en, ko] of LABEL_PAIRS) {
|
|
54
|
+
out = out.replace(new RegExp(`^(- name:\\s*)"?${quoted(en)}"?\\s*$`, "gm"), `$1${ko}`);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
})) changed.push(".github/config/issue-labels.yml");
|
|
58
|
+
|
|
59
|
+
// 이슈 템플릿 기본 라벨: front matter 의 labels 줄만 대상으로 한다.
|
|
60
|
+
const tplDir = join(gh, "ISSUE_TEMPLATE");
|
|
61
|
+
if (existsSync(tplDir)) {
|
|
62
|
+
for (const name of readdirSync(tplDir).filter((f) => f.endsWith(".md"))) {
|
|
63
|
+
const f = join(tplDir, name);
|
|
64
|
+
if (rewrite(f, (txt) => txt.replace(/^labels:\s*\[(.*)\]\s*$/m, (m, inner) => {
|
|
65
|
+
let list = inner;
|
|
66
|
+
for (const [en, ko] of LABEL_PAIRS) list = list.split(`"${en}"`).join(ko).split(`'${en}'`).join(ko);
|
|
67
|
+
return `labels: [${list}]`;
|
|
68
|
+
}))) changed.push(`.github/ISSUE_TEMPLATE/${name}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// QA 이슈 생성 봇이 붙이는 기본 라벨
|
|
73
|
+
const bot = join(gh, "workflows", "PROJECT-COMMON-QA-ISSUE-CREATION-BOT.yaml");
|
|
74
|
+
if (rewrite(bot, (txt) => txt.replace(/labels:\s*\['status: todo'\]/g, `labels: ['${EN_TO_KO.get("status: todo")}']`))) {
|
|
75
|
+
changed.push(".github/workflows/PROJECT-COMMON-QA-ISSUE-CREATION-BOT.yaml");
|
|
76
|
+
}
|
|
77
|
+
return changed;
|
|
78
|
+
}
|