@ericrisco/rsc 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -53
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/install-apply.js +158 -32
- package/scripts/install-plan.js +2 -2
- package/scripts/lib/manifest-file.js +2 -1
- package/scripts/lib/onboarding-apply.js +206 -0
- package/scripts/lib/onboarding.js +301 -0
- package/scripts/rsc.js +224 -9
- package/scripts/spec-gate.js +143 -11
- package/skills/design/scripts/verify.sh +4 -1
- package/skills/suggest/SKILL.md +3 -0
- package/targets/_md-block.js +5 -2
- package/targets/claude.js +26 -4
- package/targets/cursor.js +5 -2
- package/targets/index.js +2 -2
- package/targets/session-memory-adapter.mjs +16 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ channel"* — **272 skills across 23 domains**, every one researched against liv
|
|
|
21
21
|
2025-2026 sources and **adversarially scored ≥ 8.5/10** before it shipped.
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npx @ericrisco/rsc
|
|
24
|
+
npx @ericrisco/rsc@latest onboard # interview → exact plan → explicit acceptance → verified install
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
<br>
|
|
@@ -44,14 +44,28 @@ asked you to **install rsc / this harness / these skills**, run this in the **ro
|
|
|
44
44
|
project they're equipping** — not inside a clone of this repo (see the note under *Install*):
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
npx @ericrisco/rsc@latest
|
|
47
|
+
npx @ericrisco/rsc@latest onboard
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
That launches
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
That launches the binding plain-language onboarding. It asks the user for technical level,
|
|
51
|
+
accompaniment, project kind and goal **before it writes anything**, reads only the selected project
|
|
52
|
+
root, and shows every selected or deferred skill, agent and hook with its reason. Relay those
|
|
53
|
+
questions to the user; never invent or default their answers. The first pass writes nothing and
|
|
54
|
+
prints a SHA-256 plan id. Only rerun with `--accept-plan <id>` after the user accepts that exact plan.
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
For a non-interactive agent, collect the answers and preview the same plan explicitly:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx @ericrisco/rsc@latest onboard --technical-level mixed --accompaniment L1 \
|
|
60
|
+
--project-kind software --goal "small compound-interest website" \
|
|
61
|
+
--software-scope small --target codex
|
|
62
|
+
# After the user accepts the printed plan:
|
|
63
|
+
npx @ericrisco/rsc@latest onboard --technical-level mixed --accompaniment L1 \
|
|
64
|
+
--project-kind software --goal "small compound-interest website" \
|
|
65
|
+
--software-scope small --target codex --accept-plan PRINTED_SHA256_ID
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **Choose assistants non-interactively:** add `--target claude` (comma-separate for several) to `onboard`.
|
|
55
69
|
- **Two assistants already installed?** rsc asks instead of guessing. `--target` settles it in one word.
|
|
56
70
|
- **Already installed, just refreshing skills + hooks:** `rsc sync` (or re-run the command above).
|
|
57
71
|
- **Add one skill by id:** `rsc add <id>` · **browse the catalog:** `rsc consult "<what you want>"` or `rsc list`.
|
|
@@ -118,7 +132,7 @@ injects at most 4,096 bytes. Disable every memory surface for a project with
|
|
|
118
132
|
## Install
|
|
119
133
|
|
|
120
134
|
```bash
|
|
121
|
-
npx @ericrisco/rsc
|
|
135
|
+
npx @ericrisco/rsc@latest onboard
|
|
122
136
|
```
|
|
123
137
|
|
|
124
138
|
Prefer the short `rsc` command? Install once, globally:
|
|
@@ -142,19 +156,12 @@ cd ~/rsc-skills && npm install && npm link
|
|
|
142
156
|
> `node scripts/rsc.js …`, the `npm link` above, or pin the published build with
|
|
143
157
|
> `npx @ericrisco/rsc@latest …`.
|
|
144
158
|
|
|
145
|
-
The first run asks **
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
on every turn — so a feature request, in any language, routes through `specify`
|
|
152
|
-
first, before any skill writes code. Opt out per project with `.rsc/.no-feature-gate`.
|
|
153
|
-
It also wires a `PreToolUse` **gitmoji guard**: a `git commit` whose message carries no
|
|
154
|
-
[gitmoji](https://gitmoji.dev) is denied, with the corrected message inside the refusal
|
|
155
|
-
(`✨ feat(api): add cursor paging`) — so `git log --oneline` stays readable at a glance instead of
|
|
156
|
-
depending on the agent remembering a convention. It only judges messages it can actually read
|
|
157
|
-
(`-m`, heredoc), never an editor commit or `--amend --no-edit`. Opt out with `.rsc/.no-gitmoji`.
|
|
159
|
+
The first run asks **how technical the conversation should be**, the accompaniment level, what the
|
|
160
|
+
project is for, its goal and the assistants to target. It then presents the complete plan. A small
|
|
161
|
+
website can defer SDD, agents and code guards; an operations harness does not receive them merely
|
|
162
|
+
because it lives in a repository. Deferred components record the evidence that would make rsc
|
|
163
|
+
recommend them later. `rsc reassess` reports that evidence but still cannot install anything
|
|
164
|
+
without a newly accepted plan.
|
|
158
165
|
|
|
159
166
|
Everything stays **in the project**, and the real skill files are written
|
|
160
167
|
**once** to `.rsc/skills/<id>/`. Each assistant you pick gets a lightweight
|
|
@@ -169,53 +176,39 @@ points whose backing skill, agent or local-memory capability actually exists.
|
|
|
169
176
|
## 30-second tour
|
|
170
177
|
|
|
171
178
|
```
|
|
172
|
-
$ rsc
|
|
179
|
+
$ rsc onboard
|
|
173
180
|
██████╗ ███████╗ ██████╗ ← animated gradient wordmark
|
|
174
181
|
██╔══██╗██╔════╝██╔════╝
|
|
175
182
|
██████╔╝███████╗██║
|
|
176
183
|
272 skills · one CLI · zero bloat
|
|
177
184
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
Pick **by area** and you get a checkbox list — **↑↓ to move, space to toggle,
|
|
185
|
-
enter to confirm**:
|
|
185
|
+
How technical should the conversation be?
|
|
186
|
+
How much accompaniment do you want?
|
|
187
|
+
What are you building or running?
|
|
188
|
+
What do you want this project to achieve?
|
|
186
189
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
◯ rust
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Then it asks **which assistants** to install for — tick as many as you like:
|
|
196
|
-
|
|
197
|
-
```
|
|
198
|
-
Which assistants do you want to install for? space toggle · a all · enter confirm
|
|
199
|
-
❯ ◉ Claude Code (.claude/skills/) ⟵ detected here
|
|
200
|
-
◉ Codex CLI (AGENTS.md)
|
|
201
|
-
◯ GitHub Copilot (.github/copilot-instructions.md)
|
|
202
|
-
◯ Cursor (.cursor/rules/)
|
|
203
|
-
◉ Windsurf (.windsurf/rules/)
|
|
204
|
-
◯ Cline (.clinerules/)
|
|
205
|
-
…17 in total — Gemini, Antigravity, Zed, Continue, Roo, Amp, opencode, Jules, Junie, Kiro, Aider
|
|
190
|
+
RSC_ONBOARDING_PLAN
|
|
191
|
+
Plan id: <sha256>
|
|
192
|
+
Selected: …
|
|
193
|
+
Deferred: …
|
|
194
|
+
Accept this exact harness plan?
|
|
206
195
|
```
|
|
207
196
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
there keeps proposing the skills a task needs.
|
|
197
|
+
The terminal and chat adapters produce the same normalized answers and plan id. If project evidence
|
|
198
|
+
changes between preview and acceptance, rsc returns `RSC_PLAN_CHANGED` and writes nothing. After an
|
|
199
|
+
accepted application it verifies the receipt and managed state before printing `RSC_ONBOARDING_READY`.
|
|
212
200
|
|
|
213
201
|
---
|
|
214
202
|
|
|
215
203
|
## The CLI
|
|
216
204
|
|
|
205
|
+
Fresh projects enter through `rsc onboard`. The direct `add` and `install --profile` forms below
|
|
206
|
+
are maintenance controls for projects that already carry an `.rsc.json` declaration; they cannot
|
|
207
|
+
bypass onboarding in a new folder.
|
|
208
|
+
|
|
217
209
|
```bash
|
|
218
|
-
rsc
|
|
210
|
+
rsc onboard # binding plain-language onboarding (recommended)
|
|
211
|
+
rsc reassess # check persisted deferral triggers; never installs by itself
|
|
219
212
|
rsc add fastapi postgresdb # install specific skills, by name
|
|
220
213
|
rsc add youtube-api remotion-video # …grow a channel, edit with Remotion
|
|
221
214
|
rsc add fastapi --target claude,codex # install into several assistants at once
|
package/manifest.json
CHANGED
package/package.json
CHANGED
package/scripts/install-apply.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { rmSync, existsSync, cpSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, readdirSync } from 'node:fs';
|
|
3
|
-
import { join, dirname, relative, sep } from 'node:path';
|
|
2
|
+
import { rmSync, existsSync, cpSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, appendFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { join, dirname, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { planInstall } from './install-plan.js';
|
|
6
6
|
import { targetPaths, writeSkill, wireHook, unwireHook, baseDir, TARGET_IDS } from '../targets/index.js';
|
|
7
7
|
import {
|
|
8
8
|
targetHasAgents, reconcileAgents, agentPath, agentNames,
|
|
9
|
-
resolveAgentNames, agentByName, readDeveloperTier,
|
|
9
|
+
resolveAgentNames, agentByName, allAgentNames, readDeveloperTier,
|
|
10
10
|
} from '../targets/agents.js';
|
|
11
11
|
import { readState, writeState } from './lib/state.js';
|
|
12
12
|
import { readManifest, writeManifest } from './lib/manifest-file.js';
|
|
13
13
|
import { createBackup } from './lib/backups.js';
|
|
14
14
|
import {
|
|
15
|
-
targetHasCommands, resolveCommands, reconcileCommands, commandPath,
|
|
15
|
+
targetHasCommands, resolveCommands, reconcileCommands, commandPath, allPotentialCommandNames,
|
|
16
16
|
} from '../targets/commands.js';
|
|
17
17
|
import {
|
|
18
18
|
wireMemory, unwireMemory, memoryManagedPaths, memoryModeFor, memoryEnabledForProject, memoryArtifactsPresent,
|
|
@@ -63,48 +63,50 @@ function ensureBase(id, cwd, baseVersions) {
|
|
|
63
63
|
// reported by a --dry-run. Keep in sync with targets/claude.js wireHook(): a file it
|
|
64
64
|
// writes but this omits is silently absent from the snapshot, so `rsc restore` cannot
|
|
65
65
|
// bring it back.
|
|
66
|
-
function generatedHookFiles({ target, cwd }) {
|
|
66
|
+
export function generatedHookFiles({ target, cwd, policy }) {
|
|
67
67
|
if (target !== 'claude') return [];
|
|
68
|
-
|
|
68
|
+
const lifecycle = [
|
|
69
69
|
join(cwd, '.rsc', 'session-start.mjs'),
|
|
70
70
|
join(cwd, '.rsc', 'worklog-checkpoint.mjs'),
|
|
71
|
-
join(cwd, '.rsc', 'ship-guard.mjs'),
|
|
72
|
-
join(cwd, '.rsc', 'danger-guard.mjs'),
|
|
73
|
-
join(cwd, '.rsc', 'gitmoji-guard.mjs'),
|
|
74
|
-
join(cwd, '.rsc', 'userprompt-gate.mjs'),
|
|
75
71
|
join(cwd, '.rsc', 'hook-once.mjs'),
|
|
76
|
-
join(cwd, '.rsc', '
|
|
72
|
+
join(cwd, '.rsc', 'worktree-reaper.mjs'),
|
|
77
73
|
];
|
|
74
|
+
if (policy?.codeHooks === false) return [...lifecycle, join(cwd, '.rsc', 'suggest-always-on.md')];
|
|
75
|
+
return [...lifecycle,
|
|
76
|
+
join(cwd, '.rsc', 'ship-guard.mjs'), join(cwd, '.rsc', 'danger-guard.mjs'),
|
|
77
|
+
join(cwd, '.rsc', 'gitmoji-guard.mjs'), join(cwd, '.rsc', 'userprompt-gate.mjs'),
|
|
78
|
+
join(cwd, '.rsc', 'sello.mjs')];
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
export function managedPathsForInstall({ skillIds, agentIds = [], target, home, cwd }) {
|
|
81
|
+
export function managedPathsForInstall({ skillIds, agentIds = [], target, home, cwd, policy }) {
|
|
81
82
|
const paths = targetPaths(target, home, cwd);
|
|
82
|
-
const plan = planInstall({ skillIds, target, home, cwd });
|
|
83
|
+
const plan = planInstall({ skillIds, target, home, cwd, hooks: policy?.alwaysOn !== false });
|
|
83
84
|
const out = [paths.stateFile, versionFile(cwd), baseVersionsFile(cwd)];
|
|
84
|
-
out.push(
|
|
85
|
+
if (policy?.context7 === false) out.push(join(cwd, '.rsc', '.no-context7'));
|
|
86
|
+
if (policy?.memory !== false) out.push(...memoryManagedPaths(target, cwd));
|
|
85
87
|
if (targetHasAgents(target)) {
|
|
86
88
|
const state = readState(paths.stateFile);
|
|
87
89
|
const explicit = [...new Set([...(state.explicitAgents || readManifest(cwd)?.agents || []), ...agentIds])];
|
|
88
|
-
const desired = resolveAgentNames([...Object.keys(state.skills || {}), ...skillIds], explicit);
|
|
89
|
-
out.push(...
|
|
90
|
+
const desired = policy?.agents || resolveAgentNames([...Object.keys(state.skills || {}), ...skillIds], explicit);
|
|
91
|
+
out.push(...desired.map((n) => agentPath(target, cwd, n)));
|
|
90
92
|
}
|
|
91
93
|
if (targetHasCommands(target)) {
|
|
92
94
|
const state = readState(paths.stateFile);
|
|
93
95
|
const explicit = [...new Set([...(state.explicitAgents || readManifest(cwd)?.agents || []), ...agentIds])];
|
|
94
|
-
const desiredAgents = resolveAgentNames([...Object.keys(state.skills || {}), ...skillIds], explicit);
|
|
96
|
+
const desiredAgents = policy?.agents || resolveAgentNames([...Object.keys(state.skills || {}), ...skillIds], explicit);
|
|
95
97
|
const desiredCommands = resolveCommands({
|
|
96
98
|
target,
|
|
97
|
-
skills: [...new Set([...Object.keys(state.skills || {}), ...skillIds])],
|
|
99
|
+
skills: policy ? skillIds : [...new Set([...Object.keys(state.skills || {}), ...skillIds])],
|
|
98
100
|
agents: desiredAgents,
|
|
99
101
|
memoryMode: memoryEnabledForProject(cwd) ? memoryModeFor(target) : 'disabled',
|
|
100
102
|
});
|
|
101
|
-
out.push(...
|
|
103
|
+
out.push(...desiredCommands.map((command) => commandPath(target, cwd, command.name)));
|
|
102
104
|
}
|
|
103
105
|
for (const step of plan) {
|
|
104
106
|
if (step.kind === 'skill') {
|
|
105
107
|
out.push(step.to, baseDir(step.id, cwd));
|
|
106
108
|
} else if (step.kind === 'hook') {
|
|
107
|
-
out.push(step.to, ...generatedHookFiles({ target, cwd }));
|
|
109
|
+
out.push(step.to, ...generatedHookFiles({ target, cwd, policy }));
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
return [...new Set(out)];
|
|
@@ -129,7 +131,7 @@ function localDecisions(cwd) {
|
|
|
129
131
|
// into codex on a machine that already had claude is adding, not replacing. Union, sorted
|
|
130
132
|
// where order carries no meaning, so the file stays diffable and a merge conflict stays
|
|
131
133
|
// readable.
|
|
132
|
-
export function recordInManifest({ cwd, target, skillIds, agentIds = [], catalogVersion = CLI_VERSION, dropTarget }) {
|
|
134
|
+
export function recordInManifest({ cwd, target, skillIds, agentIds = [], catalogVersion = CLI_VERSION, dropTarget, onboarding }) {
|
|
133
135
|
const prev = readManifest(cwd) || { targets: [], skills: [], agents: [], ownSkills: [], optOuts: [] };
|
|
134
136
|
const { optOuts, tier } = localDecisions(cwd);
|
|
135
137
|
const union = (a, b) => [...new Set([...(a || []), ...(b || [])])].sort();
|
|
@@ -146,13 +148,14 @@ export function recordInManifest({ cwd, target, skillIds, agentIds = [], catalog
|
|
|
146
148
|
tier: tier ?? prev.tier ?? null,
|
|
147
149
|
optOuts: optOuts.length ? optOuts : (prev.optOuts || []),
|
|
148
150
|
memory: prev.memory,
|
|
151
|
+
onboarding: onboarding ?? prev.onboarding,
|
|
149
152
|
});
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
export async function applyInstall({ skillIds = [], agentIds = [], target, home, cwd = process.cwd(), operation = 'install', dryRun = false }) {
|
|
155
|
+
export async function applyInstall({ skillIds = [], agentIds = [], target, home, cwd = process.cwd(), operation = 'install', dryRun = false, policy, onboarding }) {
|
|
153
156
|
const paths = targetPaths(target, home, cwd);
|
|
154
|
-
const plan = planInstall({ skillIds, target, home, cwd });
|
|
155
|
-
const managedPaths = managedPathsForInstall({ skillIds, agentIds, target, home, cwd });
|
|
157
|
+
const plan = planInstall({ skillIds, target, home, cwd, hooks: policy?.alwaysOn !== false });
|
|
158
|
+
const managedPaths = managedPathsForInstall({ skillIds, agentIds, target, home, cwd, policy });
|
|
156
159
|
if (dryRun) return { dryRun: true, skills: skillIds, agents: agentIds, paths: managedPaths };
|
|
157
160
|
const state = readState(paths.stateFile);
|
|
158
161
|
const backup = createBackup({ cwd, operation, target, paths: managedPaths, cliVersion: CLI_VERSION });
|
|
@@ -161,13 +164,20 @@ export async function applyInstall({ skillIds = [], agentIds = [], target, home,
|
|
|
161
164
|
// syncs — a single global marker would be bumped by the first target and make later
|
|
162
165
|
// targets skip refreshing their exclusive skills' bases.
|
|
163
166
|
const baseVersions = readBaseVersions(cwd);
|
|
167
|
+
if (policy) {
|
|
168
|
+
for (const id of Object.keys(state.skills || {})) {
|
|
169
|
+
if (skillIds.includes(id)) continue;
|
|
170
|
+
rmSync(paths.skillDir(id), { recursive: true, force: true });
|
|
171
|
+
delete state.skills[id];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
164
174
|
for (const step of plan) {
|
|
165
175
|
if (step.kind === 'skill') {
|
|
166
176
|
const base = ensureBase(step.id, cwd, baseVersions);
|
|
167
177
|
const files = await writeSkill(target, step.id, base, step.to);
|
|
168
178
|
state.skills[step.id] = { files, base };
|
|
169
179
|
} else if (step.kind === 'hook') {
|
|
170
|
-
await wireHook(target, paths, join(ensureBase('suggest', cwd, baseVersions), 'SKILL.md'));
|
|
180
|
+
await wireHook(target, paths, join(ensureBase('suggest', cwd, baseVersions), 'SKILL.md'), policy);
|
|
171
181
|
}
|
|
172
182
|
}
|
|
173
183
|
writeBaseVersions(cwd, baseVersions);
|
|
@@ -179,8 +189,13 @@ export async function applyInstall({ skillIds = [], agentIds = [], target, home,
|
|
|
179
189
|
//
|
|
180
190
|
// The recorded names come from what was WRITTEN, not from a hardcoded list: a state entry
|
|
181
191
|
// naming an agent whose file never landed answers "you have it" for something absent.
|
|
182
|
-
const
|
|
183
|
-
const
|
|
192
|
+
const inheritedExplicit = policy ? (state.explicitAgents || []) : (state.explicitAgents || readManifest(cwd)?.agents || []);
|
|
193
|
+
const explicit = [...new Set([...inheritedExplicit, ...agentIds])].sort();
|
|
194
|
+
const desiredAgents = policy?.agents
|
|
195
|
+
? [...new Set([...policy.agents, ...explicit])].sort()
|
|
196
|
+
: policy?.baseAgents === false
|
|
197
|
+
? explicit
|
|
198
|
+
: resolveAgentNames(Object.keys(state.skills || {}), explicit);
|
|
184
199
|
const previousAgents = state.agents || [];
|
|
185
200
|
if (targetHasAgents(target)) {
|
|
186
201
|
const agentResult = reconcileAgents(target, cwd, readDeveloperTier(cwd), previousAgents, desiredAgents);
|
|
@@ -191,8 +206,23 @@ export async function applyInstall({ skillIds = [], agentIds = [], target, home,
|
|
|
191
206
|
state.agentCollisions = [];
|
|
192
207
|
}
|
|
193
208
|
state.explicitAgents = explicit;
|
|
194
|
-
const memoryResult =
|
|
209
|
+
const memoryResult = policy?.memory === false
|
|
210
|
+
? { mode: 'disabled', reason: 'onboarding-policy', paths: unwireMemory(target, cwd) }
|
|
211
|
+
: wireMemory(target, cwd);
|
|
195
212
|
state.memory = { mode: memoryResult.mode, reason: memoryResult.reason, paths: memoryResult.paths };
|
|
213
|
+
const context7OptOut = join(cwd, '.rsc', '.no-context7');
|
|
214
|
+
if (policy?.context7 === false) {
|
|
215
|
+
mkdirSync(dirname(context7OptOut), { recursive: true });
|
|
216
|
+
writeFileSync(context7OptOut, 'Managed by the accepted onboarding plan: external MCP connections require separate consent.\n');
|
|
217
|
+
}
|
|
218
|
+
state.policy = policy ? {
|
|
219
|
+
baseAgents: policy.baseAgents !== false,
|
|
220
|
+
alwaysOn: policy.alwaysOn !== false,
|
|
221
|
+
codeHooks: policy.codeHooks !== false,
|
|
222
|
+
gitmojiGuard: policy.gitmojiGuard !== false,
|
|
223
|
+
memory: policy.memory !== false,
|
|
224
|
+
context7: policy.context7 === true,
|
|
225
|
+
} : state.policy;
|
|
196
226
|
const desiredCommands = resolveCommands({
|
|
197
227
|
target,
|
|
198
228
|
skills: Object.keys(state.skills || {}),
|
|
@@ -206,7 +236,7 @@ export async function applyInstall({ skillIds = [], agentIds = [], target, home,
|
|
|
206
236
|
writeState(paths.stateFile, state);
|
|
207
237
|
mkdirSync(dirname(versionFile(cwd)), { recursive: true });
|
|
208
238
|
writeFileSync(versionFile(cwd), CLI_VERSION + '\n');
|
|
209
|
-
recordInManifest({ cwd, target, skillIds, agentIds });
|
|
239
|
+
recordInManifest({ cwd, target, skillIds, agentIds, onboarding });
|
|
210
240
|
ignoreLocalState(cwd, target);
|
|
211
241
|
return { ...state, backup };
|
|
212
242
|
}
|
|
@@ -264,6 +294,12 @@ export function ignoreLocalState(cwd = process.cwd(), target) {
|
|
|
264
294
|
const prefix = text === '' ? '' : (text.endsWith('\n') ? '' : '\n');
|
|
265
295
|
try {
|
|
266
296
|
appendFileSync(gi, `${prefix}\n# rsc local state (hooks, seals, logs) and managed skill links — machine-local\n${add.join('\n')}\n`);
|
|
297
|
+
if (target) {
|
|
298
|
+
const statePath = targetPaths(target, undefined, cwd).stateFile;
|
|
299
|
+
const state = readState(statePath);
|
|
300
|
+
state.ignoreEntriesAdded = [...new Set([...(state.ignoreEntriesAdded || []), ...add.map(norm)])].sort();
|
|
301
|
+
writeState(statePath, state);
|
|
302
|
+
}
|
|
267
303
|
return gi;
|
|
268
304
|
} catch { return null; }
|
|
269
305
|
}
|
|
@@ -356,6 +392,86 @@ export async function uninstall({ skillIds = [], agentIds = [], target, home, cw
|
|
|
356
392
|
return [...new Set(removed)];
|
|
357
393
|
}
|
|
358
394
|
|
|
395
|
+
// Reconcile an accepted onboarding plan as a replacement, not an additive install.
|
|
396
|
+
// Ownership comes only from the target state file; unrelated files in the assistant
|
|
397
|
+
// directory are left untouched.
|
|
398
|
+
export function removeTargetInstall({ target, home, cwd = process.cwd() }) {
|
|
399
|
+
const paths = targetPaths(target, home, cwd);
|
|
400
|
+
const state = readState(paths.stateFile);
|
|
401
|
+
const ignored = [paths.stateFile];
|
|
402
|
+
const ownedIgnoreEntries = new Set(state.ignoreEntriesAdded || []);
|
|
403
|
+
const owned = [
|
|
404
|
+
...Object.values(state.skills || {}).flatMap((entry) => entry.files || []),
|
|
405
|
+
...(state.agents || []).map((name) => agentPath(target, cwd, name)).filter(Boolean),
|
|
406
|
+
...(state.commands || []).map((name) => commandPath(target, cwd, name)).filter(Boolean),
|
|
407
|
+
paths.stateFile,
|
|
408
|
+
];
|
|
409
|
+
const declaredLocationsAreExact = Object.entries(state.skills || {}).every(([id, entry]) =>
|
|
410
|
+
/^[a-z0-9][a-z0-9._-]*$/i.test(id)
|
|
411
|
+
&& existsSync(join(ROOT, 'skills', id))
|
|
412
|
+
&& (entry.files || []).every((file) => resolve(file) === resolve(paths.skillDir(id))))
|
|
413
|
+
&& (state.agents || []).every((name) => {
|
|
414
|
+
if (!allAgentNames().includes(name)) return false;
|
|
415
|
+
const file = agentPath(target, cwd, name);
|
|
416
|
+
return file && owned.some((candidate) => resolve(candidate) === resolve(file));
|
|
417
|
+
})
|
|
418
|
+
&& (state.commands || []).every((name) => {
|
|
419
|
+
if (!allPotentialCommandNames().includes(name)) return false;
|
|
420
|
+
const file = commandPath(target, cwd, name);
|
|
421
|
+
return file && owned.some((candidate) => resolve(candidate) === resolve(file));
|
|
422
|
+
});
|
|
423
|
+
const root = resolve(cwd);
|
|
424
|
+
const safe = (file) => {
|
|
425
|
+
const absolute = resolve(file);
|
|
426
|
+
if (absolute === root || !absolute.startsWith(`${root}${sep}`)) return false;
|
|
427
|
+
const parts = relative(root, dirname(absolute)).split(sep).filter(Boolean);
|
|
428
|
+
let cursor = root;
|
|
429
|
+
for (const part of parts) {
|
|
430
|
+
cursor = join(cursor, part);
|
|
431
|
+
if (!existsSync(cursor)) break;
|
|
432
|
+
if (lstatSync(cursor).isSymbolicLink()) {
|
|
433
|
+
const destination = realpathSync(cursor);
|
|
434
|
+
if (destination !== root && !destination.startsWith(`${root}${sep}`)) return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
};
|
|
439
|
+
if (!declaredLocationsAreExact || owned.some((file) => !safe(file))) throw new Error(`RSC_PREVIOUS_INSTALL_INCOMPATIBLE: ${target} state contains a path outside its managed locations`);
|
|
440
|
+
for (const entry of Object.values(state.skills || {})) {
|
|
441
|
+
for (const file of entry.files || []) { ignored.push(file); rmSync(file, { recursive: true, force: true }); }
|
|
442
|
+
}
|
|
443
|
+
for (const name of state.agents || []) {
|
|
444
|
+
const file = agentPath(target, cwd, name);
|
|
445
|
+
if (file) { ignored.push(file); rmSync(file, { recursive: true, force: true }); }
|
|
446
|
+
}
|
|
447
|
+
for (const name of state.commands || []) {
|
|
448
|
+
const file = commandPath(target, cwd, name);
|
|
449
|
+
if (file) { ignored.push(file); rmSync(file, { recursive: true, force: true }); }
|
|
450
|
+
}
|
|
451
|
+
unwireHook(target, paths);
|
|
452
|
+
unwireMemory(target, cwd);
|
|
453
|
+
rmSync(paths.stateFile, { force: true });
|
|
454
|
+
const gitignore = join(cwd, '.gitignore');
|
|
455
|
+
if (existsSync(gitignore)) {
|
|
456
|
+
const stale = new Set(ignored.map((path) => relative(cwd, path).split(sep).join('/').replace(/^\//, '').replace(/\/$/, ''))
|
|
457
|
+
.filter((entry) => ownedIgnoreEntries.has(entry)));
|
|
458
|
+
const lines = readFileSync(gitignore, 'utf8').split('\n');
|
|
459
|
+
const kept = lines.filter((line) => !stale.has(line.trim().replace(/^\//, '').replace(/\/$/, '')));
|
|
460
|
+
writeFileSync(gitignore, kept.join('\n'));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function pruneSharedBases({ cwd = process.cwd(), skillIds = [] }) {
|
|
465
|
+
const versions = readBaseVersions(cwd);
|
|
466
|
+
const wanted = new Set(skillIds);
|
|
467
|
+
for (const id of Object.keys(versions)) {
|
|
468
|
+
if (wanted.has(id)) continue;
|
|
469
|
+
rmSync(baseDir(id, cwd), { recursive: true, force: true });
|
|
470
|
+
delete versions[id];
|
|
471
|
+
}
|
|
472
|
+
writeBaseVersions(cwd, versions);
|
|
473
|
+
}
|
|
474
|
+
|
|
359
475
|
export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun = false }) {
|
|
360
476
|
const paths = targetPaths(target, home, cwd);
|
|
361
477
|
const state = readState(paths.stateFile);
|
|
@@ -365,7 +481,8 @@ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun
|
|
|
365
481
|
// to rebuild from, and it is sitting right there.
|
|
366
482
|
const ids = Object.keys(state.skills || {});
|
|
367
483
|
const manifest = readManifest(cwd);
|
|
368
|
-
const
|
|
484
|
+
const governedSkills = manifest?.onboarding?.plan?.policy?.skills;
|
|
485
|
+
const declared = governedSkills || (ids.length ? ids : (manifest?.skills || []));
|
|
369
486
|
const declaredAgents = state.explicitAgents?.length ? state.explicitAgents : (manifest?.agents || []);
|
|
370
487
|
if (!declared.length && !declaredAgents.length) return dryRun ? { dryRun: true, synced: [], syncedAgents: [], paths: [] } : { synced: [], syncedAgents: [], backup: null };
|
|
371
488
|
if (dryRun) {
|
|
@@ -373,10 +490,19 @@ export async function syncInstalled({ target, home, cwd = process.cwd(), dryRun
|
|
|
373
490
|
dryRun: true,
|
|
374
491
|
synced: declared,
|
|
375
492
|
syncedAgents: declaredAgents,
|
|
376
|
-
paths: managedPathsForInstall({ skillIds: declared, agentIds: declaredAgents, target, home, cwd }),
|
|
493
|
+
paths: managedPathsForInstall({ skillIds: declared, agentIds: declaredAgents, target, home, cwd, policy: manifest?.onboarding?.plan?.policy }),
|
|
377
494
|
};
|
|
378
495
|
}
|
|
379
|
-
const nextState = await applyInstall({
|
|
496
|
+
const nextState = await applyInstall({
|
|
497
|
+
skillIds: declared,
|
|
498
|
+
agentIds: declaredAgents,
|
|
499
|
+
target,
|
|
500
|
+
home,
|
|
501
|
+
cwd,
|
|
502
|
+
operation: 'sync',
|
|
503
|
+
policy: manifest?.onboarding?.plan?.policy,
|
|
504
|
+
onboarding: manifest?.onboarding,
|
|
505
|
+
});
|
|
380
506
|
return { synced: declared, syncedAgents: declaredAgents, backup: nextState.backup };
|
|
381
507
|
}
|
|
382
508
|
|
package/scripts/install-plan.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { targetPaths } from '../targets/index.js';
|
|
2
2
|
|
|
3
|
-
export function planInstall({ skillIds, target, home, cwd }) {
|
|
3
|
+
export function planInstall({ skillIds, target, home, cwd, hooks = true }) {
|
|
4
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) });
|
|
8
8
|
}
|
|
9
|
-
if (skillIds.includes('suggest')) {
|
|
9
|
+
if (hooks && skillIds.includes('suggest')) {
|
|
10
10
|
plan.push({ kind: 'hook', target, to: t.hookTarget });
|
|
11
11
|
}
|
|
12
12
|
return plan;
|
|
@@ -15,7 +15,7 @@ import { join } from 'node:path';
|
|
|
15
15
|
export const MANIFEST = '.rsc.json';
|
|
16
16
|
export const manifestPath = (cwd = process.cwd()) => join(cwd, MANIFEST);
|
|
17
17
|
|
|
18
|
-
const KEYS = ['version', 'targets', 'skills', 'agents', 'ownSkills', 'catalogVersion', 'tier', 'optOuts', 'memory'];
|
|
18
|
+
const KEYS = ['version', 'targets', 'skills', 'agents', 'ownSkills', 'catalogVersion', 'tier', 'optOuts', 'memory', 'onboarding'];
|
|
19
19
|
|
|
20
20
|
export function readManifest(cwd = process.cwd()) {
|
|
21
21
|
const file = manifestPath(cwd);
|
|
@@ -38,6 +38,7 @@ export function readManifest(cwd = process.cwd()) {
|
|
|
38
38
|
tier: raw.tier ?? null,
|
|
39
39
|
optOuts: raw.optOuts || [],
|
|
40
40
|
memory: raw.memory ?? undefined,
|
|
41
|
+
onboarding: raw.onboarding ?? undefined,
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
|