@lifeaitools/rdc-skills 0.24.41 → 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.
Files changed (198) hide show
  1. package/.claude/settings.json +15 -15
  2. package/.claude-plugin/marketplace.json +21 -21
  3. package/.claude-plugin/plugin.json +1518 -1371
  4. package/.github/workflows/publish.yml +34 -34
  5. package/.github/workflows/self-test.yml +58 -58
  6. package/CHANGELOG.md +310 -310
  7. package/LICENSE +21 -21
  8. package/MANIFEST.md +221 -221
  9. package/README.md +375 -376
  10. package/README.sandbox.md +3 -3
  11. package/assets/watcher/viewer.html +164 -164
  12. package/bin/rdc-skills-mcp.mjs +316 -316
  13. package/commands/build.md +183 -183
  14. package/commands/collab.md +180 -180
  15. package/commands/deploy.md +152 -152
  16. package/commands/design.md +31 -31
  17. package/commands/edit.md +28 -28
  18. package/commands/fixit.md +150 -124
  19. package/commands/handoff.md +173 -173
  20. package/commands/help.md +95 -95
  21. package/commands/overnight.md +220 -220
  22. package/commands/plan.md +158 -158
  23. package/commands/preplan.md +131 -131
  24. package/commands/prototype.md +145 -145
  25. package/commands/release.md +49 -49
  26. package/commands/report.md +99 -99
  27. package/commands/review.md +120 -120
  28. package/commands/self-test.md +113 -113
  29. package/commands/status.md +86 -86
  30. package/commands/watch.md +98 -98
  31. package/commands/workitems.md +137 -137
  32. package/git-sha.json +1 -1
  33. package/guides/agent-bootstrap.md +295 -295
  34. package/guides/agents/backend.md +104 -104
  35. package/guides/agents/content.md +94 -94
  36. package/guides/agents/cs2.md +56 -56
  37. package/guides/agents/data.md +87 -87
  38. package/guides/agents/design.md +77 -77
  39. package/guides/agents/frontend.md +92 -92
  40. package/guides/agents/infrastructure.md +81 -81
  41. package/guides/agents/setup.md +281 -281
  42. package/guides/agents/verify.md +151 -151
  43. package/guides/agents/viz.md +106 -106
  44. package/guides/backend.md +146 -146
  45. package/guides/content.md +147 -147
  46. package/guides/cs2.md +190 -190
  47. package/guides/data.md +123 -123
  48. package/guides/design.md +116 -116
  49. package/guides/engineering-behavior.md +43 -43
  50. package/guides/escalation-protocol.md +125 -125
  51. package/guides/frontend.md +151 -151
  52. package/guides/history-md-spec.md +297 -297
  53. package/guides/infrastructure.md +179 -179
  54. package/guides/lessons-learned-spec.md +145 -151
  55. package/guides/output-contract.md +108 -108
  56. package/guides/publish-md-spec.md +289 -289
  57. package/guides/rdc-skills-startup.md +30 -30
  58. package/guides/verify.md +11 -11
  59. package/hooks/check-cwd.js +31 -31
  60. package/hooks/check-rdc-environment.js +164 -164
  61. package/hooks/check-services.js +6 -6
  62. package/hooks/check-stale-work-items.js +19 -19
  63. package/hooks/foreground-process-gate.js +128 -128
  64. package/hooks/gate-watchdog-selfcheck.js +257 -257
  65. package/hooks/hook-logger.js +25 -25
  66. package/hooks/lib/run-evidence-gate.mjs +241 -241
  67. package/hooks/no-stop-open-epics.js +127 -127
  68. package/hooks/post-tool-batch-gate.js +203 -203
  69. package/hooks/post-work-check.js +21 -21
  70. package/hooks/postcompact-log.js +13 -13
  71. package/hooks/precompact-log.js +13 -13
  72. package/hooks/rate-limit-retry.js +46 -46
  73. package/hooks/rdc-invocation-marker.js +157 -157
  74. package/hooks/rdc-output-contract-gate.js +94 -94
  75. package/hooks/require-work-item-on-commit.js +294 -294
  76. package/hooks/restart-brief.js +19 -19
  77. package/hooks/run-hidden-hook.ps1 +47 -47
  78. package/hooks/task-completed-gate.js +274 -274
  79. package/hooks/work-item-exit-gate.js +944 -944
  80. package/lib/catalog.mjs +236 -236
  81. package/lib/cloud-rewrite.mjs +155 -155
  82. package/package.json +57 -57
  83. package/rules/work-items-rpc.md +520 -520
  84. package/scaffold/templates/HISTORY.md.template +39 -39
  85. package/scaffold/templates/PUBLISH.md.template +21 -21
  86. package/scaffold/templates/brochure-studio-default.html +70 -70
  87. package/scripts/acceptance.mjs +502 -502
  88. package/scripts/fixtures/guides/bad-guide.md +15 -15
  89. package/scripts/fixtures/guides-clean/good-guide.md +16 -16
  90. package/scripts/install-rdc-skills.js +1289 -1289
  91. package/scripts/install.ps1 +202 -202
  92. package/scripts/install.sh +132 -132
  93. package/scripts/lib/assertions.mjs +287 -287
  94. package/scripts/lib/manifest-schema.mjs +754 -754
  95. package/scripts/lib/runner.mjs +465 -465
  96. package/scripts/lib/sandbox.mjs +435 -435
  97. package/scripts/prepack.mjs +32 -32
  98. package/scripts/rdc-brochure.mjs +482 -482
  99. package/scripts/rdc-design-cli.mjs +134 -134
  100. package/scripts/rebuild-mcp.mjs +107 -107
  101. package/scripts/self-test.mjs +1460 -1460
  102. package/scripts/stamp-git-sha.mjs +29 -29
  103. package/scripts/test-guide-validator.mjs +196 -196
  104. package/scripts/test-rdc-hooks.mjs +145 -145
  105. package/scripts/uninstall.ps1 +77 -77
  106. package/scripts/uninstall.sh +69 -69
  107. package/scripts/update.ps1 +43 -43
  108. package/scripts/update.sh +43 -43
  109. package/scripts/validate-place-histories.js +461 -461
  110. package/scripts/validate-publish-manifests.js +502 -424
  111. package/scripts/watch-init.mjs +100 -100
  112. package/skills/brochure/SKILL.md +107 -107
  113. package/skills/build/SKILL.md +563 -563
  114. package/skills/channel-formatter/SKILL.md +538 -533
  115. package/skills/co-develop/SKILL.md +196 -196
  116. package/skills/collab/SKILL.md +239 -239
  117. package/skills/convert/SKILL.md +167 -140
  118. package/skills/deploy/SKILL.md +541 -541
  119. package/skills/design/SKILL.md +211 -211
  120. package/skills/design/reference/ownership.md +16 -16
  121. package/skills/design/reference/rampa.md +92 -92
  122. package/skills/design/reference/studio-model.md +153 -153
  123. package/skills/edit/SKILL.md +98 -98
  124. package/skills/fixit/SKILL.md +203 -165
  125. package/skills/fs-mcp/SKILL.md +148 -148
  126. package/skills/handoff/SKILL.md +236 -236
  127. package/skills/help/SKILL.md +143 -143
  128. package/skills/housekeeping/SKILL.md +160 -219
  129. package/skills/lifeai-brochure-author/SKILL.md +340 -340
  130. package/skills/onramp/SKILL.md +248 -0
  131. package/skills/overnight/SKILL.md +251 -251
  132. package/skills/plan/SKILL.md +345 -345
  133. package/skills/preplan/SKILL.md +90 -90
  134. package/skills/prototype/SKILL.md +150 -150
  135. package/skills/rdc-brochurify/SKILL.md +245 -245
  136. package/skills/rdc-extract-verifier-rules/SKILL.md +191 -191
  137. package/skills/release/SKILL.md +140 -140
  138. package/skills/report/SKILL.md +100 -100
  139. package/skills/review/SKILL.md +152 -152
  140. package/skills/rpms-filemap/SKILL.cloud.md +111 -111
  141. package/skills/rpms-filemap/SKILL.md +111 -111
  142. package/skills/self-test/SKILL.md +132 -132
  143. package/skills/status/SKILL.md +99 -99
  144. package/skills/terminal-config/SKILL.md +62 -62
  145. package/skills/tests/MATRIX.md +54 -54
  146. package/skills/tests/README.md +47 -47
  147. package/skills/tests/onramp.test.json +87 -0
  148. package/skills/tests/rdc-brochure.test.json +34 -34
  149. package/skills/tests/rdc-build.test.json +36 -36
  150. package/skills/tests/rdc-channel-formatter.test.json +45 -45
  151. package/skills/tests/rdc-co-develop.test.json +29 -29
  152. package/skills/tests/rdc-collab.test.json +29 -29
  153. package/skills/tests/rdc-convert.test.json +35 -35
  154. package/skills/tests/rdc-deploy.test.json +30 -30
  155. package/skills/tests/rdc-design.test.json +27 -27
  156. package/skills/tests/rdc-edit.test.json +29 -29
  157. package/skills/tests/rdc-fixit.test.json +36 -36
  158. package/skills/tests/rdc-fs-mcp.test.json +36 -36
  159. package/skills/tests/rdc-handoff.test.json +28 -28
  160. package/skills/tests/rdc-help.test.json +29 -29
  161. package/skills/tests/rdc-housekeeping.test.json +28 -32
  162. package/skills/tests/rdc-lifeai-brochure-author.test.json +35 -35
  163. package/skills/tests/rdc-overnight.test.json +37 -37
  164. package/skills/tests/rdc-plan.test.json +27 -27
  165. package/skills/tests/rdc-preplan.test.json +31 -31
  166. package/skills/tests/rdc-prototype.test.json +28 -28
  167. package/skills/tests/rdc-rdc-brochurify.test.json +23 -23
  168. package/skills/tests/rdc-rdc-extract-verifier-rules.test.json +34 -34
  169. package/skills/tests/rdc-release.test.json +29 -29
  170. package/skills/tests/rdc-report.test.json +28 -28
  171. package/skills/tests/rdc-review.test.json +29 -29
  172. package/skills/tests/rdc-rpms-filemap.test.json +28 -28
  173. package/skills/tests/rdc-self-test.test.json +24 -24
  174. package/skills/tests/rdc-status.test.json +29 -29
  175. package/skills/tests/rdc-terminal-config.test.json +29 -29
  176. package/skills/tests/rdc-watch.test.json +24 -24
  177. package/skills/tests/rdc-workitems.test.json +27 -27
  178. package/skills/watch/SKILL.md +97 -97
  179. package/skills/workitems/SKILL.md +151 -151
  180. package/tests/acceptance.test.mjs +59 -59
  181. package/tests/channel-formatter.contract.test.mjs +251 -251
  182. package/tests/curl-surface.test.mjs +289 -289
  183. package/tests/harness-gates.test.mjs +325 -325
  184. package/tests/help-surface.test.mjs +61 -61
  185. package/tests/install-rdc-skills.test.mjs +49 -49
  186. package/tests/manifest-contract-fields.test.mjs +78 -78
  187. package/tests/mcp.test.mjs +271 -271
  188. package/tests/rdc-brochure.test.mjs +125 -125
  189. package/tests/require-work-item-on-commit.test.mjs +162 -162
  190. package/tests/run-evidence-gate.test.mjs +82 -82
  191. package/tests/skill-test-matrix.test.mjs +66 -66
  192. package/tests/validate-skills.js +27 -27
  193. package/tests/work-item-exit-gate-l2.test.mjs +368 -368
  194. package/tests/work-item-exit-gate-l3.test.mjs +197 -197
  195. package/RELEASE.md +0 -42
  196. package/tests/housekeeping-lessons-triage.test.mjs +0 -49
  197. package/tests/lessons-pipeline-contract.test.mjs +0 -26
  198. 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); });