@ericrisco/rsc 0.1.20 → 0.1.21

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/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.20",
2
+ "version": "0.1.21",
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.20",
3
+ "version": "0.1.21",
4
4
  "description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +3,7 @@ import { rmSync, existsSync, cpSync, mkdirSync, readFileSync, writeFileSync } fr
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, baseDir } from '../targets/index.js';
6
+ import { targetPaths, writeSkill, wireHook, unwireHook, baseDir, TARGET_IDS } 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)), '..');
@@ -77,6 +77,34 @@ export async function uninstall({ skillIds, target, home, cwd = process.cwd(), d
77
77
  return removed;
78
78
  }
79
79
 
80
+ // Remove EVERYTHING rsc put in this project: installed skills across all targets,
81
+ // the wired hooks (settings.json entries / AGENTS-blocks / cursor rules), and the
82
+ // shared `.rsc/` (base + hook scripts + version marker). `02-DOCS/` is the user's
83
+ // own knowledge — kept unless `withDocs` is set. Returns the paths touched.
84
+ export async function purge({ home, cwd = process.cwd(), withDocs = false, dryRun = false } = {}) {
85
+ const removed = [];
86
+ const drop = (p, recursive = false) => {
87
+ if (!existsSync(p)) return;
88
+ removed.push(p);
89
+ if (!dryRun) rmSync(p, { recursive, force: true });
90
+ };
91
+ for (const target of TARGET_IDS) {
92
+ const paths = targetPaths(target, home, cwd);
93
+ if (existsSync(paths.stateFile)) {
94
+ const state = readState(paths.stateFile);
95
+ for (const id of Object.keys(state.skills || {})) {
96
+ for (const f of state.skills[id].files || []) drop(f, true);
97
+ }
98
+ drop(paths.stateFile);
99
+ }
100
+ // unwireHook mutates files, so only run it for real (dry runs skip it).
101
+ if (!dryRun) removed.push(...unwireHook(target, paths));
102
+ }
103
+ drop(join(cwd, '.rsc'), true);
104
+ if (withDocs) drop(join(cwd, '02-DOCS'), true);
105
+ return removed;
106
+ }
107
+
80
108
  if (import.meta.url === `file://${process.argv[1]}`) {
81
109
  const ids = process.argv.slice(2);
82
110
  applyInstall({ skillIds: ids, target: 'claude' }).then(() => console.log('installed', ids.join(', ')));
package/scripts/rsc.js CHANGED
@@ -4,7 +4,7 @@ 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';
7
- import { applyInstall, listInstalled, uninstall } from './install-apply.js';
7
+ import { applyInstall, listInstalled, uninstall, purge } from './install-apply.js';
8
8
  import { doctor } from './doctor.js';
9
9
  import { say, select, pickFrom, banner, confirm } from './lib/ui.js';
10
10
  import { refreshRegistry, registryStatus } from './lib/registry.js';
@@ -19,6 +19,15 @@ function flag(name) {
19
19
  return i >= 0 ? (argv[i + 1] || true) : undefined;
20
20
  }
21
21
 
22
+ // Remove everything rsc installed in this project (skills, hooks, .rsc/), across
23
+ // every assistant. Keeps 02-DOCS/ unless --with-docs. `purge` / `uninstall --all`.
24
+ async function runPurge(dryRun, withDocs) {
25
+ const removed = await purge({ cwd: process.cwd(), withDocs, dryRun });
26
+ say(`${dryRun ? 'Would remove' : 'Removed'} ${removed.length} path(s):`);
27
+ for (const r of removed) say(` - ${r}`);
28
+ if (!withDocs) say('\nKept 02-DOCS/ (your knowledge base). Add --with-docs to remove it too.');
29
+ }
30
+
22
31
  async function recommendIds(query, { labeledOnly = false } = {}) {
23
32
  const m = loadManifest();
24
33
  const repo = detectRepo();
@@ -207,13 +216,17 @@ async function main() {
207
216
  }
208
217
  case 'uninstall': {
209
218
  const dry = argv.includes('--dry-run');
219
+ // `uninstall --all` is an alias for a full purge.
220
+ if (argv.includes('--all')) return void (await runPurge(dry, argv.includes('--with-docs')));
210
221
  const ids = argv.slice(1).filter((a) => !a.startsWith('--'));
211
222
  const removed = await uninstall({ skillIds: ids, target, dryRun: dry });
212
223
  return void say((dry ? 'Would remove:\n' : 'Removed:\n') + (removed.join('\n') || '(nothing)'));
213
224
  }
225
+ case 'purge':
226
+ return void (await runPurge(argv.includes('--dry-run'), argv.includes('--with-docs')));
214
227
  default:
215
228
  say(`rsc: unknown command '${cmd}'.`);
216
- say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | audit | registry refresh | doctor | uninstall <id>');
229
+ say('Use: npx @ericrisco/rsc | add <id...> | install --profile <p> | consult "<text>" | list | audit | registry refresh | doctor | uninstall <id> | purge');
217
230
  }
218
231
  }
219
232
 
@@ -28,6 +28,19 @@ export function wireHook(paths, sourceMd) {
28
28
  return [paths.hookTarget];
29
29
  }
30
30
 
31
+ // Inverse of wireHook: remove the marked rsc-suggest block from the shared
32
+ // instructions file, leaving the user's own content intact. No-op when absent.
33
+ export function unwireHook(paths) {
34
+ if (!existsSync(paths.hookTarget)) return [];
35
+ const doc = readFileSync(paths.hookTarget, 'utf8');
36
+ if (!doc.includes(MARK_START)) return [];
37
+ const cleaned = doc
38
+ .replace(new RegExp(`\\n*${MARK_START}[\\s\\S]*?${MARK_END}\\n*`), '\n')
39
+ .replace(/\n{3,}/g, '\n\n');
40
+ writeFileSync(paths.hookTarget, cleaned);
41
+ return [paths.hookTarget];
42
+ }
43
+
31
44
  function stripFrontmatter(md) {
32
45
  return md.replace(/^---\n[\s\S]*?\n---\n?/, '');
33
46
  }
package/targets/claude.js CHANGED
@@ -14,6 +14,29 @@ export function writeSkill(id, fromDir, toPath) {
14
14
  return linkOrCopy(fromDir, toPath);
15
15
  }
16
16
 
17
+ // Inverse of wireHook: drop every rsc-wired hook entry (any command pointing at a
18
+ // .rsc/ script — session-start, worklog-checkpoint, ship-guard, danger-guard, … —
19
+ // plus the legacy cat-form) from settings.json, across all events. User hooks and
20
+ // other settings are preserved. Empty event arrays (and an empty hooks object) are
21
+ // pruned so we don't leave noise behind.
22
+ export function unwireHook(paths) {
23
+ const file = paths.hookTarget;
24
+ if (!existsSync(file)) return [];
25
+ let settings;
26
+ try { settings = JSON.parse(readFileSync(file, 'utf8')); } catch { return []; }
27
+ if (!settings.hooks) return [];
28
+ for (const event of Object.keys(settings.hooks)) {
29
+ settings.hooks[event] = (settings.hooks[event] || []).filter((e) => {
30
+ const s = JSON.stringify(e);
31
+ return !s.includes('.rsc/') && !s.includes('skills/rsc/suggest');
32
+ });
33
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
34
+ }
35
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
36
+ writeFileSync(file, JSON.stringify(settings, null, 2) + '\n');
37
+ return [file];
38
+ }
39
+
17
40
  // SessionStart runs a project-local session-start.mjs via `node`: it prints
18
41
  // suggest's always-on body, an onboarding banner when the workspace has no harness
19
42
  // profile yet, and an auto-ingest nudge when the inbox has un-ingested material. We
package/targets/cursor.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
 
4
4
  export function writeSkill(id, fromDir, toPath) {
@@ -15,6 +15,13 @@ export function wireHook(paths, sourceMd) {
15
15
  return [paths.hookTarget];
16
16
  }
17
17
 
18
+ // Inverse of wireHook: the cursor detector is its own file, so just remove it.
19
+ export function unwireHook(paths) {
20
+ if (!existsSync(paths.hookTarget)) return [];
21
+ rmSync(paths.hookTarget, { force: true });
22
+ return [paths.hookTarget];
23
+ }
24
+
18
25
  function stripFrontmatter(md) {
19
26
  return md.replace(/^---\n[\s\S]*?\n---\n?/, '');
20
27
  }
package/targets/index.js CHANGED
@@ -125,3 +125,13 @@ export function writeSkill(target, id, fromDir, toPath) {
125
125
  export function wireHook(target, paths, sourceMd) {
126
126
  return ADAPTER[SPEC[target].adapter].wireHook(paths, sourceMd);
127
127
  }
128
+
129
+ // Inverse of wireHook — remove rsc's always-on surface for a target (settings.json
130
+ // hook entries / AGENTS-block / cursor rule file). Returns the paths it touched.
131
+ export function unwireHook(target, paths) {
132
+ const adapter = ADAPTER[SPEC[target].adapter];
133
+ return adapter.unwireHook ? adapter.unwireHook(paths) : [];
134
+ }
135
+
136
+ // Every known target id — used by `purge` to sweep all assistants, installed or not.
137
+ export const TARGET_IDS = Object.keys(SPEC);