@ericrisco/rsc 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,24 @@
1
1
  <div align="center">
2
2
 
3
3
  ```
4
- ██████╗ ███████╗ ██████╗
5
- ██╔══██╗██╔════╝██╔════╝
6
- ██████╔╝███████╗██║
7
- ██╔══██╗╚════██║██║
8
- ██║ ██║███████║╚██████╗
9
- ╚═╝ ╚═╝╚══════╝ ╚═════╝
4
+ ████████ ███████ ███████
5
+ ██ ██ ██ ██ ██
6
+ ██ ██ ██ ██
7
+ ██ ██ ██ ██
8
+ ████████ ██████ ██
9
+ ██ ██ ██ ██
10
+ ██ ██ ██ ██
11
+ ██ ██ ██ ██ ██
12
+ ██ ██ ███████ ███████
10
13
  ```
11
14
 
12
15
  # `rsc` — 231 agent skills, one CLI, zero bloat
13
16
 
14
- **A self-recommending skill catalog for Claude Code, Cursor, Codex & Gemini.**
17
+ **A self-recommending skill catalog for 17 coding assistants** Claude Code,
18
+ Codex, GitHub Copilot, Cursor, Gemini, Windsurf, Cline, Antigravity, Zed and more.
15
19
  Describe what you want in plain language. It reads your repo, installs *only* the
16
- skills that fit — one at a time — and keeps your assistant equipped as you work.
20
+ skills that fit — one at a time — into every assistant you pick, and keeps them
21
+ equipped as you work.
17
22
 
18
23
  From *"document my company"* to *"ship a FastAPI service"* to *"grow my YouTube
19
24
  channel"* — **231 skills across 21 domains**, every one researched against live
@@ -70,9 +75,17 @@ git clone https://github.com/ericrisco/skills.git ~/rsc-skills
70
75
  cd ~/rsc-skills && npm install && npm link
71
76
  ```
72
77
 
73
- The first run installs the **floor**`orient` + `rsc-suggest` (always-on) +
74
- `harness` + `init` and, in Claude Code, wires a `SessionStart` hook so your
75
- assistant proposes new skills on its own from then on.
78
+ The first run asks **which assistants** you want Claude Code, Codex, Copilot,
79
+ Cursor, Gemini, Windsurf, Cline and 11 more (pick any combination) and installs
80
+ the **floor**:
81
+ `orient` + `rsc-suggest` (always-on) + `harness` + `init`. In Claude Code it
82
+ also wires a `SessionStart` hook so your assistant proposes new skills on its
83
+ own from then on.
84
+
85
+ Everything stays **in the project**, and the real skill files are written
86
+ **once** to `.rsc/skills/<id>/`. Each assistant you pick gets a lightweight
87
+ symlink back to that shared base — no copy is duplicated across IDEs. (If the
88
+ filesystem can't symlink, it falls back to a real copy automatically.)
76
89
 
77
90
  ---
78
91
 
@@ -102,18 +115,33 @@ Languages: ↑↓ move · space toggle · a all · enter c
102
115
  ◯ rust
103
116
  ```
104
117
 
105
- It auto-detects your IDE and stack, installs only what you chose, then prints the
106
- exact next steps for **Claude Code / Cursor / Codex / Gemini** — and from there
107
- keeps proposing the skills a task needs.
118
+ Then it asks **which assistants** to install for tick as many as you like:
119
+
120
+ ```
121
+ Which assistants do you want to install for? space toggle · a all · enter confirm
122
+ ❯ ◉ Claude Code (.claude/skills/) ⟵ detected here
123
+ ◉ Codex CLI (AGENTS.md)
124
+ ◯ GitHub Copilot (.github/copilot-instructions.md)
125
+ ◯ Cursor (.cursor/rules/)
126
+ ◉ Windsurf (.windsurf/rules/)
127
+ ◯ Cline (.clinerules/)
128
+ …17 in total — Gemini, Antigravity, Zed, Continue, Roo, Amp, opencode, Jules, Junie, Kiro, Aider
129
+ ```
130
+
131
+ It detects your stack, asks which assistants to install for (the one it found in
132
+ your folder is pre-marked), installs only what you chose, then prints the exact
133
+ next steps for **Claude Code / Codex / Cursor / Gemini / Antigravity** — and from
134
+ there keeps proposing the skills a task needs.
108
135
 
109
136
  ---
110
137
 
111
138
  ## The CLI
112
139
 
113
140
  ```bash
114
- rsc # plain-language wizard (recommended)
141
+ rsc # plain-language wizard (recommended) — pick skills AND assistants
115
142
  rsc add fastapi postgresdb # install specific skills, by name
116
143
  rsc add youtube-api remotion-video # …grow a channel, edit with Remotion
144
+ rsc add fastapi --target claude,codex # install into several assistants at once
117
145
  rsc install --profile minimal # the floor: orient + suggest + harness + init
118
146
  rsc install --profile core # floor + the full SDD workflow
119
147
  rsc install --profile full # everything (all 231)
@@ -238,15 +266,33 @@ Each with a `02-DOCS` feedback loop that learns from your own results. `remotion
238
266
 
