oh-my-design-cli 1.8.6 → 1.9.0
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/.claude/hooks/lib/preferences-parser.cjs +1 -0
- package/.claude/hooks/lib/preferences-writer.cjs +1 -0
- package/.claude/hooks/post-edit-watch.cjs +1 -0
- package/.claude/hooks/session-end-foldin.cjs +1 -0
- package/.claude/hooks/session-state-loader.cjs +1 -0
- package/.claude/hooks/skill-activation.cjs +1 -0
- package/.claude/settings.json +10 -1
- package/.github/assets/logo-bg.png +0 -0
- package/AGENTS.md +20 -1
- package/CHANGELOG.md +482 -0
- package/MIGRATION.md +103 -0
- package/README.ja.md +56 -19
- package/README.ko.md +47 -30
- package/README.md +48 -30
- package/README.zh-TW.md +59 -22
- package/agents/AGENT.md +8 -7
- package/agents/omd-codex-image.md +1 -1
- package/agents/omd-critic.md +6 -7
- package/agents/omd-designer-review.md +1 -1
- package/agents/omd-final-qa.md +1 -1
- package/agents/omd-humanizer.md +38 -0
- package/agents/omd-kr-writer.md +3 -4
- package/agents/omd-locale-adapter.md +12 -16
- package/agents/omd-master.md +24 -19
- package/agents/omd-microcopy.md +1 -1
- package/agents/omd-orchestrator.md +7 -8
- package/agents/omd-slop-auditor.md +31 -0
- package/agents/omd-ux-researcher.md +1 -1
- package/data/reference-fingerprints.json +2761 -85
- package/dist/bin/oh-my-design.js +16 -8
- package/dist/bin/oh-my-design.js.map +1 -1
- package/dist/chunk-UJIHCEYF.js +99 -0
- package/dist/chunk-UJIHCEYF.js.map +1 -0
- package/dist/doctor-LDWSFM3I.js +763 -0
- package/dist/doctor-LDWSFM3I.js.map +1 -0
- package/dist/install-skills-ZZFNJ2II.js +1120 -0
- package/dist/install-skills-ZZFNJ2II.js.map +1 -0
- package/docs/CLI_QUICKSTART.md +157 -0
- package/package.json +25 -4
- package/scripts/postinstall.cjs +5 -4
- package/skills/claude-design/SKILL.md +38 -14
- package/skills/claude-design/references/claude-design-flow.md +11 -11
- package/skills/claude-design/references/codebase-analysis.md +5 -4
- package/skills/omd-final-qa/SKILL.md +1 -1
- package/skills/omd-harness/SKILL.md +51 -50
- package/skills/omd-humanize/SKILL.md +116 -0
- package/skills/omd-humanize/agents/openai.yaml +4 -0
- package/skills/omd-humanize/references/locale-playbooks.md +102 -0
- package/skills/omd-humanize/references/sources.md +11 -0
- package/skills/omd-init/SKILL.md +18 -12
- package/skills/omd-locale-adapter/SKILL.md +58 -91
- package/skills/omd-orchestrator/SKILL.md +11 -5
- package/skills/omd-reference-capture/SKILL.md +80 -26
- package/skills/omd-reference-capture/scripts/PLAYWRIGHT-LICENSE.txt +202 -0
- package/skills/omd-reference-capture/scripts/PLAYWRIGHT-NOTICE.txt +5 -0
- package/skills/omd-reference-capture/scripts/THIRD_PARTY_NOTICES.md +3 -0
- package/skills/omd-reference-capture/scripts/capture-reference-evidence.mjs +159906 -0
- package/skills/omd-slop-audit/SKILL.md +110 -0
- package/skills/omd-slop-audit/agents/openai.yaml +4 -0
- package/skills/omd-slop-audit/references/pattern-catalog.md +96 -0
- package/skills/omd-slop-audit/references/sources.md +24 -0
- package/web/references/104/DESIGN.md +354 -0
- package/web/references/11st/DESIGN.md +197 -316
- package/web/references/29cm/DESIGN.md +297 -393
- package/web/references/42dot/DESIGN.md +390 -0
- package/web/references/8percent/DESIGN.md +438 -0
- package/web/references/ably/DESIGN.md +267 -572
- package/web/references/acer/DESIGN.md +329 -0
- package/web/references/airbnb/DESIGN.md +219 -557
- package/web/references/airbridge/DESIGN.md +451 -0
- package/web/references/alipay/DESIGN.md +179 -420
- package/web/references/apple/DESIGN.md +224 -498
- package/web/references/asleep/DESIGN.md +402 -0
- package/web/references/au/DESIGN.md +334 -0
- package/web/references/autopedia/DESIGN.md +382 -0
- package/web/references/aws-cloudscape/DESIGN.md +241 -0
- package/web/references/baemin/DESIGN.md +332 -491
- package/web/references/banksalad/DESIGN.md +269 -180
- package/web/references/barogo/DESIGN.md +416 -0
- package/web/references/beusable/DESIGN.md +464 -0
- package/web/references/bigin/DESIGN.md +454 -0
- package/web/references/bilibili/DESIGN.md +206 -374
- package/web/references/bmw/DESIGN.md +162 -276
- package/web/references/brandi/DESIGN.md +184 -325
- package/web/references/buzzvil/DESIGN.md +457 -0
- package/web/references/cafe24/DESIGN.md +472 -0
- package/web/references/catchtable/DESIGN.md +219 -264
- package/web/references/cgv/DESIGN.md +317 -0
- package/web/references/channeltalk/DESIGN.md +210 -336
- package/web/references/china-airlines/DESIGN.md +291 -0
- package/web/references/cjonstyle/DESIGN.md +453 -0
- package/web/references/class101/DESIGN.md +168 -387
- package/web/references/classum/DESIGN.md +228 -203
- package/web/references/claude/DESIGN.md +242 -479
- package/web/references/clay/DESIGN.md +179 -377
- package/web/references/clickhouse/DESIGN.md +254 -343
- package/web/references/codeit/DESIGN.md +470 -0
- package/web/references/cohere/DESIGN.md +214 -324
- package/web/references/coinone/DESIGN.md +199 -179
- package/web/references/corca/DESIGN.md +414 -0
- package/web/references/coupang/DESIGN.md +205 -475
- package/web/references/cursor/DESIGN.md +240 -381
- package/web/references/dabang/DESIGN.md +248 -213
- package/web/references/dable/DESIGN.md +431 -0
- package/web/references/datarize/DESIGN.md +338 -0
- package/web/references/dealicious/DESIGN.md +402 -0
- package/web/references/drdiary/DESIGN.md +404 -0
- package/web/references/elastic/DESIGN.md +238 -0
- package/web/references/elevenlabs/DESIGN.md +183 -321
- package/web/references/elice/DESIGN.md +445 -0
- package/web/references/eslite/DESIGN.md +257 -0
- package/web/references/evaair/DESIGN.md +338 -0
- package/web/references/expo/DESIGN.md +238 -331
- package/web/references/familymart-tw/DESIGN.md +254 -0
- package/web/references/ferrari/DESIGN.md +160 -377
- package/web/references/figma/DESIGN.md +197 -451
- package/web/references/fitpet/DESIGN.md +445 -0
- package/web/references/flex/DESIGN.md +170 -253
- package/web/references/framer/DESIGN.md +197 -340
- package/web/references/freee/DESIGN.md +211 -533
- package/web/references/friendliai/DESIGN.md +431 -0
- package/web/references/frip/DESIGN.md +440 -0
- package/web/references/furiosaai/DESIGN.md +450 -0
- package/web/references/gangnamunni/DESIGN.md +154 -591
- package/web/references/gaudiolab/DESIGN.md +414 -0
- package/web/references/google/DESIGN.md +290 -398
- package/web/references/goorm/DESIGN.md +470 -0
- package/web/references/greencar/DESIGN.md +420 -0
- package/web/references/hackle/DESIGN.md +472 -0
- package/web/references/hashicorp/DESIGN.md +273 -318
- package/web/references/heydealer/DESIGN.md +429 -0
- package/web/references/humanscape/DESIGN.md +433 -0
- package/web/references/hwahae/DESIGN.md +453 -0
- package/web/references/hyundai/DESIGN.md +158 -368
- package/web/references/hyundaicard/DESIGN.md +171 -121
- package/web/references/ibm/DESIGN.md +227 -378
- package/web/references/idus/DESIGN.md +450 -0
- package/web/references/igaworks/DESIGN.md +437 -0
- package/web/references/inflearn/DESIGN.md +221 -366
- package/web/references/intuit/DESIGN.md +244 -0
- package/web/references/jandi/DESIGN.md +191 -291
- package/web/references/jobplanet/DESIGN.md +415 -0
- package/web/references/kakao/DESIGN.md +287 -475
- package/web/references/kakaobank/DESIGN.md +220 -521
- package/web/references/kakaogames/DESIGN.md +220 -0
- package/web/references/kakaopay/DESIGN.md +207 -526
- package/web/references/karrot/DESIGN.md +227 -251
- package/web/references/kb-kookmin/DESIGN.md +308 -0
- package/web/references/kbank/DESIGN.md +220 -175
- package/web/references/kcd/DESIGN.md +432 -0
- package/web/references/kmong/DESIGN.md +281 -356
- package/web/references/krds/DESIGN.md +245 -19
- package/web/references/kream/DESIGN.md +224 -359
- package/web/references/kurly/DESIGN.md +211 -533
- package/web/references/kyobobook/DESIGN.md +445 -0
- package/web/references/lablup/DESIGN.md +474 -0
- package/web/references/lamborghini/DESIGN.md +198 -360
- package/web/references/lemonbase/DESIGN.md +452 -0
- package/web/references/lguplus/DESIGN.md +259 -0
- package/web/references/likelion/DESIGN.md +283 -0
- package/web/references/line/DESIGN.md +396 -355
- package/web/references/linear.app/DESIGN.md +209 -562
- package/web/references/lotteon/DESIGN.md +386 -0
- package/web/references/lovable/DESIGN.md +255 -349
- package/web/references/makinarocks/DESIGN.md +242 -0
- package/web/references/maum-ai/DESIGN.md +423 -0
- package/web/references/medibloc/DESIGN.md +425 -0
- package/web/references/megabox/DESIGN.md +232 -0
- package/web/references/mercari/DESIGN.md +217 -434
- package/web/references/microsoft/DESIGN.md +234 -388
- package/web/references/mildang/DESIGN.md +411 -0
- package/web/references/millie/DESIGN.md +183 -504
- package/web/references/minimax/DESIGN.md +264 -322
- package/web/references/miricanvas/DESIGN.md +182 -355
- package/web/references/miro/DESIGN.md +234 -232
- package/web/references/mistral.ai/DESIGN.md +231 -314
- package/web/references/mixi/DESIGN.md +234 -0
- package/web/references/moin/DESIGN.md +445 -0
- package/web/references/mongodb/DESIGN.md +219 -316
- package/web/references/moreh/DESIGN.md +437 -0
- package/web/references/musinsa/DESIGN.md +184 -510
- package/web/references/mynavi/DESIGN.md +242 -0
- package/web/references/myrealtrip/DESIGN.md +169 -406
- package/web/references/naver/DESIGN.md +328 -499
- package/web/references/naverwebtoon/DESIGN.md +172 -395
- package/web/references/neosapience/DESIGN.md +441 -0
- package/web/references/nhn/DESIGN.md +169 -346
- package/web/references/nhncloud/DESIGN.md +185 -123
- package/web/references/nota/DESIGN.md +451 -0
- package/web/references/notion/DESIGN.md +271 -475
- package/web/references/nrise/DESIGN.md +417 -0
- package/web/references/ohouse/DESIGN.md +210 -549
- package/web/references/oliveyoung/DESIGN.md +217 -326
- package/web/references/ollama/DESIGN.md +241 -308
- package/web/references/onestore/DESIGN.md +248 -0
- package/web/references/palantir/DESIGN.md +256 -0
- package/web/references/panasonic/DESIGN.md +276 -0
- package/web/references/patternfly/DESIGN.md +259 -0
- package/web/references/payhere/DESIGN.md +438 -0
- package/web/references/pega/DESIGN.md +262 -0
- package/web/references/peoplefund/DESIGN.md +431 -0
- package/web/references/petfriends/DESIGN.md +428 -0
- package/web/references/pinkfong/DESIGN.md +199 -367
- package/web/references/pinterest/DESIGN.md +258 -308
- package/web/references/portone/DESIGN.md +446 -0
- package/web/references/poya/DESIGN.md +286 -0
- package/web/references/pozalabs/DESIGN.md +438 -0
- package/web/references/protopie/DESIGN.md +419 -0
- package/web/references/qraft/DESIGN.md +448 -0
- package/web/references/queenit/DESIGN.md +432 -0
- package/web/references/quotabook/DESIGN.md +449 -0
- package/web/references/rebellions/DESIGN.md +449 -0
- package/web/references/recruit/DESIGN.md +247 -0
- package/web/references/remember/DESIGN.md +202 -396
- package/web/references/resend/DESIGN.md +274 -348
- package/web/references/returnzero/DESIGN.md +460 -0
- package/web/references/sakura-internet/DESIGN.md +269 -0
- package/web/references/samsung/DESIGN.md +157 -359
- package/web/references/sandoll/DESIGN.md +445 -0
- package/web/references/sanity/DESIGN.md +241 -375
- package/web/references/saramin/DESIGN.md +465 -0
- package/web/references/scatterlab/DESIGN.md +412 -0
- package/web/references/selectstar/DESIGN.md +439 -0
- package/web/references/servicenow/DESIGN.md +289 -0
- package/web/references/shiftee/DESIGN.md +468 -0
- package/web/references/shinhanbank/DESIGN.md +161 -341
- package/web/references/shinhancard/DESIGN.md +208 -319
- package/web/references/sionic/DESIGN.md +454 -0
- package/web/references/sktelecom/DESIGN.md +310 -0
- package/web/references/socar/DESIGN.md +125 -32
- package/web/references/softbank/DESIGN.md +231 -0
- package/web/references/solapi/DESIGN.md +483 -0
- package/web/references/sony/DESIGN.md +234 -0
- package/web/references/soop/DESIGN.md +261 -0
- package/web/references/spacex/DESIGN.md +184 -323
- package/web/references/spotify/DESIGN.md +253 -394
- package/web/references/squeezebits/DESIGN.md +441 -0
- package/web/references/stayfolio/DESIGN.md +422 -0
- package/web/references/stripe/DESIGN.md +212 -491
- package/web/references/supabase/DESIGN.md +244 -316
- package/web/references/supertone/DESIGN.md +413 -0
- package/web/references/tabling/DESIGN.md +415 -0
- package/web/references/taishinbank/DESIGN.md +243 -0
- package/web/references/teamlab/DESIGN.md +240 -0
- package/web/references/teamsparta/DESIGN.md +472 -0
- package/web/references/tellingme/DESIGN.md +392 -0
- package/web/references/tesla/DESIGN.md +244 -427
- package/web/references/thsr/DESIGN.md +284 -0
- package/web/references/thumbtack/DESIGN.md +302 -0
- package/web/references/together.ai/DESIGN.md +257 -304
- package/web/references/toss/DESIGN.md +251 -593
- package/web/references/toss-securities/DESIGN.md +255 -178
- package/web/references/tossbank/DESIGN.md +195 -491
- package/web/references/toyota/DESIGN.md +238 -0
- package/web/references/tving/DESIGN.md +197 -245
- package/web/references/typed/DESIGN.md +413 -0
- package/web/references/uber/DESIGN.md +256 -333
- package/web/references/udn/DESIGN.md +250 -0
- package/web/references/upbit/DESIGN.md +145 -256
- package/web/references/upstage/DESIGN.md +211 -204
- package/web/references/uswds/DESIGN.md +287 -0
- package/web/references/vercel/DESIGN.md +236 -440
- package/web/references/vuno/DESIGN.md +413 -0
- package/web/references/wanted/DESIGN.md +215 -487
- package/web/references/weverse/DESIGN.md +437 -0
- package/web/references/wise/DESIGN.md +223 -231
- package/web/references/wisetracker/DESIGN.md +462 -0
- package/web/references/wooribank/DESIGN.md +245 -0
- package/web/references/yanolja/DESIGN.md +250 -419
- package/web/references/yeogiotte/DESIGN.md +235 -418
- package/web/references/zapier/DESIGN.md +256 -377
- package/web/references/zendesk/DESIGN.md +288 -0
- package/web/references/zigzag/DESIGN.md +200 -621
- package/dist/install-skills-7UUDOLG2.js +0 -734
- package/dist/install-skills-7UUDOLG2.js.map +0 -1
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CLAUDE_HOOK_PATHS,
|
|
4
|
+
isKnownLegacyHook,
|
|
5
|
+
isSelfConsistentManagedHook,
|
|
6
|
+
renderManagedHook,
|
|
7
|
+
unsafeManagedPath
|
|
8
|
+
} from "./chunk-UJIHCEYF.js";
|
|
9
|
+
|
|
10
|
+
// src/cli/install-skills.ts
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
import {
|
|
14
|
+
readFileSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
existsSync as existsSync2,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
cpSync,
|
|
20
|
+
rmSync,
|
|
21
|
+
rmdirSync,
|
|
22
|
+
statSync,
|
|
23
|
+
renameSync
|
|
24
|
+
} from "fs";
|
|
25
|
+
import { join as join2, dirname, relative } from "path";
|
|
26
|
+
import { homedir } from "os";
|
|
27
|
+
import { createHash } from "crypto";
|
|
28
|
+
import { fileURLToPath } from "url";
|
|
29
|
+
|
|
30
|
+
// src/core/agent-detect.ts
|
|
31
|
+
import { existsSync } from "fs";
|
|
32
|
+
import { join } from "path";
|
|
33
|
+
function detectInstalledAgents(projectRoot) {
|
|
34
|
+
return {
|
|
35
|
+
claudeCode: existsSync(join(projectRoot, ".claude")) || existsSync(join(projectRoot, "CLAUDE.md")),
|
|
36
|
+
codex: existsSync(join(projectRoot, ".codex")) || existsSync(join(projectRoot, "AGENTS.md")) || existsSync(join(projectRoot, "AGENTS.override.md")),
|
|
37
|
+
opencode: existsSync(join(projectRoot, ".opencode")) || existsSync(join(projectRoot, "opencode.json")) || existsSync(join(projectRoot, "opencode.jsonc")),
|
|
38
|
+
cursor: existsSync(join(projectRoot, ".cursor")) || existsSync(join(projectRoot, ".cursorrules"))
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/cli/install-skills.ts
|
|
43
|
+
var VALID_SKILL_TARGETS = ["claude-code", "codex", "opencode", "cursor"];
|
|
44
|
+
var DEVELOPMENT_ONLY_SKILLS = /* @__PURE__ */ new Set(["omd-lab-02-design-harness"]);
|
|
45
|
+
function targetsAvailableForScope(targets, scope) {
|
|
46
|
+
return scope === "global" ? targets.filter((target) => target !== "cursor") : targets;
|
|
47
|
+
}
|
|
48
|
+
function findPackageRoot() {
|
|
49
|
+
let cur = dirname(fileURLToPath(import.meta.url));
|
|
50
|
+
for (let i = 0; i < 8; i++) {
|
|
51
|
+
if (existsSync2(join2(cur, "skills"))) return cur;
|
|
52
|
+
const parent = dirname(cur);
|
|
53
|
+
if (parent === cur) break;
|
|
54
|
+
cur = parent;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function listShippedSkills(packageRoot) {
|
|
59
|
+
const skillsDir = join2(packageRoot, "skills");
|
|
60
|
+
if (!existsSync2(skillsDir)) return [];
|
|
61
|
+
return readdirSync(skillsDir).filter(
|
|
62
|
+
(name) => !DEVELOPMENT_ONLY_SKILLS.has(name) && existsSync2(join2(skillsDir, name, "SKILL.md"))
|
|
63
|
+
).sort();
|
|
64
|
+
}
|
|
65
|
+
function listCanonicalAgents(packageRoot) {
|
|
66
|
+
const dir = join2(packageRoot, "agents");
|
|
67
|
+
if (!existsSync2(dir)) return [];
|
|
68
|
+
return readdirSync(dir).filter((name) => name.startsWith("omd-") && name.endsWith(".md")).sort();
|
|
69
|
+
}
|
|
70
|
+
function parseCanonicalAgent(packageRoot, filename) {
|
|
71
|
+
const src = readFileSync(join2(packageRoot, "agents", filename), "utf8");
|
|
72
|
+
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(src);
|
|
73
|
+
if (!match) {
|
|
74
|
+
throw new Error(`agents/${filename}: missing YAML frontmatter`);
|
|
75
|
+
}
|
|
76
|
+
const fm = match[1];
|
|
77
|
+
const body = match[2];
|
|
78
|
+
const grab = (key) => {
|
|
79
|
+
const re = new RegExp(`^${key}:\\s*(.+)$`, "m");
|
|
80
|
+
const m = re.exec(fm);
|
|
81
|
+
if (!m) return "";
|
|
82
|
+
const value = m[1].trim();
|
|
83
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
84
|
+
try {
|
|
85
|
+
const decoded = JSON.parse(value);
|
|
86
|
+
if (typeof decoded === "string") return decoded;
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
91
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
name: grab("name") || filename.replace(/\.md$/, ""),
|
|
97
|
+
description: grab("description"),
|
|
98
|
+
tools: grab("tools").split(",").map((t) => t.trim()).filter(Boolean),
|
|
99
|
+
model: grab("model") || "sonnet",
|
|
100
|
+
body
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function renderClaudeAgent(a) {
|
|
104
|
+
const fm = [
|
|
105
|
+
"---",
|
|
106
|
+
`name: ${JSON.stringify(a.name)}`,
|
|
107
|
+
`description: ${JSON.stringify(a.description)}`,
|
|
108
|
+
`tools: ${JSON.stringify(a.tools)}`,
|
|
109
|
+
`model: ${JSON.stringify(a.model)}`,
|
|
110
|
+
`omd_managed: true`,
|
|
111
|
+
"---",
|
|
112
|
+
""
|
|
113
|
+
].join("\n");
|
|
114
|
+
return fm + a.body;
|
|
115
|
+
}
|
|
116
|
+
function nativeAgentBody(body, channel, scope) {
|
|
117
|
+
const skillRoot = channel === "codex" ? scope === "global" ? "~/.agents/skills" : ".agents/skills" : scope === "global" ? "~/.config/opencode/skills" : ".opencode/skills";
|
|
118
|
+
const nativeBody = body.replace(/(?:~\/)?\.claude\/skills\//g, `${skillRoot}/`).replace(/You are spawned as a Claude Code subagent/g, "You run as a host-managed subagent").replace(/Claude Code subagent/g, "host-managed subagent").replace(/the Agent tool/g, "the host's native sub-agent mechanism").replace(/Agent tool/g, "host-native sub-agent mechanism").replace(/AskUserQuestion-compatible/g, "host question-interface compatible").replace(
|
|
119
|
+
/launcher calls AskUserQuestion\(questions_file\)/g,
|
|
120
|
+
"launcher presents questions from `questions_file` to the user"
|
|
121
|
+
).replace(/launcher calls AskUserQuestion/g, "launcher presents the questions to the user").replace(/no AskUserQuestion/g, "without direct user-question tools");
|
|
122
|
+
return channel === "opencode" ? nativeBody.replace(/\bomd:([a-z0-9-]+)/g, "omd-$1") : nativeBody;
|
|
123
|
+
}
|
|
124
|
+
function nativeSkillRoot(channel, scope) {
|
|
125
|
+
if (channel === "codex") {
|
|
126
|
+
return scope === "global" ? "~/.agents/skills" : ".agents/skills";
|
|
127
|
+
}
|
|
128
|
+
return scope === "global" ? "~/.config/opencode/skills" : ".opencode/skills";
|
|
129
|
+
}
|
|
130
|
+
function renderOpenCodeAgent(a, scope, dataRoot) {
|
|
131
|
+
const skillRoot = nativeSkillRoot("opencode", scope);
|
|
132
|
+
return [
|
|
133
|
+
"---",
|
|
134
|
+
`description: ${JSON.stringify(a.description)}`,
|
|
135
|
+
"mode: subagent",
|
|
136
|
+
"---",
|
|
137
|
+
"",
|
|
138
|
+
"## Installed role runtime contract",
|
|
139
|
+
"",
|
|
140
|
+
`- Resolve referenced OmD skills from \`${skillRoot}/<skill>/SKILL.md\`.`,
|
|
141
|
+
`- Resolve the installed offline catalog from \`${dataRoot}\`.`,
|
|
142
|
+
"- Use OpenCode's native sub-agent mechanism for specialist handoffs.",
|
|
143
|
+
"",
|
|
144
|
+
nativeAgentBody(a.body, "opencode", scope).trimEnd(),
|
|
145
|
+
"",
|
|
146
|
+
"<!-- omd:installed-agent \u2014 generated from the canonical OmD role. -->",
|
|
147
|
+
""
|
|
148
|
+
].join("\n");
|
|
149
|
+
}
|
|
150
|
+
function codexSafeAgentBody(body) {
|
|
151
|
+
return body.replace(/`omd init prepare`/g, "`omd:init` skill flow").replace(/^omd remember .*$/gm, "Use the `omd:remember` skill directly with the same finding and context.").replace(/`omd remember [^`]+`/g, "the `omd:remember` skill with the same correction details").replace(/\bomd remember\b/g, "omd:remember skill");
|
|
152
|
+
}
|
|
153
|
+
function renderCodexAgent(a, scope, dataRoot) {
|
|
154
|
+
const name = a.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
155
|
+
const desc = a.description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
156
|
+
const skillRoot = nativeSkillRoot("codex", scope);
|
|
157
|
+
const instructions = [
|
|
158
|
+
"## Codex runtime contract",
|
|
159
|
+
"",
|
|
160
|
+
"- Execute the OmD behavior directly from these instructions and the installed skills.",
|
|
161
|
+
`- Resolve referenced OmD skills from \`${skillRoot}/<skill>/SKILL.md\`.`,
|
|
162
|
+
`- Resolve the installed offline catalog from \`${dataRoot}\`.`,
|
|
163
|
+
"- Use Codex's native sub-agent mechanism for specialist handoffs.",
|
|
164
|
+
"- The OmD CLI exposes installation and diagnostics only. Do not invent or invoke removed helper subcommands.",
|
|
165
|
+
"",
|
|
166
|
+
codexSafeAgentBody(nativeAgentBody(a.body, "codex", scope)).trimEnd()
|
|
167
|
+
].join("\n");
|
|
168
|
+
if (instructions.includes("'''")) {
|
|
169
|
+
throw new Error(`agents/${a.name}.md: cannot encode triple single quote in Codex TOML`);
|
|
170
|
+
}
|
|
171
|
+
return [
|
|
172
|
+
`name = "${name}"`,
|
|
173
|
+
`description = "${desc}"`,
|
|
174
|
+
"",
|
|
175
|
+
`developer_instructions = '''`,
|
|
176
|
+
instructions,
|
|
177
|
+
`'''`,
|
|
178
|
+
""
|
|
179
|
+
].join("\n");
|
|
180
|
+
}
|
|
181
|
+
function planForTarget(projectRoot, target, scope) {
|
|
182
|
+
switch (target) {
|
|
183
|
+
case "claude-code":
|
|
184
|
+
return {
|
|
185
|
+
target,
|
|
186
|
+
destDir: join2(projectRoot, ".claude", "skills"),
|
|
187
|
+
layout: "folder"
|
|
188
|
+
};
|
|
189
|
+
case "codex":
|
|
190
|
+
return {
|
|
191
|
+
target,
|
|
192
|
+
destDir: join2(projectRoot, ".agents", "skills"),
|
|
193
|
+
layout: "folder"
|
|
194
|
+
};
|
|
195
|
+
case "opencode":
|
|
196
|
+
return {
|
|
197
|
+
target,
|
|
198
|
+
destDir: scope === "global" ? join2(projectRoot, ".config", "opencode", "skills") : join2(projectRoot, ".opencode", "skills"),
|
|
199
|
+
layout: "folder"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
var CHANNEL_DATA_DIRS = {
|
|
204
|
+
"claude-code": ".claude",
|
|
205
|
+
codex: ".codex",
|
|
206
|
+
opencode: ".opencode",
|
|
207
|
+
cursor: null
|
|
208
|
+
};
|
|
209
|
+
function dataDirFor(target, targets) {
|
|
210
|
+
if (target === "cursor") {
|
|
211
|
+
return targets.includes("claude-code") ? null : ".claude";
|
|
212
|
+
}
|
|
213
|
+
return CHANNEL_DATA_DIRS[target];
|
|
214
|
+
}
|
|
215
|
+
function dataDirForScope(target, targets, scope) {
|
|
216
|
+
if (scope === "global" && target === "opencode") return ".config/opencode";
|
|
217
|
+
return dataDirFor(target, targets);
|
|
218
|
+
}
|
|
219
|
+
var MANAGED_HEADER = "<!-- omd:installed-skill \u2014 managed by `omd install-skills`. Do not edit; rerun the command to refresh. -->";
|
|
220
|
+
var MANAGED_MARKER_SUBSTR = "omd:installed-skill";
|
|
221
|
+
function withManagedMarker(src) {
|
|
222
|
+
const fm = /^(---\r?\n[\s\S]*?\r?\n---\r?\n)([\s\S]*)$/.exec(src);
|
|
223
|
+
if (!fm) {
|
|
224
|
+
return MANAGED_HEADER + "\n\n" + src;
|
|
225
|
+
}
|
|
226
|
+
return fm[1] + MANAGED_HEADER + "\n\n" + fm[2];
|
|
227
|
+
}
|
|
228
|
+
function isManagedSkillFile(content) {
|
|
229
|
+
if (!content) return false;
|
|
230
|
+
const head = content.split("\n", 30).join("\n");
|
|
231
|
+
return head.includes(MANAGED_MARKER_SUBSTR);
|
|
232
|
+
}
|
|
233
|
+
function withGlobalDataHint(src, globalDataRoot) {
|
|
234
|
+
if (!globalDataRoot) return src;
|
|
235
|
+
const hint = [
|
|
236
|
+
`> **Installed global data root (highest priority):** \`${globalDataRoot}\`.`,
|
|
237
|
+
"> Resolve catalog JSON, references, and helper scripts there before project-relative fallbacks.",
|
|
238
|
+
""
|
|
239
|
+
].join("\n");
|
|
240
|
+
const fm = /^(---\r?\n[\s\S]*?\r?\n---\r?\n)([\s\S]*)$/.exec(src);
|
|
241
|
+
return fm ? fm[1] + hint + fm[2] : hint + src;
|
|
242
|
+
}
|
|
243
|
+
function renderSkillForChannel(src, folderName, target) {
|
|
244
|
+
const installedName = target === "opencode" ? folderName : folderName.replace(/^omd-/, "omd:");
|
|
245
|
+
const rendered = src.replace(
|
|
246
|
+
/^name:\s*[^\r\n]+$/m,
|
|
247
|
+
`name: ${installedName}`
|
|
248
|
+
);
|
|
249
|
+
return target === "opencode" ? rendered.replace(/\bomd:([a-z0-9][a-z0-9-]*)\b/g, "omd-$1") : rendered;
|
|
250
|
+
}
|
|
251
|
+
function removeManagedLegacyCodexSkills(root) {
|
|
252
|
+
const legacyRoot = join2(root, ".codex", "skills");
|
|
253
|
+
if (!existsSync2(legacyRoot)) return [];
|
|
254
|
+
const results = [];
|
|
255
|
+
for (const entry of readdirSync(legacyRoot, { withFileTypes: true })) {
|
|
256
|
+
if (!entry.isDirectory()) continue;
|
|
257
|
+
const skillPath = join2(legacyRoot, entry.name, "SKILL.md");
|
|
258
|
+
if (!existsSync2(skillPath)) continue;
|
|
259
|
+
const content = readFileSync(skillPath, "utf8");
|
|
260
|
+
if (!isManagedSkillFile(content)) continue;
|
|
261
|
+
rmSync(skillPath, { force: true });
|
|
262
|
+
const dir = dirname(skillPath);
|
|
263
|
+
if (readdirSync(dir).length === 0) rmdirSync(dir);
|
|
264
|
+
results.push({
|
|
265
|
+
target: "codex",
|
|
266
|
+
skill: `legacy-skill:${entry.name}`,
|
|
267
|
+
destPath: skillPath,
|
|
268
|
+
status: "removed"
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
var IGNORED_SKILL_ENTRIES = /* @__PURE__ */ new Set([".runtime", "__pycache__", ".DS_Store"]);
|
|
274
|
+
function isIgnoredSkillTreeEntry(name) {
|
|
275
|
+
return IGNORED_SKILL_ENTRIES.has(name) || name.endsWith(".pyc");
|
|
276
|
+
}
|
|
277
|
+
function shippedSkillTreeMatches(sourceDir, destinationDir) {
|
|
278
|
+
if (!existsSync2(destinationDir)) return false;
|
|
279
|
+
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
280
|
+
if (entry.name === "SKILL.md" || isIgnoredSkillTreeEntry(entry.name)) continue;
|
|
281
|
+
const source = join2(sourceDir, entry.name);
|
|
282
|
+
const destination = join2(destinationDir, entry.name);
|
|
283
|
+
if (!existsSync2(destination)) return false;
|
|
284
|
+
try {
|
|
285
|
+
const destinationStat = statSync(destination);
|
|
286
|
+
if (entry.isDirectory()) {
|
|
287
|
+
if (!destinationStat.isDirectory() || !shippedSkillTreeMatches(source, destination)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
} else if (!destinationStat.isFile() || !readFileSync(source).equals(readFileSync(destination))) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
function parseSkillChannels(skillMd) {
|
|
300
|
+
const fm = /^---\n([\s\S]*?)\n---/.exec(skillMd);
|
|
301
|
+
if (!fm) return null;
|
|
302
|
+
const m = /^x-omd-channels:\s*(.+)$/m.exec(fm[1]);
|
|
303
|
+
if (!m) return null;
|
|
304
|
+
const valid = ["claude-code", "codex", "opencode"];
|
|
305
|
+
const list = m[1].split(/[,\s]+/).map((s) => s.trim()).filter((s) => valid.includes(s));
|
|
306
|
+
return list.length > 0 ? list : null;
|
|
307
|
+
}
|
|
308
|
+
function skillSupportedChannels(packageRoot, skill) {
|
|
309
|
+
return parseSkillChannels(readFileSync(join2(packageRoot, "skills", skill, "SKILL.md"), "utf8")) ?? ["claude-code", "codex", "opencode"];
|
|
310
|
+
}
|
|
311
|
+
function installOne(packageRoot, installRoot, plan, skill, force, globalDataRoot) {
|
|
312
|
+
const skillDir = join2(packageRoot, "skills", skill);
|
|
313
|
+
const src = readFileSync(join2(skillDir, "SKILL.md"), "utf8");
|
|
314
|
+
const channelSrc = withGlobalDataHint(
|
|
315
|
+
renderSkillForChannel(src, skill, plan.target),
|
|
316
|
+
globalDataRoot
|
|
317
|
+
);
|
|
318
|
+
const managed = withManagedMarker(channelSrc);
|
|
319
|
+
const channels = parseSkillChannels(src);
|
|
320
|
+
if (channels && !channels.includes(plan.target)) {
|
|
321
|
+
return {
|
|
322
|
+
target: plan.target,
|
|
323
|
+
skill,
|
|
324
|
+
destPath: join2(plan.destDir, skill + ".md"),
|
|
325
|
+
status: "skipped-incompat"
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const extras = readdirSync(skillDir).filter(
|
|
329
|
+
(n) => n !== "SKILL.md" && !IGNORED_SKILL_ENTRIES.has(n)
|
|
330
|
+
);
|
|
331
|
+
const isMultiFile = extras.length > 0;
|
|
332
|
+
if (plan.layout !== "folder" && isMultiFile) {
|
|
333
|
+
return {
|
|
334
|
+
target: plan.target,
|
|
335
|
+
skill,
|
|
336
|
+
destPath: join2(plan.destDir, skill + ".md"),
|
|
337
|
+
status: "skipped-incompat"
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const destPath = plan.layout === "folder" ? join2(plan.destDir, skill, "SKILL.md") : join2(plan.destDir, skill + ".md");
|
|
341
|
+
if (unsafeManagedPath(installRoot, destPath)) {
|
|
342
|
+
return { target: plan.target, skill, destPath, status: "skipped-drift", reason: "unsafe-path" };
|
|
343
|
+
}
|
|
344
|
+
const exists = existsSync2(destPath);
|
|
345
|
+
const existing = exists ? readFileSync(destPath, "utf8") : "";
|
|
346
|
+
if (exists && existing === managed) {
|
|
347
|
+
const extrasCurrent = !isMultiFile || shippedSkillTreeMatches(
|
|
348
|
+
skillDir,
|
|
349
|
+
join2(plan.destDir, skill)
|
|
350
|
+
);
|
|
351
|
+
if (extrasCurrent) {
|
|
352
|
+
return { target: plan.target, skill, destPath, status: "unchanged" };
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (exists && !isManagedSkillFile(existing) && !force) {
|
|
356
|
+
return { target: plan.target, skill, destPath, status: "skipped-drift" };
|
|
357
|
+
}
|
|
358
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
359
|
+
writeFileSync(destPath, managed, "utf8");
|
|
360
|
+
if (plan.layout === "folder" && isMultiFile) {
|
|
361
|
+
const destSkillDir = join2(plan.destDir, skill);
|
|
362
|
+
for (const entry of extras) {
|
|
363
|
+
cpSync(join2(skillDir, entry), join2(destSkillDir, entry), {
|
|
364
|
+
recursive: true,
|
|
365
|
+
filter: (s) => !/(\/__pycache__|\/\.runtime|\.pyc$|\.DS_Store$)/.test(s)
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
target: plan.target,
|
|
371
|
+
skill,
|
|
372
|
+
destPath,
|
|
373
|
+
status: exists ? "updated" : "created"
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function installHookFile(packageRoot, projectRoot, filename, force) {
|
|
377
|
+
const target = "claude-code";
|
|
378
|
+
const skillLabel = `hook:${filename}`;
|
|
379
|
+
const srcPath = join2(packageRoot, ".claude", "hooks", filename);
|
|
380
|
+
const destPath = join2(projectRoot, ".claude", "hooks", filename);
|
|
381
|
+
if (!existsSync2(srcPath)) {
|
|
382
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift" };
|
|
383
|
+
}
|
|
384
|
+
if (unsafeManagedPath(projectRoot, destPath)) {
|
|
385
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift", reason: "unsafe-path" };
|
|
386
|
+
}
|
|
387
|
+
const src = readFileSync(srcPath, "utf8");
|
|
388
|
+
const managed = renderManagedHook(src);
|
|
389
|
+
const exists = existsSync2(destPath);
|
|
390
|
+
const existing = exists ? readFileSync(destPath, "utf8") : "";
|
|
391
|
+
if (exists && existing === managed) {
|
|
392
|
+
return { target, skill: skillLabel, destPath, status: "unchanged" };
|
|
393
|
+
}
|
|
394
|
+
if (exists && !force && !isSelfConsistentManagedHook(existing) && !isKnownLegacyHook(filename, existing)) {
|
|
395
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift" };
|
|
396
|
+
}
|
|
397
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
398
|
+
const tempPath = `${destPath}.omd-${process.pid}-${Date.now()}.tmp`;
|
|
399
|
+
try {
|
|
400
|
+
writeFileSync(tempPath, managed, { encoding: "utf8", flag: "wx" });
|
|
401
|
+
renameSync(tempPath, destPath);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
rmSync(tempPath, { force: true });
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
return { target, skill: skillLabel, destPath, status: exists ? "updated" : "created" };
|
|
407
|
+
}
|
|
408
|
+
function isJsonObject(value) {
|
|
409
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
410
|
+
}
|
|
411
|
+
function parseJsonObject(value) {
|
|
412
|
+
try {
|
|
413
|
+
const parsed = JSON.parse(value);
|
|
414
|
+
return isJsonObject(parsed) ? parsed : null;
|
|
415
|
+
} catch {
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function isOmdHook(value) {
|
|
420
|
+
if (!isJsonObject(value) || typeof value.command !== "string") return false;
|
|
421
|
+
const command = value.command;
|
|
422
|
+
const ownedHookPaths = [
|
|
423
|
+
"${CLAUDE_PROJECT_DIR}/.claude/hooks/skill-activation.cjs",
|
|
424
|
+
"${CLAUDE_PROJECT_DIR}/.claude/hooks/session-state-loader.cjs",
|
|
425
|
+
"${CLAUDE_PROJECT_DIR}/.claude/hooks/post-edit-watch.cjs",
|
|
426
|
+
"${CLAUDE_PROJECT_DIR}/.claude/hooks/session-end-foldin.cjs",
|
|
427
|
+
// Pre-v1.8 installs briefly registered this repository-only helper.
|
|
428
|
+
"${CLAUDE_PROJECT_DIR}/scripts/context_restore.sh"
|
|
429
|
+
];
|
|
430
|
+
return ownedHookPaths.some((path) => command.includes(path));
|
|
431
|
+
}
|
|
432
|
+
var REPOSITORY_ONLY_CONTEXT_RESTORE_COMMAND = "bash ${CLAUDE_PROJECT_DIR}/scripts/context_restore.sh";
|
|
433
|
+
function isRepositoryOnlySourceHook(value) {
|
|
434
|
+
return isJsonObject(value) && typeof value.command === "string" && value.command === REPOSITORY_ONLY_CONTEXT_RESTORE_COMMAND;
|
|
435
|
+
}
|
|
436
|
+
function stripHookEntries(groups, shouldStrip) {
|
|
437
|
+
const retained = [];
|
|
438
|
+
for (const group of groups) {
|
|
439
|
+
if (!isJsonObject(group) || !Array.isArray(group.hooks)) {
|
|
440
|
+
retained.push(group);
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const hooks = group.hooks.filter((hook) => !shouldStrip(hook));
|
|
444
|
+
if (hooks.length > 0) retained.push({ ...group, hooks });
|
|
445
|
+
}
|
|
446
|
+
return retained;
|
|
447
|
+
}
|
|
448
|
+
function stripInstalledOmdHooks(groups) {
|
|
449
|
+
return stripHookEntries(groups, isOmdHook);
|
|
450
|
+
}
|
|
451
|
+
function stripRepositoryOnlySourceHooks(source) {
|
|
452
|
+
if (!isJsonObject(source.hooks)) return source;
|
|
453
|
+
const hooks = {};
|
|
454
|
+
for (const [event, groups] of Object.entries(source.hooks)) {
|
|
455
|
+
hooks[event] = Array.isArray(groups) ? stripHookEntries(groups, isRepositoryOnlySourceHook) : groups;
|
|
456
|
+
}
|
|
457
|
+
return { ...source, hooks };
|
|
458
|
+
}
|
|
459
|
+
function mergeClaudeSettings(existing, source) {
|
|
460
|
+
const installableSource = stripRepositoryOnlySourceHooks(source);
|
|
461
|
+
const existingHooks = isJsonObject(existing.hooks) ? existing.hooks : {};
|
|
462
|
+
const sourceHooks = isJsonObject(installableSource.hooks) ? installableSource.hooks : {};
|
|
463
|
+
const mergedHooks = { ...existingHooks };
|
|
464
|
+
for (const event of /* @__PURE__ */ new Set([...Object.keys(existingHooks), ...Object.keys(sourceHooks)])) {
|
|
465
|
+
const userGroups = Array.isArray(existingHooks[event]) ? existingHooks[event] : [];
|
|
466
|
+
const omdGroups = Array.isArray(sourceHooks[event]) ? sourceHooks[event] : [];
|
|
467
|
+
mergedHooks[event] = [...stripInstalledOmdHooks(userGroups), ...omdGroups];
|
|
468
|
+
}
|
|
469
|
+
return { ...installableSource, ...existing, hooks: mergedHooks };
|
|
470
|
+
}
|
|
471
|
+
function installSettingsJson(packageRoot, projectRoot, force) {
|
|
472
|
+
const target = "claude-code";
|
|
473
|
+
const skillLabel = "settings:.claude/settings.json";
|
|
474
|
+
const srcPath = join2(packageRoot, ".claude", "settings.json");
|
|
475
|
+
const destPath = join2(projectRoot, ".claude", "settings.json");
|
|
476
|
+
if (!existsSync2(srcPath)) {
|
|
477
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift" };
|
|
478
|
+
}
|
|
479
|
+
if (unsafeManagedPath(projectRoot, destPath)) {
|
|
480
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift", reason: "unsafe-path" };
|
|
481
|
+
}
|
|
482
|
+
const src = readFileSync(srcPath, "utf8");
|
|
483
|
+
const sourceSettings = parseJsonObject(src);
|
|
484
|
+
if (!sourceSettings) {
|
|
485
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift" };
|
|
486
|
+
}
|
|
487
|
+
const exists = existsSync2(destPath);
|
|
488
|
+
const existing = exists ? readFileSync(destPath, "utf8") : "";
|
|
489
|
+
const existingSettings = exists ? parseJsonObject(existing) : {};
|
|
490
|
+
if (exists && !existingSettings && !force) {
|
|
491
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift" };
|
|
492
|
+
}
|
|
493
|
+
const merged = existingSettings ? mergeClaudeSettings(existingSettings, sourceSettings) : stripRepositoryOnlySourceHooks(sourceSettings);
|
|
494
|
+
if (exists && existingSettings && JSON.stringify(existingSettings) === JSON.stringify(merged)) {
|
|
495
|
+
return { target, skill: skillLabel, destPath, status: "unchanged" };
|
|
496
|
+
}
|
|
497
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
498
|
+
writeFileSync(destPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
499
|
+
return { target, skill: skillLabel, destPath, status: exists ? "updated" : "created" };
|
|
500
|
+
}
|
|
501
|
+
function installDataFile(packageRoot, projectRoot, channelDir, dataFilename, _force, target) {
|
|
502
|
+
const skillLabel = `data:${dataFilename}`;
|
|
503
|
+
const srcPath = join2(packageRoot, "data", dataFilename);
|
|
504
|
+
const destPath = join2(projectRoot, channelDir, "data", dataFilename);
|
|
505
|
+
if (!existsSync2(srcPath)) {
|
|
506
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift" };
|
|
507
|
+
}
|
|
508
|
+
if (unsafeManagedPath(projectRoot, destPath)) {
|
|
509
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift", reason: "unsafe-path" };
|
|
510
|
+
}
|
|
511
|
+
const src = readFileSync(srcPath, "utf8");
|
|
512
|
+
const exists = existsSync2(destPath);
|
|
513
|
+
const existing = exists ? readFileSync(destPath, "utf8") : "";
|
|
514
|
+
if (exists && existing === src) {
|
|
515
|
+
return { target, skill: skillLabel, destPath, status: "unchanged" };
|
|
516
|
+
}
|
|
517
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
518
|
+
writeFileSync(destPath, src, "utf8");
|
|
519
|
+
return {
|
|
520
|
+
target,
|
|
521
|
+
skill: skillLabel,
|
|
522
|
+
destPath,
|
|
523
|
+
status: exists ? "updated" : "created"
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function installAgentFile(packageRoot, projectRoot, channel, filename, force, scope) {
|
|
527
|
+
const target = channel === "claude" ? "claude-code" : channel === "codex" ? "codex" : "opencode";
|
|
528
|
+
const skillLabel = `agent:${filename}`;
|
|
529
|
+
const parsed = parseCanonicalAgent(packageRoot, filename);
|
|
530
|
+
const nativeDataRoot = scope === "global" ? channel === "codex" ? join2(projectRoot, ".codex", "data") : join2(projectRoot, ".config", "opencode", "data") : channel === "codex" ? ".codex/data" : ".opencode/data";
|
|
531
|
+
const rendered = channel === "claude" ? renderClaudeAgent(parsed) : channel === "codex" ? renderCodexAgent(parsed, scope, nativeDataRoot) : renderOpenCodeAgent(parsed, scope, nativeDataRoot);
|
|
532
|
+
const destFilename = channel === "codex" ? filename.replace(/\.md$/, ".toml") : filename;
|
|
533
|
+
const channelRoot = channel === "claude" ? ".claude" : channel === "codex" ? ".codex" : scope === "global" ? join2(".config", "opencode") : ".opencode";
|
|
534
|
+
const destPath = join2(
|
|
535
|
+
projectRoot,
|
|
536
|
+
channelRoot,
|
|
537
|
+
"agents",
|
|
538
|
+
destFilename
|
|
539
|
+
);
|
|
540
|
+
const managed = channel === "claude" || channel === "opencode" ? rendered : "# omd:installed-agent \u2014 generated from agents/" + filename + " by `omd install-skills`. Do not edit; rerun the command to refresh.\n\n" + rendered;
|
|
541
|
+
if (unsafeManagedPath(projectRoot, destPath)) {
|
|
542
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift", reason: "unsafe-path" };
|
|
543
|
+
}
|
|
544
|
+
const exists = existsSync2(destPath);
|
|
545
|
+
const existing = exists ? readFileSync(destPath, "utf8") : "";
|
|
546
|
+
if (exists && existing === managed) {
|
|
547
|
+
return { target, skill: skillLabel, destPath, status: "unchanged" };
|
|
548
|
+
}
|
|
549
|
+
const isManaged = channel === "claude" ? /\nomd_managed:\s*true\b/.test(existing) : channel === "codex" ? existing.startsWith("# omd:installed-agent") : existing.includes("<!-- omd:installed-agent");
|
|
550
|
+
if (exists && !isManaged && !force) {
|
|
551
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift" };
|
|
552
|
+
}
|
|
553
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
554
|
+
writeFileSync(destPath, managed, "utf8");
|
|
555
|
+
return {
|
|
556
|
+
target,
|
|
557
|
+
skill: skillLabel,
|
|
558
|
+
destPath,
|
|
559
|
+
status: exists ? "updated" : "created"
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function installReferenceCatalog(packageRoot, installRoot, channelDir, force) {
|
|
563
|
+
const srcRoot = join2(packageRoot, "web", "references");
|
|
564
|
+
if (!existsSync2(srcRoot)) return 0;
|
|
565
|
+
const destRoot = join2(installRoot, channelDir, "data", "references");
|
|
566
|
+
if (unsafeManagedPath(installRoot, destRoot)) return 0;
|
|
567
|
+
const manifestPath = join2(destRoot, ".omd-managed.json");
|
|
568
|
+
let previouslyManaged = /* @__PURE__ */ new Set();
|
|
569
|
+
let previousHashes = /* @__PURE__ */ new Map();
|
|
570
|
+
if (existsSync2(manifestPath)) {
|
|
571
|
+
try {
|
|
572
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
573
|
+
if (isJsonObject(parsed) && Array.isArray(parsed.managedIds) && parsed.managedIds.every((id) => typeof id === "string")) {
|
|
574
|
+
previouslyManaged = new Set(parsed.managedIds);
|
|
575
|
+
if (isJsonObject(parsed.managedDesignHashes)) {
|
|
576
|
+
previousHashes = new Map(
|
|
577
|
+
Object.entries(parsed.managedDesignHashes).filter((entry) => typeof entry[1] === "string")
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
let available = 0;
|
|
585
|
+
const sourceIds = /* @__PURE__ */ new Set();
|
|
586
|
+
const managedDesignHashes = {};
|
|
587
|
+
for (const id of readdirSync(srcRoot)) {
|
|
588
|
+
const srcDesign = join2(srcRoot, id, "DESIGN.md");
|
|
589
|
+
if (!existsSync2(srcDesign)) continue;
|
|
590
|
+
sourceIds.add(id);
|
|
591
|
+
available++;
|
|
592
|
+
const destDesign = join2(destRoot, id, "DESIGN.md");
|
|
593
|
+
const src = readFileSync(srcDesign, "utf8");
|
|
594
|
+
const srcHash = createHash("sha256").update(src).digest("hex");
|
|
595
|
+
if (existsSync2(destDesign)) {
|
|
596
|
+
const existing = readFileSync(destDesign, "utf8");
|
|
597
|
+
if (existing === src) {
|
|
598
|
+
managedDesignHashes[id] = srcHash;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const previousHash = previousHashes.get(id);
|
|
602
|
+
const unchangedSinceLastInstall = previousHash !== void 0 && createHash("sha256").update(existing).digest("hex") === previousHash;
|
|
603
|
+
if (!force && !unchangedSinceLastInstall) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
mkdirSync(dirname(destDesign), { recursive: true });
|
|
608
|
+
writeFileSync(destDesign, src, "utf8");
|
|
609
|
+
managedDesignHashes[id] = srcHash;
|
|
610
|
+
}
|
|
611
|
+
if (existsSync2(destRoot)) {
|
|
612
|
+
for (const id of readdirSync(destRoot)) {
|
|
613
|
+
if (previouslyManaged.has(id) && !sourceIds.has(id)) {
|
|
614
|
+
const retiredRoot = join2(destRoot, id);
|
|
615
|
+
const retiredDesign = join2(retiredRoot, "DESIGN.md");
|
|
616
|
+
const previousHash = previousHashes.get(id);
|
|
617
|
+
const safeToRemove = existsSync2(retiredDesign) && (force || previousHash !== void 0 && createHash("sha256").update(readFileSync(retiredDesign, "utf8")).digest("hex") === previousHash);
|
|
618
|
+
if (safeToRemove) rmSync(retiredDesign, { force: true });
|
|
619
|
+
if (existsSync2(retiredRoot) && readdirSync(retiredRoot).length === 0) {
|
|
620
|
+
rmdirSync(retiredRoot);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
mkdirSync(destRoot, { recursive: true });
|
|
626
|
+
writeFileSync(
|
|
627
|
+
manifestPath,
|
|
628
|
+
JSON.stringify({
|
|
629
|
+
schemaVersion: 2,
|
|
630
|
+
managedIds: Object.keys(managedDesignHashes).sort(),
|
|
631
|
+
managedDesignHashes
|
|
632
|
+
}, null, 2) + "\n",
|
|
633
|
+
"utf8"
|
|
634
|
+
);
|
|
635
|
+
return available;
|
|
636
|
+
}
|
|
637
|
+
var CURSOR_RULE_BODY = [
|
|
638
|
+
"The authoritative design spec lives at `@DESIGN.md` (repo root). Open and read before generating/modifying UI.",
|
|
639
|
+
"",
|
|
640
|
+
"Pending preference corrections: `@.omd/preferences.md`.",
|
|
641
|
+
"",
|
|
642
|
+
"Precedence: DESIGN.md > preferences.md > framework defaults.",
|
|
643
|
+
"",
|
|
644
|
+
"If DESIGN.md is missing and the user asks to establish a design system:",
|
|
645
|
+
"1. Inspect the existing product, routes, and constraints.",
|
|
646
|
+
"2. Read `.claude/data/reference-fingerprints.json` and only recommend ids present there.",
|
|
647
|
+
"3. Load the chosen `.claude/data/references/<id>/DESIGN.md`, explain the project-specific delta, and ask before writing root DESIGN.md.",
|
|
648
|
+
"4. Unknown reference fields stay absent; never substitute a system font, generic component, or guessed token as a brand fact.",
|
|
649
|
+
"",
|
|
650
|
+
"When applying DESIGN.md, preserve existing behavior and user copy unless asked, then verify the actual product route and accessibility before reporting completion.",
|
|
651
|
+
"Cursor receives this rule and the catalog, not OmD named skills or sub-agents; execute the contract directly from natural-language requests."
|
|
652
|
+
].join("\n");
|
|
653
|
+
function renderCursorRule() {
|
|
654
|
+
const hash = createHash("sha256").update(CURSOR_RULE_BODY).digest("hex").slice(0, 12);
|
|
655
|
+
return [
|
|
656
|
+
"---",
|
|
657
|
+
"description: Authoritative brand & UI design system. Read DESIGN.md before UI work.",
|
|
658
|
+
"globs:",
|
|
659
|
+
' - "**/*.tsx"',
|
|
660
|
+
' - "**/*.jsx"',
|
|
661
|
+
' - "**/*.vue"',
|
|
662
|
+
' - "**/*.svelte"',
|
|
663
|
+
' - "**/*.css"',
|
|
664
|
+
' - "**/*.scss"',
|
|
665
|
+
' - "**/tailwind.config.*"',
|
|
666
|
+
' - "**/components/**"',
|
|
667
|
+
' - "**/app/**/page.*"',
|
|
668
|
+
"alwaysApply: false",
|
|
669
|
+
"---",
|
|
670
|
+
"",
|
|
671
|
+
`<!-- omd:start v=1 hash=${hash} -->`,
|
|
672
|
+
CURSOR_RULE_BODY,
|
|
673
|
+
"<!-- omd:end -->",
|
|
674
|
+
""
|
|
675
|
+
].join("\n");
|
|
676
|
+
}
|
|
677
|
+
function installCursorRule(installRoot, force) {
|
|
678
|
+
const target = "cursor";
|
|
679
|
+
const skillLabel = "rule:omd-design.mdc";
|
|
680
|
+
const destPath = join2(installRoot, ".cursor", "rules", "omd-design.mdc");
|
|
681
|
+
const rendered = renderCursorRule();
|
|
682
|
+
if (unsafeManagedPath(installRoot, destPath)) {
|
|
683
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift", reason: "unsafe-path" };
|
|
684
|
+
}
|
|
685
|
+
const exists = existsSync2(destPath);
|
|
686
|
+
const existing = exists ? readFileSync(destPath, "utf8") : "";
|
|
687
|
+
if (exists && existing === rendered) {
|
|
688
|
+
return { target, skill: skillLabel, destPath, status: "unchanged" };
|
|
689
|
+
}
|
|
690
|
+
if (exists && !existing.includes("<!-- omd:start") && !force) {
|
|
691
|
+
return { target, skill: skillLabel, destPath, status: "skipped-drift" };
|
|
692
|
+
}
|
|
693
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
694
|
+
writeFileSync(destPath, rendered, "utf8");
|
|
695
|
+
return { target, skill: skillLabel, destPath, status: exists ? "updated" : "created" };
|
|
696
|
+
}
|
|
697
|
+
var STATUS_LABEL = {
|
|
698
|
+
created: pc.green("created"),
|
|
699
|
+
updated: pc.cyan("updated"),
|
|
700
|
+
removed: pc.magenta("removed"),
|
|
701
|
+
unchanged: pc.dim("unchanged"),
|
|
702
|
+
"skipped-drift": pc.yellow("skipped"),
|
|
703
|
+
"skipped-incompat": pc.yellow("skipped (claude-code only)")
|
|
704
|
+
};
|
|
705
|
+
function autoDetectTargets(projectRoot) {
|
|
706
|
+
const presence = detectInstalledAgents(projectRoot);
|
|
707
|
+
const targets = [];
|
|
708
|
+
if (presence.claudeCode) targets.push("claude-code");
|
|
709
|
+
if (presence.codex) targets.push("codex");
|
|
710
|
+
if (presence.opencode) targets.push("opencode");
|
|
711
|
+
if (presence.cursor) targets.push("cursor");
|
|
712
|
+
if (targets.length === 0) {
|
|
713
|
+
return ["claude-code", "codex", "opencode"];
|
|
714
|
+
}
|
|
715
|
+
return targets;
|
|
716
|
+
}
|
|
717
|
+
async function runInstallSkills(opts = {}) {
|
|
718
|
+
const projectRoot = opts.dir ?? process.cwd();
|
|
719
|
+
const packageRoot = findPackageRoot();
|
|
720
|
+
if (!packageRoot) {
|
|
721
|
+
console.error(pc.red("omd install-skills: package data not found"));
|
|
722
|
+
return 1;
|
|
723
|
+
}
|
|
724
|
+
const allSkills = listShippedSkills(packageRoot);
|
|
725
|
+
if (allSkills.length === 0) {
|
|
726
|
+
console.error(pc.red("omd install-skills: no skills found in package"));
|
|
727
|
+
return 1;
|
|
728
|
+
}
|
|
729
|
+
const allAgents = listCanonicalAgents(packageRoot);
|
|
730
|
+
if (opts.agents) {
|
|
731
|
+
const invalidTargets = opts.agents.filter(
|
|
732
|
+
(target) => !VALID_SKILL_TARGETS.includes(target)
|
|
733
|
+
);
|
|
734
|
+
if (invalidTargets.length > 0) {
|
|
735
|
+
console.error(
|
|
736
|
+
pc.red(
|
|
737
|
+
`omd install-skills: invalid agent channel(s): ${invalidTargets.join(", ")}. Choose from ${VALID_SKILL_TARGETS.join(", ")}.`
|
|
738
|
+
)
|
|
739
|
+
);
|
|
740
|
+
return 1;
|
|
741
|
+
}
|
|
742
|
+
if (opts.agents.length === 0) {
|
|
743
|
+
console.error(pc.red("omd install-skills: no agent channel selected"));
|
|
744
|
+
return 1;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (opts.skillsFilter) {
|
|
748
|
+
const unknownSkills = opts.skillsFilter.filter((skill) => !allSkills.includes(skill));
|
|
749
|
+
if (unknownSkills.length > 0) {
|
|
750
|
+
console.error(
|
|
751
|
+
pc.red(`omd install-skills: unknown skill(s): ${unknownSkills.join(", ")}`)
|
|
752
|
+
);
|
|
753
|
+
return 1;
|
|
754
|
+
}
|
|
755
|
+
if (opts.skillsFilter.length === 0) {
|
|
756
|
+
console.error(pc.red("omd install-skills: no skill selected"));
|
|
757
|
+
return 1;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (opts.agentsFilter) {
|
|
761
|
+
const availableAgentIds = new Set(allAgents.map((name) => name.replace(/\.md$/, "")));
|
|
762
|
+
const unknownAgents = opts.agentsFilter.filter((agent) => !availableAgentIds.has(agent));
|
|
763
|
+
if (unknownAgents.length > 0) {
|
|
764
|
+
console.error(
|
|
765
|
+
pc.red(`omd install-skills: unknown sub-agent(s): ${unknownAgents.join(", ")}`)
|
|
766
|
+
);
|
|
767
|
+
return 1;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const force = opts.force ?? false;
|
|
771
|
+
const minimal = opts.skillsOnly === true;
|
|
772
|
+
let scope = opts.global ? "global" : "project";
|
|
773
|
+
p.intro(
|
|
774
|
+
pc.bold("omd install-skills") + pc.dim(` (${relative(process.cwd(), projectRoot) || "."})`)
|
|
775
|
+
);
|
|
776
|
+
const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
777
|
+
const interactive = isTTY && !opts.all;
|
|
778
|
+
const detected = autoDetectTargets(projectRoot);
|
|
779
|
+
const presence = detectInstalledAgents(projectRoot);
|
|
780
|
+
const actuallyDetected = [
|
|
781
|
+
presence.claudeCode ? "claude-code" : null,
|
|
782
|
+
presence.codex ? "codex" : null,
|
|
783
|
+
presence.opencode ? "opencode" : null,
|
|
784
|
+
presence.cursor ? "cursor" : null
|
|
785
|
+
].filter((x) => x !== null);
|
|
786
|
+
if (!opts.global && interactive) {
|
|
787
|
+
const scopeResult = await p.select({
|
|
788
|
+
message: "Install scope \xB7 \uC5B4\uB514\uC5D0 \uC124\uCE58\uD560\uAE4C\uC694?",
|
|
789
|
+
options: [
|
|
790
|
+
{ value: "project", label: "Project", hint: `${relative(process.cwd(), projectRoot) || "."} \xB7 \uC774 \uD504\uB85C\uC81D\uD2B8\uB9CC` },
|
|
791
|
+
{ value: "global", label: "Global", hint: "~/.claude \xB7 ~/.agents \xB7 ~/.config/opencode (hooks/settings \uC81C\uC678)" }
|
|
792
|
+
],
|
|
793
|
+
initialValue: "project"
|
|
794
|
+
});
|
|
795
|
+
if (p.isCancel(scopeResult)) {
|
|
796
|
+
p.cancel("Install cancelled.");
|
|
797
|
+
return 130;
|
|
798
|
+
}
|
|
799
|
+
scope = scopeResult;
|
|
800
|
+
}
|
|
801
|
+
let skills;
|
|
802
|
+
if (opts.skillsFilter) {
|
|
803
|
+
skills = allSkills.filter((s) => opts.skillsFilter.includes(s));
|
|
804
|
+
} else if (interactive) {
|
|
805
|
+
const skillResult = await p.multiselect({
|
|
806
|
+
message: "Skills \xB7 space = \uD1A0\uAE00 \xB7 a = \uC804\uCCB4 \xB7 enter = \uD655\uC778 (default ALL)",
|
|
807
|
+
options: allSkills.map((s) => ({ value: s, label: s, hint: "omd skill" })),
|
|
808
|
+
initialValues: allSkills,
|
|
809
|
+
required: true
|
|
810
|
+
});
|
|
811
|
+
if (p.isCancel(skillResult)) {
|
|
812
|
+
p.cancel("Install cancelled.");
|
|
813
|
+
return 130;
|
|
814
|
+
}
|
|
815
|
+
skills = skillResult;
|
|
816
|
+
} else {
|
|
817
|
+
skills = allSkills;
|
|
818
|
+
}
|
|
819
|
+
let canonicalAgents;
|
|
820
|
+
if (minimal) {
|
|
821
|
+
canonicalAgents = [];
|
|
822
|
+
} else if (opts.agentsFilter) {
|
|
823
|
+
canonicalAgents = allAgents.filter((a) => opts.agentsFilter.includes(a.replace(/\.md$/, "")));
|
|
824
|
+
} else if (interactive && allAgents.length > 0) {
|
|
825
|
+
const agentResult = await p.multiselect({
|
|
826
|
+
message: "Sub-agents \xB7 space = \uD1A0\uAE00 \xB7 a = \uC804\uCCB4 \xB7 enter = \uD655\uC778 (default ALL)",
|
|
827
|
+
options: allAgents.map((a) => ({ value: a, label: a.replace(/\.md$/, ""), hint: "subagent" })),
|
|
828
|
+
initialValues: allAgents,
|
|
829
|
+
required: false
|
|
830
|
+
});
|
|
831
|
+
if (p.isCancel(agentResult)) {
|
|
832
|
+
p.cancel("Install cancelled.");
|
|
833
|
+
return 130;
|
|
834
|
+
}
|
|
835
|
+
canonicalAgents = agentResult;
|
|
836
|
+
} else {
|
|
837
|
+
canonicalAgents = allAgents;
|
|
838
|
+
}
|
|
839
|
+
const supportedTargets = (() => {
|
|
840
|
+
const set = new Set(skills.flatMap((s) => skillSupportedChannels(packageRoot, s)));
|
|
841
|
+
set.add("cursor");
|
|
842
|
+
return targetsAvailableForScope(
|
|
843
|
+
["claude-code", "codex", "opencode", "cursor"].filter((t) => set.has(t)),
|
|
844
|
+
scope
|
|
845
|
+
);
|
|
846
|
+
})();
|
|
847
|
+
const channelLabel = {
|
|
848
|
+
"claude-code": "Claude Code",
|
|
849
|
+
codex: "Codex",
|
|
850
|
+
opencode: "OpenCode",
|
|
851
|
+
cursor: "Cursor"
|
|
852
|
+
};
|
|
853
|
+
const channelDir = {
|
|
854
|
+
"claude-code": ".claude",
|
|
855
|
+
codex: ".codex",
|
|
856
|
+
opencode: ".opencode",
|
|
857
|
+
cursor: ".cursor"
|
|
858
|
+
};
|
|
859
|
+
let targets;
|
|
860
|
+
if (opts.agents) {
|
|
861
|
+
targets = opts.agents;
|
|
862
|
+
} else if (interactive) {
|
|
863
|
+
const defaults = actuallyDetected.filter((t) => supportedTargets.includes(t));
|
|
864
|
+
const targetResult = await p.multiselect({
|
|
865
|
+
message: "Agent channels \xB7 \uC5B4\uB514\uC5D0 \uC124\uCE58\uD560\uAE4C\uC694? \xB7 space = \uD1A0\uAE00 \xB7 enter = \uD655\uC778",
|
|
866
|
+
options: supportedTargets.map((t) => ({
|
|
867
|
+
value: t,
|
|
868
|
+
label: channelLabel[t],
|
|
869
|
+
hint: actuallyDetected.includes(t) ? `${channelDir[t]}/ detected` : ""
|
|
870
|
+
})),
|
|
871
|
+
initialValues: defaults.length > 0 ? defaults : supportedTargets,
|
|
872
|
+
required: true
|
|
873
|
+
});
|
|
874
|
+
if (p.isCancel(targetResult)) {
|
|
875
|
+
p.cancel("Install cancelled.");
|
|
876
|
+
return 130;
|
|
877
|
+
}
|
|
878
|
+
targets = targetResult;
|
|
879
|
+
} else {
|
|
880
|
+
targets = opts.all ? ["claude-code", "codex", "opencode"] : minimal ? actuallyDetected.length > 0 ? actuallyDetected : ["claude-code"] : detected;
|
|
881
|
+
const narrowed = targets.filter((t) => supportedTargets.includes(t));
|
|
882
|
+
if (narrowed.length > 0) targets = narrowed;
|
|
883
|
+
}
|
|
884
|
+
if (targets.length === 0) {
|
|
885
|
+
console.error(pc.red("omd install-skills: no compatible agent channel selected"));
|
|
886
|
+
return 1;
|
|
887
|
+
}
|
|
888
|
+
if (scope === "global" && targets.includes("cursor")) {
|
|
889
|
+
console.error(
|
|
890
|
+
pc.red("omd install-skills: Cursor rules are project-scoped. Run the Cursor install from the project root without --global.")
|
|
891
|
+
);
|
|
892
|
+
return 1;
|
|
893
|
+
}
|
|
894
|
+
if (opts.repairHooks && (scope !== "project" || minimal || !targets.includes("claude-code"))) {
|
|
895
|
+
console.error(
|
|
896
|
+
pc.red("omd install-skills: --repair-hooks requires a project-scoped Claude Code target and cannot be combined with --skills-only.")
|
|
897
|
+
);
|
|
898
|
+
return 1;
|
|
899
|
+
}
|
|
900
|
+
const installRoot = scope === "global" ? homedir() : projectRoot;
|
|
901
|
+
const skillChannelTargets = targets.filter(
|
|
902
|
+
(t) => t !== "cursor"
|
|
903
|
+
);
|
|
904
|
+
const plans = skillChannelTargets.map((t) => planForTarget(installRoot, t, scope));
|
|
905
|
+
p.log.message(
|
|
906
|
+
pc.bold("Scope: ") + pc.cyan(scope) + pc.dim(scope === "global" ? " (channel user directories)" : ` (${relative(process.cwd(), projectRoot) || "."})`)
|
|
907
|
+
);
|
|
908
|
+
p.log.message(
|
|
909
|
+
pc.bold(`Skills (${skills.length}): `) + skills.map((s) => pc.cyan(s)).join(", ")
|
|
910
|
+
);
|
|
911
|
+
if (minimal) {
|
|
912
|
+
canonicalAgents = [];
|
|
913
|
+
p.log.message(pc.bold("Agents: ") + pc.dim("skipped (--skills-only)"));
|
|
914
|
+
} else if (canonicalAgents.length > 0) {
|
|
915
|
+
p.log.message(
|
|
916
|
+
pc.bold(`Agents (${canonicalAgents.length}): `) + canonicalAgents.map((a) => pc.cyan(a.replace(/\.md$/, ""))).join(", ")
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
p.log.message(
|
|
920
|
+
pc.bold("Targets: ") + targets.map((t) => pc.cyan(t)).join(", ")
|
|
921
|
+
);
|
|
922
|
+
const results = [];
|
|
923
|
+
let catalogCount = 0;
|
|
924
|
+
const catalogDestinations = /* @__PURE__ */ new Set();
|
|
925
|
+
for (const plan of plans) {
|
|
926
|
+
const dataDir = dataDirForScope(plan.target, targets, scope);
|
|
927
|
+
const globalDataRoot = scope === "global" && dataDir ? join2(installRoot, dataDir, "data") : null;
|
|
928
|
+
for (const skill of skills) {
|
|
929
|
+
results.push(installOne(packageRoot, installRoot, plan, skill, force, globalDataRoot));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (targets.includes("codex")) {
|
|
933
|
+
results.push(...removeManagedLegacyCodexSkills(installRoot));
|
|
934
|
+
}
|
|
935
|
+
for (const target of targets) {
|
|
936
|
+
if (target === "claude-code") {
|
|
937
|
+
for (const filename of canonicalAgents) {
|
|
938
|
+
results.push(installAgentFile(packageRoot, installRoot, "claude", filename, force, scope));
|
|
939
|
+
}
|
|
940
|
+
} else if (target === "codex") {
|
|
941
|
+
for (const filename of canonicalAgents) {
|
|
942
|
+
results.push(installAgentFile(packageRoot, installRoot, "codex", filename, force, scope));
|
|
943
|
+
}
|
|
944
|
+
} else if (target === "opencode") {
|
|
945
|
+
for (const filename of canonicalAgents) {
|
|
946
|
+
results.push(installAgentFile(packageRoot, installRoot, "opencode", filename, force, scope));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (!minimal) {
|
|
951
|
+
if (targets.includes("cursor")) {
|
|
952
|
+
results.push(installCursorRule(installRoot, force));
|
|
953
|
+
}
|
|
954
|
+
const dataFiles = [
|
|
955
|
+
"reference-fingerprints.json",
|
|
956
|
+
"reference-tags.md",
|
|
957
|
+
"vocabulary.json",
|
|
958
|
+
"synonyms.json",
|
|
959
|
+
"opt-out-corpus.json"
|
|
960
|
+
];
|
|
961
|
+
for (const target of targets) {
|
|
962
|
+
const dataDir = dataDirForScope(target, targets, scope);
|
|
963
|
+
if (!dataDir) continue;
|
|
964
|
+
for (const dataFile of dataFiles) {
|
|
965
|
+
results.push(installDataFile(packageRoot, installRoot, dataDir, dataFile, force, target));
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
for (const target of targets) {
|
|
969
|
+
const dataDir = dataDirForScope(target, targets, scope);
|
|
970
|
+
if (dataDir) {
|
|
971
|
+
const catalogRoot = join2(installRoot, dataDir, "data", "references");
|
|
972
|
+
if (unsafeManagedPath(installRoot, catalogRoot)) {
|
|
973
|
+
results.push({
|
|
974
|
+
target,
|
|
975
|
+
skill: "data:references",
|
|
976
|
+
destPath: catalogRoot,
|
|
977
|
+
status: "skipped-drift",
|
|
978
|
+
reason: "unsafe-path"
|
|
979
|
+
});
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
const count = installReferenceCatalog(packageRoot, installRoot, dataDir, force);
|
|
983
|
+
catalogCount = Math.max(catalogCount, count);
|
|
984
|
+
if (count > 0) {
|
|
985
|
+
catalogDestinations.add(`${dataDir}/data/references/<id>/DESIGN.md`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
for (const target of targets) {
|
|
990
|
+
if (target === "cursor") continue;
|
|
991
|
+
const cd = dataDirForScope(target, targets, scope);
|
|
992
|
+
if (!cd) continue;
|
|
993
|
+
for (const helper of ["ctx-prime.cjs", "context.cjs"]) {
|
|
994
|
+
const srcHelper = join2(packageRoot, "scripts", helper);
|
|
995
|
+
if (!existsSync2(srcHelper)) continue;
|
|
996
|
+
const destHelper = join2(installRoot, cd, "data", "scripts", helper);
|
|
997
|
+
if (unsafeManagedPath(installRoot, destHelper)) {
|
|
998
|
+
results.push({
|
|
999
|
+
target,
|
|
1000
|
+
skill: `data-script:${helper}`,
|
|
1001
|
+
destPath: destHelper,
|
|
1002
|
+
status: "skipped-drift",
|
|
1003
|
+
reason: "unsafe-path"
|
|
1004
|
+
});
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const srcTxt = readFileSync(srcHelper, "utf8");
|
|
1008
|
+
if (existsSync2(destHelper) && readFileSync(destHelper, "utf8") === srcTxt) continue;
|
|
1009
|
+
mkdirSync(dirname(destHelper), { recursive: true });
|
|
1010
|
+
writeFileSync(destHelper, srcTxt, "utf8");
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (scope === "project" && targets.includes("claude-code")) {
|
|
1014
|
+
for (const hookFile of CLAUDE_HOOK_PATHS) {
|
|
1015
|
+
results.push(
|
|
1016
|
+
installHookFile(
|
|
1017
|
+
packageRoot,
|
|
1018
|
+
installRoot,
|
|
1019
|
+
hookFile,
|
|
1020
|
+
force || opts.repairHooks === true
|
|
1021
|
+
)
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
results.push(installSettingsJson(packageRoot, installRoot, force));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
p.log.message(pc.bold("\nResults:"));
|
|
1028
|
+
for (const r of results) {
|
|
1029
|
+
const rel = relative(installRoot, r.destPath);
|
|
1030
|
+
p.log.message(
|
|
1031
|
+
` ${STATUS_LABEL[r.status]} ${pc.dim(r.target.padEnd(12))} ${rel}`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
const driftCount = results.filter((r) => r.status === "skipped-drift").length;
|
|
1035
|
+
const changedCount = results.filter(
|
|
1036
|
+
(r) => r.status === "created" || r.status === "updated" || r.status === "removed"
|
|
1037
|
+
).length;
|
|
1038
|
+
const currentCount = results.filter((r) => r.status === "unchanged").length;
|
|
1039
|
+
if (driftCount > 0) {
|
|
1040
|
+
const unsafeCount = results.filter(
|
|
1041
|
+
(result) => result.status === "skipped-drift" && result.reason === "unsafe-path"
|
|
1042
|
+
).length;
|
|
1043
|
+
const hookDrift = results.some(
|
|
1044
|
+
(result) => result.status === "skipped-drift" && result.skill.startsWith("hook:")
|
|
1045
|
+
);
|
|
1046
|
+
const repairHint = unsafeCount > 0 ? ` ${unsafeCount} unsafe symlinked managed path(s) were refused; replace them with project-local files and run doctor. --force will not bypass this safety check.` : hookDrift ? " Use --repair-hooks for Claude hooks only, or --force to overwrite all unmarked files." : " Rerun with --force to overwrite.";
|
|
1047
|
+
p.outro(
|
|
1048
|
+
pc.yellow(
|
|
1049
|
+
`${changedCount} changed, ${currentCount} already current, ${driftCount} skipped (existing files lack a valid omd ownership marker).${repairHint}`
|
|
1050
|
+
)
|
|
1051
|
+
);
|
|
1052
|
+
return 2;
|
|
1053
|
+
}
|
|
1054
|
+
if (minimal) {
|
|
1055
|
+
for (const r of results.filter((x) => x.status === "skipped-incompat")) {
|
|
1056
|
+
p.log.warn(
|
|
1057
|
+
`${pc.bold(r.skill)} ${pc.dim("skipped for ")}${pc.cyan(r.target)}${pc.dim(" \u2014 declares x-omd-channels (channel not supported).")}`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
const installed = results.filter(
|
|
1061
|
+
(r) => r.status === "created" || r.status === "updated" || r.status === "unchanged"
|
|
1062
|
+
);
|
|
1063
|
+
if (installed.length === 0) {
|
|
1064
|
+
p.outro(pc.yellow("Nothing installed \u2014 no compatible skill/channel match."));
|
|
1065
|
+
return 0;
|
|
1066
|
+
}
|
|
1067
|
+
p.outro(
|
|
1068
|
+
pc.green(
|
|
1069
|
+
`Done. Installed ${skills.map((s) => pc.bold(s)).join(", ")} ${scope === "global" ? `globally for ${targets.join(", ")}` : `for ${targets.join(", ")}`}.`
|
|
1070
|
+
) + pc.dim(" \u2192 restart your agent, then use the skill (e.g. ") + pc.cyan("/claude-design") + pc.dim(").")
|
|
1071
|
+
);
|
|
1072
|
+
return 0;
|
|
1073
|
+
}
|
|
1074
|
+
const cursorOnly = targets.length === 1 && targets[0] === "cursor";
|
|
1075
|
+
const nextSteps = cursorOnly ? [
|
|
1076
|
+
`${pc.bold("Restart Cursor, then establish or apply DESIGN.md:")}`,
|
|
1077
|
+
"",
|
|
1078
|
+
` ${pc.cyan("EN")} ${pc.dim("Set up our design system \u2014 Toss-style, for a family meal-tracking app. Ask before writing DESIGN.md.")}`,
|
|
1079
|
+
` ${pc.cyan("KR")} ${pc.dim("\uD1A0\uC2A4 \uC2A4\uD0C0\uC77C\uB85C \uAC00\uC871 \uC2DD\uB2E8 \uACF5\uC720 \uC571 \uB514\uC790\uC778 \uC2DC\uC2A4\uD15C\uC744 \uC81C\uC548\uD558\uACE0 DESIGN.md \uC791\uC131 \uC804\uC5D0 \uD655\uC778\uD574\uC918")}`,
|
|
1080
|
+
"",
|
|
1081
|
+
`${pc.dim("Cursor uses the installed rule + local catalog directly. It does not receive OmD named skills or sub-agents.")}`,
|
|
1082
|
+
`${pc.dim("You can also choose a reference in the Builder and download DESIGN.md before asking Cursor to build.")}`,
|
|
1083
|
+
"",
|
|
1084
|
+
`${pc.yellow("\u26A0 Already-running session?")} ${pc.dim("Restart Cursor so it reloads the project rule.")}`
|
|
1085
|
+
].join("\n") : [
|
|
1086
|
+
`${pc.bold("Restart your agent, then type your first prompt:")}`,
|
|
1087
|
+
"",
|
|
1088
|
+
` ${pc.cyan("EN")} ${pc.dim("Set up our design system \u2014 Toss-style, for a family meal-tracking app.")}`,
|
|
1089
|
+
` ${pc.cyan("KR")} ${pc.dim("\uD1A0\uC2A4 \uC2A4\uD0C0\uC77C\uB85C \uAC00\uC871 \uC2DD\uB2E8 \uACF5\uC720 \uC571 \uB514\uC790\uC778 \uC2DC\uC2A4\uD15C \uC7A1\uC544\uC918")}`,
|
|
1090
|
+
"",
|
|
1091
|
+
`${pc.dim("Your agent routes through omd:init and writes DESIGN.md after confirmation. Then build against it:")}`,
|
|
1092
|
+
` ${pc.cyan("EN")} ${pc.dim("Design the home screen.")} ${pc.cyan("KR")} ${pc.dim("\uD648 \uD654\uBA74 \uB514\uC790\uC778\uD574\uC918")}`,
|
|
1093
|
+
"",
|
|
1094
|
+
`${pc.dim('Full walkthrough \u2192 "Your first 60 seconds" in the README. Routing is automatic \u2014 no slash command needed.')}`,
|
|
1095
|
+
`${pc.dim("Power user: ")}${pc.cyan("/omd-harness <task>")}${pc.dim(" \u2014 jump straight into the pipeline.")}`,
|
|
1096
|
+
"",
|
|
1097
|
+
`${pc.yellow("\u26A0 Already-running session?")} ${pc.dim("Restart the coding agent after install. Codex must also trust the project before it loads project-local .codex/agents roles.")}`
|
|
1098
|
+
].join("\n");
|
|
1099
|
+
p.note(nextSteps, "Next");
|
|
1100
|
+
const hookCount = scope === "project" && targets.includes("claude-code") ? 4 : 0;
|
|
1101
|
+
if (catalogCount > 0) {
|
|
1102
|
+
p.log.message(
|
|
1103
|
+
pc.bold("Reference catalog: ") + pc.cyan(`${catalogCount}`) + pc.dim(` DESIGN.md synced \u2192 ${[...catalogDestinations].join(" + ")}`)
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
const reportedSkillCount = skillChannelTargets.length > 0 ? skills.length : 0;
|
|
1107
|
+
const reportedAgentCount = skillChannelTargets.length > 0 ? canonicalAgents.length : 0;
|
|
1108
|
+
p.outro(
|
|
1109
|
+
pc.green(
|
|
1110
|
+
`Done. ${reportedSkillCount} skills \xB7 ${reportedAgentCount} sub-agents \xB7 ${hookCount} hooks \xB7 ${catalogCount} catalog refs ready (${changedCount} changed \xB7 ${currentCount} already current)${scope === "global" ? " globally (channel user directories)" : ""}.`
|
|
1111
|
+
)
|
|
1112
|
+
);
|
|
1113
|
+
return 0;
|
|
1114
|
+
}
|
|
1115
|
+
export {
|
|
1116
|
+
dataDirFor,
|
|
1117
|
+
runInstallSkills,
|
|
1118
|
+
targetsAvailableForScope
|
|
1119
|
+
};
|
|
1120
|
+
//# sourceMappingURL=install-skills-ZZFNJ2II.js.map
|