@lifeaitools/rdc-skills 0.24.42 → 0.25.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/settings.json +15 -15
- package/.claude-plugin/marketplace.json +21 -21
- package/.claude-plugin/plugin.json +1518 -1371
- package/.github/workflows/publish.yml +34 -34
- package/.github/workflows/self-test.yml +58 -58
- package/CHANGELOG.md +310 -310
- package/LICENSE +21 -21
- package/MANIFEST.md +221 -221
- package/README.md +375 -376
- package/README.sandbox.md +3 -3
- package/assets/watcher/viewer.html +164 -164
- package/bin/rdc-skills-mcp.mjs +316 -316
- package/commands/build.md +183 -183
- package/commands/collab.md +180 -180
- package/commands/deploy.md +152 -152
- package/commands/design.md +31 -31
- package/commands/edit.md +28 -28
- package/commands/fixit.md +150 -124
- package/commands/handoff.md +173 -173
- package/commands/help.md +95 -95
- package/commands/overnight.md +220 -220
- package/commands/plan.md +158 -158
- package/commands/preplan.md +131 -131
- package/commands/prototype.md +145 -145
- package/commands/release.md +49 -49
- package/commands/report.md +99 -99
- package/commands/review.md +120 -120
- package/commands/self-test.md +113 -113
- package/commands/status.md +86 -86
- package/commands/watch.md +98 -98
- package/commands/workitems.md +137 -137
- package/git-sha.json +1 -1
- package/guides/agent-bootstrap.md +295 -295
- package/guides/agents/backend.md +104 -104
- package/guides/agents/content.md +94 -94
- package/guides/agents/cs2.md +56 -56
- package/guides/agents/data.md +87 -87
- package/guides/agents/design.md +77 -77
- package/guides/agents/frontend.md +92 -92
- package/guides/agents/infrastructure.md +81 -81
- package/guides/agents/setup.md +281 -281
- package/guides/agents/verify.md +151 -151
- package/guides/agents/viz.md +106 -106
- package/guides/backend.md +146 -146
- package/guides/content.md +147 -147
- package/guides/cs2.md +190 -190
- package/guides/data.md +123 -123
- package/guides/design.md +116 -116
- package/guides/engineering-behavior.md +43 -43
- package/guides/escalation-protocol.md +125 -125
- package/guides/frontend.md +151 -151
- package/guides/history-md-spec.md +297 -297
- package/guides/infrastructure.md +179 -179
- package/guides/lessons-learned-spec.md +145 -151
- package/guides/output-contract.md +108 -108
- package/guides/publish-md-spec.md +289 -289
- package/guides/rdc-skills-startup.md +30 -30
- package/guides/verify.md +11 -11
- package/hooks/check-cwd.js +31 -31
- package/hooks/check-rdc-environment.js +164 -164
- package/hooks/check-services.js +6 -6
- package/hooks/check-stale-work-items.js +19 -19
- package/hooks/foreground-process-gate.js +128 -128
- package/hooks/gate-watchdog-selfcheck.js +257 -257
- package/hooks/hook-logger.js +25 -25
- package/hooks/lib/run-evidence-gate.mjs +241 -241
- package/hooks/no-stop-open-epics.js +127 -127
- package/hooks/post-tool-batch-gate.js +203 -203
- package/hooks/post-work-check.js +21 -21
- package/hooks/postcompact-log.js +13 -13
- package/hooks/precompact-log.js +13 -13
- package/hooks/rate-limit-retry.js +46 -46
- package/hooks/rdc-invocation-marker.js +157 -157
- package/hooks/rdc-output-contract-gate.js +94 -94
- package/hooks/require-work-item-on-commit.js +294 -294
- package/hooks/restart-brief.js +19 -19
- package/hooks/run-hidden-hook.ps1 +47 -47
- package/hooks/task-completed-gate.js +274 -274
- package/hooks/work-item-exit-gate.js +944 -944
- package/lib/catalog.mjs +236 -236
- package/lib/cloud-rewrite.mjs +155 -155
- package/package.json +57 -57
- package/rules/work-items-rpc.md +520 -520
- package/scaffold/templates/HISTORY.md.template +39 -39
- package/scaffold/templates/PUBLISH.md.template +21 -21
- package/scaffold/templates/brochure-studio-default.html +70 -70
- package/scripts/acceptance.mjs +502 -502
- package/scripts/fixtures/guides/bad-guide.md +15 -15
- package/scripts/fixtures/guides-clean/good-guide.md +16 -16
- package/scripts/install-rdc-skills.js +1289 -1289
- package/scripts/install.ps1 +202 -202
- package/scripts/install.sh +132 -132
- package/scripts/lib/assertions.mjs +287 -287
- package/scripts/lib/manifest-schema.mjs +754 -754
- package/scripts/lib/runner.mjs +465 -465
- package/scripts/lib/sandbox.mjs +435 -435
- package/scripts/prepack.mjs +32 -32
- package/scripts/rdc-brochure.mjs +482 -482
- package/scripts/rdc-design-cli.mjs +134 -134
- package/scripts/rebuild-mcp.mjs +107 -107
- package/scripts/self-test.mjs +1460 -1460
- package/scripts/stamp-git-sha.mjs +29 -29
- package/scripts/test-guide-validator.mjs +196 -196
- package/scripts/test-rdc-hooks.mjs +145 -145
- package/scripts/uninstall.ps1 +77 -77
- package/scripts/uninstall.sh +69 -69
- package/scripts/update.ps1 +43 -43
- package/scripts/update.sh +43 -43
- package/scripts/validate-place-histories.js +461 -461
- package/scripts/validate-publish-manifests.js +502 -424
- package/scripts/watch-init.mjs +100 -100
- package/skills/brochure/SKILL.md +107 -107
- package/skills/build/SKILL.md +563 -563
- package/skills/channel-formatter/SKILL.md +538 -533
- package/skills/co-develop/SKILL.md +196 -196
- package/skills/collab/SKILL.md +239 -239
- package/skills/convert/SKILL.md +167 -140
- package/skills/deploy/SKILL.md +541 -541
- package/skills/design/SKILL.md +211 -211
- package/skills/design/reference/ownership.md +16 -16
- package/skills/design/reference/rampa.md +92 -92
- package/skills/design/reference/studio-model.md +153 -153
- package/skills/edit/SKILL.md +98 -98
- package/skills/fixit/SKILL.md +203 -165
- package/skills/fs-mcp/SKILL.md +148 -148
- package/skills/handoff/SKILL.md +236 -236
- package/skills/help/SKILL.md +143 -143
- package/skills/housekeeping/SKILL.md +160 -219
- package/skills/lifeai-brochure-author/SKILL.md +340 -340
- package/skills/onramp/SKILL.md +248 -0
- package/skills/overnight/SKILL.md +251 -251
- package/skills/plan/SKILL.md +345 -345
- package/skills/preplan/SKILL.md +90 -90
- package/skills/prototype/SKILL.md +150 -150
- package/skills/rdc-brochurify/SKILL.md +245 -245
- package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
- package/skills/release/SKILL.md +140 -140
- package/skills/report/SKILL.md +100 -100
- package/skills/review/SKILL.md +152 -152
- package/skills/rpms-filemap/SKILL.cloud.md +111 -111
- package/skills/rpms-filemap/SKILL.md +111 -111
- package/skills/self-test/SKILL.md +132 -132
- package/skills/status/SKILL.md +99 -99
- package/skills/terminal-config/SKILL.md +62 -62
- package/skills/tests/MATRIX.md +54 -54
- package/skills/tests/README.md +47 -47
- package/skills/tests/onramp.test.json +87 -0
- package/skills/tests/rdc-brochure.test.json +34 -34
- package/skills/tests/rdc-build.test.json +36 -36
- package/skills/tests/rdc-channel-formatter.test.json +45 -45
- package/skills/tests/rdc-co-develop.test.json +29 -29
- package/skills/tests/rdc-collab.test.json +29 -29
- package/skills/tests/rdc-convert.test.json +35 -35
- package/skills/tests/rdc-deploy.test.json +30 -30
- package/skills/tests/rdc-design.test.json +27 -27
- package/skills/tests/rdc-edit.test.json +29 -29
- package/skills/tests/rdc-fixit.test.json +36 -36
- package/skills/tests/rdc-fs-mcp.test.json +36 -36
- package/skills/tests/rdc-handoff.test.json +28 -28
- package/skills/tests/rdc-help.test.json +29 -29
- package/skills/tests/rdc-housekeeping.test.json +28 -32
- package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
- package/skills/tests/rdc-overnight.test.json +37 -37
- package/skills/tests/rdc-plan.test.json +27 -27
- package/skills/tests/rdc-preplan.test.json +31 -31
- package/skills/tests/rdc-prototype.test.json +28 -28
- package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
- package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
- package/skills/tests/rdc-release.test.json +29 -29
- package/skills/tests/rdc-report.test.json +28 -28
- package/skills/tests/rdc-review.test.json +29 -29
- package/skills/tests/rdc-rpms-filemap.test.json +28 -28
- package/skills/tests/rdc-self-test.test.json +24 -24
- package/skills/tests/rdc-status.test.json +29 -29
- package/skills/tests/rdc-terminal-config.test.json +29 -29
- package/skills/tests/rdc-watch.test.json +24 -24
- package/skills/tests/rdc-workitems.test.json +27 -27
- package/skills/watch/SKILL.md +97 -97
- package/skills/workitems/SKILL.md +151 -151
- package/tests/acceptance.test.mjs +59 -59
- package/tests/channel-formatter.contract.test.mjs +251 -251
- package/tests/curl-surface.test.mjs +289 -289
- package/tests/harness-gates.test.mjs +325 -325
- package/tests/help-surface.test.mjs +61 -61
- package/tests/install-rdc-skills.test.mjs +49 -49
- package/tests/manifest-contract-fields.test.mjs +78 -78
- package/tests/mcp.test.mjs +271 -271
- package/tests/rdc-brochure.test.mjs +125 -125
- package/tests/require-work-item-on-commit.test.mjs +162 -162
- package/tests/run-evidence-gate.test.mjs +82 -82
- package/tests/skill-test-matrix.test.mjs +66 -66
- package/tests/validate-skills.js +27 -27
- package/tests/work-item-exit-gate-l2.test.mjs +368 -368
- package/tests/work-item-exit-gate-l3.test.mjs +197 -197
- package/RELEASE.md +0 -42
- package/tests/housekeeping-lessons-triage.test.mjs +0 -49
- package/tests/lessons-pipeline-contract.test.mjs +0 -27
- package/tests/release-contract.test.mjs +0 -16
|
@@ -1,1289 +1,1289 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* install-rdc-skills — registers rdc-skills on every Claude surface.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node scripts/install-rdc-skills.js ← standard
|
|
7
|
-
* node scripts/install-rdc-skills.js --skip-hooks ← skip hook wiring
|
|
8
|
-
* node scripts/install-rdc-skills.js --profile core ← clean-box portable hooks
|
|
9
|
-
* node scripts/install-rdc-skills.js --profile lifeai ← LIFEAI/regen-root hooks
|
|
10
|
-
* node scripts/install-rdc-skills.js --claude-home <path> ← custom CLI home
|
|
11
|
-
* node scripts/install-rdc-skills.js --codex-root <path> ← also install to .agents/skills/user/
|
|
12
|
-
* node scripts/install-rdc-skills.js --codex-skill-dir <path> ← also install to a Codex skill dir
|
|
13
|
-
* node scripts/install-rdc-skills.js --project-root <path> --write-startup-blocks
|
|
14
|
-
* node scripts/install-rdc-skills.js --migrate <path> ← migrate docs/ → .rdc/
|
|
15
|
-
*
|
|
16
|
-
* What it does:
|
|
17
|
-
* 1. git pull (latest commands + guides)
|
|
18
|
-
* 2. CLI plugin — registers in ~/.claude/plugins/ + settings.json
|
|
19
|
-
* 3. Cowork — registers in Desktop cowork_plugins/ + cowork_settings.json
|
|
20
|
-
* 3.5 Codex — copies skills to detected Codex skill dirs
|
|
21
|
-
* 4. Hook files — copies hooks/*.js → ~/.claude/hooks/
|
|
22
|
-
* 5. Hook wiring — wires hooks into ~/.claude/settings.json
|
|
23
|
-
* 6. Zip — builds dist/rdc-skills-plugin.zip for claude.ai / distribution
|
|
24
|
-
* 7. Preflight — Node version, clauth daemon
|
|
25
|
-
* 8. Commands — lists all /rdc:* commands
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
'use strict';
|
|
29
|
-
const fs = require('fs');
|
|
30
|
-
const path = require('path');
|
|
31
|
-
const os = require('os');
|
|
32
|
-
const readline = require('readline');
|
|
33
|
-
const { execSync } = require('child_process');
|
|
34
|
-
|
|
35
|
-
// ── Args ──────────────────────────────────────────────────────────────────────
|
|
36
|
-
const args = process.argv.slice(2);
|
|
37
|
-
const skipHooks = args.includes('--skip-hooks');
|
|
38
|
-
const profileIdx = args.indexOf('--profile');
|
|
39
|
-
const profileArg = profileIdx >= 0 ? String(args[profileIdx + 1] || '').toLowerCase() : 'auto';
|
|
40
|
-
const homeIdx = args.indexOf('--claude-home');
|
|
41
|
-
const claudeHome = homeIdx >= 0 ? args[homeIdx + 1] : path.join(os.homedir(), '.claude');
|
|
42
|
-
const migrateIdx = args.indexOf('--migrate');
|
|
43
|
-
const doMigrate = migrateIdx >= 0;
|
|
44
|
-
const migratePath = doMigrate ? (args[migrateIdx + 1] || process.cwd()) : null;
|
|
45
|
-
const projectIdx = args.indexOf('--project-root');
|
|
46
|
-
const projectRootArg = projectIdx >= 0 ? path.resolve(args[projectIdx + 1]) : null;
|
|
47
|
-
const shouldWriteStartupBlocks = args.includes('--write-startup-blocks');
|
|
48
|
-
|
|
49
|
-
const repoRoot = path.resolve(__dirname, '..');
|
|
50
|
-
|
|
51
|
-
const codexIdx = args.indexOf('--codex-root');
|
|
52
|
-
const codexRoot = codexIdx >= 0
|
|
53
|
-
? path.resolve(args[codexIdx + 1])
|
|
54
|
-
: (() => {
|
|
55
|
-
// Auto-detect the consuming project's .agents tree so a plain `install`
|
|
56
|
-
// refreshes Codex without needing --codex-root. Check, in order: an
|
|
57
|
-
// explicit --project-root, the current working directory, then the
|
|
58
|
-
// regen-root sibling of this repo. First one with a `.agents` wins.
|
|
59
|
-
const candidates = [
|
|
60
|
-
projectRootArg ? path.resolve(projectRootArg) : null,
|
|
61
|
-
process.cwd(),
|
|
62
|
-
path.resolve(repoRoot, '..', 'regen-root'),
|
|
63
|
-
].filter(Boolean);
|
|
64
|
-
for (const c of candidates) {
|
|
65
|
-
if (fs.existsSync(path.join(c, '.agents'))) return c;
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
})();
|
|
69
|
-
const codexSkillDirIdx = args.indexOf('--codex-skill-dir');
|
|
70
|
-
const explicitCodexSkillDir = codexSkillDirIdx >= 0
|
|
71
|
-
? path.resolve(args[codexSkillDirIdx + 1])
|
|
72
|
-
: null;
|
|
73
|
-
const hooksSrc = path.join(repoRoot, 'hooks');
|
|
74
|
-
const hooksDst = path.join(claudeHome, 'hooks');
|
|
75
|
-
const settingsPath = path.join(claudeHome, 'settings.json');
|
|
76
|
-
const detectedLifeaiRoot = (() => {
|
|
77
|
-
const sibling = path.resolve(repoRoot, '..', 'regen-root');
|
|
78
|
-
return fs.existsSync(path.join(sibling, 'CLAUDE.md')) && fs.existsSync(path.join(sibling, '.rdc')) ? sibling : null;
|
|
79
|
-
})();
|
|
80
|
-
const installProfile = (() => {
|
|
81
|
-
if (profileArg === 'core' || profileArg === 'lifeai') return profileArg;
|
|
82
|
-
if (profileArg !== 'auto') {
|
|
83
|
-
console.log(` \x1b[33m⚠\x1b[0m Unknown --profile "${profileArg}" — using auto`);
|
|
84
|
-
}
|
|
85
|
-
return detectedLifeaiRoot ? 'lifeai' : 'core';
|
|
86
|
-
})();
|
|
87
|
-
const projectRoot = projectRootArg || codexRoot || detectedLifeaiRoot;
|
|
88
|
-
|
|
89
|
-
const PLUGIN_KEY = 'rdc-skills@rdc-skills';
|
|
90
|
-
const MARKETPLACE = 'rdc-skills';
|
|
91
|
-
const NPM_PACKAGE = '@lifeaitools/rdc-skills';
|
|
92
|
-
const MCP_NAME = 'rdc-skills-mcp';
|
|
93
|
-
const MCP_PORT = '3110';
|
|
94
|
-
|
|
95
|
-
// ── Logging ───────────────────────────────────────────────────────────────────
|
|
96
|
-
const ok = msg => console.log(` \x1b[32m✓\x1b[0m ${msg}`);
|
|
97
|
-
const info = msg => console.log(` \x1b[36m→\x1b[0m ${msg}`);
|
|
98
|
-
const warn = msg => console.log(` \x1b[33m⚠\x1b[0m ${msg}`);
|
|
99
|
-
const fail = msg => console.log(` \x1b[31m✗\x1b[0m ${msg}`);
|
|
100
|
-
|
|
101
|
-
function run(cmd, options = {}) {
|
|
102
|
-
return execSync(cmd, { encoding: 'utf8', stdio: 'pipe', ...options }).trim();
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function updateCodexMcpToml(toml, mcpUrl) {
|
|
106
|
-
const blockRe = /(^|\n)(\[mcp_servers\.rdc-skills\]\n)([\s\S]*?)(?=\n\[|\s*$)/;
|
|
107
|
-
const desiredLine = `url = '${mcpUrl}'`;
|
|
108
|
-
if (blockRe.test(toml)) {
|
|
109
|
-
return toml.replace(blockRe, (match, prefix, header, body) => {
|
|
110
|
-
if (/^url\s*=.*$/m.test(body)) {
|
|
111
|
-
return `${prefix}${header}${body.replace(/^url\s*=.*$/m, desiredLine)}`;
|
|
112
|
-
}
|
|
113
|
-
const trimmed = body.replace(/\s*$/, '');
|
|
114
|
-
return `${prefix}${header}${trimmed}${trimmed ? '\n' : ''}${desiredLine}\n`;
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return toml.replace(/\s*$/, '\n') + `\n[mcp_servers.rdc-skills]\n${desiredLine}\n`;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function selfTestCodexMcpToml() {
|
|
121
|
-
const url = 'https://rdc-skills.regendevcorp.com/mcp';
|
|
122
|
-
const stale = "[mcp_servers.clauth]\nurl = 'https://clauth.regendevcorp.com/mcp'\n\n[mcp_servers.rdc-skills]\nurl = 'https://rdc-skills.dev.regendevcorp.com/mcp'\n\n[mcp_servers.web-research]\nurl = 'https://research.regendevcorp.com/mcp'\n";
|
|
123
|
-
const updated = updateCodexMcpToml(stale, url);
|
|
124
|
-
if (!updated.includes(`url = '${url}'`)) throw new Error('did not write production rdc-skills URL');
|
|
125
|
-
if (updated.includes('rdc-skills.dev.regendevcorp.com')) throw new Error('stale dev URL survived');
|
|
126
|
-
if (!updated.includes('[mcp_servers.clauth]') || !updated.includes('[mcp_servers.web-research]')) throw new Error('neighbor MCP blocks were damaged');
|
|
127
|
-
|
|
128
|
-
const missing = updateCodexMcpToml("[mcp_servers.clauth]\nurl = 'https://clauth.regendevcorp.com/mcp'\n", url);
|
|
129
|
-
if (!missing.includes('[mcp_servers.rdc-skills]')) throw new Error('missing block was not appended');
|
|
130
|
-
|
|
131
|
-
const noUrl = updateCodexMcpToml('[mcp_servers.rdc-skills]\nstartup_timeout_sec = 30\n', url);
|
|
132
|
-
if (!noUrl.includes("startup_timeout_sec = 30") || !noUrl.includes(`url = '${url}'`)) throw new Error('url-less block was not repaired');
|
|
133
|
-
|
|
134
|
-
console.log('install-rdc-skills codex MCP TOML self-test — PASS');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (args.includes('--self-test-codex-mcp-toml')) {
|
|
138
|
-
try {
|
|
139
|
-
selfTestCodexMcpToml();
|
|
140
|
-
process.exit(0);
|
|
141
|
-
} catch (e) {
|
|
142
|
-
console.error(e.message || e);
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ── Filesystem helpers ────────────────────────────────────────────────────────
|
|
148
|
-
function copyDir(src, dst, ext) {
|
|
149
|
-
if (!fs.existsSync(src)) { warn(`Source not found: ${src}`); return 0; }
|
|
150
|
-
fs.mkdirSync(dst, { recursive: true });
|
|
151
|
-
const files = fs.readdirSync(src).filter(f => !ext || f.endsWith(ext));
|
|
152
|
-
let count = 0;
|
|
153
|
-
for (const f of files) {
|
|
154
|
-
const s = path.join(src, f);
|
|
155
|
-
if (fs.statSync(s).isFile()) { fs.copyFileSync(s, path.join(dst, f)); count++; }
|
|
156
|
-
}
|
|
157
|
-
return count;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function copyHookFiles(src, dst) {
|
|
161
|
-
if (!fs.existsSync(src)) { warn(`Source not found: ${src}`); return 0; }
|
|
162
|
-
fs.mkdirSync(dst, { recursive: true });
|
|
163
|
-
const files = fs.readdirSync(src).filter(f => /\.(?:js|ps1)$/i.test(f));
|
|
164
|
-
let count = 0;
|
|
165
|
-
for (const f of files) {
|
|
166
|
-
const s = path.join(src, f);
|
|
167
|
-
if (fs.statSync(s).isFile()) { fs.copyFileSync(s, path.join(dst, f)); count++; }
|
|
168
|
-
}
|
|
169
|
-
return count;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function copyDirRecursive(src, dst) {
|
|
173
|
-
if (!fs.existsSync(src)) return;
|
|
174
|
-
fs.mkdirSync(dst, { recursive: true });
|
|
175
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
176
|
-
const s = path.join(src, entry.name);
|
|
177
|
-
const d = path.join(dst, entry.name);
|
|
178
|
-
if (entry.isDirectory()) copyDirRecursive(s, d);
|
|
179
|
-
else {
|
|
180
|
-
// Atomic write (temp + rename) so a live Claude Code / Codex session never
|
|
181
|
-
// reads a half-written skill file when the installer runs over a live box.
|
|
182
|
-
const tmp = `${d}.tmp-${process.pid}`;
|
|
183
|
-
fs.copyFileSync(s, tmp);
|
|
184
|
-
fs.renameSync(tmp, d);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function copyMissingProjectGuides(projectRoot) {
|
|
190
|
-
if (!projectRoot) return 0;
|
|
191
|
-
const src = path.join(repoRoot, 'guides');
|
|
192
|
-
const dst = path.join(projectRoot, '.rdc', 'guides');
|
|
193
|
-
if (!fs.existsSync(src) || !fs.existsSync(dst)) return 0;
|
|
194
|
-
let copied = 0;
|
|
195
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
196
|
-
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
197
|
-
if (entry.name === 'rdc-skills-startup.md') continue; // installed only by --write-startup-blocks
|
|
198
|
-
const target = path.join(dst, entry.name);
|
|
199
|
-
if (fs.existsSync(target)) continue;
|
|
200
|
-
fs.copyFileSync(path.join(src, entry.name), target);
|
|
201
|
-
copied++;
|
|
202
|
-
}
|
|
203
|
-
return copied;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function readJson(p, fallback = {}) {
|
|
207
|
-
if (!fs.existsSync(p)) return fallback;
|
|
208
|
-
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function writeJson(p, data, indent = 2) {
|
|
212
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
213
|
-
fs.writeFileSync(p, JSON.stringify(data, null, indent));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function upsertManagedBlock(filePath, title, body) {
|
|
217
|
-
const begin = `<!-- BEGIN RDC-SKILLS:${title} -->`;
|
|
218
|
-
const end = `<!-- END RDC-SKILLS:${title} -->`;
|
|
219
|
-
const block = `${begin}\n${body.trim()}\n${end}\n`;
|
|
220
|
-
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
221
|
-
const re = new RegExp(`${begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'm');
|
|
222
|
-
const next = re.test(current)
|
|
223
|
-
? current.replace(re, block)
|
|
224
|
-
: `${current.replace(/\s*$/, '')}${current.trim() ? '\n\n' : ''}${block}`;
|
|
225
|
-
if (next !== current) {
|
|
226
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
227
|
-
fs.writeFileSync(filePath, next);
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function writeStartupBlocks(targetRoot, profile) {
|
|
234
|
-
if (!targetRoot) return { wrote: 0, skipped: 'no-project-root' };
|
|
235
|
-
const startupSrc = path.join(repoRoot, 'guides', 'rdc-skills-startup.md');
|
|
236
|
-
const guidesDir = path.join(targetRoot, '.rdc', 'guides');
|
|
237
|
-
const startupDst = path.join(guidesDir, 'rdc-skills-startup.md');
|
|
238
|
-
fs.mkdirSync(guidesDir, { recursive: true });
|
|
239
|
-
fs.copyFileSync(startupSrc, startupDst);
|
|
240
|
-
|
|
241
|
-
let wrote = 1;
|
|
242
|
-
const claudeBody = [
|
|
243
|
-
'## RDC Skills',
|
|
244
|
-
'',
|
|
245
|
-
'@.rdc/guides/rdc-skills-startup.md',
|
|
246
|
-
'',
|
|
247
|
-
`Installed profile: \`${profile}\`.`,
|
|
248
|
-
'For `/rdc:*` work, follow `.rdc/guides/output-contract.md` and `.rdc/guides/engineering-behavior.md`.',
|
|
249
|
-
].join('\n');
|
|
250
|
-
const agentsBody = [
|
|
251
|
-
'## RDC Skills',
|
|
252
|
-
'',
|
|
253
|
-
'Read `.rdc/guides/rdc-skills-startup.md` before using any `rdc:*` workflow.',
|
|
254
|
-
`Installed profile: \`${profile}\`.`,
|
|
255
|
-
'For `/rdc:*` work, follow `.rdc/guides/output-contract.md` and `.rdc/guides/engineering-behavior.md`.',
|
|
256
|
-
].join('\n');
|
|
257
|
-
|
|
258
|
-
if (upsertManagedBlock(path.join(targetRoot, 'CLAUDE.md'), 'STARTUP', claudeBody)) wrote++;
|
|
259
|
-
if (upsertManagedBlock(path.join(targetRoot, 'AGENTS.md'), 'STARTUP', agentsBody)) wrote++;
|
|
260
|
-
return { wrote, skipped: null };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ── Frontmatter parser ────────────────────────────────────────────────────────
|
|
264
|
-
function readFrontmatter(filePath) {
|
|
265
|
-
try {
|
|
266
|
-
const content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
267
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
268
|
-
if (!match) return {};
|
|
269
|
-
const fm = {};
|
|
270
|
-
let key = null, multiline = false, multilineVal = '';
|
|
271
|
-
for (const line of match[1].split('\n')) {
|
|
272
|
-
if (multiline) {
|
|
273
|
-
if (/^\s+/.test(line)) { multilineVal += ' ' + line.trim(); continue; }
|
|
274
|
-
fm[key] = multilineVal.trim();
|
|
275
|
-
multiline = false;
|
|
276
|
-
}
|
|
277
|
-
const kv = line.match(/^(\w+):\s*(>-|>)?\s*(.*)?$/);
|
|
278
|
-
if (!kv) continue;
|
|
279
|
-
key = kv[1];
|
|
280
|
-
if (kv[2]) { multiline = true; multilineVal = kv[3] || ''; }
|
|
281
|
-
else fm[key] = kv[3] || '';
|
|
282
|
-
}
|
|
283
|
-
if (multiline && key) fm[key] = multilineVal.trim();
|
|
284
|
-
return fm;
|
|
285
|
-
} catch { return {}; }
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// ── Plugin cache builder (shared between CLI + Cowork) ────────────────────────
|
|
289
|
-
function buildPluginCache(cacheDir, version, gitSha) {
|
|
290
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
291
|
-
for (const item of ['.claude-plugin', 'commands', 'skills', 'guides', 'hooks', 'package.json', 'README.md']) {
|
|
292
|
-
const src = path.join(repoRoot, item);
|
|
293
|
-
if (!fs.existsSync(src)) continue;
|
|
294
|
-
const dst = path.join(cacheDir, item);
|
|
295
|
-
if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dst);
|
|
296
|
-
else fs.copyFileSync(src, dst);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function getPm2Process(name) {
|
|
301
|
-
try {
|
|
302
|
-
const list = JSON.parse(run('pm2 jlist'));
|
|
303
|
-
return Array.isArray(list) ? list.find((p) => p.name === name) || null : null;
|
|
304
|
-
} catch {
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function isGlobalNpmRdcSkillsPath(scriptPath) {
|
|
310
|
-
if (!scriptPath) return false;
|
|
311
|
-
const normalized = scriptPath.replace(/\\/g, '/').toLowerCase();
|
|
312
|
-
return normalized.includes('/node_modules/@lifeaitools/rdc-skills/');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function packageRootFromMcpScript(scriptPath) {
|
|
316
|
-
return scriptPath ? path.resolve(path.dirname(scriptPath), '..') : null;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function readInstalledPackageVersion(scriptPath) {
|
|
320
|
-
const packageRoot = packageRootFromMcpScript(scriptPath);
|
|
321
|
-
if (!packageRoot) return null;
|
|
322
|
-
return readJson(path.join(packageRoot, 'package.json'), {}).version || null;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function syncGlobalMcpInstall(version) {
|
|
326
|
-
const proc = getPm2Process(MCP_NAME);
|
|
327
|
-
if (!proc) {
|
|
328
|
-
info('[2.9] MCP pkg — PM2 process not registered yet; start handled below');
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const scriptPath = proc.pm2_env?.pm_exec_path || proc.pm_exec_path || '';
|
|
333
|
-
if (!isGlobalNpmRdcSkillsPath(scriptPath)) {
|
|
334
|
-
info(`[2.9] MCP pkg — PM2 uses source checkout, not global npm (${scriptPath || 'unknown path'})`);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const installedVersion = readInstalledPackageVersion(scriptPath);
|
|
339
|
-
if (installedVersion === version) {
|
|
340
|
-
ok(`[2.9] MCP pkg — global ${NPM_PACKAGE}@${version} already installed`);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
let stopped = false;
|
|
345
|
-
let installed = false;
|
|
346
|
-
try {
|
|
347
|
-
info(`[2.9] MCP pkg — updating global ${NPM_PACKAGE} ${installedVersion || '?'} → ${version}`);
|
|
348
|
-
run(`pm2 stop ${MCP_NAME}`);
|
|
349
|
-
stopped = true;
|
|
350
|
-
run(`npm install -g ${NPM_PACKAGE}@${version}`);
|
|
351
|
-
installed = true;
|
|
352
|
-
ok(`[2.9] MCP pkg — installed global ${NPM_PACKAGE}@${version}`);
|
|
353
|
-
} catch (e) {
|
|
354
|
-
warn(`[2.9] MCP pkg — global install failed (${String(e.message || e).split('\n')[0]})`);
|
|
355
|
-
if (String(e.message || '').includes('EBUSY')) {
|
|
356
|
-
info(' PM2 was stopped first; if EBUSY persists, another Node/npm process still holds the package directory.');
|
|
357
|
-
}
|
|
358
|
-
} finally {
|
|
359
|
-
if (stopped) {
|
|
360
|
-
try {
|
|
361
|
-
run(`pm2 restart ${MCP_NAME} --update-env`, { env: { ...process.env, PORT: MCP_PORT } });
|
|
362
|
-
ok(`[2.9] MCP pkg — pm2 restarted ${MCP_NAME}${installed ? '' : ' (previous install restored)'}`);
|
|
363
|
-
} catch (e) {
|
|
364
|
-
warn(`[2.9] MCP pkg — pm2 restart failed (${String(e.message || e).split('\n')[0]})`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// ── User-skills cleanup ───────────────────────────────────────────────────────
|
|
371
|
-
// Older installer versions wrote skill files directly to ~/.claude/skills/user/.
|
|
372
|
-
// Claude Code loads that directory AND the plugin cache, so any rdc skills left
|
|
373
|
-
// there produce duplicate registrations and break the resolver.
|
|
374
|
-
// This function nukes any entry whose frontmatter name starts with "rdc:".
|
|
375
|
-
// Scans BOTH the immediate dir and nested .md files (e.g. `user/skill.md`,
|
|
376
|
-
// `user/rdc-build/SKILL.md`) so pre-plugin orphans are caught regardless of
|
|
377
|
-
// naming convention.
|
|
378
|
-
function cleanUserSkills(userSkillsDir) {
|
|
379
|
-
if (!fs.existsSync(userSkillsDir)) return 0;
|
|
380
|
-
let removed = 0;
|
|
381
|
-
for (const entry of fs.readdirSync(userSkillsDir, { withFileTypes: true })) {
|
|
382
|
-
const candidate = path.join(userSkillsDir, entry.name);
|
|
383
|
-
if (entry.isDirectory()) {
|
|
384
|
-
// Subdir form: <name>/SKILL.md or <name>/skill.md
|
|
385
|
-
let skillFile = null;
|
|
386
|
-
for (const sf of ['SKILL.md', 'skill.md']) {
|
|
387
|
-
const p = path.join(candidate, sf);
|
|
388
|
-
if (fs.existsSync(p)) { skillFile = p; break; }
|
|
389
|
-
}
|
|
390
|
-
if (!skillFile) continue;
|
|
391
|
-
const fm = readFrontmatter(skillFile);
|
|
392
|
-
if (fm.name && fm.name.startsWith('rdc:')) {
|
|
393
|
-
try { fs.rmSync(candidate, { recursive: true, force: true }); removed++; } catch {}
|
|
394
|
-
}
|
|
395
|
-
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
396
|
-
// ANY .md file at this level — including skill.md / SKILL.md / README.md
|
|
397
|
-
// if their frontmatter declares an rdc:* skill. A previous version skipped
|
|
398
|
-
// those names; that left an orphan rdc:build copy at user/skill.md which
|
|
399
|
-
// registered as a duplicate "user" skill.
|
|
400
|
-
const fm = readFrontmatter(candidate);
|
|
401
|
-
if (fm.name && fm.name.startsWith('rdc:')) {
|
|
402
|
-
try { fs.unlinkSync(candidate); removed++; } catch {}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
return removed;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Scrub legacy rdc orphans from ~/.claude/skills/ (top-level), not just user/.
|
|
410
|
-
// Some older installs landed flat skill files alongside the plugin tree.
|
|
411
|
-
function cleanGlobalSkillsRoot(skillsDir) {
|
|
412
|
-
if (!fs.existsSync(skillsDir)) return 0;
|
|
413
|
-
let removed = 0;
|
|
414
|
-
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
415
|
-
if (entry.name === 'user') continue; // handled separately
|
|
416
|
-
const candidate = path.join(skillsDir, entry.name);
|
|
417
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
418
|
-
const fm = readFrontmatter(candidate);
|
|
419
|
-
if (fm.name && fm.name.startsWith('rdc:')) {
|
|
420
|
-
try { fs.unlinkSync(candidate); removed++; } catch {}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return removed;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// ── Stale hook cleanup ────────────────────────────────────────────────────────
|
|
428
|
-
// Remove ONLY explicitly orphaned hook files — hooks that were previously shipped
|
|
429
|
-
// by rdc-skills and have since been removed from the project.
|
|
430
|
-
// NEVER use "not in source = remove" logic: most hooks in ~/.claude/hooks/ are
|
|
431
|
-
// not managed by rdc-skills (they come from other plugins or were written directly).
|
|
432
|
-
function cleanStaleHooks(hooksDstDir) {
|
|
433
|
-
if (!fs.existsSync(hooksDstDir)) return 0;
|
|
434
|
-
// Explicit orphan list — add entries here when a hook is intentionally removed.
|
|
435
|
-
// Format: filename that should be deleted if it still exists.
|
|
436
|
-
const ORPHANED_HOOKS = [
|
|
437
|
-
'verify-rdc-skills.js', // removed in v0.9.7 — was checking for old flat-file format
|
|
438
|
-
];
|
|
439
|
-
let removed = 0;
|
|
440
|
-
for (const f of ORPHANED_HOOKS) {
|
|
441
|
-
const p = path.join(hooksDstDir, f);
|
|
442
|
-
if (fs.existsSync(p)) {
|
|
443
|
-
try { fs.unlinkSync(p); removed++; } catch {}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return removed;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Project-local hookify docs were a temporary workaround for work item exit
|
|
450
|
-
// enforcement. The plugin hook is now authoritative, so installs clean those
|
|
451
|
-
// local shims from known RDC workspaces instead of leaving two gates to drift.
|
|
452
|
-
function cleanProjectHookifyShims(projectRoot) {
|
|
453
|
-
if (!projectRoot) return 0;
|
|
454
|
-
const hookifyDir = path.join(projectRoot, '.claude');
|
|
455
|
-
if (!fs.existsSync(hookifyDir)) return 0;
|
|
456
|
-
const stale = [
|
|
457
|
-
'hookify.work-item-done-gate-bash.local.md',
|
|
458
|
-
'hookify.work-item-done-gate-mcp.local.md',
|
|
459
|
-
'hookify.work-item-review-gate-bash.local.md',
|
|
460
|
-
'hookify.work-item-review-gate-mcp.local.md',
|
|
461
|
-
];
|
|
462
|
-
let removed = 0;
|
|
463
|
-
for (const f of stale) {
|
|
464
|
-
const p = path.join(hookifyDir, f);
|
|
465
|
-
if (fs.existsSync(p)) {
|
|
466
|
-
try { fs.unlinkSync(p); removed++; } catch {}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
return removed;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// ── Cache flush helper ────────────────────────────────────────────────────────
|
|
473
|
-
// Only `latest/` is ever kept. Earlier versions wrote BOTH `<version>/` and
|
|
474
|
-
// `latest/`, which caused the plugin loader to scan and register every rdc:*
|
|
475
|
-
// skill twice (once per directory). The fix is permanent single-dir layout.
|
|
476
|
-
function flushOldCaches(cacheBase /* keepVersion intentionally unused */) {
|
|
477
|
-
if (!fs.existsSync(cacheBase)) return 0;
|
|
478
|
-
let flushed = 0;
|
|
479
|
-
for (const entry of fs.readdirSync(cacheBase)) {
|
|
480
|
-
if (entry === 'latest') continue;
|
|
481
|
-
try {
|
|
482
|
-
fs.rmSync(path.join(cacheBase, entry), { recursive: true, force: true });
|
|
483
|
-
flushed++;
|
|
484
|
-
} catch {}
|
|
485
|
-
}
|
|
486
|
-
return flushed;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// ── Step 2: CLI plugin registration (→ ~/.claude/plugins/) ───────────────────
|
|
490
|
-
function registerCLI(version, gitSha) {
|
|
491
|
-
const pluginDir = path.join(claudeHome, 'plugins');
|
|
492
|
-
const mktDir = path.join(pluginDir, 'marketplaces', MARKETPLACE);
|
|
493
|
-
const mktPlugDir = path.join(mktDir, '.claude-plugin');
|
|
494
|
-
const cacheBase = path.join(pluginDir, 'cache', MARKETPLACE, 'rdc-skills');
|
|
495
|
-
const latestDir = path.join(cacheBase, 'latest');
|
|
496
|
-
|
|
497
|
-
// 1. Marketplace manifest
|
|
498
|
-
fs.mkdirSync(mktPlugDir, { recursive: true });
|
|
499
|
-
fs.copyFileSync(path.join(repoRoot, '.claude-plugin', 'marketplace.json'), path.join(mktPlugDir, 'marketplace.json'));
|
|
500
|
-
|
|
501
|
-
// 2. known_marketplaces.json
|
|
502
|
-
const kmpPath = path.join(pluginDir, 'known_marketplaces.json');
|
|
503
|
-
const knownMp = readJson(kmpPath);
|
|
504
|
-
knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
|
|
505
|
-
writeJson(kmpPath, knownMp, 4);
|
|
506
|
-
|
|
507
|
-
// 3. Flush every cache dir except `latest/`, then rewrite `latest/`. We
|
|
508
|
-
// intentionally do NOT keep a versioned dir — the plugin loader registers
|
|
509
|
-
// every dir it finds, so two dirs = duplicate skills.
|
|
510
|
-
const flushed = flushOldCaches(cacheBase);
|
|
511
|
-
if (flushed > 0) info(` flushed : ${flushed} stale cache dir(s)`);
|
|
512
|
-
if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
|
|
513
|
-
buildPluginCache(latestDir, version, gitSha);
|
|
514
|
-
|
|
515
|
-
// 4. installed_plugins.json — register 'latest/' as stable installPath so
|
|
516
|
-
// re-installs overwrite in-place rather than creating orphaned version dirs.
|
|
517
|
-
// Open terminals that re-read installed_plugins.json mid-session will pick
|
|
518
|
-
// up the updated path; otherwise a terminal restart is needed.
|
|
519
|
-
const ipPath = path.join(pluginDir, 'installed_plugins.json');
|
|
520
|
-
const installed = readJson(ipPath, { version: 2, plugins: {} });
|
|
521
|
-
// Also overwrite whichever installPath the old entry had, so any open
|
|
522
|
-
// terminal that already loaded that path sees fresh files.
|
|
523
|
-
const oldEntries = installed.plugins[PLUGIN_KEY] || [];
|
|
524
|
-
for (const old of oldEntries) {
|
|
525
|
-
if (old.installPath && fs.existsSync(old.installPath) && old.installPath !== latestDir) {
|
|
526
|
-
try { buildPluginCache(old.installPath, version, gitSha); } catch {}
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
for (const key of Object.keys(installed.plugins || {})) {
|
|
530
|
-
if (key.startsWith('rdc-skills@')) delete installed.plugins[key];
|
|
531
|
-
}
|
|
532
|
-
installed.plugins[PLUGIN_KEY] = [{ scope: 'user', installPath: latestDir, version, installedAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), gitCommitSha: gitSha }];
|
|
533
|
-
writeJson(ipPath, installed, 4);
|
|
534
|
-
|
|
535
|
-
// 5. settings.json enabledPlugins
|
|
536
|
-
const settings = readJson(settingsPath);
|
|
537
|
-
if (!settings.enabledPlugins) settings.enabledPlugins = {};
|
|
538
|
-
for (const key of Object.keys(settings.enabledPlugins)) {
|
|
539
|
-
if (key.startsWith('rdc-skills@')) delete settings.enabledPlugins[key];
|
|
540
|
-
}
|
|
541
|
-
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
542
|
-
writeJson(settingsPath, settings);
|
|
543
|
-
|
|
544
|
-
return latestDir;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// ── Step 3: Cowork (Claude Desktop) registration ──────────────────────────────
|
|
548
|
-
function findCoworkBases() {
|
|
549
|
-
// Cowork stores per-workspace state at:
|
|
550
|
-
// %LOCALAPPDATA%/Packages/Claude_*/LocalCache/Roaming/Claude/local-agent-mode-sessions/<workspace>/<device>/
|
|
551
|
-
// Each has cowork_settings.json + cowork_plugins/
|
|
552
|
-
const results = [];
|
|
553
|
-
|
|
554
|
-
// Candidate MSIX package roots
|
|
555
|
-
const localAppData = process.env.LOCALAPPDATA || '';
|
|
556
|
-
const pkgsDir = path.join(localAppData, 'Packages');
|
|
557
|
-
if (!fs.existsSync(pkgsDir)) return results;
|
|
558
|
-
|
|
559
|
-
let claudePkg = null;
|
|
560
|
-
for (const dir of fs.readdirSync(pkgsDir)) {
|
|
561
|
-
if (/^Claude_/i.test(dir)) { claudePkg = path.join(pkgsDir, dir); break; }
|
|
562
|
-
}
|
|
563
|
-
if (!claudePkg) return results;
|
|
564
|
-
|
|
565
|
-
const sessionsRoot = path.join(claudePkg, 'LocalCache', 'Roaming', 'Claude', 'local-agent-mode-sessions');
|
|
566
|
-
if (!fs.existsSync(sessionsRoot)) return results;
|
|
567
|
-
|
|
568
|
-
// Walk two levels: <workspace>/<device>/cowork_settings.json
|
|
569
|
-
for (const ws of fs.readdirSync(sessionsRoot)) {
|
|
570
|
-
const wsDir = path.join(sessionsRoot, ws);
|
|
571
|
-
if (!fs.statSync(wsDir).isDirectory()) continue;
|
|
572
|
-
for (const dev of fs.readdirSync(wsDir)) {
|
|
573
|
-
const devDir = path.join(wsDir, dev);
|
|
574
|
-
if (!fs.statSync(devDir).isDirectory()) continue;
|
|
575
|
-
const settingsFile = path.join(devDir, 'cowork_settings.json');
|
|
576
|
-
if (fs.existsSync(settingsFile)) {
|
|
577
|
-
results.push({ dir: devDir, settingsFile });
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return results;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
function registerCowork(version, gitSha) {
|
|
585
|
-
const bases = findCoworkBases();
|
|
586
|
-
if (bases.length === 0) {
|
|
587
|
-
warn('Cowork — Claude Desktop not found (MSIX package missing)');
|
|
588
|
-
return 0;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
for (const { dir, settingsFile } of bases) {
|
|
592
|
-
const pluginsDir = path.join(dir, 'cowork_plugins');
|
|
593
|
-
const cacheBase = path.join(pluginsDir, 'cache', MARKETPLACE, 'rdc-skills');
|
|
594
|
-
const latestDir = path.join(cacheBase, 'latest');
|
|
595
|
-
const mktDir = path.join(pluginsDir, 'marketplaces', MARKETPLACE);
|
|
596
|
-
const mktPlugDir = path.join(mktDir, '.claude-plugin');
|
|
597
|
-
|
|
598
|
-
// Marketplace manifest
|
|
599
|
-
fs.mkdirSync(mktPlugDir, { recursive: true });
|
|
600
|
-
fs.copyFileSync(path.join(repoRoot, '.claude-plugin', 'marketplace.json'), path.join(mktPlugDir, 'marketplace.json'));
|
|
601
|
-
|
|
602
|
-
// known_marketplaces.json
|
|
603
|
-
const kmpPath = path.join(pluginsDir, 'known_marketplaces.json');
|
|
604
|
-
const knownMp = readJson(kmpPath);
|
|
605
|
-
knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
|
|
606
|
-
writeJson(kmpPath, knownMp, 4);
|
|
607
|
-
|
|
608
|
-
// Flush all non-latest caches and rewrite `latest/` only — single-dir layout
|
|
609
|
-
flushOldCaches(cacheBase);
|
|
610
|
-
if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
|
|
611
|
-
buildPluginCache(latestDir, version, gitSha);
|
|
612
|
-
|
|
613
|
-
// installed_plugins.json — use stable 'latest/' path
|
|
614
|
-
const ipPath = path.join(pluginsDir, 'installed_plugins.json');
|
|
615
|
-
const installed = readJson(ipPath, { version: 2, plugins: {} });
|
|
616
|
-
for (const key of Object.keys(installed.plugins || {})) {
|
|
617
|
-
if (key.startsWith('rdc-skills@')) delete installed.plugins[key];
|
|
618
|
-
}
|
|
619
|
-
installed.plugins[PLUGIN_KEY] = [{ scope: 'user', installPath: latestDir, version, installedAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), gitCommitSha: gitSha }];
|
|
620
|
-
writeJson(ipPath, installed, 4);
|
|
621
|
-
|
|
622
|
-
// cowork_settings.json — enabledPlugins + extraKnownMarketplaces
|
|
623
|
-
const settings = readJson(settingsFile);
|
|
624
|
-
if (!settings.enabledPlugins) settings.enabledPlugins = {};
|
|
625
|
-
for (const key of Object.keys(settings.enabledPlugins)) {
|
|
626
|
-
if (key.startsWith('rdc-skills@')) delete settings.enabledPlugins[key];
|
|
627
|
-
}
|
|
628
|
-
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
629
|
-
if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
|
|
630
|
-
settings.extraKnownMarketplaces[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' } };
|
|
631
|
-
writeJson(settingsFile, settings);
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return bases.length;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// ── Codex registration (→ Codex skill dirs/rdc-*/) ───────────────────────────
|
|
638
|
-
function addCodexTarget(targets, label, targetDir) {
|
|
639
|
-
if (!targetDir) return;
|
|
640
|
-
const resolved = path.resolve(targetDir);
|
|
641
|
-
if (targets.some(t => t.targetDir.toLowerCase() === resolved.toLowerCase())) return;
|
|
642
|
-
targets.push({ label, targetDir: resolved });
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Register the rdc-skills MCP endpoint at the USER/GLOBAL level for every client
|
|
646
|
-
// so all Claude Code projects and all Codex sessions can reach the skills via MCP
|
|
647
|
-
// (not just where a project .mcp.json exists). Idempotent + non-fatal. Mirrors the
|
|
648
|
-
// existing clauth/codeflow entries exactly. claude.ai web still needs the one-time
|
|
649
|
-
// connector add in its UI (no programmatic API).
|
|
650
|
-
function registerMcpEndpoints() {
|
|
651
|
-
const MCP_URL = 'https://rdc-skills.regendevcorp.com/mcp';
|
|
652
|
-
const out = [];
|
|
653
|
-
|
|
654
|
-
// Claude Code — user-level ~/.claude.json mcpServers (covers EVERY project).
|
|
655
|
-
try {
|
|
656
|
-
const claudeJson = path.join(os.homedir(), '.claude.json');
|
|
657
|
-
if (fs.existsSync(claudeJson)) {
|
|
658
|
-
const data = readJson(claudeJson);
|
|
659
|
-
if (!data.mcpServers || typeof data.mcpServers !== 'object') data.mcpServers = {};
|
|
660
|
-
const cur = data.mcpServers['rdc-skills'];
|
|
661
|
-
if (!cur || cur.url !== MCP_URL) {
|
|
662
|
-
data.mcpServers['rdc-skills'] = { type: 'http', url: MCP_URL };
|
|
663
|
-
writeJson(claudeJson, data, 2);
|
|
664
|
-
out.push('claude(~/.claude.json)');
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
} catch (e) { out.push(`claude WARN:${e.message}`); }
|
|
668
|
-
|
|
669
|
-
// Codex — ensure [mcp_servers.rdc-skills] exists and points at the live shared endpoint.
|
|
670
|
-
try {
|
|
671
|
-
const codexToml = path.join(os.homedir(), '.codex', 'config.toml');
|
|
672
|
-
if (fs.existsSync(codexToml)) {
|
|
673
|
-
const toml = fs.readFileSync(codexToml, 'utf8');
|
|
674
|
-
const next = updateCodexMcpToml(toml, MCP_URL);
|
|
675
|
-
if (next !== toml) {
|
|
676
|
-
fs.writeFileSync(codexToml, next);
|
|
677
|
-
out.push('codex(~/.codex/config.toml)');
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
} catch (e) { out.push(`codex WARN:${e.message}`); }
|
|
681
|
-
|
|
682
|
-
return out;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function findCodexTargets() {
|
|
686
|
-
const targets = [];
|
|
687
|
-
if (codexRoot) {
|
|
688
|
-
addCodexTarget(targets, 'project .agents', path.join(codexRoot, '.agents', 'skills', 'user'));
|
|
689
|
-
}
|
|
690
|
-
if (explicitCodexSkillDir) {
|
|
691
|
-
addCodexTarget(targets, 'explicit', explicitCodexSkillDir);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const codexHomeSkills = path.join(os.homedir(), '.codex', 'skills');
|
|
695
|
-
if (fs.existsSync(codexHomeSkills)) {
|
|
696
|
-
addCodexTarget(targets, 'global .codex', codexHomeSkills);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const globalAgentSkills = path.join(os.homedir(), '.agents', 'skills');
|
|
700
|
-
if (fs.existsSync(globalAgentSkills)) {
|
|
701
|
-
addCodexTarget(targets, 'global .agents', globalAgentSkills);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
return targets;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function registerCodexTarget(targetDir) {
|
|
708
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
709
|
-
|
|
710
|
-
// Clean: remove dirs that are rdc skills — by prefix OR by frontmatter name
|
|
711
|
-
let removed = 0;
|
|
712
|
-
for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
|
|
713
|
-
if (!entry.isDirectory()) continue;
|
|
714
|
-
const candidate = path.join(targetDir, entry.name);
|
|
715
|
-
if (/^rdc-/.test(entry.name)) {
|
|
716
|
-
fs.rmSync(candidate, { recursive: true, force: true });
|
|
717
|
-
removed++;
|
|
718
|
-
} else {
|
|
719
|
-
const fm = readFrontmatter(path.join(candidate, 'SKILL.md'));
|
|
720
|
-
if (fm.name && fm.name.startsWith('rdc:')) {
|
|
721
|
-
fs.rmSync(candidate, { recursive: true, force: true });
|
|
722
|
-
removed++;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Copy: each source skill dir that has a SKILL.md → rdc-<name>/
|
|
728
|
-
const skillsSrc = path.join(repoRoot, 'skills');
|
|
729
|
-
let copied = 0;
|
|
730
|
-
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
731
|
-
if (!entry.isDirectory()) continue;
|
|
732
|
-
const skillFile = path.join(skillsSrc, entry.name, 'SKILL.md');
|
|
733
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
734
|
-
const dst = path.join(targetDir, `rdc-${entry.name}`);
|
|
735
|
-
copyDirRecursive(path.join(skillsSrc, entry.name), dst);
|
|
736
|
-
copied++;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
return { removed, copied };
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// ── Step 6: Zip for claude.ai / distribution ─────────────────────────────────
|
|
743
|
-
function buildZip(version) {
|
|
744
|
-
const distDir = path.join(repoRoot, 'dist');
|
|
745
|
-
fs.mkdirSync(distDir, { recursive: true });
|
|
746
|
-
const zipPath = path.join(distDir, `rdc-skills-plugin-v${version}.zip`);
|
|
747
|
-
|
|
748
|
-
// Remove old zips
|
|
749
|
-
if (fs.existsSync(distDir)) {
|
|
750
|
-
for (const f of fs.readdirSync(distDir)) {
|
|
751
|
-
if (f.startsWith('rdc-skills-plugin') && f.endsWith('.zip')) {
|
|
752
|
-
fs.unlinkSync(path.join(distDir, f));
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Use PowerShell Compress-Archive on Windows, zip on Unix
|
|
758
|
-
const items = ['.claude-plugin', 'commands', 'skills', 'guides', 'hooks', 'package.json', 'README.md']
|
|
759
|
-
.filter(i => fs.existsSync(path.join(repoRoot, i)));
|
|
760
|
-
|
|
761
|
-
try {
|
|
762
|
-
if (process.platform === 'win32') {
|
|
763
|
-
// Stage to a temp dir so we get a clean zip root
|
|
764
|
-
const tmp = path.join(os.tmpdir(), `rdc-skills-zip-${Date.now()}`);
|
|
765
|
-
fs.mkdirSync(tmp, { recursive: true });
|
|
766
|
-
for (const item of items) {
|
|
767
|
-
const src = path.join(repoRoot, item);
|
|
768
|
-
const dst = path.join(tmp, item);
|
|
769
|
-
if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dst);
|
|
770
|
-
else fs.copyFileSync(src, dst);
|
|
771
|
-
}
|
|
772
|
-
// Try pwsh first (PowerShell 7+), fall back to Windows PowerShell
|
|
773
|
-
const psExe = fs.existsSync('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
|
|
774
|
-
? 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
|
|
775
|
-
: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
776
|
-
execSync(
|
|
777
|
-
`"${psExe}" -NoProfile -NonInteractive -WindowStyle Hidden -Command "Compress-Archive -Path '${tmp}\\*' -DestinationPath '${zipPath}' -Force"`,
|
|
778
|
-
{ stdio: 'pipe' }
|
|
779
|
-
);
|
|
780
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
781
|
-
} else {
|
|
782
|
-
const itemList = items.join(' ');
|
|
783
|
-
execSync(`zip -r "${zipPath}" ${itemList}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
784
|
-
}
|
|
785
|
-
return zipPath;
|
|
786
|
-
} catch (e) {
|
|
787
|
-
warn(`Zip failed: ${e.message}`);
|
|
788
|
-
return null;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// ── Hook config ───────────────────────────────────────────────────────────────
|
|
793
|
-
function buildHooksConfig(hooksDir, profile = 'core') {
|
|
794
|
-
const base = hooksDir.replace(/\\/g, '/');
|
|
795
|
-
const cmd = (file, msg) => {
|
|
796
|
-
const command = process.platform === 'win32'
|
|
797
|
-
? `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${base}/run-hidden-hook.ps1" "${base}/${file}"`
|
|
798
|
-
: `node "${base}/${file}"`;
|
|
799
|
-
const entry = { type: 'command', command };
|
|
800
|
-
if (msg) entry.statusMessage = msg;
|
|
801
|
-
return entry;
|
|
802
|
-
};
|
|
803
|
-
const config = {
|
|
804
|
-
UserPromptExpansion: [{ hooks: [
|
|
805
|
-
cmd('rdc-invocation-marker.js', 'Marking RDC slash command...'),
|
|
806
|
-
]}],
|
|
807
|
-
UserPromptSubmit: [{ hooks: [
|
|
808
|
-
cmd('rdc-invocation-marker.js', 'Marking RDC prompt...'),
|
|
809
|
-
]}],
|
|
810
|
-
PreToolUse: [
|
|
811
|
-
{ hooks: [
|
|
812
|
-
cmd('foreground-process-gate.js', 'Checking foreground process policy...'),
|
|
813
|
-
]},
|
|
814
|
-
{ matcher: 'Bash', hooks: [
|
|
815
|
-
cmd('require-work-item-on-commit.js'),
|
|
816
|
-
]},
|
|
817
|
-
],
|
|
818
|
-
PostToolUse: [
|
|
819
|
-
{ hooks: [
|
|
820
|
-
cmd('check-services.js'),
|
|
821
|
-
]},
|
|
822
|
-
{ matcher: 'Bash', hooks: [
|
|
823
|
-
cmd('require-work-item-on-commit.js', 'Capturing commit SHA for work item...'),
|
|
824
|
-
]},
|
|
825
|
-
],
|
|
826
|
-
Stop: [{ hooks: [
|
|
827
|
-
cmd('rdc-output-contract-gate.js', 'Checking RDC output contract...'),
|
|
828
|
-
cmd('post-work-check.js', 'Checking for undocumented work...'),
|
|
829
|
-
]}],
|
|
830
|
-
};
|
|
831
|
-
|
|
832
|
-
if (profile === 'lifeai') {
|
|
833
|
-
config.SessionStart = [{ hooks: [
|
|
834
|
-
cmd('check-rdc-environment.js', 'Checking RDC skills runtime...'),
|
|
835
|
-
cmd('check-cwd.js'),
|
|
836
|
-
cmd('check-stale-work-items.js', 'Checking for stale work items...'),
|
|
837
|
-
// Truth Gate 3.0 Layer 6 — gate watchdog (ADVISORY; SessionStart cannot block).
|
|
838
|
-
cmd('gate-watchdog-selfcheck.js', 'Truth Gate watchdog: verifying gate registration...'),
|
|
839
|
-
]}];
|
|
840
|
-
config.PreToolUse[0].hooks.push(
|
|
841
|
-
cmd('work-item-exit-gate.js', 'Checking work item exit gates...'),
|
|
842
|
-
);
|
|
843
|
-
// Truth Gate 3.0 Layer 5 — harness completion gates. Both blocking hooks are
|
|
844
|
-
// FLAG-GATED, default OFF (no-op until the env/DB flag is flipped at deploy),
|
|
845
|
-
// so registering them does NOT disrupt the in-flight build session.
|
|
846
|
-
config.TaskCompleted = [{ hooks: [
|
|
847
|
-
cmd('task-completed-gate.js', 'Truth Gate: verifying task closure...'),
|
|
848
|
-
]}];
|
|
849
|
-
config.PostToolBatch = [{ hooks: [
|
|
850
|
-
cmd('post-tool-batch-gate.js', 'Truth Gate: checking build-wave worktree bases...'),
|
|
851
|
-
]}];
|
|
852
|
-
config.PreCompact = [{ hooks: [
|
|
853
|
-
cmd('precompact-log.js'),
|
|
854
|
-
]}];
|
|
855
|
-
config.PostCompact = [{ hooks: [
|
|
856
|
-
cmd('postcompact-log.js'),
|
|
857
|
-
cmd('restart-brief.js', 'Writing restart brief...'),
|
|
858
|
-
]}];
|
|
859
|
-
config.Stop[0].hooks.unshift(
|
|
860
|
-
cmd('rate-limit-retry.js', 'Checking for rate limits...'),
|
|
861
|
-
);
|
|
862
|
-
config.Stop[0].hooks.push(
|
|
863
|
-
cmd('no-stop-open-epics.js', 'Checking for open epics...'),
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return config;
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// ── MCP server registration (non-fatal) ───────────────────────────────────────
|
|
871
|
-
// Ensures the rdc-skills MCP deps are installed, registers/starts the local MCP
|
|
872
|
-
// under PM2 as `rdc-skills-mcp` on PORT=3110, and prints the claude.ai connector
|
|
873
|
-
// line. Every failure here WARNs — it must never abort the installer.
|
|
874
|
-
function registerMcpServer() {
|
|
875
|
-
const binPath = path.join(repoRoot, 'bin', 'rdc-skills-mcp.mjs');
|
|
876
|
-
const connector = 'https://rdc-skills.regendevcorp.com/mcp';
|
|
877
|
-
|
|
878
|
-
try {
|
|
879
|
-
// (a) ensure MCP runtime deps are present (express, @modelcontextprotocol/sdk, yaml, zod)
|
|
880
|
-
const needDeps = ['express', '@modelcontextprotocol/sdk', 'yaml', 'zod'].some((d) => {
|
|
881
|
-
try { require.resolve(d, { paths: [repoRoot] }); return false; } catch { return true; }
|
|
882
|
-
});
|
|
883
|
-
if (needDeps) {
|
|
884
|
-
try {
|
|
885
|
-
execSync('npm install --no-audit --no-fund', { cwd: repoRoot, stdio: 'pipe' });
|
|
886
|
-
ok('[7/7] MCP deps — installed');
|
|
887
|
-
} catch (e) {
|
|
888
|
-
warn(`[7/7] MCP deps — npm install failed (${e.message.split('\n')[0]}); install manually with \`npm install\``);
|
|
889
|
-
}
|
|
890
|
-
} else {
|
|
891
|
-
ok('[7/7] MCP deps — already present');
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// (b) register/start under PM2 (tolerate pm2 missing)
|
|
895
|
-
let pm2Ok = false;
|
|
896
|
-
try { execSync('pm2 -v', { stdio: 'pipe' }); pm2Ok = true; } catch { pm2Ok = false; }
|
|
897
|
-
|
|
898
|
-
if (!pm2Ok) {
|
|
899
|
-
warn('[7/7] MCP server — pm2 not found; start manually:');
|
|
900
|
-
info(` PORT=${MCP_PORT} pm2 start ${binPath} --name ${MCP_NAME}`);
|
|
901
|
-
} else {
|
|
902
|
-
let registered = false;
|
|
903
|
-
try {
|
|
904
|
-
const jlist = JSON.parse(execSync('pm2 jlist', { encoding: 'utf8', stdio: 'pipe' }));
|
|
905
|
-
registered = Array.isArray(jlist) && jlist.some((p) => p.name === MCP_NAME);
|
|
906
|
-
} catch {}
|
|
907
|
-
try {
|
|
908
|
-
if (registered) {
|
|
909
|
-
execSync(`pm2 restart ${MCP_NAME} --update-env`, { cwd: repoRoot, stdio: 'pipe', env: { ...process.env, PORT: MCP_PORT } });
|
|
910
|
-
ok(`[7/7] MCP server — pm2 restarted ${MCP_NAME} (PORT=${MCP_PORT})`);
|
|
911
|
-
} else {
|
|
912
|
-
execSync(`pm2 start "${binPath}" --name ${MCP_NAME}`, { cwd: repoRoot, stdio: 'pipe', env: { ...process.env, PORT: MCP_PORT } });
|
|
913
|
-
ok(`[7/7] MCP server — pm2 started ${MCP_NAME} (PORT=${MCP_PORT})`);
|
|
914
|
-
}
|
|
915
|
-
} catch (e) {
|
|
916
|
-
warn(`[7/7] MCP server — pm2 start/restart failed (${e.message.split('\n')[0]})`);
|
|
917
|
-
info(` PORT=${MCP_PORT} pm2 start ${binPath} --name ${MCP_NAME}`);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// (c) print the claude.ai connector line
|
|
922
|
-
info(` claude.ai connector: ${connector} (Auth: none — URL is the shared secret)`);
|
|
923
|
-
} catch (e) {
|
|
924
|
-
warn(`[7/7] MCP server — skipped (${e.message.split('\n')[0]})`);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
// ── Preflight ─────────────────────────────────────────────────────────────────
|
|
929
|
-
function runPreflight() {
|
|
930
|
-
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
931
|
-
if (nodeMajor < 18) { fail(`Node.js >= 18 required — found v${process.versions.node}`); process.exit(1); }
|
|
932
|
-
ok(`Node.js v${process.versions.node}`);
|
|
933
|
-
try {
|
|
934
|
-
execSync('curl -s --max-time 2 http://127.0.0.1:52437/ping', { stdio: 'pipe' });
|
|
935
|
-
ok('clauth daemon is running');
|
|
936
|
-
} catch {
|
|
937
|
-
warn('clauth daemon not responding — start it before using credential-dependent commands');
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
// ── Commands listing ──────────────────────────────────────────────────────────
|
|
942
|
-
function listCommands() {
|
|
943
|
-
const cmdsDir = path.join(repoRoot, 'commands');
|
|
944
|
-
if (!fs.existsSync(cmdsDir)) return;
|
|
945
|
-
const files = fs.readdirSync(cmdsDir).filter(f => f.endsWith('.md')).sort();
|
|
946
|
-
const plugin = readJson(path.join(repoRoot, '.claude-plugin', 'plugin.json'), {});
|
|
947
|
-
const skillCount = Array.isArray(plugin.skills_meta)
|
|
948
|
-
? plugin.skills_meta.length
|
|
949
|
-
: (plugin.skills_meta && typeof plugin.skills_meta === 'object' ? Object.keys(plugin.skills_meta).length : null);
|
|
950
|
-
console.log('');
|
|
951
|
-
if (skillCount !== null) {
|
|
952
|
-
console.log(` \x1b[32mAvailable MCP skills (${skillCount}) and /rdc:* command shorthands (${files.length}):\x1b[0m`);
|
|
953
|
-
console.log(' Use MCP tools rdc_skill_list, rdc_skill_search, and rdc_skill_get for the full skill catalog.');
|
|
954
|
-
} else {
|
|
955
|
-
console.log(` \x1b[32mAvailable /rdc:* command shorthands (${files.length}):\x1b[0m`);
|
|
956
|
-
}
|
|
957
|
-
console.log('');
|
|
958
|
-
const COL = 18;
|
|
959
|
-
for (const f of files) {
|
|
960
|
-
const name = 'rdc:' + f.replace(/\.md$/, '');
|
|
961
|
-
const fm = readFrontmatter(path.join(cmdsDir, f));
|
|
962
|
-
const desc = (fm.description || '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
963
|
-
const short = desc.length > 70 ? desc.slice(0, 70) + '…' : desc;
|
|
964
|
-
const pad = ' '.repeat(Math.max(1, COL - name.length));
|
|
965
|
-
console.log(` \x1b[36m/${name}\x1b[0m${pad}${short}`);
|
|
966
|
-
}
|
|
967
|
-
console.log('');
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
// ── Migrate helper ────────────────────────────────────────────────────────────
|
|
971
|
-
async function runMigrate(projectRoot) {
|
|
972
|
-
const absRoot = path.resolve(projectRoot);
|
|
973
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
974
|
-
const ask = q => new Promise(resolve => rl.question(q, resolve));
|
|
975
|
-
|
|
976
|
-
console.log('\n \x1b[35mrdc-skills — Migrate to .rdc/ Layout\x1b[0m\n');
|
|
977
|
-
console.log(` Project root: ${absRoot}\n`);
|
|
978
|
-
|
|
979
|
-
const candidates = [
|
|
980
|
-
{ src: 'docs/guides', dst: '.rdc/guides' }, { src: 'docs/plans', dst: '.rdc/plans' },
|
|
981
|
-
{ src: 'docs/reports', dst: '.rdc/reports' }, { src: 'docs/research', dst: '.rdc/research' },
|
|
982
|
-
].filter(c => fs.existsSync(path.join(absRoot, c.src)));
|
|
983
|
-
|
|
984
|
-
if (candidates.length === 0) { info('No migratable directories found.'); rl.close(); return; }
|
|
985
|
-
console.log(' Found:');
|
|
986
|
-
candidates.forEach(c => console.log(` ${c.src}`));
|
|
987
|
-
console.log('');
|
|
988
|
-
|
|
989
|
-
for (const c of candidates) {
|
|
990
|
-
const srcAbs = path.join(absRoot, c.src);
|
|
991
|
-
const dstAbs = path.join(absRoot, c.dst);
|
|
992
|
-
const ans = await ask(` Move ${c.src} → ${c.dst}? [Y/n]: `);
|
|
993
|
-
if (ans.toLowerCase() === 'n') { info(`Skipped ${c.src}`); continue; }
|
|
994
|
-
if (fs.existsSync(dstAbs)) {
|
|
995
|
-
warn(`${c.dst} exists — merging`);
|
|
996
|
-
for (const f of fs.readdirSync(srcAbs)) {
|
|
997
|
-
const sf = path.join(srcAbs, f), df = path.join(dstAbs, f);
|
|
998
|
-
if (!fs.existsSync(df)) fs.renameSync(sf, df);
|
|
999
|
-
else warn(` Skipped ${f} (exists in dst)`);
|
|
1000
|
-
}
|
|
1001
|
-
} else {
|
|
1002
|
-
fs.mkdirSync(path.dirname(dstAbs), { recursive: true });
|
|
1003
|
-
fs.renameSync(srcAbs, dstAbs);
|
|
1004
|
-
}
|
|
1005
|
-
ok(`Moved ${c.src} → ${c.dst}`);
|
|
1006
|
-
}
|
|
1007
|
-
rl.close();
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
1011
|
-
async function main() {
|
|
1012
|
-
if (doMigrate) { await runMigrate(migratePath); return; }
|
|
1013
|
-
|
|
1014
|
-
const bannerVersion = (readJson(path.join(repoRoot, 'package.json')).version || '?');
|
|
1015
|
-
const bannerLine = `║ install-rdc-skills v${bannerVersion}`;
|
|
1016
|
-
const bannerPadded = bannerLine.padEnd(41) + '║';
|
|
1017
|
-
console.log('');
|
|
1018
|
-
console.log(' \x1b[32m╔═══════════════════════════════════════╗\x1b[0m');
|
|
1019
|
-
console.log(` \x1b[32m${bannerPadded}\x1b[0m`);
|
|
1020
|
-
console.log(' \x1b[32m╚═══════════════════════════════════════╝\x1b[0m');
|
|
1021
|
-
console.log('');
|
|
1022
|
-
console.log(` CLAUDE_HOME : ${claudeHome}`);
|
|
1023
|
-
console.log(` Plugin root : ${repoRoot}`);
|
|
1024
|
-
console.log(` Profile : ${installProfile}${profileArg === 'auto' ? ' (auto)' : ''}`);
|
|
1025
|
-
console.log('');
|
|
1026
|
-
|
|
1027
|
-
if (!fs.existsSync(claudeHome)) {
|
|
1028
|
-
fs.mkdirSync(claudeHome, { recursive: true });
|
|
1029
|
-
warn(`CLAUDE_HOME did not exist — created ${claudeHome}`);
|
|
1030
|
-
}
|
|
1031
|
-
if (!fs.existsSync(settingsPath)) {
|
|
1032
|
-
writeJson(settingsPath, {});
|
|
1033
|
-
info(` created settings.json`);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// 0. Pull latest
|
|
1037
|
-
try {
|
|
1038
|
-
const before = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1039
|
-
execSync('git pull --ff-only', { cwd: repoRoot, stdio: 'pipe' });
|
|
1040
|
-
const after = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1041
|
-
ok(`[0/6] git pull — ${before === after ? after.slice(0, 7) : `${before.slice(0,7)} → ${after.slice(0,7)}`}`);
|
|
1042
|
-
} catch {
|
|
1043
|
-
warn('[0/6] git pull failed — installing from local copy');
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
// Read version + git SHA after pull so plugin caches and the MCP install do
|
|
1047
|
-
// not stamp the pre-update checkout.
|
|
1048
|
-
const pkg = readJson(path.join(repoRoot, 'package.json'));
|
|
1049
|
-
const version = pkg.version || '0.7.0';
|
|
1050
|
-
let gitSha = '';
|
|
1051
|
-
try { gitSha = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim(); } catch {}
|
|
1052
|
-
|
|
1053
|
-
// 0.5a. User-skills cleanup — remove any rdc: skills from ~/.claude/skills/user/
|
|
1054
|
-
// (older installer versions wrote there; plugin cache is the only authoritative source)
|
|
1055
|
-
{
|
|
1056
|
-
const userSkillsDir = path.join(claudeHome, 'skills', 'user');
|
|
1057
|
-
const purged = cleanUserSkills(userSkillsDir);
|
|
1058
|
-
if (purged > 0) ok(`[0.5a] Skills cleanup — removed ${purged} stale rdc: skill(s) from skills/user/`);
|
|
1059
|
-
// Also scan the parent ~/.claude/skills/ for any flat-file rdc orphans.
|
|
1060
|
-
const skillsRoot = path.join(claudeHome, 'skills');
|
|
1061
|
-
const rootPurged = cleanGlobalSkillsRoot(skillsRoot);
|
|
1062
|
-
if (rootPurged > 0) ok(`[0.5a] Skills cleanup — removed ${rootPurged} stale rdc: file(s) from skills/`);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// 0.5b. Stale hook cleanup — remove hooks we no longer ship
|
|
1066
|
-
{
|
|
1067
|
-
const staleRemoved = cleanStaleHooks(hooksDst);
|
|
1068
|
-
if (staleRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${staleRemoved} orphaned hook file(s)`);
|
|
1069
|
-
const shimRemoved = cleanProjectHookifyShims(codexRoot);
|
|
1070
|
-
if (shimRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${shimRemoved} project-local work-item hookify shim(s)`);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// 0.5. Legacy cleanup — remove old commands/rdc/ (pre-plugin-system format)
|
|
1074
|
-
const legacyCommandsDir = path.join(claudeHome, 'commands', 'rdc');
|
|
1075
|
-
if (fs.existsSync(legacyCommandsDir)) {
|
|
1076
|
-
fs.rmSync(legacyCommandsDir, { recursive: true, force: true });
|
|
1077
|
-
ok('[0.5] Cleanup — removed legacy commands/rdc/');
|
|
1078
|
-
}
|
|
1079
|
-
// Also remove extraKnownMarketplaces.rdc-skills from settings.json to prevent
|
|
1080
|
-
// Claude Code from syncing a directory-source marketplace back to known_marketplaces.json
|
|
1081
|
-
// (which causes skills to load from both the cache AND the live source directory)
|
|
1082
|
-
{
|
|
1083
|
-
const st = readJson(settingsPath);
|
|
1084
|
-
if (st.extraKnownMarketplaces && st.extraKnownMarketplaces[MARKETPLACE]) {
|
|
1085
|
-
delete st.extraKnownMarketplaces[MARKETPLACE];
|
|
1086
|
-
if (Object.keys(st.extraKnownMarketplaces).length === 0) delete st.extraKnownMarketplaces;
|
|
1087
|
-
writeJson(settingsPath, st);
|
|
1088
|
-
ok('[0.5] Cleanup — removed extraKnownMarketplaces.rdc-skills from settings.json');
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
// 1. CLI registration
|
|
1093
|
-
const cliCacheDir = registerCLI(version, gitSha);
|
|
1094
|
-
ok(`[1/6] CLI plugin — ${PLUGIN_KEY} v${version}`);
|
|
1095
|
-
info(` cache : ${cliCacheDir}`);
|
|
1096
|
-
|
|
1097
|
-
// 2. Cowork registration
|
|
1098
|
-
const coworkCount = registerCowork(version, gitSha);
|
|
1099
|
-
if (coworkCount > 0) {
|
|
1100
|
-
ok(`[2/6] Cowork — registered in ${coworkCount} workspace(s)`);
|
|
1101
|
-
} else {
|
|
1102
|
-
warn('[2/6] Cowork — no Desktop workspaces found (open Claude Desktop once to create them)');
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// 2.5. Codex registration
|
|
1106
|
-
const codexTargets = findCodexTargets();
|
|
1107
|
-
if (codexTargets.length > 0) {
|
|
1108
|
-
let copiedTotal = 0;
|
|
1109
|
-
let removedTotal = 0;
|
|
1110
|
-
for (const target of codexTargets) {
|
|
1111
|
-
const { removed, copied } = registerCodexTarget(target.targetDir);
|
|
1112
|
-
copiedTotal += copied;
|
|
1113
|
-
removedTotal += removed;
|
|
1114
|
-
info(` ${target.label.padEnd(15)}: ${target.targetDir} (${copied} installed, ${removed} stale removed)`);
|
|
1115
|
-
}
|
|
1116
|
-
ok(`[2.5] Codex — ${copiedTotal} skill install(s), ${removedTotal} stale removed across ${codexTargets.length} target(s)`);
|
|
1117
|
-
} else {
|
|
1118
|
-
info('[2.5] Codex — skipped (no Codex skill dirs found; use --codex-root or --codex-skill-dir)');
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
// 2.6. Register the rdc-skills MCP endpoint globally (Claude Code + Codex) so
|
|
1122
|
-
// EVERY agent can reach the skills via MCP, not only where a project .mcp.json exists.
|
|
1123
|
-
const mcpReg = registerMcpEndpoints();
|
|
1124
|
-
if (mcpReg.length > 0) {
|
|
1125
|
-
ok(`[2.6] MCP — registered rdc-skills endpoint: ${mcpReg.join(', ')}`);
|
|
1126
|
-
} else {
|
|
1127
|
-
info('[2.6] MCP — rdc-skills endpoint already registered (claude + codex)');
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
// If the live MCP is served from the global npm package, update that exact
|
|
1131
|
-
// install before restarting PM2. Windows otherwise holds the package tree open
|
|
1132
|
-
// and `npm install -g` can fail with EBUSY.
|
|
1133
|
-
syncGlobalMcpInstall(version);
|
|
1134
|
-
|
|
1135
|
-
// 2.7. Symlinks in regen-root/.claude/skills/ (FS MCP + claude.ai access)
|
|
1136
|
-
if (codexRoot) {
|
|
1137
|
-
const skillsLinkDir = path.join(codexRoot, '.claude', 'skills');
|
|
1138
|
-
const skillsSrc = path.join(repoRoot, 'skills');
|
|
1139
|
-
fs.mkdirSync(skillsLinkDir, { recursive: true });
|
|
1140
|
-
let linked = 0;
|
|
1141
|
-
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
1142
|
-
if (!entry.isDirectory()) continue;
|
|
1143
|
-
if (!fs.existsSync(path.join(skillsSrc, entry.name, 'SKILL.md'))) continue;
|
|
1144
|
-
const linkPath = path.join(skillsLinkDir, entry.name);
|
|
1145
|
-
const target = path.join(skillsSrc, entry.name);
|
|
1146
|
-
try {
|
|
1147
|
-
if (fs.existsSync(linkPath) || (() => { try { fs.lstatSync(linkPath); return true; } catch { return false; } })()) {
|
|
1148
|
-
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
1149
|
-
}
|
|
1150
|
-
} catch {}
|
|
1151
|
-
try {
|
|
1152
|
-
if (process.platform === 'win32') {
|
|
1153
|
-
fs.symlinkSync(target, linkPath, 'junction');
|
|
1154
|
-
} else {
|
|
1155
|
-
fs.symlinkSync(target, linkPath, 'dir');
|
|
1156
|
-
}
|
|
1157
|
-
linked++;
|
|
1158
|
-
} catch {}
|
|
1159
|
-
}
|
|
1160
|
-
if (linked > 0) {
|
|
1161
|
-
ok(`[2.7] Symlinks — ${linked} skill link(s) in ${skillsLinkDir}`);
|
|
1162
|
-
} else {
|
|
1163
|
-
info('[2.7] Symlinks — no rdc skill links created (skills may already be linked)');
|
|
1164
|
-
}
|
|
1165
|
-
const projectGuideCount = copyMissingProjectGuides(codexRoot);
|
|
1166
|
-
if (projectGuideCount > 0) {
|
|
1167
|
-
ok(`[2.8] Guides — ${projectGuideCount} missing guide(s) copied to ${path.join(codexRoot, '.rdc', 'guides')}`);
|
|
1168
|
-
} else {
|
|
1169
|
-
info('[2.8] Guides — project .rdc/guides already has base guide files or is absent');
|
|
1170
|
-
}
|
|
1171
|
-
} else {
|
|
1172
|
-
info('[2.7] Symlinks — skipped (no codex root found)');
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
// 3. Hook files
|
|
1176
|
-
const hookCount = copyHookFiles(hooksSrc, hooksDst);
|
|
1177
|
-
ok(`[3/6] Hook files — ${hookCount} file(s) → ${hooksDst}`);
|
|
1178
|
-
|
|
1179
|
-
// 4. Hook wiring
|
|
1180
|
-
if (skipHooks) {
|
|
1181
|
-
info('[4/6] Hook wiring — skipped (--skip-hooks)');
|
|
1182
|
-
} else {
|
|
1183
|
-
const settings = readJson(settingsPath);
|
|
1184
|
-
settings.hooks = buildHooksConfig(hooksDst, installProfile);
|
|
1185
|
-
writeJson(settingsPath, settings);
|
|
1186
|
-
ok(`[4/6] Hook wiring — ${settingsPath}`);
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
// 4.5 Optional startup blocks
|
|
1190
|
-
if (shouldWriteStartupBlocks) {
|
|
1191
|
-
const startup = writeStartupBlocks(projectRoot, installProfile);
|
|
1192
|
-
if (startup.skipped) warn(`[4.5] Startup — skipped (${startup.skipped})`);
|
|
1193
|
-
else ok(`[4.5] Startup — wrote managed startup guide/block(s) under ${projectRoot}`);
|
|
1194
|
-
} else {
|
|
1195
|
-
info('[4.5] Startup — skipped (use --project-root <path> --write-startup-blocks)');
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
// 5. Zip for claude.ai / distribution
|
|
1199
|
-
const zipPath = buildZip(version);
|
|
1200
|
-
if (zipPath) {
|
|
1201
|
-
ok(`[5/6] Plugin zip — ${zipPath}`);
|
|
1202
|
-
info(' claude.ai : optional plugin-upload artifact; MCP callers normally use the connector URL below');
|
|
1203
|
-
} else {
|
|
1204
|
-
warn('[5/6] Plugin zip — build failed (zip/powershell missing?)');
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// 6. Preflight
|
|
1208
|
-
console.log('');
|
|
1209
|
-
console.log(' \x1b[36mPreflight:\x1b[0m');
|
|
1210
|
-
runPreflight();
|
|
1211
|
-
|
|
1212
|
-
// 6.5 Post-install verification — duplication guard
|
|
1213
|
-
console.log('');
|
|
1214
|
-
console.log(' \x1b[36mPost-install verification:\x1b[0m');
|
|
1215
|
-
let verifyFailed = false;
|
|
1216
|
-
{
|
|
1217
|
-
const cacheBase = path.join(claudeHome, 'plugins', 'cache', MARKETPLACE, 'rdc-skills');
|
|
1218
|
-
const cacheDirs = fs.existsSync(cacheBase) ? fs.readdirSync(cacheBase) : [];
|
|
1219
|
-
if (cacheDirs.length === 1 && cacheDirs[0] === 'latest') {
|
|
1220
|
-
ok(`plugin cache : 1 dir (latest/)`);
|
|
1221
|
-
} else {
|
|
1222
|
-
fail(`plugin cache : expected exactly [latest/], found [${cacheDirs.join(', ')}]`);
|
|
1223
|
-
verifyFailed = true;
|
|
1224
|
-
}
|
|
1225
|
-
const ipPath = path.join(claudeHome, 'plugins', 'installed_plugins.json');
|
|
1226
|
-
const installed = readJson(ipPath, { plugins: {} });
|
|
1227
|
-
const rdcEntries = installed.plugins[PLUGIN_KEY] || [];
|
|
1228
|
-
if (rdcEntries.length === 1) {
|
|
1229
|
-
ok(`installed_plugins: 1 entry for ${PLUGIN_KEY}`);
|
|
1230
|
-
} else {
|
|
1231
|
-
fail(`installed_plugins: expected 1 entry, found ${rdcEntries.length}`);
|
|
1232
|
-
verifyFailed = true;
|
|
1233
|
-
}
|
|
1234
|
-
const userSkillsDir = path.join(claudeHome, 'skills', 'user');
|
|
1235
|
-
const stillThere = fs.existsSync(userSkillsDir)
|
|
1236
|
-
? fs.readdirSync(userSkillsDir).filter(f => {
|
|
1237
|
-
const p = path.join(userSkillsDir, f);
|
|
1238
|
-
const skillFile = fs.statSync(p).isDirectory()
|
|
1239
|
-
? ['SKILL.md','skill.md'].map(s => path.join(p, s)).find(fs.existsSync)
|
|
1240
|
-
: (f.endsWith('.md') ? p : null);
|
|
1241
|
-
if (!skillFile) return false;
|
|
1242
|
-
const fm = readFrontmatter(skillFile);
|
|
1243
|
-
return fm.name && fm.name.startsWith('rdc:');
|
|
1244
|
-
})
|
|
1245
|
-
: [];
|
|
1246
|
-
if (stillThere.length === 0) {
|
|
1247
|
-
ok(`skills/user/ : no rdc: orphans`);
|
|
1248
|
-
} else {
|
|
1249
|
-
fail(`skills/user/ : still has rdc: orphans: ${stillThere.join(', ')}`);
|
|
1250
|
-
verifyFailed = true;
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
if (verifyFailed) {
|
|
1254
|
-
console.log('');
|
|
1255
|
-
fail('Post-install verification FAILED — duplicates or orphans remain. Investigate.');
|
|
1256
|
-
process.exit(2);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// 7. MCP server registration (non-fatal — WARNs only, never aborts)
|
|
1260
|
-
console.log('');
|
|
1261
|
-
console.log(' \x1b[36mMCP server:\x1b[0m');
|
|
1262
|
-
try { registerMcpServer(); } catch (e) { warn(`[7/7] MCP server — unexpected error (${e.message})`); }
|
|
1263
|
-
|
|
1264
|
-
// Done
|
|
1265
|
-
console.log('');
|
|
1266
|
-
console.log(' \x1b[32mDone!\x1b[0m');
|
|
1267
|
-
console.log('');
|
|
1268
|
-
console.log(' \x1b[33mNext steps:\x1b[0m');
|
|
1269
|
-
console.log(' CLI : restart Claude Code — /rdc:status to verify');
|
|
1270
|
-
console.log(' Cowork : restart Claude Desktop — /rdc:status in a new Cowork session');
|
|
1271
|
-
console.log(' claude.ai : no plugin upload needed for MCP — use the connector URL above');
|
|
1272
|
-
console.log(' dist/rdc-skills-plugin-v' + version + '.zip available if needed');
|
|
1273
|
-
console.log('');
|
|
1274
|
-
console.log(` Profile: ${installProfile}`);
|
|
1275
|
-
if (installProfile === 'core') {
|
|
1276
|
-
console.log(' Core hooks are portable: RDC output contract + foreground process + commit-message hygiene.');
|
|
1277
|
-
console.log(' Project services are not provisioned. Configure work items, credentials, deploys, and project guides before using infrastructure-heavy skills.');
|
|
1278
|
-
} else {
|
|
1279
|
-
console.log(' LIFEAI hooks are active: regen-root cwd lock, Supabase work-item gates, clauth-aware checks, and overnight queue guard.');
|
|
1280
|
-
}
|
|
1281
|
-
console.log(' Startup blocks: run with --project-root <path> --write-startup-blocks to add managed CLAUDE.md/AGENTS.md sections.');
|
|
1282
|
-
|
|
1283
|
-
listCommands();
|
|
1284
|
-
|
|
1285
|
-
console.log(' Docs: https://github.com/LIFEAI/rdc-skills#readme');
|
|
1286
|
-
console.log('');
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
main().catch(e => { fail(e.message); process.exit(1); });
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* install-rdc-skills — registers rdc-skills on every Claude surface.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node scripts/install-rdc-skills.js ← standard
|
|
7
|
+
* node scripts/install-rdc-skills.js --skip-hooks ← skip hook wiring
|
|
8
|
+
* node scripts/install-rdc-skills.js --profile core ← clean-box portable hooks
|
|
9
|
+
* node scripts/install-rdc-skills.js --profile lifeai ← LIFEAI/regen-root hooks
|
|
10
|
+
* node scripts/install-rdc-skills.js --claude-home <path> ← custom CLI home
|
|
11
|
+
* node scripts/install-rdc-skills.js --codex-root <path> ← also install to .agents/skills/user/
|
|
12
|
+
* node scripts/install-rdc-skills.js --codex-skill-dir <path> ← also install to a Codex skill dir
|
|
13
|
+
* node scripts/install-rdc-skills.js --project-root <path> --write-startup-blocks
|
|
14
|
+
* node scripts/install-rdc-skills.js --migrate <path> ← migrate docs/ → .rdc/
|
|
15
|
+
*
|
|
16
|
+
* What it does:
|
|
17
|
+
* 1. git pull (latest commands + guides)
|
|
18
|
+
* 2. CLI plugin — registers in ~/.claude/plugins/ + settings.json
|
|
19
|
+
* 3. Cowork — registers in Desktop cowork_plugins/ + cowork_settings.json
|
|
20
|
+
* 3.5 Codex — copies skills to detected Codex skill dirs
|
|
21
|
+
* 4. Hook files — copies hooks/*.js → ~/.claude/hooks/
|
|
22
|
+
* 5. Hook wiring — wires hooks into ~/.claude/settings.json
|
|
23
|
+
* 6. Zip — builds dist/rdc-skills-plugin.zip for claude.ai / distribution
|
|
24
|
+
* 7. Preflight — Node version, clauth daemon
|
|
25
|
+
* 8. Commands — lists all /rdc:* commands
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const readline = require('readline');
|
|
33
|
+
const { execSync } = require('child_process');
|
|
34
|
+
|
|
35
|
+
// ── Args ──────────────────────────────────────────────────────────────────────
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
const skipHooks = args.includes('--skip-hooks');
|
|
38
|
+
const profileIdx = args.indexOf('--profile');
|
|
39
|
+
const profileArg = profileIdx >= 0 ? String(args[profileIdx + 1] || '').toLowerCase() : 'auto';
|
|
40
|
+
const homeIdx = args.indexOf('--claude-home');
|
|
41
|
+
const claudeHome = homeIdx >= 0 ? args[homeIdx + 1] : path.join(os.homedir(), '.claude');
|
|
42
|
+
const migrateIdx = args.indexOf('--migrate');
|
|
43
|
+
const doMigrate = migrateIdx >= 0;
|
|
44
|
+
const migratePath = doMigrate ? (args[migrateIdx + 1] || process.cwd()) : null;
|
|
45
|
+
const projectIdx = args.indexOf('--project-root');
|
|
46
|
+
const projectRootArg = projectIdx >= 0 ? path.resolve(args[projectIdx + 1]) : null;
|
|
47
|
+
const shouldWriteStartupBlocks = args.includes('--write-startup-blocks');
|
|
48
|
+
|
|
49
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
50
|
+
|
|
51
|
+
const codexIdx = args.indexOf('--codex-root');
|
|
52
|
+
const codexRoot = codexIdx >= 0
|
|
53
|
+
? path.resolve(args[codexIdx + 1])
|
|
54
|
+
: (() => {
|
|
55
|
+
// Auto-detect the consuming project's .agents tree so a plain `install`
|
|
56
|
+
// refreshes Codex without needing --codex-root. Check, in order: an
|
|
57
|
+
// explicit --project-root, the current working directory, then the
|
|
58
|
+
// regen-root sibling of this repo. First one with a `.agents` wins.
|
|
59
|
+
const candidates = [
|
|
60
|
+
projectRootArg ? path.resolve(projectRootArg) : null,
|
|
61
|
+
process.cwd(),
|
|
62
|
+
path.resolve(repoRoot, '..', 'regen-root'),
|
|
63
|
+
].filter(Boolean);
|
|
64
|
+
for (const c of candidates) {
|
|
65
|
+
if (fs.existsSync(path.join(c, '.agents'))) return c;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
})();
|
|
69
|
+
const codexSkillDirIdx = args.indexOf('--codex-skill-dir');
|
|
70
|
+
const explicitCodexSkillDir = codexSkillDirIdx >= 0
|
|
71
|
+
? path.resolve(args[codexSkillDirIdx + 1])
|
|
72
|
+
: null;
|
|
73
|
+
const hooksSrc = path.join(repoRoot, 'hooks');
|
|
74
|
+
const hooksDst = path.join(claudeHome, 'hooks');
|
|
75
|
+
const settingsPath = path.join(claudeHome, 'settings.json');
|
|
76
|
+
const detectedLifeaiRoot = (() => {
|
|
77
|
+
const sibling = path.resolve(repoRoot, '..', 'regen-root');
|
|
78
|
+
return fs.existsSync(path.join(sibling, 'CLAUDE.md')) && fs.existsSync(path.join(sibling, '.rdc')) ? sibling : null;
|
|
79
|
+
})();
|
|
80
|
+
const installProfile = (() => {
|
|
81
|
+
if (profileArg === 'core' || profileArg === 'lifeai') return profileArg;
|
|
82
|
+
if (profileArg !== 'auto') {
|
|
83
|
+
console.log(` \x1b[33m⚠\x1b[0m Unknown --profile "${profileArg}" — using auto`);
|
|
84
|
+
}
|
|
85
|
+
return detectedLifeaiRoot ? 'lifeai' : 'core';
|
|
86
|
+
})();
|
|
87
|
+
const projectRoot = projectRootArg || codexRoot || detectedLifeaiRoot;
|
|
88
|
+
|
|
89
|
+
const PLUGIN_KEY = 'rdc-skills@rdc-skills';
|
|
90
|
+
const MARKETPLACE = 'rdc-skills';
|
|
91
|
+
const NPM_PACKAGE = '@lifeaitools/rdc-skills';
|
|
92
|
+
const MCP_NAME = 'rdc-skills-mcp';
|
|
93
|
+
const MCP_PORT = '3110';
|
|
94
|
+
|
|
95
|
+
// ── Logging ───────────────────────────────────────────────────────────────────
|
|
96
|
+
const ok = msg => console.log(` \x1b[32m✓\x1b[0m ${msg}`);
|
|
97
|
+
const info = msg => console.log(` \x1b[36m→\x1b[0m ${msg}`);
|
|
98
|
+
const warn = msg => console.log(` \x1b[33m⚠\x1b[0m ${msg}`);
|
|
99
|
+
const fail = msg => console.log(` \x1b[31m✗\x1b[0m ${msg}`);
|
|
100
|
+
|
|
101
|
+
function run(cmd, options = {}) {
|
|
102
|
+
return execSync(cmd, { encoding: 'utf8', stdio: 'pipe', ...options }).trim();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateCodexMcpToml(toml, mcpUrl) {
|
|
106
|
+
const blockRe = /(^|\n)(\[mcp_servers\.rdc-skills\]\n)([\s\S]*?)(?=\n\[|\s*$)/;
|
|
107
|
+
const desiredLine = `url = '${mcpUrl}'`;
|
|
108
|
+
if (blockRe.test(toml)) {
|
|
109
|
+
return toml.replace(blockRe, (match, prefix, header, body) => {
|
|
110
|
+
if (/^url\s*=.*$/m.test(body)) {
|
|
111
|
+
return `${prefix}${header}${body.replace(/^url\s*=.*$/m, desiredLine)}`;
|
|
112
|
+
}
|
|
113
|
+
const trimmed = body.replace(/\s*$/, '');
|
|
114
|
+
return `${prefix}${header}${trimmed}${trimmed ? '\n' : ''}${desiredLine}\n`;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return toml.replace(/\s*$/, '\n') + `\n[mcp_servers.rdc-skills]\n${desiredLine}\n`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function selfTestCodexMcpToml() {
|
|
121
|
+
const url = 'https://rdc-skills.regendevcorp.com/mcp';
|
|
122
|
+
const stale = "[mcp_servers.clauth]\nurl = 'https://clauth.regendevcorp.com/mcp'\n\n[mcp_servers.rdc-skills]\nurl = 'https://rdc-skills.dev.regendevcorp.com/mcp'\n\n[mcp_servers.web-research]\nurl = 'https://research.regendevcorp.com/mcp'\n";
|
|
123
|
+
const updated = updateCodexMcpToml(stale, url);
|
|
124
|
+
if (!updated.includes(`url = '${url}'`)) throw new Error('did not write production rdc-skills URL');
|
|
125
|
+
if (updated.includes('rdc-skills.dev.regendevcorp.com')) throw new Error('stale dev URL survived');
|
|
126
|
+
if (!updated.includes('[mcp_servers.clauth]') || !updated.includes('[mcp_servers.web-research]')) throw new Error('neighbor MCP blocks were damaged');
|
|
127
|
+
|
|
128
|
+
const missing = updateCodexMcpToml("[mcp_servers.clauth]\nurl = 'https://clauth.regendevcorp.com/mcp'\n", url);
|
|
129
|
+
if (!missing.includes('[mcp_servers.rdc-skills]')) throw new Error('missing block was not appended');
|
|
130
|
+
|
|
131
|
+
const noUrl = updateCodexMcpToml('[mcp_servers.rdc-skills]\nstartup_timeout_sec = 30\n', url);
|
|
132
|
+
if (!noUrl.includes("startup_timeout_sec = 30") || !noUrl.includes(`url = '${url}'`)) throw new Error('url-less block was not repaired');
|
|
133
|
+
|
|
134
|
+
console.log('install-rdc-skills codex MCP TOML self-test — PASS');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (args.includes('--self-test-codex-mcp-toml')) {
|
|
138
|
+
try {
|
|
139
|
+
selfTestCodexMcpToml();
|
|
140
|
+
process.exit(0);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
console.error(e.message || e);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Filesystem helpers ────────────────────────────────────────────────────────
|
|
148
|
+
function copyDir(src, dst, ext) {
|
|
149
|
+
if (!fs.existsSync(src)) { warn(`Source not found: ${src}`); return 0; }
|
|
150
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
151
|
+
const files = fs.readdirSync(src).filter(f => !ext || f.endsWith(ext));
|
|
152
|
+
let count = 0;
|
|
153
|
+
for (const f of files) {
|
|
154
|
+
const s = path.join(src, f);
|
|
155
|
+
if (fs.statSync(s).isFile()) { fs.copyFileSync(s, path.join(dst, f)); count++; }
|
|
156
|
+
}
|
|
157
|
+
return count;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function copyHookFiles(src, dst) {
|
|
161
|
+
if (!fs.existsSync(src)) { warn(`Source not found: ${src}`); return 0; }
|
|
162
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
163
|
+
const files = fs.readdirSync(src).filter(f => /\.(?:js|ps1)$/i.test(f));
|
|
164
|
+
let count = 0;
|
|
165
|
+
for (const f of files) {
|
|
166
|
+
const s = path.join(src, f);
|
|
167
|
+
if (fs.statSync(s).isFile()) { fs.copyFileSync(s, path.join(dst, f)); count++; }
|
|
168
|
+
}
|
|
169
|
+
return count;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function copyDirRecursive(src, dst) {
|
|
173
|
+
if (!fs.existsSync(src)) return;
|
|
174
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
175
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
176
|
+
const s = path.join(src, entry.name);
|
|
177
|
+
const d = path.join(dst, entry.name);
|
|
178
|
+
if (entry.isDirectory()) copyDirRecursive(s, d);
|
|
179
|
+
else {
|
|
180
|
+
// Atomic write (temp + rename) so a live Claude Code / Codex session never
|
|
181
|
+
// reads a half-written skill file when the installer runs over a live box.
|
|
182
|
+
const tmp = `${d}.tmp-${process.pid}`;
|
|
183
|
+
fs.copyFileSync(s, tmp);
|
|
184
|
+
fs.renameSync(tmp, d);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function copyMissingProjectGuides(projectRoot) {
|
|
190
|
+
if (!projectRoot) return 0;
|
|
191
|
+
const src = path.join(repoRoot, 'guides');
|
|
192
|
+
const dst = path.join(projectRoot, '.rdc', 'guides');
|
|
193
|
+
if (!fs.existsSync(src) || !fs.existsSync(dst)) return 0;
|
|
194
|
+
let copied = 0;
|
|
195
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
196
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
197
|
+
if (entry.name === 'rdc-skills-startup.md') continue; // installed only by --write-startup-blocks
|
|
198
|
+
const target = path.join(dst, entry.name);
|
|
199
|
+
if (fs.existsSync(target)) continue;
|
|
200
|
+
fs.copyFileSync(path.join(src, entry.name), target);
|
|
201
|
+
copied++;
|
|
202
|
+
}
|
|
203
|
+
return copied;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function readJson(p, fallback = {}) {
|
|
207
|
+
if (!fs.existsSync(p)) return fallback;
|
|
208
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function writeJson(p, data, indent = 2) {
|
|
212
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
213
|
+
fs.writeFileSync(p, JSON.stringify(data, null, indent));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function upsertManagedBlock(filePath, title, body) {
|
|
217
|
+
const begin = `<!-- BEGIN RDC-SKILLS:${title} -->`;
|
|
218
|
+
const end = `<!-- END RDC-SKILLS:${title} -->`;
|
|
219
|
+
const block = `${begin}\n${body.trim()}\n${end}\n`;
|
|
220
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
221
|
+
const re = new RegExp(`${begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'm');
|
|
222
|
+
const next = re.test(current)
|
|
223
|
+
? current.replace(re, block)
|
|
224
|
+
: `${current.replace(/\s*$/, '')}${current.trim() ? '\n\n' : ''}${block}`;
|
|
225
|
+
if (next !== current) {
|
|
226
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
227
|
+
fs.writeFileSync(filePath, next);
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function writeStartupBlocks(targetRoot, profile) {
|
|
234
|
+
if (!targetRoot) return { wrote: 0, skipped: 'no-project-root' };
|
|
235
|
+
const startupSrc = path.join(repoRoot, 'guides', 'rdc-skills-startup.md');
|
|
236
|
+
const guidesDir = path.join(targetRoot, '.rdc', 'guides');
|
|
237
|
+
const startupDst = path.join(guidesDir, 'rdc-skills-startup.md');
|
|
238
|
+
fs.mkdirSync(guidesDir, { recursive: true });
|
|
239
|
+
fs.copyFileSync(startupSrc, startupDst);
|
|
240
|
+
|
|
241
|
+
let wrote = 1;
|
|
242
|
+
const claudeBody = [
|
|
243
|
+
'## RDC Skills',
|
|
244
|
+
'',
|
|
245
|
+
'@.rdc/guides/rdc-skills-startup.md',
|
|
246
|
+
'',
|
|
247
|
+
`Installed profile: \`${profile}\`.`,
|
|
248
|
+
'For `/rdc:*` work, follow `.rdc/guides/output-contract.md` and `.rdc/guides/engineering-behavior.md`.',
|
|
249
|
+
].join('\n');
|
|
250
|
+
const agentsBody = [
|
|
251
|
+
'## RDC Skills',
|
|
252
|
+
'',
|
|
253
|
+
'Read `.rdc/guides/rdc-skills-startup.md` before using any `rdc:*` workflow.',
|
|
254
|
+
`Installed profile: \`${profile}\`.`,
|
|
255
|
+
'For `/rdc:*` work, follow `.rdc/guides/output-contract.md` and `.rdc/guides/engineering-behavior.md`.',
|
|
256
|
+
].join('\n');
|
|
257
|
+
|
|
258
|
+
if (upsertManagedBlock(path.join(targetRoot, 'CLAUDE.md'), 'STARTUP', claudeBody)) wrote++;
|
|
259
|
+
if (upsertManagedBlock(path.join(targetRoot, 'AGENTS.md'), 'STARTUP', agentsBody)) wrote++;
|
|
260
|
+
return { wrote, skipped: null };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Frontmatter parser ────────────────────────────────────────────────────────
|
|
264
|
+
function readFrontmatter(filePath) {
|
|
265
|
+
try {
|
|
266
|
+
const content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
267
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
268
|
+
if (!match) return {};
|
|
269
|
+
const fm = {};
|
|
270
|
+
let key = null, multiline = false, multilineVal = '';
|
|
271
|
+
for (const line of match[1].split('\n')) {
|
|
272
|
+
if (multiline) {
|
|
273
|
+
if (/^\s+/.test(line)) { multilineVal += ' ' + line.trim(); continue; }
|
|
274
|
+
fm[key] = multilineVal.trim();
|
|
275
|
+
multiline = false;
|
|
276
|
+
}
|
|
277
|
+
const kv = line.match(/^(\w+):\s*(>-|>)?\s*(.*)?$/);
|
|
278
|
+
if (!kv) continue;
|
|
279
|
+
key = kv[1];
|
|
280
|
+
if (kv[2]) { multiline = true; multilineVal = kv[3] || ''; }
|
|
281
|
+
else fm[key] = kv[3] || '';
|
|
282
|
+
}
|
|
283
|
+
if (multiline && key) fm[key] = multilineVal.trim();
|
|
284
|
+
return fm;
|
|
285
|
+
} catch { return {}; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Plugin cache builder (shared between CLI + Cowork) ────────────────────────
|
|
289
|
+
function buildPluginCache(cacheDir, version, gitSha) {
|
|
290
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
291
|
+
for (const item of ['.claude-plugin', 'commands', 'skills', 'guides', 'hooks', 'package.json', 'README.md']) {
|
|
292
|
+
const src = path.join(repoRoot, item);
|
|
293
|
+
if (!fs.existsSync(src)) continue;
|
|
294
|
+
const dst = path.join(cacheDir, item);
|
|
295
|
+
if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dst);
|
|
296
|
+
else fs.copyFileSync(src, dst);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function getPm2Process(name) {
|
|
301
|
+
try {
|
|
302
|
+
const list = JSON.parse(run('pm2 jlist'));
|
|
303
|
+
return Array.isArray(list) ? list.find((p) => p.name === name) || null : null;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function isGlobalNpmRdcSkillsPath(scriptPath) {
|
|
310
|
+
if (!scriptPath) return false;
|
|
311
|
+
const normalized = scriptPath.replace(/\\/g, '/').toLowerCase();
|
|
312
|
+
return normalized.includes('/node_modules/@lifeaitools/rdc-skills/');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function packageRootFromMcpScript(scriptPath) {
|
|
316
|
+
return scriptPath ? path.resolve(path.dirname(scriptPath), '..') : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function readInstalledPackageVersion(scriptPath) {
|
|
320
|
+
const packageRoot = packageRootFromMcpScript(scriptPath);
|
|
321
|
+
if (!packageRoot) return null;
|
|
322
|
+
return readJson(path.join(packageRoot, 'package.json'), {}).version || null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function syncGlobalMcpInstall(version) {
|
|
326
|
+
const proc = getPm2Process(MCP_NAME);
|
|
327
|
+
if (!proc) {
|
|
328
|
+
info('[2.9] MCP pkg — PM2 process not registered yet; start handled below');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const scriptPath = proc.pm2_env?.pm_exec_path || proc.pm_exec_path || '';
|
|
333
|
+
if (!isGlobalNpmRdcSkillsPath(scriptPath)) {
|
|
334
|
+
info(`[2.9] MCP pkg — PM2 uses source checkout, not global npm (${scriptPath || 'unknown path'})`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const installedVersion = readInstalledPackageVersion(scriptPath);
|
|
339
|
+
if (installedVersion === version) {
|
|
340
|
+
ok(`[2.9] MCP pkg — global ${NPM_PACKAGE}@${version} already installed`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let stopped = false;
|
|
345
|
+
let installed = false;
|
|
346
|
+
try {
|
|
347
|
+
info(`[2.9] MCP pkg — updating global ${NPM_PACKAGE} ${installedVersion || '?'} → ${version}`);
|
|
348
|
+
run(`pm2 stop ${MCP_NAME}`);
|
|
349
|
+
stopped = true;
|
|
350
|
+
run(`npm install -g ${NPM_PACKAGE}@${version}`);
|
|
351
|
+
installed = true;
|
|
352
|
+
ok(`[2.9] MCP pkg — installed global ${NPM_PACKAGE}@${version}`);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
warn(`[2.9] MCP pkg — global install failed (${String(e.message || e).split('\n')[0]})`);
|
|
355
|
+
if (String(e.message || '').includes('EBUSY')) {
|
|
356
|
+
info(' PM2 was stopped first; if EBUSY persists, another Node/npm process still holds the package directory.');
|
|
357
|
+
}
|
|
358
|
+
} finally {
|
|
359
|
+
if (stopped) {
|
|
360
|
+
try {
|
|
361
|
+
run(`pm2 restart ${MCP_NAME} --update-env`, { env: { ...process.env, PORT: MCP_PORT } });
|
|
362
|
+
ok(`[2.9] MCP pkg — pm2 restarted ${MCP_NAME}${installed ? '' : ' (previous install restored)'}`);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
warn(`[2.9] MCP pkg — pm2 restart failed (${String(e.message || e).split('\n')[0]})`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── User-skills cleanup ───────────────────────────────────────────────────────
|
|
371
|
+
// Older installer versions wrote skill files directly to ~/.claude/skills/user/.
|
|
372
|
+
// Claude Code loads that directory AND the plugin cache, so any rdc skills left
|
|
373
|
+
// there produce duplicate registrations and break the resolver.
|
|
374
|
+
// This function nukes any entry whose frontmatter name starts with "rdc:".
|
|
375
|
+
// Scans BOTH the immediate dir and nested .md files (e.g. `user/skill.md`,
|
|
376
|
+
// `user/rdc-build/SKILL.md`) so pre-plugin orphans are caught regardless of
|
|
377
|
+
// naming convention.
|
|
378
|
+
function cleanUserSkills(userSkillsDir) {
|
|
379
|
+
if (!fs.existsSync(userSkillsDir)) return 0;
|
|
380
|
+
let removed = 0;
|
|
381
|
+
for (const entry of fs.readdirSync(userSkillsDir, { withFileTypes: true })) {
|
|
382
|
+
const candidate = path.join(userSkillsDir, entry.name);
|
|
383
|
+
if (entry.isDirectory()) {
|
|
384
|
+
// Subdir form: <name>/SKILL.md or <name>/skill.md
|
|
385
|
+
let skillFile = null;
|
|
386
|
+
for (const sf of ['SKILL.md', 'skill.md']) {
|
|
387
|
+
const p = path.join(candidate, sf);
|
|
388
|
+
if (fs.existsSync(p)) { skillFile = p; break; }
|
|
389
|
+
}
|
|
390
|
+
if (!skillFile) continue;
|
|
391
|
+
const fm = readFrontmatter(skillFile);
|
|
392
|
+
if (fm.name && fm.name.startsWith('rdc:')) {
|
|
393
|
+
try { fs.rmSync(candidate, { recursive: true, force: true }); removed++; } catch {}
|
|
394
|
+
}
|
|
395
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
396
|
+
// ANY .md file at this level — including skill.md / SKILL.md / README.md
|
|
397
|
+
// if their frontmatter declares an rdc:* skill. A previous version skipped
|
|
398
|
+
// those names; that left an orphan rdc:build copy at user/skill.md which
|
|
399
|
+
// registered as a duplicate "user" skill.
|
|
400
|
+
const fm = readFrontmatter(candidate);
|
|
401
|
+
if (fm.name && fm.name.startsWith('rdc:')) {
|
|
402
|
+
try { fs.unlinkSync(candidate); removed++; } catch {}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return removed;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Scrub legacy rdc orphans from ~/.claude/skills/ (top-level), not just user/.
|
|
410
|
+
// Some older installs landed flat skill files alongside the plugin tree.
|
|
411
|
+
function cleanGlobalSkillsRoot(skillsDir) {
|
|
412
|
+
if (!fs.existsSync(skillsDir)) return 0;
|
|
413
|
+
let removed = 0;
|
|
414
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
415
|
+
if (entry.name === 'user') continue; // handled separately
|
|
416
|
+
const candidate = path.join(skillsDir, entry.name);
|
|
417
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
418
|
+
const fm = readFrontmatter(candidate);
|
|
419
|
+
if (fm.name && fm.name.startsWith('rdc:')) {
|
|
420
|
+
try { fs.unlinkSync(candidate); removed++; } catch {}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return removed;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ── Stale hook cleanup ────────────────────────────────────────────────────────
|
|
428
|
+
// Remove ONLY explicitly orphaned hook files — hooks that were previously shipped
|
|
429
|
+
// by rdc-skills and have since been removed from the project.
|
|
430
|
+
// NEVER use "not in source = remove" logic: most hooks in ~/.claude/hooks/ are
|
|
431
|
+
// not managed by rdc-skills (they come from other plugins or were written directly).
|
|
432
|
+
function cleanStaleHooks(hooksDstDir) {
|
|
433
|
+
if (!fs.existsSync(hooksDstDir)) return 0;
|
|
434
|
+
// Explicit orphan list — add entries here when a hook is intentionally removed.
|
|
435
|
+
// Format: filename that should be deleted if it still exists.
|
|
436
|
+
const ORPHANED_HOOKS = [
|
|
437
|
+
'verify-rdc-skills.js', // removed in v0.9.7 — was checking for old flat-file format
|
|
438
|
+
];
|
|
439
|
+
let removed = 0;
|
|
440
|
+
for (const f of ORPHANED_HOOKS) {
|
|
441
|
+
const p = path.join(hooksDstDir, f);
|
|
442
|
+
if (fs.existsSync(p)) {
|
|
443
|
+
try { fs.unlinkSync(p); removed++; } catch {}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return removed;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Project-local hookify docs were a temporary workaround for work item exit
|
|
450
|
+
// enforcement. The plugin hook is now authoritative, so installs clean those
|
|
451
|
+
// local shims from known RDC workspaces instead of leaving two gates to drift.
|
|
452
|
+
function cleanProjectHookifyShims(projectRoot) {
|
|
453
|
+
if (!projectRoot) return 0;
|
|
454
|
+
const hookifyDir = path.join(projectRoot, '.claude');
|
|
455
|
+
if (!fs.existsSync(hookifyDir)) return 0;
|
|
456
|
+
const stale = [
|
|
457
|
+
'hookify.work-item-done-gate-bash.local.md',
|
|
458
|
+
'hookify.work-item-done-gate-mcp.local.md',
|
|
459
|
+
'hookify.work-item-review-gate-bash.local.md',
|
|
460
|
+
'hookify.work-item-review-gate-mcp.local.md',
|
|
461
|
+
];
|
|
462
|
+
let removed = 0;
|
|
463
|
+
for (const f of stale) {
|
|
464
|
+
const p = path.join(hookifyDir, f);
|
|
465
|
+
if (fs.existsSync(p)) {
|
|
466
|
+
try { fs.unlinkSync(p); removed++; } catch {}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return removed;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// ── Cache flush helper ────────────────────────────────────────────────────────
|
|
473
|
+
// Only `latest/` is ever kept. Earlier versions wrote BOTH `<version>/` and
|
|
474
|
+
// `latest/`, which caused the plugin loader to scan and register every rdc:*
|
|
475
|
+
// skill twice (once per directory). The fix is permanent single-dir layout.
|
|
476
|
+
function flushOldCaches(cacheBase /* keepVersion intentionally unused */) {
|
|
477
|
+
if (!fs.existsSync(cacheBase)) return 0;
|
|
478
|
+
let flushed = 0;
|
|
479
|
+
for (const entry of fs.readdirSync(cacheBase)) {
|
|
480
|
+
if (entry === 'latest') continue;
|
|
481
|
+
try {
|
|
482
|
+
fs.rmSync(path.join(cacheBase, entry), { recursive: true, force: true });
|
|
483
|
+
flushed++;
|
|
484
|
+
} catch {}
|
|
485
|
+
}
|
|
486
|
+
return flushed;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── Step 2: CLI plugin registration (→ ~/.claude/plugins/) ───────────────────
|
|
490
|
+
function registerCLI(version, gitSha) {
|
|
491
|
+
const pluginDir = path.join(claudeHome, 'plugins');
|
|
492
|
+
const mktDir = path.join(pluginDir, 'marketplaces', MARKETPLACE);
|
|
493
|
+
const mktPlugDir = path.join(mktDir, '.claude-plugin');
|
|
494
|
+
const cacheBase = path.join(pluginDir, 'cache', MARKETPLACE, 'rdc-skills');
|
|
495
|
+
const latestDir = path.join(cacheBase, 'latest');
|
|
496
|
+
|
|
497
|
+
// 1. Marketplace manifest
|
|
498
|
+
fs.mkdirSync(mktPlugDir, { recursive: true });
|
|
499
|
+
fs.copyFileSync(path.join(repoRoot, '.claude-plugin', 'marketplace.json'), path.join(mktPlugDir, 'marketplace.json'));
|
|
500
|
+
|
|
501
|
+
// 2. known_marketplaces.json
|
|
502
|
+
const kmpPath = path.join(pluginDir, 'known_marketplaces.json');
|
|
503
|
+
const knownMp = readJson(kmpPath);
|
|
504
|
+
knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
|
|
505
|
+
writeJson(kmpPath, knownMp, 4);
|
|
506
|
+
|
|
507
|
+
// 3. Flush every cache dir except `latest/`, then rewrite `latest/`. We
|
|
508
|
+
// intentionally do NOT keep a versioned dir — the plugin loader registers
|
|
509
|
+
// every dir it finds, so two dirs = duplicate skills.
|
|
510
|
+
const flushed = flushOldCaches(cacheBase);
|
|
511
|
+
if (flushed > 0) info(` flushed : ${flushed} stale cache dir(s)`);
|
|
512
|
+
if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
|
|
513
|
+
buildPluginCache(latestDir, version, gitSha);
|
|
514
|
+
|
|
515
|
+
// 4. installed_plugins.json — register 'latest/' as stable installPath so
|
|
516
|
+
// re-installs overwrite in-place rather than creating orphaned version dirs.
|
|
517
|
+
// Open terminals that re-read installed_plugins.json mid-session will pick
|
|
518
|
+
// up the updated path; otherwise a terminal restart is needed.
|
|
519
|
+
const ipPath = path.join(pluginDir, 'installed_plugins.json');
|
|
520
|
+
const installed = readJson(ipPath, { version: 2, plugins: {} });
|
|
521
|
+
// Also overwrite whichever installPath the old entry had, so any open
|
|
522
|
+
// terminal that already loaded that path sees fresh files.
|
|
523
|
+
const oldEntries = installed.plugins[PLUGIN_KEY] || [];
|
|
524
|
+
for (const old of oldEntries) {
|
|
525
|
+
if (old.installPath && fs.existsSync(old.installPath) && old.installPath !== latestDir) {
|
|
526
|
+
try { buildPluginCache(old.installPath, version, gitSha); } catch {}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
for (const key of Object.keys(installed.plugins || {})) {
|
|
530
|
+
if (key.startsWith('rdc-skills@')) delete installed.plugins[key];
|
|
531
|
+
}
|
|
532
|
+
installed.plugins[PLUGIN_KEY] = [{ scope: 'user', installPath: latestDir, version, installedAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), gitCommitSha: gitSha }];
|
|
533
|
+
writeJson(ipPath, installed, 4);
|
|
534
|
+
|
|
535
|
+
// 5. settings.json enabledPlugins
|
|
536
|
+
const settings = readJson(settingsPath);
|
|
537
|
+
if (!settings.enabledPlugins) settings.enabledPlugins = {};
|
|
538
|
+
for (const key of Object.keys(settings.enabledPlugins)) {
|
|
539
|
+
if (key.startsWith('rdc-skills@')) delete settings.enabledPlugins[key];
|
|
540
|
+
}
|
|
541
|
+
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
542
|
+
writeJson(settingsPath, settings);
|
|
543
|
+
|
|
544
|
+
return latestDir;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ── Step 3: Cowork (Claude Desktop) registration ──────────────────────────────
|
|
548
|
+
function findCoworkBases() {
|
|
549
|
+
// Cowork stores per-workspace state at:
|
|
550
|
+
// %LOCALAPPDATA%/Packages/Claude_*/LocalCache/Roaming/Claude/local-agent-mode-sessions/<workspace>/<device>/
|
|
551
|
+
// Each has cowork_settings.json + cowork_plugins/
|
|
552
|
+
const results = [];
|
|
553
|
+
|
|
554
|
+
// Candidate MSIX package roots
|
|
555
|
+
const localAppData = process.env.LOCALAPPDATA || '';
|
|
556
|
+
const pkgsDir = path.join(localAppData, 'Packages');
|
|
557
|
+
if (!fs.existsSync(pkgsDir)) return results;
|
|
558
|
+
|
|
559
|
+
let claudePkg = null;
|
|
560
|
+
for (const dir of fs.readdirSync(pkgsDir)) {
|
|
561
|
+
if (/^Claude_/i.test(dir)) { claudePkg = path.join(pkgsDir, dir); break; }
|
|
562
|
+
}
|
|
563
|
+
if (!claudePkg) return results;
|
|
564
|
+
|
|
565
|
+
const sessionsRoot = path.join(claudePkg, 'LocalCache', 'Roaming', 'Claude', 'local-agent-mode-sessions');
|
|
566
|
+
if (!fs.existsSync(sessionsRoot)) return results;
|
|
567
|
+
|
|
568
|
+
// Walk two levels: <workspace>/<device>/cowork_settings.json
|
|
569
|
+
for (const ws of fs.readdirSync(sessionsRoot)) {
|
|
570
|
+
const wsDir = path.join(sessionsRoot, ws);
|
|
571
|
+
if (!fs.statSync(wsDir).isDirectory()) continue;
|
|
572
|
+
for (const dev of fs.readdirSync(wsDir)) {
|
|
573
|
+
const devDir = path.join(wsDir, dev);
|
|
574
|
+
if (!fs.statSync(devDir).isDirectory()) continue;
|
|
575
|
+
const settingsFile = path.join(devDir, 'cowork_settings.json');
|
|
576
|
+
if (fs.existsSync(settingsFile)) {
|
|
577
|
+
results.push({ dir: devDir, settingsFile });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return results;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function registerCowork(version, gitSha) {
|
|
585
|
+
const bases = findCoworkBases();
|
|
586
|
+
if (bases.length === 0) {
|
|
587
|
+
warn('Cowork — Claude Desktop not found (MSIX package missing)');
|
|
588
|
+
return 0;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
for (const { dir, settingsFile } of bases) {
|
|
592
|
+
const pluginsDir = path.join(dir, 'cowork_plugins');
|
|
593
|
+
const cacheBase = path.join(pluginsDir, 'cache', MARKETPLACE, 'rdc-skills');
|
|
594
|
+
const latestDir = path.join(cacheBase, 'latest');
|
|
595
|
+
const mktDir = path.join(pluginsDir, 'marketplaces', MARKETPLACE);
|
|
596
|
+
const mktPlugDir = path.join(mktDir, '.claude-plugin');
|
|
597
|
+
|
|
598
|
+
// Marketplace manifest
|
|
599
|
+
fs.mkdirSync(mktPlugDir, { recursive: true });
|
|
600
|
+
fs.copyFileSync(path.join(repoRoot, '.claude-plugin', 'marketplace.json'), path.join(mktPlugDir, 'marketplace.json'));
|
|
601
|
+
|
|
602
|
+
// known_marketplaces.json
|
|
603
|
+
const kmpPath = path.join(pluginsDir, 'known_marketplaces.json');
|
|
604
|
+
const knownMp = readJson(kmpPath);
|
|
605
|
+
knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
|
|
606
|
+
writeJson(kmpPath, knownMp, 4);
|
|
607
|
+
|
|
608
|
+
// Flush all non-latest caches and rewrite `latest/` only — single-dir layout
|
|
609
|
+
flushOldCaches(cacheBase);
|
|
610
|
+
if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
|
|
611
|
+
buildPluginCache(latestDir, version, gitSha);
|
|
612
|
+
|
|
613
|
+
// installed_plugins.json — use stable 'latest/' path
|
|
614
|
+
const ipPath = path.join(pluginsDir, 'installed_plugins.json');
|
|
615
|
+
const installed = readJson(ipPath, { version: 2, plugins: {} });
|
|
616
|
+
for (const key of Object.keys(installed.plugins || {})) {
|
|
617
|
+
if (key.startsWith('rdc-skills@')) delete installed.plugins[key];
|
|
618
|
+
}
|
|
619
|
+
installed.plugins[PLUGIN_KEY] = [{ scope: 'user', installPath: latestDir, version, installedAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), gitCommitSha: gitSha }];
|
|
620
|
+
writeJson(ipPath, installed, 4);
|
|
621
|
+
|
|
622
|
+
// cowork_settings.json — enabledPlugins + extraKnownMarketplaces
|
|
623
|
+
const settings = readJson(settingsFile);
|
|
624
|
+
if (!settings.enabledPlugins) settings.enabledPlugins = {};
|
|
625
|
+
for (const key of Object.keys(settings.enabledPlugins)) {
|
|
626
|
+
if (key.startsWith('rdc-skills@')) delete settings.enabledPlugins[key];
|
|
627
|
+
}
|
|
628
|
+
settings.enabledPlugins[PLUGIN_KEY] = true;
|
|
629
|
+
if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
|
|
630
|
+
settings.extraKnownMarketplaces[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' } };
|
|
631
|
+
writeJson(settingsFile, settings);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return bases.length;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── Codex registration (→ Codex skill dirs/rdc-*/) ───────────────────────────
|
|
638
|
+
function addCodexTarget(targets, label, targetDir) {
|
|
639
|
+
if (!targetDir) return;
|
|
640
|
+
const resolved = path.resolve(targetDir);
|
|
641
|
+
if (targets.some(t => t.targetDir.toLowerCase() === resolved.toLowerCase())) return;
|
|
642
|
+
targets.push({ label, targetDir: resolved });
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Register the rdc-skills MCP endpoint at the USER/GLOBAL level for every client
|
|
646
|
+
// so all Claude Code projects and all Codex sessions can reach the skills via MCP
|
|
647
|
+
// (not just where a project .mcp.json exists). Idempotent + non-fatal. Mirrors the
|
|
648
|
+
// existing clauth/codeflow entries exactly. claude.ai web still needs the one-time
|
|
649
|
+
// connector add in its UI (no programmatic API).
|
|
650
|
+
function registerMcpEndpoints() {
|
|
651
|
+
const MCP_URL = 'https://rdc-skills.regendevcorp.com/mcp';
|
|
652
|
+
const out = [];
|
|
653
|
+
|
|
654
|
+
// Claude Code — user-level ~/.claude.json mcpServers (covers EVERY project).
|
|
655
|
+
try {
|
|
656
|
+
const claudeJson = path.join(os.homedir(), '.claude.json');
|
|
657
|
+
if (fs.existsSync(claudeJson)) {
|
|
658
|
+
const data = readJson(claudeJson);
|
|
659
|
+
if (!data.mcpServers || typeof data.mcpServers !== 'object') data.mcpServers = {};
|
|
660
|
+
const cur = data.mcpServers['rdc-skills'];
|
|
661
|
+
if (!cur || cur.url !== MCP_URL) {
|
|
662
|
+
data.mcpServers['rdc-skills'] = { type: 'http', url: MCP_URL };
|
|
663
|
+
writeJson(claudeJson, data, 2);
|
|
664
|
+
out.push('claude(~/.claude.json)');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} catch (e) { out.push(`claude WARN:${e.message}`); }
|
|
668
|
+
|
|
669
|
+
// Codex — ensure [mcp_servers.rdc-skills] exists and points at the live shared endpoint.
|
|
670
|
+
try {
|
|
671
|
+
const codexToml = path.join(os.homedir(), '.codex', 'config.toml');
|
|
672
|
+
if (fs.existsSync(codexToml)) {
|
|
673
|
+
const toml = fs.readFileSync(codexToml, 'utf8');
|
|
674
|
+
const next = updateCodexMcpToml(toml, MCP_URL);
|
|
675
|
+
if (next !== toml) {
|
|
676
|
+
fs.writeFileSync(codexToml, next);
|
|
677
|
+
out.push('codex(~/.codex/config.toml)');
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} catch (e) { out.push(`codex WARN:${e.message}`); }
|
|
681
|
+
|
|
682
|
+
return out;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function findCodexTargets() {
|
|
686
|
+
const targets = [];
|
|
687
|
+
if (codexRoot) {
|
|
688
|
+
addCodexTarget(targets, 'project .agents', path.join(codexRoot, '.agents', 'skills', 'user'));
|
|
689
|
+
}
|
|
690
|
+
if (explicitCodexSkillDir) {
|
|
691
|
+
addCodexTarget(targets, 'explicit', explicitCodexSkillDir);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const codexHomeSkills = path.join(os.homedir(), '.codex', 'skills');
|
|
695
|
+
if (fs.existsSync(codexHomeSkills)) {
|
|
696
|
+
addCodexTarget(targets, 'global .codex', codexHomeSkills);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const globalAgentSkills = path.join(os.homedir(), '.agents', 'skills');
|
|
700
|
+
if (fs.existsSync(globalAgentSkills)) {
|
|
701
|
+
addCodexTarget(targets, 'global .agents', globalAgentSkills);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return targets;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function registerCodexTarget(targetDir) {
|
|
708
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
709
|
+
|
|
710
|
+
// Clean: remove dirs that are rdc skills — by prefix OR by frontmatter name
|
|
711
|
+
let removed = 0;
|
|
712
|
+
for (const entry of fs.readdirSync(targetDir, { withFileTypes: true })) {
|
|
713
|
+
if (!entry.isDirectory()) continue;
|
|
714
|
+
const candidate = path.join(targetDir, entry.name);
|
|
715
|
+
if (/^rdc-/.test(entry.name)) {
|
|
716
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
717
|
+
removed++;
|
|
718
|
+
} else {
|
|
719
|
+
const fm = readFrontmatter(path.join(candidate, 'SKILL.md'));
|
|
720
|
+
if (fm.name && fm.name.startsWith('rdc:')) {
|
|
721
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
722
|
+
removed++;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Copy: each source skill dir that has a SKILL.md → rdc-<name>/
|
|
728
|
+
const skillsSrc = path.join(repoRoot, 'skills');
|
|
729
|
+
let copied = 0;
|
|
730
|
+
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
731
|
+
if (!entry.isDirectory()) continue;
|
|
732
|
+
const skillFile = path.join(skillsSrc, entry.name, 'SKILL.md');
|
|
733
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
734
|
+
const dst = path.join(targetDir, `rdc-${entry.name}`);
|
|
735
|
+
copyDirRecursive(path.join(skillsSrc, entry.name), dst);
|
|
736
|
+
copied++;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return { removed, copied };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// ── Step 6: Zip for claude.ai / distribution ─────────────────────────────────
|
|
743
|
+
function buildZip(version) {
|
|
744
|
+
const distDir = path.join(repoRoot, 'dist');
|
|
745
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
746
|
+
const zipPath = path.join(distDir, `rdc-skills-plugin-v${version}.zip`);
|
|
747
|
+
|
|
748
|
+
// Remove old zips
|
|
749
|
+
if (fs.existsSync(distDir)) {
|
|
750
|
+
for (const f of fs.readdirSync(distDir)) {
|
|
751
|
+
if (f.startsWith('rdc-skills-plugin') && f.endsWith('.zip')) {
|
|
752
|
+
fs.unlinkSync(path.join(distDir, f));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Use PowerShell Compress-Archive on Windows, zip on Unix
|
|
758
|
+
const items = ['.claude-plugin', 'commands', 'skills', 'guides', 'hooks', 'package.json', 'README.md']
|
|
759
|
+
.filter(i => fs.existsSync(path.join(repoRoot, i)));
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
if (process.platform === 'win32') {
|
|
763
|
+
// Stage to a temp dir so we get a clean zip root
|
|
764
|
+
const tmp = path.join(os.tmpdir(), `rdc-skills-zip-${Date.now()}`);
|
|
765
|
+
fs.mkdirSync(tmp, { recursive: true });
|
|
766
|
+
for (const item of items) {
|
|
767
|
+
const src = path.join(repoRoot, item);
|
|
768
|
+
const dst = path.join(tmp, item);
|
|
769
|
+
if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dst);
|
|
770
|
+
else fs.copyFileSync(src, dst);
|
|
771
|
+
}
|
|
772
|
+
// Try pwsh first (PowerShell 7+), fall back to Windows PowerShell
|
|
773
|
+
const psExe = fs.existsSync('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
|
|
774
|
+
? 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
|
|
775
|
+
: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
776
|
+
execSync(
|
|
777
|
+
`"${psExe}" -NoProfile -NonInteractive -WindowStyle Hidden -Command "Compress-Archive -Path '${tmp}\\*' -DestinationPath '${zipPath}' -Force"`,
|
|
778
|
+
{ stdio: 'pipe' }
|
|
779
|
+
);
|
|
780
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
781
|
+
} else {
|
|
782
|
+
const itemList = items.join(' ');
|
|
783
|
+
execSync(`zip -r "${zipPath}" ${itemList}`, { cwd: repoRoot, stdio: 'pipe' });
|
|
784
|
+
}
|
|
785
|
+
return zipPath;
|
|
786
|
+
} catch (e) {
|
|
787
|
+
warn(`Zip failed: ${e.message}`);
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ── Hook config ───────────────────────────────────────────────────────────────
|
|
793
|
+
function buildHooksConfig(hooksDir, profile = 'core') {
|
|
794
|
+
const base = hooksDir.replace(/\\/g, '/');
|
|
795
|
+
const cmd = (file, msg) => {
|
|
796
|
+
const command = process.platform === 'win32'
|
|
797
|
+
? `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${base}/run-hidden-hook.ps1" "${base}/${file}"`
|
|
798
|
+
: `node "${base}/${file}"`;
|
|
799
|
+
const entry = { type: 'command', command };
|
|
800
|
+
if (msg) entry.statusMessage = msg;
|
|
801
|
+
return entry;
|
|
802
|
+
};
|
|
803
|
+
const config = {
|
|
804
|
+
UserPromptExpansion: [{ hooks: [
|
|
805
|
+
cmd('rdc-invocation-marker.js', 'Marking RDC slash command...'),
|
|
806
|
+
]}],
|
|
807
|
+
UserPromptSubmit: [{ hooks: [
|
|
808
|
+
cmd('rdc-invocation-marker.js', 'Marking RDC prompt...'),
|
|
809
|
+
]}],
|
|
810
|
+
PreToolUse: [
|
|
811
|
+
{ hooks: [
|
|
812
|
+
cmd('foreground-process-gate.js', 'Checking foreground process policy...'),
|
|
813
|
+
]},
|
|
814
|
+
{ matcher: 'Bash', hooks: [
|
|
815
|
+
cmd('require-work-item-on-commit.js'),
|
|
816
|
+
]},
|
|
817
|
+
],
|
|
818
|
+
PostToolUse: [
|
|
819
|
+
{ hooks: [
|
|
820
|
+
cmd('check-services.js'),
|
|
821
|
+
]},
|
|
822
|
+
{ matcher: 'Bash', hooks: [
|
|
823
|
+
cmd('require-work-item-on-commit.js', 'Capturing commit SHA for work item...'),
|
|
824
|
+
]},
|
|
825
|
+
],
|
|
826
|
+
Stop: [{ hooks: [
|
|
827
|
+
cmd('rdc-output-contract-gate.js', 'Checking RDC output contract...'),
|
|
828
|
+
cmd('post-work-check.js', 'Checking for undocumented work...'),
|
|
829
|
+
]}],
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
if (profile === 'lifeai') {
|
|
833
|
+
config.SessionStart = [{ hooks: [
|
|
834
|
+
cmd('check-rdc-environment.js', 'Checking RDC skills runtime...'),
|
|
835
|
+
cmd('check-cwd.js'),
|
|
836
|
+
cmd('check-stale-work-items.js', 'Checking for stale work items...'),
|
|
837
|
+
// Truth Gate 3.0 Layer 6 — gate watchdog (ADVISORY; SessionStart cannot block).
|
|
838
|
+
cmd('gate-watchdog-selfcheck.js', 'Truth Gate watchdog: verifying gate registration...'),
|
|
839
|
+
]}];
|
|
840
|
+
config.PreToolUse[0].hooks.push(
|
|
841
|
+
cmd('work-item-exit-gate.js', 'Checking work item exit gates...'),
|
|
842
|
+
);
|
|
843
|
+
// Truth Gate 3.0 Layer 5 — harness completion gates. Both blocking hooks are
|
|
844
|
+
// FLAG-GATED, default OFF (no-op until the env/DB flag is flipped at deploy),
|
|
845
|
+
// so registering them does NOT disrupt the in-flight build session.
|
|
846
|
+
config.TaskCompleted = [{ hooks: [
|
|
847
|
+
cmd('task-completed-gate.js', 'Truth Gate: verifying task closure...'),
|
|
848
|
+
]}];
|
|
849
|
+
config.PostToolBatch = [{ hooks: [
|
|
850
|
+
cmd('post-tool-batch-gate.js', 'Truth Gate: checking build-wave worktree bases...'),
|
|
851
|
+
]}];
|
|
852
|
+
config.PreCompact = [{ hooks: [
|
|
853
|
+
cmd('precompact-log.js'),
|
|
854
|
+
]}];
|
|
855
|
+
config.PostCompact = [{ hooks: [
|
|
856
|
+
cmd('postcompact-log.js'),
|
|
857
|
+
cmd('restart-brief.js', 'Writing restart brief...'),
|
|
858
|
+
]}];
|
|
859
|
+
config.Stop[0].hooks.unshift(
|
|
860
|
+
cmd('rate-limit-retry.js', 'Checking for rate limits...'),
|
|
861
|
+
);
|
|
862
|
+
config.Stop[0].hooks.push(
|
|
863
|
+
cmd('no-stop-open-epics.js', 'Checking for open epics...'),
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return config;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// ── MCP server registration (non-fatal) ───────────────────────────────────────
|
|
871
|
+
// Ensures the rdc-skills MCP deps are installed, registers/starts the local MCP
|
|
872
|
+
// under PM2 as `rdc-skills-mcp` on PORT=3110, and prints the claude.ai connector
|
|
873
|
+
// line. Every failure here WARNs — it must never abort the installer.
|
|
874
|
+
function registerMcpServer() {
|
|
875
|
+
const binPath = path.join(repoRoot, 'bin', 'rdc-skills-mcp.mjs');
|
|
876
|
+
const connector = 'https://rdc-skills.regendevcorp.com/mcp';
|
|
877
|
+
|
|
878
|
+
try {
|
|
879
|
+
// (a) ensure MCP runtime deps are present (express, @modelcontextprotocol/sdk, yaml, zod)
|
|
880
|
+
const needDeps = ['express', '@modelcontextprotocol/sdk', 'yaml', 'zod'].some((d) => {
|
|
881
|
+
try { require.resolve(d, { paths: [repoRoot] }); return false; } catch { return true; }
|
|
882
|
+
});
|
|
883
|
+
if (needDeps) {
|
|
884
|
+
try {
|
|
885
|
+
execSync('npm install --no-audit --no-fund', { cwd: repoRoot, stdio: 'pipe' });
|
|
886
|
+
ok('[7/7] MCP deps — installed');
|
|
887
|
+
} catch (e) {
|
|
888
|
+
warn(`[7/7] MCP deps — npm install failed (${e.message.split('\n')[0]}); install manually with \`npm install\``);
|
|
889
|
+
}
|
|
890
|
+
} else {
|
|
891
|
+
ok('[7/7] MCP deps — already present');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// (b) register/start under PM2 (tolerate pm2 missing)
|
|
895
|
+
let pm2Ok = false;
|
|
896
|
+
try { execSync('pm2 -v', { stdio: 'pipe' }); pm2Ok = true; } catch { pm2Ok = false; }
|
|
897
|
+
|
|
898
|
+
if (!pm2Ok) {
|
|
899
|
+
warn('[7/7] MCP server — pm2 not found; start manually:');
|
|
900
|
+
info(` PORT=${MCP_PORT} pm2 start ${binPath} --name ${MCP_NAME}`);
|
|
901
|
+
} else {
|
|
902
|
+
let registered = false;
|
|
903
|
+
try {
|
|
904
|
+
const jlist = JSON.parse(execSync('pm2 jlist', { encoding: 'utf8', stdio: 'pipe' }));
|
|
905
|
+
registered = Array.isArray(jlist) && jlist.some((p) => p.name === MCP_NAME);
|
|
906
|
+
} catch {}
|
|
907
|
+
try {
|
|
908
|
+
if (registered) {
|
|
909
|
+
execSync(`pm2 restart ${MCP_NAME} --update-env`, { cwd: repoRoot, stdio: 'pipe', env: { ...process.env, PORT: MCP_PORT } });
|
|
910
|
+
ok(`[7/7] MCP server — pm2 restarted ${MCP_NAME} (PORT=${MCP_PORT})`);
|
|
911
|
+
} else {
|
|
912
|
+
execSync(`pm2 start "${binPath}" --name ${MCP_NAME}`, { cwd: repoRoot, stdio: 'pipe', env: { ...process.env, PORT: MCP_PORT } });
|
|
913
|
+
ok(`[7/7] MCP server — pm2 started ${MCP_NAME} (PORT=${MCP_PORT})`);
|
|
914
|
+
}
|
|
915
|
+
} catch (e) {
|
|
916
|
+
warn(`[7/7] MCP server — pm2 start/restart failed (${e.message.split('\n')[0]})`);
|
|
917
|
+
info(` PORT=${MCP_PORT} pm2 start ${binPath} --name ${MCP_NAME}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// (c) print the claude.ai connector line
|
|
922
|
+
info(` claude.ai connector: ${connector} (Auth: none — URL is the shared secret)`);
|
|
923
|
+
} catch (e) {
|
|
924
|
+
warn(`[7/7] MCP server — skipped (${e.message.split('\n')[0]})`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ── Preflight ─────────────────────────────────────────────────────────────────
|
|
929
|
+
function runPreflight() {
|
|
930
|
+
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
931
|
+
if (nodeMajor < 18) { fail(`Node.js >= 18 required — found v${process.versions.node}`); process.exit(1); }
|
|
932
|
+
ok(`Node.js v${process.versions.node}`);
|
|
933
|
+
try {
|
|
934
|
+
execSync('curl -s --max-time 2 http://127.0.0.1:52437/ping', { stdio: 'pipe' });
|
|
935
|
+
ok('clauth daemon is running');
|
|
936
|
+
} catch {
|
|
937
|
+
warn('clauth daemon not responding — start it before using credential-dependent commands');
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// ── Commands listing ──────────────────────────────────────────────────────────
|
|
942
|
+
function listCommands() {
|
|
943
|
+
const cmdsDir = path.join(repoRoot, 'commands');
|
|
944
|
+
if (!fs.existsSync(cmdsDir)) return;
|
|
945
|
+
const files = fs.readdirSync(cmdsDir).filter(f => f.endsWith('.md')).sort();
|
|
946
|
+
const plugin = readJson(path.join(repoRoot, '.claude-plugin', 'plugin.json'), {});
|
|
947
|
+
const skillCount = Array.isArray(plugin.skills_meta)
|
|
948
|
+
? plugin.skills_meta.length
|
|
949
|
+
: (plugin.skills_meta && typeof plugin.skills_meta === 'object' ? Object.keys(plugin.skills_meta).length : null);
|
|
950
|
+
console.log('');
|
|
951
|
+
if (skillCount !== null) {
|
|
952
|
+
console.log(` \x1b[32mAvailable MCP skills (${skillCount}) and /rdc:* command shorthands (${files.length}):\x1b[0m`);
|
|
953
|
+
console.log(' Use MCP tools rdc_skill_list, rdc_skill_search, and rdc_skill_get for the full skill catalog.');
|
|
954
|
+
} else {
|
|
955
|
+
console.log(` \x1b[32mAvailable /rdc:* command shorthands (${files.length}):\x1b[0m`);
|
|
956
|
+
}
|
|
957
|
+
console.log('');
|
|
958
|
+
const COL = 18;
|
|
959
|
+
for (const f of files) {
|
|
960
|
+
const name = 'rdc:' + f.replace(/\.md$/, '');
|
|
961
|
+
const fm = readFrontmatter(path.join(cmdsDir, f));
|
|
962
|
+
const desc = (fm.description || '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
|
|
963
|
+
const short = desc.length > 70 ? desc.slice(0, 70) + '…' : desc;
|
|
964
|
+
const pad = ' '.repeat(Math.max(1, COL - name.length));
|
|
965
|
+
console.log(` \x1b[36m/${name}\x1b[0m${pad}${short}`);
|
|
966
|
+
}
|
|
967
|
+
console.log('');
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// ── Migrate helper ────────────────────────────────────────────────────────────
|
|
971
|
+
async function runMigrate(projectRoot) {
|
|
972
|
+
const absRoot = path.resolve(projectRoot);
|
|
973
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
974
|
+
const ask = q => new Promise(resolve => rl.question(q, resolve));
|
|
975
|
+
|
|
976
|
+
console.log('\n \x1b[35mrdc-skills — Migrate to .rdc/ Layout\x1b[0m\n');
|
|
977
|
+
console.log(` Project root: ${absRoot}\n`);
|
|
978
|
+
|
|
979
|
+
const candidates = [
|
|
980
|
+
{ src: 'docs/guides', dst: '.rdc/guides' }, { src: 'docs/plans', dst: '.rdc/plans' },
|
|
981
|
+
{ src: 'docs/reports', dst: '.rdc/reports' }, { src: 'docs/research', dst: '.rdc/research' },
|
|
982
|
+
].filter(c => fs.existsSync(path.join(absRoot, c.src)));
|
|
983
|
+
|
|
984
|
+
if (candidates.length === 0) { info('No migratable directories found.'); rl.close(); return; }
|
|
985
|
+
console.log(' Found:');
|
|
986
|
+
candidates.forEach(c => console.log(` ${c.src}`));
|
|
987
|
+
console.log('');
|
|
988
|
+
|
|
989
|
+
for (const c of candidates) {
|
|
990
|
+
const srcAbs = path.join(absRoot, c.src);
|
|
991
|
+
const dstAbs = path.join(absRoot, c.dst);
|
|
992
|
+
const ans = await ask(` Move ${c.src} → ${c.dst}? [Y/n]: `);
|
|
993
|
+
if (ans.toLowerCase() === 'n') { info(`Skipped ${c.src}`); continue; }
|
|
994
|
+
if (fs.existsSync(dstAbs)) {
|
|
995
|
+
warn(`${c.dst} exists — merging`);
|
|
996
|
+
for (const f of fs.readdirSync(srcAbs)) {
|
|
997
|
+
const sf = path.join(srcAbs, f), df = path.join(dstAbs, f);
|
|
998
|
+
if (!fs.existsSync(df)) fs.renameSync(sf, df);
|
|
999
|
+
else warn(` Skipped ${f} (exists in dst)`);
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
fs.mkdirSync(path.dirname(dstAbs), { recursive: true });
|
|
1003
|
+
fs.renameSync(srcAbs, dstAbs);
|
|
1004
|
+
}
|
|
1005
|
+
ok(`Moved ${c.src} → ${c.dst}`);
|
|
1006
|
+
}
|
|
1007
|
+
rl.close();
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
1011
|
+
async function main() {
|
|
1012
|
+
if (doMigrate) { await runMigrate(migratePath); return; }
|
|
1013
|
+
|
|
1014
|
+
const bannerVersion = (readJson(path.join(repoRoot, 'package.json')).version || '?');
|
|
1015
|
+
const bannerLine = `║ install-rdc-skills v${bannerVersion}`;
|
|
1016
|
+
const bannerPadded = bannerLine.padEnd(41) + '║';
|
|
1017
|
+
console.log('');
|
|
1018
|
+
console.log(' \x1b[32m╔═══════════════════════════════════════╗\x1b[0m');
|
|
1019
|
+
console.log(` \x1b[32m${bannerPadded}\x1b[0m`);
|
|
1020
|
+
console.log(' \x1b[32m╚═══════════════════════════════════════╝\x1b[0m');
|
|
1021
|
+
console.log('');
|
|
1022
|
+
console.log(` CLAUDE_HOME : ${claudeHome}`);
|
|
1023
|
+
console.log(` Plugin root : ${repoRoot}`);
|
|
1024
|
+
console.log(` Profile : ${installProfile}${profileArg === 'auto' ? ' (auto)' : ''}`);
|
|
1025
|
+
console.log('');
|
|
1026
|
+
|
|
1027
|
+
if (!fs.existsSync(claudeHome)) {
|
|
1028
|
+
fs.mkdirSync(claudeHome, { recursive: true });
|
|
1029
|
+
warn(`CLAUDE_HOME did not exist — created ${claudeHome}`);
|
|
1030
|
+
}
|
|
1031
|
+
if (!fs.existsSync(settingsPath)) {
|
|
1032
|
+
writeJson(settingsPath, {});
|
|
1033
|
+
info(` created settings.json`);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// 0. Pull latest
|
|
1037
|
+
try {
|
|
1038
|
+
const before = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1039
|
+
execSync('git pull --ff-only', { cwd: repoRoot, stdio: 'pipe' });
|
|
1040
|
+
const after = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
1041
|
+
ok(`[0/6] git pull — ${before === after ? after.slice(0, 7) : `${before.slice(0,7)} → ${after.slice(0,7)}`}`);
|
|
1042
|
+
} catch {
|
|
1043
|
+
warn('[0/6] git pull failed — installing from local copy');
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Read version + git SHA after pull so plugin caches and the MCP install do
|
|
1047
|
+
// not stamp the pre-update checkout.
|
|
1048
|
+
const pkg = readJson(path.join(repoRoot, 'package.json'));
|
|
1049
|
+
const version = pkg.version || '0.7.0';
|
|
1050
|
+
let gitSha = '';
|
|
1051
|
+
try { gitSha = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim(); } catch {}
|
|
1052
|
+
|
|
1053
|
+
// 0.5a. User-skills cleanup — remove any rdc: skills from ~/.claude/skills/user/
|
|
1054
|
+
// (older installer versions wrote there; plugin cache is the only authoritative source)
|
|
1055
|
+
{
|
|
1056
|
+
const userSkillsDir = path.join(claudeHome, 'skills', 'user');
|
|
1057
|
+
const purged = cleanUserSkills(userSkillsDir);
|
|
1058
|
+
if (purged > 0) ok(`[0.5a] Skills cleanup — removed ${purged} stale rdc: skill(s) from skills/user/`);
|
|
1059
|
+
// Also scan the parent ~/.claude/skills/ for any flat-file rdc orphans.
|
|
1060
|
+
const skillsRoot = path.join(claudeHome, 'skills');
|
|
1061
|
+
const rootPurged = cleanGlobalSkillsRoot(skillsRoot);
|
|
1062
|
+
if (rootPurged > 0) ok(`[0.5a] Skills cleanup — removed ${rootPurged} stale rdc: file(s) from skills/`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// 0.5b. Stale hook cleanup — remove hooks we no longer ship
|
|
1066
|
+
{
|
|
1067
|
+
const staleRemoved = cleanStaleHooks(hooksDst);
|
|
1068
|
+
if (staleRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${staleRemoved} orphaned hook file(s)`);
|
|
1069
|
+
const shimRemoved = cleanProjectHookifyShims(codexRoot);
|
|
1070
|
+
if (shimRemoved > 0) ok(`[0.5b] Hook cleanup — removed ${shimRemoved} project-local work-item hookify shim(s)`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// 0.5. Legacy cleanup — remove old commands/rdc/ (pre-plugin-system format)
|
|
1074
|
+
const legacyCommandsDir = path.join(claudeHome, 'commands', 'rdc');
|
|
1075
|
+
if (fs.existsSync(legacyCommandsDir)) {
|
|
1076
|
+
fs.rmSync(legacyCommandsDir, { recursive: true, force: true });
|
|
1077
|
+
ok('[0.5] Cleanup — removed legacy commands/rdc/');
|
|
1078
|
+
}
|
|
1079
|
+
// Also remove extraKnownMarketplaces.rdc-skills from settings.json to prevent
|
|
1080
|
+
// Claude Code from syncing a directory-source marketplace back to known_marketplaces.json
|
|
1081
|
+
// (which causes skills to load from both the cache AND the live source directory)
|
|
1082
|
+
{
|
|
1083
|
+
const st = readJson(settingsPath);
|
|
1084
|
+
if (st.extraKnownMarketplaces && st.extraKnownMarketplaces[MARKETPLACE]) {
|
|
1085
|
+
delete st.extraKnownMarketplaces[MARKETPLACE];
|
|
1086
|
+
if (Object.keys(st.extraKnownMarketplaces).length === 0) delete st.extraKnownMarketplaces;
|
|
1087
|
+
writeJson(settingsPath, st);
|
|
1088
|
+
ok('[0.5] Cleanup — removed extraKnownMarketplaces.rdc-skills from settings.json');
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// 1. CLI registration
|
|
1093
|
+
const cliCacheDir = registerCLI(version, gitSha);
|
|
1094
|
+
ok(`[1/6] CLI plugin — ${PLUGIN_KEY} v${version}`);
|
|
1095
|
+
info(` cache : ${cliCacheDir}`);
|
|
1096
|
+
|
|
1097
|
+
// 2. Cowork registration
|
|
1098
|
+
const coworkCount = registerCowork(version, gitSha);
|
|
1099
|
+
if (coworkCount > 0) {
|
|
1100
|
+
ok(`[2/6] Cowork — registered in ${coworkCount} workspace(s)`);
|
|
1101
|
+
} else {
|
|
1102
|
+
warn('[2/6] Cowork — no Desktop workspaces found (open Claude Desktop once to create them)');
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// 2.5. Codex registration
|
|
1106
|
+
const codexTargets = findCodexTargets();
|
|
1107
|
+
if (codexTargets.length > 0) {
|
|
1108
|
+
let copiedTotal = 0;
|
|
1109
|
+
let removedTotal = 0;
|
|
1110
|
+
for (const target of codexTargets) {
|
|
1111
|
+
const { removed, copied } = registerCodexTarget(target.targetDir);
|
|
1112
|
+
copiedTotal += copied;
|
|
1113
|
+
removedTotal += removed;
|
|
1114
|
+
info(` ${target.label.padEnd(15)}: ${target.targetDir} (${copied} installed, ${removed} stale removed)`);
|
|
1115
|
+
}
|
|
1116
|
+
ok(`[2.5] Codex — ${copiedTotal} skill install(s), ${removedTotal} stale removed across ${codexTargets.length} target(s)`);
|
|
1117
|
+
} else {
|
|
1118
|
+
info('[2.5] Codex — skipped (no Codex skill dirs found; use --codex-root or --codex-skill-dir)');
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// 2.6. Register the rdc-skills MCP endpoint globally (Claude Code + Codex) so
|
|
1122
|
+
// EVERY agent can reach the skills via MCP, not only where a project .mcp.json exists.
|
|
1123
|
+
const mcpReg = registerMcpEndpoints();
|
|
1124
|
+
if (mcpReg.length > 0) {
|
|
1125
|
+
ok(`[2.6] MCP — registered rdc-skills endpoint: ${mcpReg.join(', ')}`);
|
|
1126
|
+
} else {
|
|
1127
|
+
info('[2.6] MCP — rdc-skills endpoint already registered (claude + codex)');
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// If the live MCP is served from the global npm package, update that exact
|
|
1131
|
+
// install before restarting PM2. Windows otherwise holds the package tree open
|
|
1132
|
+
// and `npm install -g` can fail with EBUSY.
|
|
1133
|
+
syncGlobalMcpInstall(version);
|
|
1134
|
+
|
|
1135
|
+
// 2.7. Symlinks in regen-root/.claude/skills/ (FS MCP + claude.ai access)
|
|
1136
|
+
if (codexRoot) {
|
|
1137
|
+
const skillsLinkDir = path.join(codexRoot, '.claude', 'skills');
|
|
1138
|
+
const skillsSrc = path.join(repoRoot, 'skills');
|
|
1139
|
+
fs.mkdirSync(skillsLinkDir, { recursive: true });
|
|
1140
|
+
let linked = 0;
|
|
1141
|
+
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
1142
|
+
if (!entry.isDirectory()) continue;
|
|
1143
|
+
if (!fs.existsSync(path.join(skillsSrc, entry.name, 'SKILL.md'))) continue;
|
|
1144
|
+
const linkPath = path.join(skillsLinkDir, entry.name);
|
|
1145
|
+
const target = path.join(skillsSrc, entry.name);
|
|
1146
|
+
try {
|
|
1147
|
+
if (fs.existsSync(linkPath) || (() => { try { fs.lstatSync(linkPath); return true; } catch { return false; } })()) {
|
|
1148
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
1149
|
+
}
|
|
1150
|
+
} catch {}
|
|
1151
|
+
try {
|
|
1152
|
+
if (process.platform === 'win32') {
|
|
1153
|
+
fs.symlinkSync(target, linkPath, 'junction');
|
|
1154
|
+
} else {
|
|
1155
|
+
fs.symlinkSync(target, linkPath, 'dir');
|
|
1156
|
+
}
|
|
1157
|
+
linked++;
|
|
1158
|
+
} catch {}
|
|
1159
|
+
}
|
|
1160
|
+
if (linked > 0) {
|
|
1161
|
+
ok(`[2.7] Symlinks — ${linked} skill link(s) in ${skillsLinkDir}`);
|
|
1162
|
+
} else {
|
|
1163
|
+
info('[2.7] Symlinks — no rdc skill links created (skills may already be linked)');
|
|
1164
|
+
}
|
|
1165
|
+
const projectGuideCount = copyMissingProjectGuides(codexRoot);
|
|
1166
|
+
if (projectGuideCount > 0) {
|
|
1167
|
+
ok(`[2.8] Guides — ${projectGuideCount} missing guide(s) copied to ${path.join(codexRoot, '.rdc', 'guides')}`);
|
|
1168
|
+
} else {
|
|
1169
|
+
info('[2.8] Guides — project .rdc/guides already has base guide files or is absent');
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
info('[2.7] Symlinks — skipped (no codex root found)');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// 3. Hook files
|
|
1176
|
+
const hookCount = copyHookFiles(hooksSrc, hooksDst);
|
|
1177
|
+
ok(`[3/6] Hook files — ${hookCount} file(s) → ${hooksDst}`);
|
|
1178
|
+
|
|
1179
|
+
// 4. Hook wiring
|
|
1180
|
+
if (skipHooks) {
|
|
1181
|
+
info('[4/6] Hook wiring — skipped (--skip-hooks)');
|
|
1182
|
+
} else {
|
|
1183
|
+
const settings = readJson(settingsPath);
|
|
1184
|
+
settings.hooks = buildHooksConfig(hooksDst, installProfile);
|
|
1185
|
+
writeJson(settingsPath, settings);
|
|
1186
|
+
ok(`[4/6] Hook wiring — ${settingsPath}`);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// 4.5 Optional startup blocks
|
|
1190
|
+
if (shouldWriteStartupBlocks) {
|
|
1191
|
+
const startup = writeStartupBlocks(projectRoot, installProfile);
|
|
1192
|
+
if (startup.skipped) warn(`[4.5] Startup — skipped (${startup.skipped})`);
|
|
1193
|
+
else ok(`[4.5] Startup — wrote managed startup guide/block(s) under ${projectRoot}`);
|
|
1194
|
+
} else {
|
|
1195
|
+
info('[4.5] Startup — skipped (use --project-root <path> --write-startup-blocks)');
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// 5. Zip for claude.ai / distribution
|
|
1199
|
+
const zipPath = buildZip(version);
|
|
1200
|
+
if (zipPath) {
|
|
1201
|
+
ok(`[5/6] Plugin zip — ${zipPath}`);
|
|
1202
|
+
info(' claude.ai : optional plugin-upload artifact; MCP callers normally use the connector URL below');
|
|
1203
|
+
} else {
|
|
1204
|
+
warn('[5/6] Plugin zip — build failed (zip/powershell missing?)');
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// 6. Preflight
|
|
1208
|
+
console.log('');
|
|
1209
|
+
console.log(' \x1b[36mPreflight:\x1b[0m');
|
|
1210
|
+
runPreflight();
|
|
1211
|
+
|
|
1212
|
+
// 6.5 Post-install verification — duplication guard
|
|
1213
|
+
console.log('');
|
|
1214
|
+
console.log(' \x1b[36mPost-install verification:\x1b[0m');
|
|
1215
|
+
let verifyFailed = false;
|
|
1216
|
+
{
|
|
1217
|
+
const cacheBase = path.join(claudeHome, 'plugins', 'cache', MARKETPLACE, 'rdc-skills');
|
|
1218
|
+
const cacheDirs = fs.existsSync(cacheBase) ? fs.readdirSync(cacheBase) : [];
|
|
1219
|
+
if (cacheDirs.length === 1 && cacheDirs[0] === 'latest') {
|
|
1220
|
+
ok(`plugin cache : 1 dir (latest/)`);
|
|
1221
|
+
} else {
|
|
1222
|
+
fail(`plugin cache : expected exactly [latest/], found [${cacheDirs.join(', ')}]`);
|
|
1223
|
+
verifyFailed = true;
|
|
1224
|
+
}
|
|
1225
|
+
const ipPath = path.join(claudeHome, 'plugins', 'installed_plugins.json');
|
|
1226
|
+
const installed = readJson(ipPath, { plugins: {} });
|
|
1227
|
+
const rdcEntries = installed.plugins[PLUGIN_KEY] || [];
|
|
1228
|
+
if (rdcEntries.length === 1) {
|
|
1229
|
+
ok(`installed_plugins: 1 entry for ${PLUGIN_KEY}`);
|
|
1230
|
+
} else {
|
|
1231
|
+
fail(`installed_plugins: expected 1 entry, found ${rdcEntries.length}`);
|
|
1232
|
+
verifyFailed = true;
|
|
1233
|
+
}
|
|
1234
|
+
const userSkillsDir = path.join(claudeHome, 'skills', 'user');
|
|
1235
|
+
const stillThere = fs.existsSync(userSkillsDir)
|
|
1236
|
+
? fs.readdirSync(userSkillsDir).filter(f => {
|
|
1237
|
+
const p = path.join(userSkillsDir, f);
|
|
1238
|
+
const skillFile = fs.statSync(p).isDirectory()
|
|
1239
|
+
? ['SKILL.md','skill.md'].map(s => path.join(p, s)).find(fs.existsSync)
|
|
1240
|
+
: (f.endsWith('.md') ? p : null);
|
|
1241
|
+
if (!skillFile) return false;
|
|
1242
|
+
const fm = readFrontmatter(skillFile);
|
|
1243
|
+
return fm.name && fm.name.startsWith('rdc:');
|
|
1244
|
+
})
|
|
1245
|
+
: [];
|
|
1246
|
+
if (stillThere.length === 0) {
|
|
1247
|
+
ok(`skills/user/ : no rdc: orphans`);
|
|
1248
|
+
} else {
|
|
1249
|
+
fail(`skills/user/ : still has rdc: orphans: ${stillThere.join(', ')}`);
|
|
1250
|
+
verifyFailed = true;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (verifyFailed) {
|
|
1254
|
+
console.log('');
|
|
1255
|
+
fail('Post-install verification FAILED — duplicates or orphans remain. Investigate.');
|
|
1256
|
+
process.exit(2);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// 7. MCP server registration (non-fatal — WARNs only, never aborts)
|
|
1260
|
+
console.log('');
|
|
1261
|
+
console.log(' \x1b[36mMCP server:\x1b[0m');
|
|
1262
|
+
try { registerMcpServer(); } catch (e) { warn(`[7/7] MCP server — unexpected error (${e.message})`); }
|
|
1263
|
+
|
|
1264
|
+
// Done
|
|
1265
|
+
console.log('');
|
|
1266
|
+
console.log(' \x1b[32mDone!\x1b[0m');
|
|
1267
|
+
console.log('');
|
|
1268
|
+
console.log(' \x1b[33mNext steps:\x1b[0m');
|
|
1269
|
+
console.log(' CLI : restart Claude Code — /rdc:status to verify');
|
|
1270
|
+
console.log(' Cowork : restart Claude Desktop — /rdc:status in a new Cowork session');
|
|
1271
|
+
console.log(' claude.ai : no plugin upload needed for MCP — use the connector URL above');
|
|
1272
|
+
console.log(' dist/rdc-skills-plugin-v' + version + '.zip available if needed');
|
|
1273
|
+
console.log('');
|
|
1274
|
+
console.log(` Profile: ${installProfile}`);
|
|
1275
|
+
if (installProfile === 'core') {
|
|
1276
|
+
console.log(' Core hooks are portable: RDC output contract + foreground process + commit-message hygiene.');
|
|
1277
|
+
console.log(' Project services are not provisioned. Configure work items, credentials, deploys, and project guides before using infrastructure-heavy skills.');
|
|
1278
|
+
} else {
|
|
1279
|
+
console.log(' LIFEAI hooks are active: regen-root cwd lock, Supabase work-item gates, clauth-aware checks, and overnight queue guard.');
|
|
1280
|
+
}
|
|
1281
|
+
console.log(' Startup blocks: run with --project-root <path> --write-startup-blocks to add managed CLAUDE.md/AGENTS.md sections.');
|
|
1282
|
+
|
|
1283
|
+
listCommands();
|
|
1284
|
+
|
|
1285
|
+
console.log(' Docs: https://github.com/LIFEAI/rdc-skills#readme');
|
|
1286
|
+
console.log('');
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
main().catch(e => { fail(e.message); process.exit(1); });
|