239
267
  ## Multi-target
240
268
 
241
- `skills/<name>/` is the source; the installer writes the right format for your
242
- IDE (auto-detected, or `--target`):
269
+ `skills/<name>/` is the catalog source. On install the real files land **once**
270
+ in the project at `.rsc/skills/<id>/`; each assistant you pick gets a symlink
271
+ (or a converted file) back to that shared base — pick several and nothing is
272
+ duplicated. The wizard asks which ones; `--target a,b` does it non-interactively.
243
273
 
244
- | Target | Destination | Always-on detector |
274
+ | Target | Skill destination (→ `.rsc/skills/<id>/`) | Always-on detector |
245
275
  | --- | --- | --- |
246
- | `claude` | `~/.claude/skills/rsc/<id>/` | SessionStart hook in `settings.json` |
247
- | `cursor` | `.cursor/rules/<id>.mdc` | always-apply rule |
248
- | `codex` | `.codex/rsc/<id>/` + `AGENTS.md` | block in `AGENTS.md` |
249
- | `gemini` | `.gemini/rsc/<id>/` + `GEMINI.md` | block in `GEMINI.md` |
276
+ | `claude` | `.claude/skills/rsc/<id>/` → symlink | SessionStart hook in `.claude/settings.json` |
277
+ | `codex` | `.codex/rsc/<id>/` → symlink | block in `AGENTS.md` |
278
+ | `copilot` | `.github/rsc/<id>/` symlink | block in `.github/copilot-instructions.md` |
279
+ | `cursor` | `.cursor/rules/<id>.mdc` (converted) | always-apply rule |
280
+ | `gemini` | `.gemini/rsc/<id>/` → symlink | block in `GEMINI.md` |
281
+ | `windsurf` | `.windsurf/rsc/<id>/` → symlink | rule in `.windsurf/rules/rsc-suggest.md` |
282
+ | `cline` | `.clinerules/rsc/<id>/` → symlink | rule in `.clinerules/rsc-suggest.md` |
283
+ | `antigravity` | `.antigravity/rsc/<id>/` → symlink | block in `.antigravity/AGENTS.md` |
284
+ | `zed` | `.zed/rsc/<id>/` → symlink | block in `AGENTS.md` |
285
+ | `continue` | `.continue/rsc/<id>/` → symlink | rule in `.continue/rules/rsc-suggest.md` |
286
+ | `roo` | `.roo/rsc/<id>/` → symlink | rule in `.roo/rules/rsc-suggest.md` |
287
+ | `amp` | `.amp/rsc/<id>/` → symlink | block in `AGENTS.md` |
288
+ | `opencode` | `.opencode/rsc/<id>/` → symlink | block in `AGENTS.md` |
289
+ | `jules` | `.jules/rsc/<id>/` → symlink | block in `AGENTS.md` |
290
+ | `junie` | `.junie/rsc/<id>/` → symlink | block in `.junie/guidelines.md` |
291
+ | `kiro` | `.kiro/rsc/<id>/` → symlink | doc in `.kiro/steering/rsc-suggest.md` |
292
+ | `aider` | `.aider/rsc/<id>/` → symlink | block in `CONVENTIONS.md` |
293
+
294
+ > `codex`, `zed`, `amp`, `opencode` and `jules` all share the one root
295
+ > `AGENTS.md`; the block is idempotent, so picking several writes it once.
250
296
 
251
297
  ---
252
298
 
