@lifeaitools/rdc-skills 0.8.7

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 (97) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.claude-plugin/marketplace.json +21 -0
  3. package/.claude-plugin/plugin.json +11 -0
  4. package/.github/workflows/publish.yml +25 -0
  5. package/.github/workflows/self-test.yml +53 -0
  6. package/CHANGELOG.md +246 -0
  7. package/LICENSE +21 -0
  8. package/MANIFEST.md +190 -0
  9. package/README.md +188 -0
  10. package/README.sandbox.md +3 -0
  11. package/assets/watcher/viewer.html +164 -0
  12. package/commands/build.md +183 -0
  13. package/commands/collab.md +180 -0
  14. package/commands/deploy.md +138 -0
  15. package/commands/fixit.md +112 -0
  16. package/commands/handoff.md +173 -0
  17. package/commands/help.md +88 -0
  18. package/commands/overnight.md +220 -0
  19. package/commands/plan.md +158 -0
  20. package/commands/preplan.md +131 -0
  21. package/commands/prototype.md +145 -0
  22. package/commands/release.md +159 -0
  23. package/commands/report.md +99 -0
  24. package/commands/review.md +120 -0
  25. package/commands/self-test.md +107 -0
  26. package/commands/status.md +86 -0
  27. package/commands/watch.md +92 -0
  28. package/commands/workitems.md +132 -0
  29. package/guides/.gitkeep +0 -0
  30. package/guides/agent-bootstrap.md +191 -0
  31. package/guides/agents/backend.md +104 -0
  32. package/guides/agents/content.md +94 -0
  33. package/guides/agents/cs2.md +56 -0
  34. package/guides/agents/data.md +87 -0
  35. package/guides/agents/design.md +77 -0
  36. package/guides/agents/frontend.md +92 -0
  37. package/guides/agents/infrastructure.md +81 -0
  38. package/guides/agents/setup.md +279 -0
  39. package/guides/agents/verify.md +132 -0
  40. package/guides/agents/viz.md +106 -0
  41. package/guides/backend.md +146 -0
  42. package/guides/content.md +147 -0
  43. package/guides/cs2.md +190 -0
  44. package/guides/data.md +123 -0
  45. package/guides/design.md +116 -0
  46. package/guides/frontend.md +151 -0
  47. package/guides/infrastructure.md +179 -0
  48. package/guides/output-contract.md +98 -0
  49. package/hooks/no-stop-open-epics.js +125 -0
  50. package/package.json +31 -0
  51. package/rules/work-items-rpc.md +399 -0
  52. package/scripts/install-rdc-skills.js +559 -0
  53. package/scripts/install.ps1 +165 -0
  54. package/scripts/install.sh +132 -0
  55. package/scripts/lib/assertions.mjs +264 -0
  56. package/scripts/lib/manifest-schema.mjs +607 -0
  57. package/scripts/lib/runner.mjs +429 -0
  58. package/scripts/lib/sandbox.mjs +435 -0
  59. package/scripts/self-test.mjs +1108 -0
  60. package/scripts/uninstall.ps1 +77 -0
  61. package/scripts/uninstall.sh +69 -0
  62. package/scripts/update.ps1 +43 -0
  63. package/scripts/update.sh +43 -0
  64. package/scripts/watch-init.mjs +100 -0
  65. package/skills/.gitkeep +0 -0
  66. package/skills/build/SKILL.md +238 -0
  67. package/skills/collab/SKILL.md +218 -0
  68. package/skills/deploy/SKILL.md +144 -0
  69. package/skills/fixit/SKILL.md +112 -0
  70. package/skills/handoff/SKILL.md +175 -0
  71. package/skills/help/SKILL.md +101 -0
  72. package/skills/overnight/SKILL.md +220 -0
  73. package/skills/plan/SKILL.md +96 -0
  74. package/skills/preplan/SKILL.md +87 -0
  75. package/skills/prototype/SKILL.md +151 -0
  76. package/skills/release/SKILL.md +221 -0
  77. package/skills/report/SKILL.md +101 -0
  78. package/skills/review/SKILL.md +120 -0
  79. package/skills/self-test/SKILL.md +127 -0
  80. package/skills/status/SKILL.md +86 -0
  81. package/skills/tests/README.md +29 -0
  82. package/skills/tests/rdc-build.test.json +22 -0
  83. package/skills/tests/rdc-deploy.test.json +15 -0
  84. package/skills/tests/rdc-fixit.test.json +21 -0
  85. package/skills/tests/rdc-handoff.test.json +14 -0
  86. package/skills/tests/rdc-overnight.test.json +21 -0
  87. package/skills/tests/rdc-plan.test.json +14 -0
  88. package/skills/tests/rdc-preplan.test.json +15 -0
  89. package/skills/tests/rdc-prototype.test.json +14 -0
  90. package/skills/tests/rdc-release.test.json +15 -0
  91. package/skills/tests/rdc-report.test.json +14 -0
  92. package/skills/tests/rdc-review.test.json +14 -0
  93. package/skills/tests/rdc-status.test.json +16 -0
  94. package/skills/tests/rdc-workitems.test.json +15 -0
  95. package/skills/watch/SKILL.md +92 -0
  96. package/skills/workitems/SKILL.md +147 -0
  97. package/tests/validate-skills.js +183 -0