package/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.9",
2
+ "version": "0.1.11",
3
3
  "counts": {
4
4
  "skills": 231
5
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "cursor",
41
41
  "codex",
42
42
  "gemini",
43
+ "antigravity",
43
44
  "cli",
44
45
  "skills",
45
46
  "ai"
package/scripts/doctor.js CHANGED
@@ -3,8 +3,8 @@ import { targetPaths } from '../targets/index.js';
3
3
  import { readState } from './lib/state.js';
4
4
  import { loadManifest } from './lib/manifest.js';
5
5
 
6
- export function doctor({ target, home }) {
7
- const paths = targetPaths(target, home);
6
+ export function doctor({ target, home, cwd }) {
7
+ const paths = targetPaths(target, home, cwd);
8
8
  const state = readState(paths.stateFile);
9
9
  const manifest = loadManifest();
10
10
  const report = {
@@ -1,36 +1,48 @@
1
1
  #!/usr/bin/env node
2
- import { rmSync, existsSync } from 'node:fs';
2
+ import { rmSync, existsSync, cpSync, mkdirSync } from 'node:fs';
3
3
  import { join, dirname } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { planInstall } from './install-plan.js';
6
- import { targetPaths, writeSkill, wireHook } from '../targets/index.js';
6
+ import { targetPaths, writeSkill, wireHook, baseDir } from '../targets/index.js';
7
7
  import { readState, writeState } from './lib/state.js';
8
8
 
9
9
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
10
10
 
11
- export async function applyInstall({ skillIds, target, home }) {
12
- const paths = targetPaths(target, home);
13
- const plan = planInstall({ skillIds, target, home });
11
+ // Materialize the real skill files into the project-local base exactly once.
12
+ // Re-installs and other assistants reuse it instead of copying again.
13
+ function ensureBase(id, cwd) {
14
+ const dest = baseDir(id, cwd);
15
+ if (!existsSync(dest)) {
16
+ mkdirSync(dirname(dest), { recursive: true });
17
+ cpSync(join(ROOT, 'skills', id), dest, { recursive: true });
18
+ }
19
+ return dest;
20
+ }
21
+
22
+ export async function applyInstall({ skillIds, target, home, cwd = process.cwd() }) {
23
+ const paths = targetPaths(target, home, cwd);
24
+ const plan = planInstall({ skillIds, target, home, cwd });
14
25
  const state = readState(paths.stateFile);
15
26
  for (const step of plan) {
16
27
  if (step.kind === 'skill') {
17
- const files = await writeSkill(target, step.id, join(ROOT, step.from), step.to);
18
- state.skills[step.id] = { files };
28
+ const base = ensureBase(step.id, cwd);
29
+ const files = await writeSkill(target, step.id, base, step.to);
30
+ state.skills[step.id] = { files, base };
19
31
  } else if (step.kind === 'hook') {
20
- await wireHook(target, paths, join(ROOT, 'skills', 'suggest', 'SKILL.md'));
32
+ await wireHook(target, paths, join(ensureBase('suggest', cwd), 'SKILL.md'));
21
33
  }
22
34
  }
23
35
  writeState(paths.stateFile, state);
24
36
  return state;
25
37
  }
26
38
 
27
- export function listInstalled({ target, home }) {
28
- const paths = targetPaths(target, home);
39
+ export function listInstalled({ target, home, cwd = process.cwd() }) {
40
+ const paths = targetPaths(target, home, cwd);
29
41
  return Object.keys(readState(paths.stateFile).skills);
30
42
  }
31
43
 
32
- export async function uninstall({ skillIds, target, home, dryRun }) {
33
- const paths = targetPaths(target, home);
44
+ export async function uninstall({ skillIds, target, home, cwd = process.cwd(), dryRun }) {
45
+ const paths = targetPaths(target, home, cwd);
34
46
  const state = readState(paths.stateFile);
35
47
  const removed = [];
36
48
  for (const id of skillIds) {
@@ -1,7 +1,7 @@
1
1
  import { targetPaths } from '../targets/index.js';
2
2
 
3
- export function planInstall({ skillIds, target, home }) {
4
- const t = targetPaths(target, home);
3
+ export function planInstall({ skillIds, target, home, cwd }) {
4
+ const t = targetPaths(target, home, cwd);
5
5
  const plan = [];
6
6
  for (const id of skillIds) {
7
7
  plan.push({ kind: 'skill', id, from: `skills/${id}`, to: t.skillDir(id) });
package/scripts/lib/ui.js CHANGED
@@ -36,26 +36,42 @@ const C = {
36
36
  green: (s) => `\x1b[32m${s}\x1b[39m`,
37
37
  };
38
38
 
39
- const ART = [
40
- ' ██████╗ ███████╗ ██████╗',
41
- ' ██╔══██╗██╔════╝██╔════╝',
42
- ' ██████╔╝███████╗██║ ',
43
- ' ██╔══██╗╚════██║██║ ',
44
- ' ██║ ██║███████║╚██████╗',
45
- ' ╚═╝ ╚═╝╚══════╝ ╚═════╝',
46
- ];
47
-
48
- // Vertical truecolor gradient (sky → violet), phase-shiftable for the wave.
49
- function gradientLine(text, row, phase) {
50
- const a = [56, 189, 248]; const b = [167, 139, 250];
51
- const t = ((row + phase) % ART.length) / (ART.length - 1);
52
- const r = Math.round(a[0] + (b[0] - a[0]) * t);
53
- const g = Math.round(a[1] + (b[1] - a[1]) * t);
54
- const bl = Math.round(a[2] + (b[2] - a[2]) * t);
55
- return `\x1b[38;2;${r};${g};${bl}m${text}\x1b[39m`;
39
+ // Big block RSC wordmark, built from per-letter grids so rows always align.
40
+ const _R = ['████████', '██ ██', '██ ██', '██ ██', '████████', '██ ██', '██ ██', '██ ██', '██ ██'];
41
+ const _S = [' ███████', '██', '██', '██', ' ██████', ' ██', ' ██', ' ██', '███████'];
42
+ const _C = [' ███████', '██ ██', '██', '██', '██', '██', '██', '██ ██', ' ███████'];
43
+ const _padW = 9;
44
+ const _pad = (s) => s + ' '.repeat(Math.max(0, _padW - [...s].length));
45
+ const ART = _R.map((_, i) => ` ${_pad(_R[i])} ${_pad(_S[i])} ${_pad(_C[i])}`);
46
+
47
+ // HSL → RGB (s=1, l=0.6) for a true rainbow.
48
+ function hsl(h, s = 1, l = 0.6) {
49
+ const c = (1 - Math.abs(2 * l - 1)) * s;
50
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
51
+ const m = l - c / 2;
52
+ let r = 0; let g = 0; let b = 0;
53
+ if (h < 60) { r = c; g = x; } else if (h < 120) { r = x; g = c; }
54
+ else if (h < 180) { g = c; b = x; } else if (h < 240) { g = x; b = c; }
55
+ else if (h < 300) { r = x; b = c; } else { r = c; b = x; }
56
+ return [Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255)];
56
57
  }
57
58
 
58
- // Animated ASCII wordmark the "wooow". Static plain text when not a TTY.
59
+ // One frame: rainbow per character, diagonal phase, revealed up to `cols`.
60
+ function frame(phase, cols) {
61
+ return ART.map((line, r) => {
62
+ const chars = [...line];
63
+ let out = '';
64
+ for (let c = 0; c < chars.length && c < cols; c++) {
65
+ const ch = chars[c];
66
+ if (ch === ' ') { out += ' '; continue; }
67
+ const [rr, gg, bb] = hsl((c * 5 + r * 14 + phase * 20) % 360);
68
+ out += `\x1b[1;38;2;${rr};${gg};${bb}m${ch}`;
69
+ }
70
+ return `\x1b[2K${out}\x1b[0m`;
71
+ }).join('\n') + '\n';
72
+ }
73
+
74
+ // Animated ASCII wordmark — the exaggerated "WOW". Static plain text when not a TTY.
59
75
  export async function banner() {
60
76
  if (!stdout.isTTY) {
61
77
  say('');
@@ -63,23 +79,36 @@ export async function banner() {
63
79
  say(' 231 skills · one CLI · zero bloat');
64
80
  return;
65
81
  }
66
- const W = Math.max(...ART.map((l) => l.length));
82
+ const rows = ART.length;
83
+ const W = Math.max(...ART.map((l) => [...l].length));
67
84
  stdout.write('\x1b[?25l'); // hide cursor
68
85
  say('');
69
- // 1) letters slide in left → right, column by column
86
+ // 1) letters slide in left → right
70
87
  let first = true;
71
- for (let w = 1; w <= W; w++) {
72
- if (!first) stdout.write(`\x1b[${ART.length}A`);
88
+ for (let cols = 2; cols <= W; cols += 2) {
89
+ if (!first) stdout.write(`\x1b[${rows}A`);
73
90
  first = false;
74
- stdout.write(ART.map((l, i) => `\x1b[2K${gradientLine(l.slice(0, w), i, 0)}`).join('\n') + '\n');
75
- await sleep(20);
91
+ stdout.write(frame(0, cols));
92
+ await sleep(12);
93
+ }
94
+ // 2) flowing rainbow diagonal sweeps
95
+ for (let phase = 1; phase <= 30; phase++) {
96
+ stdout.write(`\x1b[${rows}A`);
97
+ stdout.write(frame(phase, W));
98
+ await sleep(26);
76
99
  }
77
- // 2) one flowing color wave to settle
78
- for (let phase = 1; phase <= ART.length; phase++) {
79
- stdout.write(`\x1b[${ART.length}A`);
80
- stdout.write(ART.map((l, i) => `\x1b[2K${gradientLine(l, i, phase)}`).join('\n') + '\n');
81
- await sleep(45);
100
+ // 3) double white flash the pop
101
+ for (let f = 0; f < 2; f++) {
102
+ stdout.write(`\x1b[${rows}A`);
103
+ stdout.write(ART.map((l) => `\x1b[2K\x1b[1;97m${l}\x1b[0m`).join('\n') + '\n');
104
+ await sleep(60);
105
+ stdout.write(`\x1b[${rows}A`);
106
+ stdout.write(frame(15, W));
107
+ await sleep(60);
82
108
  }
109
+ // settle on a final rainbow snapshot
110
+ stdout.write(`\x1b[${rows}A`);
111
+ stdout.write(frame(15, W));
83
112
  say(C.dim(' 231 skills · one CLI · zero bloat'));
84
113
  stdout.write('\x1b[?25h'); // show cursor
85
114
  }
package/scripts/rsc.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { loadManifest, skillsForProfile } from './lib/manifest.js';
3
- import { detectTarget } from '../targets/index.js';
3
+ import { detectTarget, TARGETS } from '../targets/index.js';
4
4
  import { detectRepo } from './detect-repo.js';
5
5
  import { rank } from './consult.js';
6
6
  import { expandRecommends, toOutcomes, hasOutcome } from './lib/recommend.js';
@@ -45,18 +45,26 @@ async function manualSelect() {
45
45
  return [...chosen];
46
46
  }
47
47
 
48
+ // Ask which assistants to install into. The detected one is pre-labelled but
49
+ // nothing is auto-applied — the user always confirms the set (one or many).
50
+ async function selectAgents() {
51
+ const detected = detectTarget();
52
+ const items = TARGETS.map((t) => ({
53
+ id: t.id,
54
+ label: `${t.label} (${t.hint})${t.id === detected ? ' ⟵ detected here' : ''}`,
55
+ }));
56
+ const chosen = await pickFrom('Which assistants do you want to install for? (space to toggle, a = all)', items);
57
+ return chosen.length ? chosen : [detected];
58
+ }
59
+
48
60
  // After installing, remind the user how to actually start — per IDE — and that
49
61
  // rsc keeps recommending skills as they work. The harness/SDD *init* runs INSIDE
50
62
  // the assistant (with the user present), never blindly from this CLI.
51
- function printNextSteps(target, ids) {
63
+ function printNextSteps(targets, ids) {
52
64
  const hasHarness = ids.includes('harness');
53
65
  const hasSdd = ids.includes('sdd') || ids.includes('sdd-init');
54
- const openLine = {
55
- claude: 'Open **Claude Code** in this project folder.',
56
- cursor: 'Open **Cursor** in this project folder.',
57
- codex: 'Open **Codex CLI** (it reads AGENTS.md) in this project folder.',
58
- gemini: 'Open **Gemini CLI** in this project folder.',
59
- }[target] || 'Open your assistant in this project folder.';
66
+ const label = (id) => TARGETS.find((t) => t.id === id)?.label || id;
67
+ const openLine = `Open this project in: ${targets.map((t) => `**${label(t)}**`).join(' · ')}`;
60
68
 
61
69
  say('\n────────────────────────────────────────────────────────');
62
70
  say('👉 When you start working (these steps happen in your assistant, not here):');
@@ -82,7 +90,7 @@ function printNextSteps(target, ids) {
82
90
  async function wizard() {
83
91
  const m = loadManifest();
84
92
  await banner();
85
- say(' the skill catalog for your assistant (Claude Code · Cursor · Codex · Gemini)\n');
93
+ say(' the skill catalog for your assistant (Claude Code · Codex · Cursor · Gemini · Antigravity)\n');
86
94
  const choice = await select('What do you want to do?', [
87
95
  { key: 'base', label: 'Base install — the essentials (orient + suggest + harness + init)' },
88
96
  { key: 'sdd', label: 'Base + Spec-Driven Development — the specify → plan → implement → ship flow' },
@@ -102,33 +110,50 @@ async function wizard() {
102
110
  return;
103
111
  }
104
112
 
105
- const target = detectTarget();
106
- say(`\nI'll install ${ids.length} skills into ${target}:`);
113
+ const targets = await selectAgents();
114
+ say(`\nI'll install ${ids.length} skills for: ${targets.join(', ')}`);
107
115
  say(' ' + ids.join(', '));
116
+ say(' (real files live once in .rsc/skills/ — each assistant just links to them)');
108
117
  if (!(await confirm('Install it?'))) {
109
118
  say('No problem. Anytime: npx @ericrisco/rsc');
110
119
  return;
111
120
  }
112
- await applyInstall({ skillIds: ids, target });
113
- say(`\n✅ Installed ${ids.length} skills into ${target}.`);
114
- printNextSteps(target, ids);
121
+ for (const target of targets) {
122
+ await applyInstall({ skillIds: ids, target });
123
+ say(` ✅ ${target}`);
124
+ }
125
+ say(`\n✅ Installed ${ids.length} skills for ${targets.length} assistant(s).`);
126
+ printNextSteps(targets, ids);
115
127
  }
116
128
 
117
129
  async function main() {
118
- const target = flag('target') || detectTarget();
130
+ // --target accepts one id or a comma list (e.g. --target claude,codex). No flag → detect.
131
+ const f = flag('target');
132
+ const targets = typeof f === 'string'
133
+ ? f.split(',').map((s) => s.trim()).filter(Boolean)
134
+ : [detectTarget()];
135
+ const target = targets[0];
119
136
  switch (cmd) {
120
137
  case undefined:
121
138
  return wizard();
122
- case 'add':
123
- await applyInstall({ skillIds: [...new Set(['orient', 'suggest', ...argv.slice(1)])], target });
124
- return void say(`✅ Installed: ${argv.slice(1).join(', ')}`);
139
+ case 'add': {
140
+ // Positional args = skill ids; skip flags and any flag value (the token after a --flag).
141
+ const requested = [];
142
+ for (let i = 1; i < argv.length; i++) {
143
+ if (argv[i].startsWith('--')) { i++; continue; }
144
+ requested.push(argv[i]);
145
+ }
146
+ const ids = [...new Set(['orient', 'suggest', ...requested])];
147
+ for (const t of targets) await applyInstall({ skillIds: ids, target: t });
148
+ return void say(`✅ Installed for ${targets.join(', ')}: ${requested.join(', ')}`);
149
+ }
125
150
  case 'install': {
126
151
  const profile = flag('profile') || 'minimal';
127
152
  const without = argv.filter((a, i) => argv[i - 1] === '--without');
128
153
  let ids = skillsForProfile(loadManifest(), profile);
129
154
  ids = [...new Set(['orient', 'suggest', ...ids])].filter((id) => !without.includes(id));
130
- await applyInstall({ skillIds: ids, target });
131
- return void say(`✅ Profile '${profile}' installed into ${target} (${ids.length} skills)`);
155
+ for (const t of targets) await applyInstall({ skillIds: ids, target: t });
156
+ return void say(`✅ Profile '${profile}' installed for ${targets.join(', ')} (${ids.length} skills)`);
132
157
  }
133
158
  case 'consult': {
134
159
  const ids = await recommendIds(argv.slice(1).join(' '));
@@ -1,13 +1,17 @@
1
- import { cpSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
+ import { linkOrCopy } from './index.js';
3
4
 
5
+ // Shared adapter for every assistant whose "always-on" surface is a plain
6
+ // markdown instructions/rules file (AGENTS.md, copilot-instructions.md, a
7
+ // .windsurf/.roo/.continue rule, …). Skills are symlinked to the shared base;
8
+ // the suggest block is injected between idempotent markers so re-installs and
9
+ // multiple assistants sharing one file never duplicate it.
4
10
  const MARK_START = '<!-- rsc-suggest:start -->';
5
11
  const MARK_END = '<!-- rsc-suggest:end -->';
6
12
 
7
13
  export function writeSkill(id, fromDir, toPath) {
8
- mkdirSync(dirname(toPath), { recursive: true });
9
- cpSync(fromDir, toPath, { recursive: true });
10
- return [toPath];
14
+ return linkOrCopy(fromDir, toPath);
11
15
  }
12
16
 
13
17
  export function wireHook(paths, sourceMd) {
package/targets/claude.js CHANGED
@@ -1,10 +1,9 @@
1
- import { cpSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
2
  import { dirname } from 'node:path';
3
+ import { linkOrCopy } from './index.js';
3
4
 
4
5
  export function writeSkill(id, fromDir, toPath) {
5
- mkdirSync(dirname(toPath), { recursive: true });
6
- cpSync(fromDir, toPath, { recursive: true });
7
- return [toPath];
6
+ return linkOrCopy(fromDir, toPath);
8
7
  }
9
8
 
10
9
  export function wireHook(paths) {
package/targets/index.js CHANGED
@@ -1,55 +1,120 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync, cpSync } from 'node:fs';
2
+ import { join, dirname, relative } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
+ import * as claudeAdapter from './claude.js';
5
+ import * as cursorAdapter from './cursor.js';
6
+ import * as mdAdapter from './_md-block.js';
4
7
 
8
+ // Project-local single source of truth. Real skill files live here exactly once;
9
+ // every assistant gets a lightweight pointer (symlink) back to it — no duplication.
10
+ export function baseDir(id, cwd = process.cwd()) {
11
+ return join(cwd, '.rsc', 'skills', id);
12
+ }
13
+
14
+ // Point an assistant's skill folder at the shared base via a relative symlink.
15
+ // Falls back to a real copy when the filesystem rejects symlinks (e.g. Windows
16
+ // without privileges). Idempotent: replaces any existing link/dir at toPath.
17
+ export function linkOrCopy(fromDir, toPath) {
18
+ mkdirSync(dirname(toPath), { recursive: true });
19
+ try { lstatSync(toPath); rmSync(toPath, { recursive: true, force: true }); } catch { /* nothing there */ }
20
+ try {
21
+ symlinkSync(relative(dirname(toPath), fromDir), toPath, 'dir');
22
+ } catch {
23
+ cpSync(fromDir, toPath, { recursive: true });
24
+ }
25
+ return [toPath];
26
+ }
27
+
28
+ // One row per assistant. `root` is where its skill folder lives (relative to the
29
+ // project), `hook` is the file that gets the always-on suggest block, `adapter`
30
+ // picks how skills + hook are written. `skillExt` (cursor only) means each skill
31
+ // is a single converted file, not a linked directory.
32
+ const SPEC = {
33
+ // JSON-hook + linked skill dirs
34
+ claude: { root: '.claude/skills/rsc', hook: '.claude/settings.json', adapter: 'claude' },
35
+ // Converted .mdc rules
36
+ cursor: { root: '.cursor/rules', hook: '.cursor/rules/rsc-suggest.mdc', adapter: 'cursor', skillExt: '.mdc' },
37
+ // AGENTS.md family — all read the same root AGENTS.md
38
+ codex: { root: '.codex/rsc', hook: 'AGENTS.md', adapter: 'md' },
39
+ opencode: { root: '.opencode/rsc', hook: 'AGENTS.md', adapter: 'md' },
40
+ amp: { root: '.amp/rsc', hook: 'AGENTS.md', adapter: 'md' },
41
+ jules: { root: '.jules/rsc', hook: 'AGENTS.md', adapter: 'md' },
42
+ zed: { root: '.zed/rsc', hook: 'AGENTS.md', adapter: 'md' },
43
+ // Own markdown instructions/rules file
44
+ gemini: { root: '.gemini/rsc', hook: 'GEMINI.md', adapter: 'md' },
45
+ antigravity: { root: '.antigravity/rsc', hook: '.antigravity/AGENTS.md', adapter: 'md' },
46
+ copilot: { root: '.github/rsc', hook: '.github/copilot-instructions.md', adapter: 'md' },
47
+ windsurf: { root: '.windsurf/rsc', hook: '.windsurf/rules/rsc-suggest.md', adapter: 'md' },
48
+ cline: { root: '.clinerules/rsc', hook: '.clinerules/rsc-suggest.md', adapter: 'md' },
49
+ roo: { root: '.roo/rsc', hook: '.roo/rules/rsc-suggest.md', adapter: 'md' },
50
+ continue: { root: '.continue/rsc', hook: '.continue/rules/rsc-suggest.md', adapter: 'md' },
51
+ junie: { root: '.junie/rsc', hook: '.junie/guidelines.md', adapter: 'md' },
52
+ kiro: { root: '.kiro/rsc', hook: '.kiro/steering/rsc-suggest.md', adapter: 'md' },
53
+ aider: { root: '.aider/rsc', hook: 'CONVENTIONS.md', adapter: 'md' },
54
+ };
55
+
56
+ const ADAPTER = { claude: claudeAdapter, cursor: cursorAdapter, md: mdAdapter };
57
+
58
+ // Wizard multi-select list, in "most famous first" order. label/hint are display
59
+ // only; detectTarget just pre-marks the one found in the folder.
60
+ export const TARGETS = [
61
+ { id: 'claude', label: 'Claude Code', hint: '.claude/skills/' },
62
+ { id: 'codex', label: 'Codex CLI', hint: 'AGENTS.md' },
63
+ { id: 'copilot', label: 'GitHub Copilot', hint: '.github/copilot-instructions.md' },
64
+ { id: 'cursor', label: 'Cursor', hint: '.cursor/rules/' },
65
+ { id: 'gemini', label: 'Gemini CLI', hint: 'GEMINI.md' },
66
+ { id: 'windsurf', label: 'Windsurf', hint: '.windsurf/rules/' },
67
+ { id: 'cline', label: 'Cline', hint: '.clinerules/' },
68
+ { id: 'antigravity', label: 'Antigravity', hint: '.antigravity/' },
69
+ { id: 'zed', label: 'Zed', hint: 'AGENTS.md' },
70
+ { id: 'continue', label: 'Continue', hint: '.continue/rules/' },
71
+ { id: 'roo', label: 'Roo Code', hint: '.roo/rules/' },
72
+ { id: 'amp', label: 'Amp', hint: 'AGENTS.md' },
73
+ { id: 'opencode', label: 'opencode', hint: 'AGENTS.md' },
74
+ { id: 'jules', label: 'Jules', hint: 'AGENTS.md' },
75
+ { id: 'junie', label: 'JetBrains Junie', hint: '.junie/guidelines.md' },
76
+ { id: 'kiro', label: 'Kiro', hint: '.kiro/steering/' },
77
+ { id: 'aider', label: 'Aider', hint: 'CONVENTIONS.md' },
78
+ ];
79
+
80
+ // Best-effort default for the wizard's pre-mark. Unique config dirs win; the
81
+ // shared AGENTS.md / GEMINI.md fall through to codex / gemini.
5
82
  export function detectTarget(cwd = process.cwd()) {
6
- if (existsSync(join(cwd, '.cursor'))) return 'cursor';
7
- if (existsSync(join(cwd, '.codex')) || existsSync(join(cwd, 'AGENTS.md'))) return 'codex';
8
- if (existsSync(join(cwd, '.gemini')) || existsSync(join(cwd, 'GEMINI.md'))) return 'gemini';
83
+ const dir = (d) => existsSync(join(cwd, d));
84
+ if (dir('.cursor')) return 'cursor';
85
+ if (dir('.windsurf')) return 'windsurf';
86
+ if (dir('.clinerules')) return 'cline';
87
+ if (dir('.roo')) return 'roo';
88
+ if (dir('.continue')) return 'continue';
89
+ if (dir('.junie')) return 'junie';
90
+ if (dir('.kiro')) return 'kiro';
91
+ if (dir('.zed')) return 'zed';
92
+ if (dir('.opencode')) return 'opencode';
93
+ if (dir('.amp')) return 'amp';
94
+ if (dir('.jules')) return 'jules';
95
+ if (dir('.antigravity')) return 'antigravity';
96
+ if (existsSync(join(cwd, '.github', 'copilot-instructions.md'))) return 'copilot';
97
+ if (dir('.codex') || dir('AGENTS.md')) return 'codex';
98
+ if (dir('.gemini') || dir('GEMINI.md')) return 'gemini';
9
99
  return 'claude';
10
100
  }
11
101
 
12
102
  export function targetPaths(target, home = homedir(), cwd = process.cwd()) {
13
- switch (target) {
14
- case 'claude':
15
- return {
16
- root: join(home, '.claude', 'skills', 'rsc'),
17
- skillDir: (id) => join(home, '.claude', 'skills', 'rsc', id),
18
- stateFile: join(home, '.claude', 'skills', 'rsc', '.rsc-state.json'),
19
- hookTarget: join(home, '.claude', 'settings.json'),
20
- };
21
- case 'cursor':
22
- return {
23
- root: join(cwd, '.cursor', 'rules'),
24
- skillDir: (id) => join(cwd, '.cursor', 'rules', `${id}.mdc`),
25
- stateFile: join(cwd, '.cursor', 'rules', '.rsc-state.json'),
26
- hookTarget: join(cwd, '.cursor', 'rules', 'rsc-suggest.mdc'),
27
- };
28
- case 'codex':
29
- return {
30
- root: join(cwd, '.codex', 'rsc'),
31
- skillDir: (id) => join(cwd, '.codex', 'rsc', id),
32
- stateFile: join(cwd, '.codex', 'rsc', '.rsc-state.json'),
33
- hookTarget: join(cwd, 'AGENTS.md'),
34
- };
35
- case 'gemini':
36
- return {
37
- root: join(cwd, '.gemini', 'rsc'),
38
- skillDir: (id) => join(cwd, '.gemini', 'rsc', id),
39
- stateFile: join(cwd, '.gemini', 'rsc', '.rsc-state.json'),
40
- hookTarget: join(cwd, 'GEMINI.md'),
41
- };
42
- default:
43
- throw new Error(`unknown target ${target}`);
44
- }
103
+ const s = SPEC[target];
104
+ if (!s) throw new Error(`unknown target ${target}`);
105
+ const rootAbs = join(cwd, ...s.root.split('/'));
106
+ return {
107
+ root: rootAbs,
108
+ skillDir: (id) => (s.skillExt ? join(rootAbs, `${id}${s.skillExt}`) : join(rootAbs, id)),
109
+ stateFile: join(rootAbs, '.rsc-state.json'),
110
+ hookTarget: join(cwd, ...s.hook.split('/')),
111
+ };
45
112
  }
46
113
 
47
- export async function writeSkill(target, id, fromDir, toPath) {
48
- const { writeSkill: w } = await import(`./${target}.js`);
49
- return w(id, fromDir, toPath);
114
+ export function writeSkill(target, id, fromDir, toPath) {
115
+ return ADAPTER[SPEC[target].adapter].writeSkill(id, fromDir, toPath);
50
116
  }
51
117
 
52
- export async function wireHook(target, paths, sourceMd) {
53
- const { wireHook: w } = await import(`./${target}.js`);
54
- return w(paths, sourceMd);
118
+ export function wireHook(target, paths, sourceMd) {
119
+ return ADAPTER[SPEC[target].adapter].wireHook(paths, sourceMd);
55
120
  }
package/targets/gemini.js DELETED
@@ -1,29 +0,0 @@
1
- import { cpSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
-
4
- const MARK_START = '<!-- rsc-suggest:start -->';
5
- const MARK_END = '<!-- rsc-suggest:end -->';
6
-
7
- export function writeSkill(id, fromDir, toPath) {
8
- mkdirSync(dirname(toPath), { recursive: true });
9
- cpSync(fromDir, toPath, { recursive: true });
10
- return [toPath];
11
- }
12
-
13
- export function wireHook(paths, sourceMd) {
14
- const body = stripFrontmatter(readFileSync(sourceMd, 'utf8'));
15
- const block = `${MARK_START}\n${body}\n${MARK_END}`;
16
- let doc = existsSync(paths.hookTarget) ? readFileSync(paths.hookTarget, 'utf8') : '';
17
- if (doc.includes(MARK_START)) {
18
- doc = doc.replace(new RegExp(`${MARK_START}[\\s\\S]*?${MARK_END}`), block);
19
- } else {
20
- doc += `\n\n${block}\n`;
21
- }
22
- mkdirSync(dirname(paths.hookTarget), { recursive: true });
23
- writeFileSync(paths.hookTarget, doc);
24
- return [paths.hookTarget];
25
- }
26
-
27
- function stripFrontmatter(md) {
28
- return md.replace(/^---\n[\s\S]*?\n---\n?/, '');
29
- }