@@ -0,0 +1,559 @@
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 --claude-home <path> ← custom CLI home
9
+ * node scripts/install-rdc-skills.js --migrate <path> ← migrate docs/ → .rdc/
10
+ *
11
+ * What it does:
12
+ * 1. git pull (latest commands + guides)
13
+ * 2. CLI plugin — registers in ~/.claude/plugins/ + settings.json
14
+ * 3. Cowork — registers in Desktop cowork_plugins/ + cowork_settings.json
15
+ * 4. Hook files — copies hooks/*.js → ~/.claude/hooks/
16
+ * 5. Hook wiring — wires hooks into ~/.claude/settings.json
17
+ * 6. Zip — builds dist/rdc-skills-plugin.zip for claude.ai / distribution
18
+ * 7. Preflight — Node version, clauth daemon
19
+ * 8. Commands — lists all /rdc:* commands
20
+ */
21
+
22
+ 'use strict';
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const os = require('os');
26
+ const readline = require('readline');
27
+ const { execSync } = require('child_process');
28
+
29
+ // ── Args ──────────────────────────────────────────────────────────────────────
30
+ const args = process.argv.slice(2);
31
+ const skipHooks = args.includes('--skip-hooks');
32
+ const homeIdx = args.indexOf('--claude-home');
33
+ const claudeHome = homeIdx >= 0 ? args[homeIdx + 1] : path.join(os.homedir(), '.claude');
34
+ const migrateIdx = args.indexOf('--migrate');
35
+ const doMigrate = migrateIdx >= 0;
36
+ const migratePath = doMigrate ? (args[migrateIdx + 1] || process.cwd()) : null;
37
+
38
+ const repoRoot = path.resolve(__dirname, '..');
39
+ const hooksSrc = path.join(repoRoot, 'hooks');
40
+ const hooksDst = path.join(claudeHome, 'hooks');
41
+ const settingsPath = path.join(claudeHome, 'settings.json');
42
+
43
+ const PLUGIN_KEY = 'rdc-skills@rdc-skills';
44
+ const MARKETPLACE = 'rdc-skills';
45
+
46
+ // ── Logging ───────────────────────────────────────────────────────────────────
47
+ const ok = msg => console.log(` \x1b[32m✓\x1b[0m ${msg}`);
48
+ const info = msg => console.log(` \x1b[36m→\x1b[0m ${msg}`);
49
+ const warn = msg => console.log(` \x1b[33m⚠\x1b[0m ${msg}`);
50
+ const fail = msg => console.log(` \x1b[31m✗\x1b[0m ${msg}`);
51
+
52
+ // ── Filesystem helpers ────────────────────────────────────────────────────────
53
+ function copyDir(src, dst, ext) {
54
+ if (!fs.existsSync(src)) { warn(`Source not found: ${src}`); return 0; }
55
+ fs.mkdirSync(dst, { recursive: true });
56
+ const files = fs.readdirSync(src).filter(f => !ext || f.endsWith(ext));
57
+ let count = 0;
58
+ for (const f of files) {
59
+ const s = path.join(src, f);
60
+ if (fs.statSync(s).isFile()) { fs.copyFileSync(s, path.join(dst, f)); count++; }
61
+ }
62
+ return count;
63
+ }
64
+
65
+ function copyDirRecursive(src, dst) {
66
+ if (!fs.existsSync(src)) return;
67
+ fs.mkdirSync(dst, { recursive: true });
68
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
69
+ const s = path.join(src, entry.name);
70
+ const d = path.join(dst, entry.name);
71
+ if (entry.isDirectory()) copyDirRecursive(s, d);
72
+ else fs.copyFileSync(s, d);
73
+ }
74
+ }
75
+
76
+ function readJson(p, fallback = {}) {
77
+ if (!fs.existsSync(p)) return fallback;
78
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
79
+ }
80
+
81
+ function writeJson(p, data, indent = 2) {
82
+ fs.mkdirSync(path.dirname(p), { recursive: true });
83
+ fs.writeFileSync(p, JSON.stringify(data, null, indent));
84
+ }
85
+
86
+ // ── Frontmatter parser ────────────────────────────────────────────────────────
87
+ function readFrontmatter(filePath) {
88
+ try {
89
+ const content = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
90
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
91
+ if (!match) return {};
92
+ const fm = {};
93
+ let key = null, multiline = false, multilineVal = '';
94
+ for (const line of match[1].split('\n')) {
95
+ if (multiline) {
96
+ if (/^\s+/.test(line)) { multilineVal += ' ' + line.trim(); continue; }
97
+ fm[key] = multilineVal.trim();
98
+ multiline = false;
99
+ }
100
+ const kv = line.match(/^(\w+):\s*(>-|>)?\s*(.*)?$/);
101
+ if (!kv) continue;
102
+ key = kv[1];
103
+ if (kv[2]) { multiline = true; multilineVal = kv[3] || ''; }
104
+ else fm[key] = kv[3] || '';
105
+ }
106
+ if (multiline && key) fm[key] = multilineVal.trim();
107
+ return fm;
108
+ } catch { return {}; }
109
+ }
110
+
111
+ // ── Plugin cache builder (shared between CLI + Cowork) ────────────────────────
112
+ function buildPluginCache(cacheDir, version, gitSha) {
113
+ fs.mkdirSync(cacheDir, { recursive: true });
114
+ for (const item of ['.claude-plugin', 'commands', 'skills', 'guides', 'hooks', 'package.json', 'README.md']) {
115
+ const src = path.join(repoRoot, item);
116
+ if (!fs.existsSync(src)) continue;
117
+ const dst = path.join(cacheDir, item);
118
+ if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dst);
119
+ else fs.copyFileSync(src, dst);
120
+ }
121
+ }
122
+
123
+ // ── Cache flush helper ────────────────────────────────────────────────────────
124
+ function flushOldCaches(cacheBase, keepVersion) {
125
+ if (!fs.existsSync(cacheBase)) return 0;
126
+ let flushed = 0;
127
+ for (const entry of fs.readdirSync(cacheBase)) {
128
+ if (entry === keepVersion || entry === 'latest') continue;
129
+ try {
130
+ fs.rmSync(path.join(cacheBase, entry), { recursive: true, force: true });
131
+ flushed++;
132
+ } catch {}
133
+ }
134
+ return flushed;
135
+ }
136
+
137
+ // ── Step 2: CLI plugin registration (→ ~/.claude/plugins/) ───────────────────
138
+ function registerCLI(version, gitSha) {
139
+ const pluginDir = path.join(claudeHome, 'plugins');
140
+ const mktDir = path.join(pluginDir, 'marketplaces', MARKETPLACE);
141
+ const mktPlugDir = path.join(mktDir, '.claude-plugin');
142
+ const cacheBase = path.join(pluginDir, 'cache', MARKETPLACE, 'rdc-skills');
143
+ const cacheDir = path.join(cacheBase, version);
144
+ const latestDir = path.join(cacheBase, 'latest');
145
+
146
+ // 1. Marketplace manifest
147
+ fs.mkdirSync(mktPlugDir, { recursive: true });
148
+ fs.copyFileSync(path.join(repoRoot, '.claude-plugin', 'marketplace.json'), path.join(mktPlugDir, 'marketplace.json'));
149
+
150
+ // 2. known_marketplaces.json
151
+ const kmpPath = path.join(pluginDir, 'known_marketplaces.json');
152
+ const knownMp = readJson(kmpPath);
153
+ knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
154
+ writeJson(kmpPath, knownMp, 4);
155
+
156
+ // 3. Flush stale version caches, then write versioned + stable latest
157
+ const flushed = flushOldCaches(cacheBase, version);
158
+ if (flushed > 0) info(` flushed : ${flushed} stale cache dir(s)`);
159
+ buildPluginCache(cacheDir, version, gitSha);
160
+ // Write to stable 'latest/' so open terminals can pick up changes if they
161
+ // re-read installed_plugins.json between skill invocations.
162
+ if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
163
+ buildPluginCache(latestDir, version, gitSha);
164
+
165
+ // 4. installed_plugins.json — register 'latest/' as stable installPath so
166
+ // re-installs overwrite in-place rather than creating orphaned version dirs.
167
+ // Open terminals that re-read installed_plugins.json mid-session will pick
168
+ // up the updated path; otherwise a terminal restart is needed.
169
+ const ipPath = path.join(pluginDir, 'installed_plugins.json');
170
+ const installed = readJson(ipPath, { version: 2, plugins: {} });
171
+ // Also overwrite whichever installPath the old entry had, so any open
172
+ // terminal that already loaded that path sees fresh files.
173
+ const oldEntries = installed.plugins[PLUGIN_KEY] || [];
174
+ for (const old of oldEntries) {
175
+ if (old.installPath && fs.existsSync(old.installPath) && old.installPath !== latestDir) {
176
+ try { buildPluginCache(old.installPath, version, gitSha); } catch {}
177
+ }
178
+ }
179
+ for (const key of Object.keys(installed.plugins || {})) {
180
+ if (key.startsWith('rdc-skills@')) delete installed.plugins[key];
181
+ }
182
+ installed.plugins[PLUGIN_KEY] = [{ scope: 'user', installPath: latestDir, version, installedAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), gitCommitSha: gitSha }];
183
+ writeJson(ipPath, installed, 4);
184
+
185
+ // 5. settings.json enabledPlugins
186
+ const settings = readJson(settingsPath);
187
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
188
+ for (const key of Object.keys(settings.enabledPlugins)) {
189
+ if (key.startsWith('rdc-skills@')) delete settings.enabledPlugins[key];
190
+ }
191
+ settings.enabledPlugins[PLUGIN_KEY] = true;
192
+ writeJson(settingsPath, settings);
193
+
194
+ return latestDir;
195
+ }
196
+
197
+ // ── Step 3: Cowork (Claude Desktop) registration ──────────────────────────────
198
+ function findCoworkBases() {
199
+ // Cowork stores per-workspace state at:
200
+ // %LOCALAPPDATA%/Packages/Claude_*/LocalCache/Roaming/Claude/local-agent-mode-sessions/<workspace>/<device>/
201
+ // Each has cowork_settings.json + cowork_plugins/
202
+ const results = [];
203
+
204
+ // Candidate MSIX package roots
205
+ const localAppData = process.env.LOCALAPPDATA || '';
206
+ const pkgsDir = path.join(localAppData, 'Packages');
207
+ if (!fs.existsSync(pkgsDir)) return results;
208
+
209
+ let claudePkg = null;
210
+ for (const dir of fs.readdirSync(pkgsDir)) {
211
+ if (/^Claude_/i.test(dir)) { claudePkg = path.join(pkgsDir, dir); break; }
212
+ }
213
+ if (!claudePkg) return results;
214
+
215
+ const sessionsRoot = path.join(claudePkg, 'LocalCache', 'Roaming', 'Claude', 'local-agent-mode-sessions');
216
+ if (!fs.existsSync(sessionsRoot)) return results;
217
+
218
+ // Walk two levels: <workspace>/<device>/cowork_settings.json
219
+ for (const ws of fs.readdirSync(sessionsRoot)) {
220
+ const wsDir = path.join(sessionsRoot, ws);
221
+ if (!fs.statSync(wsDir).isDirectory()) continue;
222
+ for (const dev of fs.readdirSync(wsDir)) {
223
+ const devDir = path.join(wsDir, dev);
224
+ if (!fs.statSync(devDir).isDirectory()) continue;
225
+ const settingsFile = path.join(devDir, 'cowork_settings.json');
226
+ if (fs.existsSync(settingsFile)) {
227
+ results.push({ dir: devDir, settingsFile });
228
+ }
229
+ }
230
+ }
231
+ return results;
232
+ }
233
+
234
+ function registerCowork(version, gitSha) {
235
+ const bases = findCoworkBases();
236
+ if (bases.length === 0) {
237
+ warn('Cowork — Claude Desktop not found (MSIX package missing)');
238
+ return 0;
239
+ }
240
+
241
+ for (const { dir, settingsFile } of bases) {
242
+ const pluginsDir = path.join(dir, 'cowork_plugins');
243
+ const cacheBase = path.join(pluginsDir, 'cache', MARKETPLACE, 'rdc-skills');
244
+ const cacheDir = path.join(cacheBase, version);
245
+ const latestDir = path.join(cacheBase, 'latest');
246
+ const mktDir = path.join(pluginsDir, 'marketplaces', MARKETPLACE);
247
+ const mktPlugDir = path.join(mktDir, '.claude-plugin');
248
+
249
+ // Marketplace manifest
250
+ fs.mkdirSync(mktPlugDir, { recursive: true });
251
+ fs.copyFileSync(path.join(repoRoot, '.claude-plugin', 'marketplace.json'), path.join(mktPlugDir, 'marketplace.json'));
252
+
253
+ // known_marketplaces.json
254
+ const kmpPath = path.join(pluginsDir, 'known_marketplaces.json');
255
+ const knownMp = readJson(kmpPath);
256
+ knownMp[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' }, installLocation: mktDir, lastUpdated: new Date().toISOString() };
257
+ writeJson(kmpPath, knownMp, 4);
258
+
259
+ // Flush stale caches, write versioned + stable latest
260
+ flushOldCaches(cacheBase, version);
261
+ buildPluginCache(cacheDir, version, gitSha);
262
+ if (fs.existsSync(latestDir)) fs.rmSync(latestDir, { recursive: true, force: true });
263
+ buildPluginCache(latestDir, version, gitSha);
264
+
265
+ // installed_plugins.json — use stable 'latest/' path
266
+ const ipPath = path.join(pluginsDir, 'installed_plugins.json');
267
+ const installed = readJson(ipPath, { version: 2, plugins: {} });
268
+ for (const key of Object.keys(installed.plugins || {})) {
269
+ if (key.startsWith('rdc-skills@')) delete installed.plugins[key];
270
+ }
271
+ installed.plugins[PLUGIN_KEY] = [{ scope: 'user', installPath: latestDir, version, installedAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), gitCommitSha: gitSha }];
272
+ writeJson(ipPath, installed, 4);
273
+
274
+ // cowork_settings.json — enabledPlugins + extraKnownMarketplaces
275
+ const settings = readJson(settingsFile);
276
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
277
+ for (const key of Object.keys(settings.enabledPlugins)) {
278
+ if (key.startsWith('rdc-skills@')) delete settings.enabledPlugins[key];
279
+ }
280
+ settings.enabledPlugins[PLUGIN_KEY] = true;
281
+ if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
282
+ settings.extraKnownMarketplaces[MARKETPLACE] = { source: { source: 'github', repo: 'LIFEAI/rdc-skills' } };
283
+ writeJson(settingsFile, settings);
284
+ }
285
+
286
+ return bases.length;
287
+ }
288
+
289
+ // ── Step 6: Zip for claude.ai / distribution ─────────────────────────────────
290
+ function buildZip(version) {
291
+ const distDir = path.join(repoRoot, 'dist');
292
+ fs.mkdirSync(distDir, { recursive: true });
293
+ const zipPath = path.join(distDir, `rdc-skills-plugin-v${version}.zip`);
294
+
295
+ // Remove old zips
296
+ if (fs.existsSync(distDir)) {
297
+ for (const f of fs.readdirSync(distDir)) {
298
+ if (f.startsWith('rdc-skills-plugin') && f.endsWith('.zip')) {
299
+ fs.unlinkSync(path.join(distDir, f));
300
+ }
301
+ }
302
+ }
303
+
304
+ // Use PowerShell Compress-Archive on Windows, zip on Unix
305
+ const items = ['.claude-plugin', 'commands', 'skills', 'guides', 'hooks', 'package.json', 'README.md']
306
+ .filter(i => fs.existsSync(path.join(repoRoot, i)));
307
+
308
+ try {
309
+ if (process.platform === 'win32') {
310
+ // Stage to a temp dir so we get a clean zip root
311
+ const tmp = path.join(os.tmpdir(), `rdc-skills-zip-${Date.now()}`);
312
+ fs.mkdirSync(tmp, { recursive: true });
313
+ for (const item of items) {
314
+ const src = path.join(repoRoot, item);
315
+ const dst = path.join(tmp, item);
316
+ if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dst);
317
+ else fs.copyFileSync(src, dst);
318
+ }
319
+ // Try pwsh first (PowerShell 7+), fall back to Windows PowerShell
320
+ const psExe = fs.existsSync('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
321
+ ? 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
322
+ : 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
323
+ execSync(
324
+ `"${psExe}" -NoProfile -Command "Compress-Archive -Path '${tmp}\\*' -DestinationPath '${zipPath}' -Force"`,
325
+ { stdio: 'pipe' }
326
+ );
327
+ fs.rmSync(tmp, { recursive: true, force: true });
328
+ } else {
329
+ const itemList = items.join(' ');
330
+ execSync(`zip -r "${zipPath}" ${itemList}`, { cwd: repoRoot, stdio: 'pipe' });
331
+ }
332
+ return zipPath;
333
+ } catch (e) {
334
+ warn(`Zip failed: ${e.message}`);
335
+ return null;
336
+ }
337
+ }
338
+
339
+ // ── Hook config ───────────────────────────────────────────────────────────────
340
+ function buildHooksConfig(hooksDir) {
341
+ const base = hooksDir.replace(/\\/g, '/');
342
+ const cmd = (file, msg) => {
343
+ const entry = { type: 'command', command: `node "${base}/${file}"` };
344
+ if (msg) entry.statusMessage = msg;
345
+ return entry;
346
+ };
347
+ return {
348
+ SessionStart: [{ hooks: [
349
+ cmd('check-cwd.js'),
350
+ cmd('check-stale-work-items.js', 'Checking for stale work items...'),
351
+ ]}],
352
+ PreToolUse: [{ matcher: 'Bash', hooks: [
353
+ cmd('require-work-item-on-commit.js'),
354
+ ]}],
355
+ PostToolUse: [{ hooks: [
356
+ cmd('check-services.js'),
357
+ ]}],
358
+ PreCompact: [{ hooks: [
359
+ cmd('precompact-log.js'),
360
+ ]}],
361
+ PostCompact: [{ hooks: [
362
+ cmd('postcompact-log.js'),
363
+ cmd('restart-brief.js', 'Writing restart brief...'),
364
+ ]}],
365
+ Stop: [{ hooks: [
366
+ cmd('rate-limit-retry.js', 'Checking for rate limits...'),
367
+ cmd('post-work-check.js', 'Checking for undocumented work...'),
368
+ cmd('no-stop-open-epics.js', 'Checking for open epics...'),
369
+ ]}],
370
+ };
371
+ }
372
+
373
+ // ── Preflight ─────────────────────────────────────────────────────────────────
374
+ function runPreflight() {
375
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
376
+ if (nodeMajor < 18) { fail(`Node.js >= 18 required — found v${process.versions.node}`); process.exit(1); }
377
+ ok(`Node.js v${process.versions.node}`);
378
+ try {
379
+ execSync('curl -s --max-time 2 http://127.0.0.1:52437/ping', { stdio: 'pipe' });
380
+ ok('clauth daemon is running');
381
+ } catch {
382
+ warn('clauth daemon not responding — start it before using credential-dependent commands');
383
+ }
384
+ }
385
+
386
+ // ── Commands listing ──────────────────────────────────────────────────────────
387
+ function listCommands() {
388
+ const cmdsDir = path.join(repoRoot, 'commands');
389
+ if (!fs.existsSync(cmdsDir)) return;
390
+ const files = fs.readdirSync(cmdsDir).filter(f => f.endsWith('.md')).sort();
391
+ console.log('');
392
+ console.log(` \x1b[32mAvailable /rdc:* commands (${files.length}):\x1b[0m`);
393
+ console.log('');
394
+ const COL = 18;
395
+ for (const f of files) {
396
+ const name = 'rdc:' + f.replace(/\.md$/, '');
397
+ const fm = readFrontmatter(path.join(cmdsDir, f));
398
+ const desc = (fm.description || '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
399
+ const short = desc.length > 70 ? desc.slice(0, 70) + '…' : desc;
400
+ const pad = ' '.repeat(Math.max(1, COL - name.length));
401
+ console.log(` \x1b[36m/${name}\x1b[0m${pad}${short}`);
402
+ }
403
+ console.log('');
404
+ }
405
+
406
+ // ── Migrate helper ────────────────────────────────────────────────────────────
407
+ async function runMigrate(projectRoot) {
408
+ const absRoot = path.resolve(projectRoot);
409
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
410
+ const ask = q => new Promise(resolve => rl.question(q, resolve));
411
+
412
+ console.log('\n \x1b[35mrdc-skills — Migrate to .rdc/ Layout\x1b[0m\n');
413
+ console.log(` Project root: ${absRoot}\n`);
414
+
415
+ const candidates = [
416
+ { src: 'docs/guides', dst: '.rdc/guides' }, { src: 'docs/plans', dst: '.rdc/plans' },
417
+ { src: 'docs/reports', dst: '.rdc/reports' }, { src: 'docs/research', dst: '.rdc/research' },
418
+ ].filter(c => fs.existsSync(path.join(absRoot, c.src)));
419
+
420
+ if (candidates.length === 0) { info('No migratable directories found.'); rl.close(); return; }
421
+ console.log(' Found:');
422
+ candidates.forEach(c => console.log(` ${c.src}`));
423
+ console.log('');
424
+
425
+ for (const c of candidates) {
426
+ const srcAbs = path.join(absRoot, c.src);
427
+ const dstAbs = path.join(absRoot, c.dst);
428
+ const ans = await ask(` Move ${c.src} → ${c.dst}? [Y/n]: `);
429
+ if (ans.toLowerCase() === 'n') { info(`Skipped ${c.src}`); continue; }
430
+ if (fs.existsSync(dstAbs)) {
431
+ warn(`${c.dst} exists — merging`);
432
+ for (const f of fs.readdirSync(srcAbs)) {
433
+ const sf = path.join(srcAbs, f), df = path.join(dstAbs, f);
434
+ if (!fs.existsSync(df)) fs.renameSync(sf, df);
435
+ else warn(` Skipped ${f} (exists in dst)`);
436
+ }
437
+ } else {
438
+ fs.mkdirSync(path.dirname(dstAbs), { recursive: true });
439
+ fs.renameSync(srcAbs, dstAbs);
440
+ }
441
+ ok(`Moved ${c.src} → ${c.dst}`);
442
+ }
443
+ rl.close();
444
+ }
445
+
446
+ // ── Main ──────────────────────────────────────────────────────────────────────
447
+ async function main() {
448
+ if (doMigrate) { await runMigrate(migratePath); return; }
449
+
450
+ const bannerVersion = (readJson(path.join(repoRoot, 'package.json')).version || '?');
451
+ const bannerLine = `║ install-rdc-skills v${bannerVersion}`;
452
+ const bannerPadded = bannerLine.padEnd(41) + '║';
453
+ console.log('');
454
+ console.log(' \x1b[32m╔═══════════════════════════════════════╗\x1b[0m');
455
+ console.log(` \x1b[32m${bannerPadded}\x1b[0m`);
456
+ console.log(' \x1b[32m╚═══════════════════════════════════════╝\x1b[0m');
457
+ console.log('');
458
+ console.log(` CLAUDE_HOME : ${claudeHome}`);
459
+ console.log(` Plugin root : ${repoRoot}`);
460
+ console.log('');
461
+
462
+ if (!fs.existsSync(claudeHome)) {
463
+ fail(`CLAUDE_HOME not found: ${claudeHome}`);
464
+ process.exit(1);
465
+ }
466
+
467
+ // Read version + git SHA once
468
+ const pkg = readJson(path.join(repoRoot, 'package.json'));
469
+ const version = pkg.version || '0.7.0';
470
+ let gitSha = '';
471
+ try { gitSha = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim(); } catch {}
472
+
473
+ // 0. Pull latest
474
+ try {
475
+ const before = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
476
+ execSync('git pull --ff-only', { cwd: repoRoot, stdio: 'pipe' });
477
+ const after = execSync('git rev-parse HEAD', { cwd: repoRoot, encoding: 'utf8', stdio: 'pipe' }).trim();
478
+ ok(`[0/6] git pull — ${before === after ? after.slice(0, 7) : `${before.slice(0,7)} → ${after.slice(0,7)}`}`);
479
+ } catch {
480
+ warn('[0/6] git pull failed — installing from local copy');
481
+ }
482
+
483
+ // 0.5. Legacy cleanup — remove old commands/rdc/ (pre-plugin-system format)
484
+ const legacyCommandsDir = path.join(claudeHome, 'commands', 'rdc');
485
+ if (fs.existsSync(legacyCommandsDir)) {
486
+ fs.rmSync(legacyCommandsDir, { recursive: true, force: true });
487
+ ok('[0.5] Cleanup — removed legacy commands/rdc/');
488
+ }
489
+ // Also remove extraKnownMarketplaces.rdc-skills from settings.json to prevent
490
+ // Claude Code from syncing a directory-source marketplace back to known_marketplaces.json
491
+ // (which causes skills to load from both the cache AND the live source directory)
492
+ {
493
+ const st = readJson(settingsPath);
494
+ if (st.extraKnownMarketplaces && st.extraKnownMarketplaces[MARKETPLACE]) {
495
+ delete st.extraKnownMarketplaces[MARKETPLACE];
496
+ if (Object.keys(st.extraKnownMarketplaces).length === 0) delete st.extraKnownMarketplaces;
497
+ writeJson(settingsPath, st);
498
+ ok('[0.5] Cleanup — removed extraKnownMarketplaces.rdc-skills from settings.json');
499
+ }
500
+ }
501
+
502
+ // 1. CLI registration
503
+ const cliCacheDir = registerCLI(version, gitSha);
504
+ ok(`[1/6] CLI plugin — ${PLUGIN_KEY} v${version}`);
505
+ info(` cache : ${cliCacheDir}`);
506
+
507
+ // 2. Cowork registration
508
+ const coworkCount = registerCowork(version, gitSha);
509
+ if (coworkCount > 0) {
510
+ ok(`[2/6] Cowork — registered in ${coworkCount} workspace(s)`);
511
+ } else {
512
+ warn('[2/6] Cowork — no Desktop workspaces found (open Claude Desktop once to create them)');
513
+ }
514
+
515
+ // 3. Hook files
516
+ const hookCount = copyDir(hooksSrc, hooksDst, '.js');
517
+ ok(`[3/6] Hook files — ${hookCount} file(s) → ${hooksDst}`);
518
+
519
+ // 4. Hook wiring
520
+ if (skipHooks) {
521
+ info('[4/6] Hook wiring — skipped (--skip-hooks)');
522
+ } else {
523
+ const settings = readJson(settingsPath);
524
+ settings.hooks = buildHooksConfig(hooksDst);
525
+ writeJson(settingsPath, settings);
526
+ ok(`[4/6] Hook wiring — ${settingsPath}`);
527
+ }
528
+
529
+ // 5. Zip for claude.ai / distribution
530
+ const zipPath = buildZip(version);
531
+ if (zipPath) {
532
+ ok(`[5/6] Plugin zip — ${zipPath}`);
533
+ info(' claude.ai : upload this zip when prompted to add a plugin');
534
+ } else {
535
+ warn('[5/6] Plugin zip — build failed (zip/powershell missing?)');
536
+ }
537
+
538
+ // 6. Preflight
539
+ console.log('');
540
+ console.log(' \x1b[36mPreflight:\x1b[0m');
541
+ runPreflight();
542
+
543
+ // Done
544
+ console.log('');
545
+ console.log(' \x1b[32mDone!\x1b[0m');
546
+ console.log('');
547
+ console.log(' \x1b[33mNext steps:\x1b[0m');
548
+ console.log(' CLI : restart Claude Code — /rdc:status to verify');
549
+ console.log(' Cowork : restart Claude Desktop — /rdc:status in a new Cowork session');
550
+ console.log(' claude.ai : no plugin install needed — use FS MCP to read commands on demand');
551
+ console.log(' dist/rdc-skills-plugin-v' + version + '.zip available if needed');
552
+
553
+ listCommands();
554
+
555
+ console.log(' Docs: https://github.com/LIFEAI/rdc-skills#readme');
556
+ console.log('');
557
+ }
558
+
559
+ main().catch(e => { fail(e.message); process.exit(1); });