@sassywares/skills 0.3.2 → 0.4.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 +18 -23
- package/package.json +1 -5
- package/skills/sassy-debug/SKILL.md +0 -2
- package/skills/sassy-lfg/SKILL.md +0 -2
- package/skills/sassy-plan/SKILL.md +0 -2
- package/skills/sassy-review/SKILL.md +0 -2
- package/skills/sassy-setup/SKILL.md +8 -10
- package/skills/sassy-setup/assets/sassywares-readme.md +7 -5
- package/skills/sassy-ship/SKILL.md +0 -2
- package/skills/sassy-validate/SKILL.md +0 -2
- package/skills/sassy-work/SKILL.md +0 -2
- package/bin/sassy-skills.mjs +0 -250
package/README.md
CHANGED
|
@@ -271,44 +271,39 @@ sassy-skills/
|
|
|
271
271
|
|
|
272
272
|
## Installation
|
|
273
273
|
|
|
274
|
-
The suite
|
|
275
|
-
|
|
276
|
-
### As a project dependency (recommended for teams)
|
|
277
|
-
|
|
278
|
-
```bash
|
|
279
|
-
bun add --dev --exact @sassywares/skills@0.3.2
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Nothing scans `node_modules` on its own, so link it where harnesses look — the package does that itself:
|
|
274
|
+
The suite installs from this repository with the open `skills` CLI. That CLI vendors the skill files into your project and pins them, so a clone needs no network and every checkout runs the same skills.
|
|
283
275
|
|
|
284
276
|
```bash
|
|
285
|
-
|
|
277
|
+
npx skills add sassywares/sassy-skills
|
|
286
278
|
```
|
|
287
279
|
|
|
288
|
-
That writes
|
|
289
|
-
|
|
290
|
-
Ignore both paths — they're derived from the lockfile, like any build output. `sassy-setup` does all of this for you, including the gitignore lines and the pinned version.
|
|
280
|
+
That writes three things:
|
|
291
281
|
|
|
292
|
-
|
|
282
|
+
- `.agents/skills/sassy-*` — the skill files themselves, read by pi and Cursor
|
|
283
|
+
- `.claude/skills/sassy-*` — links for Claude Code, which reads only its own directory
|
|
284
|
+
- `skills-lock.json` — source and content hash per skill
|
|
293
285
|
|
|
294
|
-
|
|
286
|
+
Commit the vendored skills and the lock; ignore `.claude/skills`, which is a link created at install time.
|
|
295
287
|
|
|
296
288
|
```bash
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
npx skills update -y # pull the latest from this repo
|
|
290
|
+
npx skills experimental_install # restore exactly what the lock pins
|
|
291
|
+
npx skills remove # take the suite out again
|
|
299
292
|
```
|
|
300
293
|
|
|
294
|
+
`sassy-setup` does all of this for you, including the ignore line and the two package scripts.
|
|
295
|
+
|
|
301
296
|
### While editing the suite
|
|
302
297
|
|
|
303
|
-
|
|
298
|
+
`.pi/settings.json` in this repo declares `./skills`, so pi loads your working copy directly. Consumers see a change once it is pushed to `main` and they run `npx skills update` — no release step in between.
|
|
304
299
|
|
|
305
|
-
|
|
306
|
-
bun link @sassywares/skills
|
|
307
|
-
```
|
|
300
|
+
### Also published as a package
|
|
308
301
|
|
|
309
|
-
|
|
302
|
+
`@sassywares/skills` is on npm for harnesses that install packages rather than skill directories:
|
|
310
303
|
|
|
311
|
-
|
|
304
|
+
```bash
|
|
305
|
+
pi install npm:@sassywares/skills@0.4.0
|
|
306
|
+
```
|
|
312
307
|
|
|
313
308
|
## Repository maintenance
|
|
314
309
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sassywares/skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Lean, explicit-invocation Agent Skills for shipping fast: plan, work, review, validate, ship.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
],
|
|
25
25
|
"files": [
|
|
26
26
|
"skills",
|
|
27
|
-
"bin",
|
|
28
27
|
"README.md",
|
|
29
28
|
"CONCEPTS.md",
|
|
30
29
|
"LICENSE"
|
|
@@ -37,9 +36,6 @@
|
|
|
37
36
|
"./skills"
|
|
38
37
|
]
|
|
39
38
|
},
|
|
40
|
-
"bin": {
|
|
41
|
-
"sassy-skills": "./bin/sassy-skills.mjs"
|
|
42
|
-
},
|
|
43
39
|
"engines": {
|
|
44
40
|
"node": ">=18"
|
|
45
41
|
}
|
|
@@ -25,8 +25,6 @@ Trivial bugs whose cause is readable from the input may skip the scout, but stil
|
|
|
25
25
|
|
|
26
26
|
**Search prior learnings first.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. For every existing path in `knowledge.read`, search the docs for the symptom's keywords, the module, and the failure type. A matching doc **seeds or confirms** a hypothesis — it never replaces the causal chain or the reproduction. Name the docs you consulted, and say plainly when none matched: no match is not evidence that nothing similar has happened.
|
|
27
27
|
|
|
28
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
29
|
-
|
|
30
28
|
## Phase 1 — Diagnose inline, with `scout` as a helper
|
|
31
29
|
|
|
32
30
|
You own the diagnosis. The causal reasoning happens here, in the main context, because that is where the full picture lives and where the fix gate is presented.
|
|
@@ -16,8 +16,6 @@ disable-model-invocation: true
|
|
|
16
16
|
|
|
17
17
|
**Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. Plans live under `artifacts.plans`; `git.repo` decides how the close-out persists.
|
|
18
18
|
|
|
19
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
20
|
-
|
|
21
19
|
## Input
|
|
22
20
|
|
|
23
21
|
Detect the shape from the argument:
|
|
@@ -17,8 +17,6 @@ disable-model-invocation: true
|
|
|
17
17
|
|
|
18
18
|
**Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. Plans land in `artifacts.plans`.
|
|
19
19
|
|
|
20
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
21
|
-
|
|
22
20
|
## Phase A — Discuss (brainstorm)
|
|
23
21
|
|
|
24
22
|
Have one focused discussion to turn the idea into something plannable.
|
|
@@ -13,8 +13,6 @@ disable-model-invocation: true
|
|
|
13
13
|
|
|
14
14
|
**Outcome:** the scope reviewed with concrete findings; the approved fixes applied; everything left uncommitted.
|
|
15
15
|
|
|
16
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
17
|
-
|
|
18
16
|
**Orchestrator only.** Never edit code yourself. The `reviewer` finds; the `worker` fixes.
|
|
19
17
|
|
|
20
18
|
## Review dimensions
|
|
@@ -30,16 +30,14 @@ disable-model-invocation: true
|
|
|
30
30
|
- **`pr.*`** — default `babysit: true`, `merge: false`, `delete_branch: false`. Say plainly that `merge: true` authorizes merging every babysat PR in this project without asking.
|
|
31
31
|
- **Hooks** — offer, do not impose. Ask whether the project wants steps injected at `before_create`, `after_create`, `before_merge`, or `after_merge`, and write the skills they name. Leave them `[]` when they have nothing to add. **Never write a hook skill yourself** — hooks are project skills, and copying one of ours into that role is the fork this mechanism exists to prevent.
|
|
32
32
|
4. **Write the config.** Build the file from the proposal plus the answers. It is written **fully commented**: every key keeps a description and its allowed values, exactly as the detector emits them. Never strip those comments to save lines — for a teammate opening the file, the comments are the documentation. If a config already exists, show what changes and ask before writing — never silently overwrite. Keep every key; an unset key falls back to its documented default, so a short file is legal when nothing deviates.
|
|
33
|
-
5. **Wire the skills.** The suite is the
|
|
33
|
+
5. **Wire the skills.** The suite is installed from upstream with the open `skills` CLI, which vendors the skill files into the project and pins them. Never copy skill files in by hand, never add the suite as a dependency.
|
|
34
34
|
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **
|
|
39
|
-
- **Run `sassy-skills sync`.** The package's CLI is now in `node_modules/.bin`; it links `.agents/skills/sassy-*` and creates `.claude/skills → ../.agents/skills`. The linking rules travel with the lockfile, so they upgrade with the suite instead of drifting in the repo.
|
|
40
|
-
- **Ignore the derived links — `repo` topology only.** Add `/.agents/skills/sassy-*` and `/.claude/skills` to `.gitignore` when missing, creating it only when there is none. In `workspace` topology never touch the project's root `.gitignore`; the snapshot's own ignore file covers that case.
|
|
35
|
+
- **Install it**: `npx -y skills add sassywares/sassy-skills -y`. That writes `.agents/skills/sassy-*` (real files), per-agent links such as `.claude/skills/sassy-*`, and `skills-lock.json`.
|
|
36
|
+
- **Scripts**, adding only what is missing: `"skills:sync": "npx -y skills experimental_install"` (restores exactly what the lock pins) and `"skills:update": "npx -y skills update -y"` (pulls the latest from upstream). **Never rewrite an existing `postinstall`** — leave it alone and tell the user to chain what they need.
|
|
37
|
+
- **Commit the vendored skills and the lock.** `.agents/skills/sassy-*` and `skills-lock.json` are the pin; a clone then needs no network.
|
|
38
|
+
- **Ignore `.claude/skills` only.** It is a link created at install time. Create `.gitignore` only when there is none — and in `workspace` topology never touch the project's root `.gitignore` at all.
|
|
41
39
|
- **Write `.sassywares/README.md`** from `assets/sassywares-readme.md` when absent. Never overwrite it — it may carry project notes.
|
|
42
|
-
- **Tell the user to `/reload`.** The
|
|
40
|
+
- **Tell the user to `/reload`.** The skills are invisible to the harness until it rescans.
|
|
43
41
|
|
|
44
42
|
6. **Install the snapshot, `workspace` only.** Read `references/workspace.md` and follow it.
|
|
45
43
|
|
|
@@ -55,9 +53,9 @@ disable-model-invocation: true
|
|
|
55
53
|
| No silent overwrite | An existing config or an existing `.sassywares/workspace/snapshot` is shown and asked about, never replaced blind. |
|
|
56
54
|
| Version is reported | Setup names the snapshot version it installed; `--check` flags an installed copy older than the skill's. |
|
|
57
55
|
| Never commit | Setup writes files and stops. Committing is `sassy-commit`. |
|
|
58
|
-
| Skills
|
|
56
|
+
| Skills come from upstream | Install with `npx skills add`; never hand-copy skill files or maintain the links yourself. |
|
|
59
57
|
| Scripts are shared | Adding a missing script is fine; rewriting an existing `postinstall` or `skills:*` entry is not. |
|
|
60
|
-
| Write boundary | Only `.sassywares/`, the `package.json` scripts
|
|
58
|
+
| Write boundary | Only `.sassywares/`, the vendored `.agents/skills/sassy-*`, `skills-lock.json`, two `package.json` scripts, and one `.gitignore` line. Never a root `.gitignore` in a workspace. |
|
|
61
59
|
| Detect, don't guess | An unknown base branch stays unknown and gets asked about. A probed value is reported as probed. |
|
|
62
60
|
| Comments are part of the file | Every key keeps its description and allowed values. A stripped config is an incomplete config. |
|
|
63
61
|
| Hooks are named, not written | Setup records hook skill names. Creating those skills is the project's work. |
|
|
@@ -4,14 +4,16 @@ Project config and tooling for the Sassy Skills suite. Written by `sassy-setup`.
|
|
|
4
4
|
|
|
5
5
|
## Skills
|
|
6
6
|
|
|
7
|
-
Installed
|
|
7
|
+
Installed from upstream with the open `skills` CLI, which vendors the skills into
|
|
8
|
+
`.agents/skills/` and pins them in `skills-lock.json`:
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
|
-
|
|
11
|
-
bun run skills:
|
|
11
|
+
npx skills add sassywares/sassy-skills # first install
|
|
12
|
+
bun run skills:sync # restore exactly what the lock pins
|
|
13
|
+
bun run skills:update # pull the latest from upstream
|
|
12
14
|
```
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
The vendored skills and the lock are committed; `.claude/skills` is a link and is not.
|
|
15
17
|
|
|
16
18
|
`.agents/skills/` is read by pi and Cursor; Claude Code reads `.claude/skills/` only, which is why that symlink exists. After any change, `/reload` your harness.
|
|
17
19
|
|
|
@@ -22,7 +24,7 @@ If a skill tells you another skill is missing, run `skills:sync` and restart. Su
|
|
|
22
24
|
`config.yaml` holds the project's paths, git topology, and PR defaults. Every key is documented inline, including its allowed values, and the full schema is in the `sassy-setup` skill's `references/config.md`. Verify the file still matches the tree:
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
|
|
27
|
+
.agents/skills/sassy-setup/scripts/detect.sh --root . --check
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
## Hooks
|
|
@@ -18,8 +18,6 @@ disable-model-invocation: true
|
|
|
18
18
|
|
|
19
19
|
**Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. `pr.babysit`, `pr.merge`, and `pr.delete_branch` seed the close-out; `git.repo` decides whether this run commits, snapshots, or refuses; `hooks.pr.*` adds project steps to the PR pipeline.
|
|
20
20
|
|
|
21
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
22
|
-
|
|
23
21
|
**Subagent-maximal by design.** The orchestrator delegates every heavy step (docs, tree reads, git, gh) to subagents and only receives compact summaries. It never reads diffs, file contents, or the tree itself.
|
|
24
22
|
|
|
25
23
|
## Steps (in order)
|
|
@@ -13,8 +13,6 @@ disable-model-invocation: true
|
|
|
13
13
|
|
|
14
14
|
**Outcome:** a grounded conformance judgment against an intent; the approved gaps closed; everything left uncommitted.
|
|
15
15
|
|
|
16
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
17
|
-
|
|
18
16
|
**Orchestrator only.** Never edit code yourself. The `validator` finds gaps; the `worker` closes them.
|
|
19
17
|
|
|
20
18
|
**This skill needs an intent.** Validation without something to validate against is meaningless. If the user gave no plan path or intent, ask for one before doing anything.
|
|
@@ -19,8 +19,6 @@ disable-model-invocation: true
|
|
|
19
19
|
|
|
20
20
|
**Config.** Resolve `.sassywares/config.yaml` (nearest, walking up) per the `sassy-setup` skill's `references/config.md`. Precedence: explicit token > session instruction > config > skill default. `git.repo` decides what the phase commit does, `artifacts.plans` decides where the plan lives.
|
|
21
21
|
|
|
22
|
-
**Update nudge.** Once per run, if `node_modules/.bin/sassy-skills` exists, run `node_modules/.bin/sassy-skills outdated`. It is throttled to once a day per machine and silently does nothing offline. Pass any newer version on in one line; never block, retry, or fail on it.
|
|
23
|
-
|
|
24
22
|
## Input
|
|
25
23
|
|
|
26
24
|
Detect the shape from the argument:
|
package/bin/sassy-skills.mjs
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// sassy-skills — wire this package's skills into a project.
|
|
3
|
-
//
|
|
4
|
-
// Nothing scans node_modules on its own, so the harnesses that read a project
|
|
5
|
-
// skills directory need links. This tool creates them, checks them, and updates
|
|
6
|
-
// the pinned version.
|
|
7
|
-
//
|
|
8
|
-
// sassy-skills sync link skills into .agents/skills (and .claude/skills)
|
|
9
|
-
// sassy-skills check exit 1 if the wiring is missing or stale
|
|
10
|
-
// sassy-skills update bump to the latest published version, then sync
|
|
11
|
-
// sassy-skills outdated one-line nudge when a newer version is published
|
|
12
|
-
//
|
|
13
|
-
// Options: --root <dir> --quiet --force --help
|
|
14
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
15
|
-
import { tmpdir } from "node:os";
|
|
16
|
-
import { execFileSync } from "node:child_process";
|
|
17
|
-
import { dirname, join, resolve } from "node:path";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
|
-
|
|
20
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const SKILLS_DIR = resolve(HERE, "..", "skills");
|
|
22
|
-
const PKG = JSON.parse(readFileSync(resolve(HERE, "..", "package.json"), "utf8"));
|
|
23
|
-
const NAME = PKG.name;
|
|
24
|
-
const VERSION = PKG.version;
|
|
25
|
-
const REGISTRY = process.env.SASSY_SKILLS_REGISTRY ?? "https://registry.npmjs.org";
|
|
26
|
-
|
|
27
|
-
const args = process.argv.slice(2);
|
|
28
|
-
const quiet = args.includes("--quiet");
|
|
29
|
-
const rootFlag = args.indexOf("--root");
|
|
30
|
-
const rootValueIndex = rootFlag >= 0 ? rootFlag + 1 : -1;
|
|
31
|
-
const rootArg = rootFlag >= 0 ? args[rootFlag + 1] : undefined;
|
|
32
|
-
if (rootFlag >= 0 && (!rootArg || rootArg.startsWith("-"))) {
|
|
33
|
-
console.error(" ✗ --root needs a directory");
|
|
34
|
-
process.exit(2);
|
|
35
|
-
}
|
|
36
|
-
const ROOT = rootArg ? resolve(rootArg) : process.cwd();
|
|
37
|
-
if (rootFlag >= 0) {
|
|
38
|
-
let ok = false;
|
|
39
|
-
try { ok = statSync(ROOT).isDirectory(); } catch { ok = false; }
|
|
40
|
-
if (!ok) { console.error(` ✗ --root ${ROOT} is not an existing directory`); process.exit(2); }
|
|
41
|
-
}
|
|
42
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
43
|
-
console.log(`sassy-skills@${VERSION}\n\n sync link skills into .agents/skills and .claude/skills\n check exit 1 when the wiring is missing or stale\n update bump ${NAME} to the latest version, then sync\n outdated print a one-line nudge when a newer version is published (throttled, offline-safe)\n\nOptions: --root <dir> --quiet --force`);
|
|
44
|
-
process.exit(0);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const command = args.find((a, i) => !a.startsWith("-") && i !== rootValueIndex) ?? "sync";
|
|
48
|
-
|
|
49
|
-
const log = (...a) => { if (!quiet) console.log(...a); };
|
|
50
|
-
const ok = (m) => log(` ✓ ${m}`);
|
|
51
|
-
const warn = (m) => console.error(` ! ${m}`);
|
|
52
|
-
const die = (m) => { console.error(` ✗ ${m}`); process.exit(1); };
|
|
53
|
-
|
|
54
|
-
const IS_WIN = process.platform === "win32";
|
|
55
|
-
|
|
56
|
-
function linkedSkills() {
|
|
57
|
-
if (!existsSync(SKILLS_DIR)) die(`package has no skills/ directory at ${SKILLS_DIR}`);
|
|
58
|
-
return readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
59
|
-
.filter((e) => e.isDirectory() && existsSync(join(SKILLS_DIR, e.name, "SKILL.md")))
|
|
60
|
-
.map((e) => e.name)
|
|
61
|
-
.sort();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Returns "linked", "kept", or "blocked". A blocked path is a real directory the
|
|
65
|
-
// user owns: never replaced, and always a failure, because the wiring is then
|
|
66
|
-
// incomplete and nothing the tool can do will fix it.
|
|
67
|
-
function makeLink(target, linkPath) {
|
|
68
|
-
try {
|
|
69
|
-
const st = lstatSync(linkPath);
|
|
70
|
-
if (st.isSymbolicLink()) {
|
|
71
|
-
if (resolve(dirname(linkPath), readlinkSync(linkPath)) === target) return "kept";
|
|
72
|
-
rmSync(linkPath);
|
|
73
|
-
} else {
|
|
74
|
-
warn(`${linkPath} is a real directory, not a link — move it aside; nothing was replaced`);
|
|
75
|
-
return "blocked";
|
|
76
|
-
}
|
|
77
|
-
} catch { /* does not exist */ }
|
|
78
|
-
symlinkSync(target, linkPath, IS_WIN ? "junction" : "dir");
|
|
79
|
-
return "linked";
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function sync() {
|
|
83
|
-
const skills = linkedSkills();
|
|
84
|
-
const agents = join(ROOT, ".agents", "skills");
|
|
85
|
-
mkdirSync(agents, { recursive: true });
|
|
86
|
-
|
|
87
|
-
let linked = 0;
|
|
88
|
-
let blocked = 0;
|
|
89
|
-
for (const name of skills) {
|
|
90
|
-
const target = resolve(SKILLS_DIR, name);
|
|
91
|
-
const result = makeLink(target, join(agents, name));
|
|
92
|
-
if (result === "linked") linked++;
|
|
93
|
-
else if (result === "blocked") blocked++;
|
|
94
|
-
}
|
|
95
|
-
if (blocked) {
|
|
96
|
-
console.error(` ✗ ${blocked} of ${skills.length} skills could not be linked — see above`);
|
|
97
|
-
return 1;
|
|
98
|
-
}
|
|
99
|
-
ok(linked
|
|
100
|
-
? `${skills.length} skills linked into .agents/skills (${NAME}@${VERSION})`
|
|
101
|
-
: `${skills.length} skills already linked (${NAME}@${VERSION})`);
|
|
102
|
-
|
|
103
|
-
// Claude Code reads .claude/skills only. A directory symlink is enough, and it
|
|
104
|
-
// is the least invasive form when the directory does not already exist.
|
|
105
|
-
const claude = join(ROOT, ".claude", "skills");
|
|
106
|
-
try {
|
|
107
|
-
const st = lstatSync(claude);
|
|
108
|
-
if (!st.isSymbolicLink()) {
|
|
109
|
-
warn(".claude/skills is a real directory — Claude Code will not see the project skills until you link them");
|
|
110
|
-
return 1;
|
|
111
|
-
}
|
|
112
|
-
const points = resolve(dirname(claude), readlinkSync(claude));
|
|
113
|
-
if (points !== resolve(agents)) {
|
|
114
|
-
warn(`.claude/skills points at ${points}, not .agents/skills — relink it`);
|
|
115
|
-
return 1;
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
mkdirSync(dirname(claude), { recursive: true });
|
|
119
|
-
symlinkSync(IS_WIN ? join(ROOT, ".agents", "skills") : "../.agents/skills", claude, IS_WIN ? "junction" : "dir");
|
|
120
|
-
ok("linked .claude/skills → .agents/skills");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (linked) log(`\nRestart or /reload your harness to pick up the new skills.`);
|
|
124
|
-
return 0;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function check() {
|
|
128
|
-
const skills = linkedSkills();
|
|
129
|
-
const agents = join(ROOT, ".agents", "skills");
|
|
130
|
-
let problems = 0;
|
|
131
|
-
|
|
132
|
-
for (const name of skills) {
|
|
133
|
-
const link = join(agents, name);
|
|
134
|
-
const target = resolve(SKILLS_DIR, name);
|
|
135
|
-
try {
|
|
136
|
-
const st = lstatSync(link);
|
|
137
|
-
if (!st.isSymbolicLink()) { warn(`${name}: real directory, not a link`); problems++; continue; }
|
|
138
|
-
const points = resolve(agents, readlinkSync(link));
|
|
139
|
-
if (points !== target) { warn(`${name}: points at ${points}, expected ${target}`); problems++; }
|
|
140
|
-
} catch {
|
|
141
|
-
warn(`${name}: not linked`);
|
|
142
|
-
problems++;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const claude = join(ROOT, ".claude", "skills");
|
|
146
|
-
try {
|
|
147
|
-
const st = lstatSync(claude);
|
|
148
|
-
if (!st.isSymbolicLink()) { warn(".claude/skills is a real directory, so Claude Code sees none of these"); problems++; }
|
|
149
|
-
else if (resolve(dirname(claude), readlinkSync(claude)) !== resolve(agents)) {
|
|
150
|
-
warn(".claude/skills points somewhere else"); problems++;
|
|
151
|
-
}
|
|
152
|
-
} catch { warn(".claude/skills missing (Claude Code only)"); }
|
|
153
|
-
if (problems) { console.error(` ✗ ${problems} skill link(s) need attention — run: sassy-skills sync`); return 1; }
|
|
154
|
-
ok(`${skills.length} skills correctly linked (${NAME}@${VERSION})`);
|
|
155
|
-
return 0;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function detectPM() {
|
|
159
|
-
if (existsSync(join(ROOT, "bun.lock")) || existsSync(join(ROOT, "bun.lockb"))) return "bun";
|
|
160
|
-
if (existsSync(join(ROOT, "pnpm-lock.yaml"))) return "pnpm";
|
|
161
|
-
if (existsSync(join(ROOT, "yarn.lock"))) return "yarn";
|
|
162
|
-
if (existsSync(join(ROOT, "package-lock.json"))) return "npm";
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function update() {
|
|
167
|
-
const pm = detectPM();
|
|
168
|
-
if (!pm) die("no lockfile found — cannot tell which package manager to update with");
|
|
169
|
-
const spec = `${NAME}@latest`;
|
|
170
|
-
const cmd = {
|
|
171
|
-
bun: ["bun", ["add", "--dev", "--exact", spec]],
|
|
172
|
-
npm: ["npm", ["install", "--save-dev", "--save-exact", spec]],
|
|
173
|
-
pnpm: ["pnpm", ["add", "--save-dev", "--save-exact", spec]],
|
|
174
|
-
yarn: ["yarn", ["add", "--dev", "--exact", spec]],
|
|
175
|
-
}[pm];
|
|
176
|
-
log(`Updating ${NAME} with ${pm}…`);
|
|
177
|
-
try {
|
|
178
|
-
execFileSync(cmd[0], cmd[1], { cwd: ROOT, stdio: "inherit" });
|
|
179
|
-
} catch {
|
|
180
|
-
die(`${pm} could not update ${NAME} — nothing was relinked`);
|
|
181
|
-
}
|
|
182
|
-
// The running copy is the old version; its skills directory may be gone. Hand
|
|
183
|
-
// over to the freshly installed binary so the linker matches the new package.
|
|
184
|
-
const fresh = join(ROOT, "node_modules", ".bin", "sassy-skills");
|
|
185
|
-
if (existsSync(fresh)) {
|
|
186
|
-
try {
|
|
187
|
-
execFileSync(fresh, ["sync", "--root", ROOT], { stdio: "inherit" });
|
|
188
|
-
} catch {
|
|
189
|
-
die("updated, but the new binary could not relink — run: sassy-skills sync");
|
|
190
|
-
}
|
|
191
|
-
return 0;
|
|
192
|
-
}
|
|
193
|
-
return sync();
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function newerThan(a, b) {
|
|
197
|
-
const pa = String(a).split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
|
|
198
|
-
const pb = String(b).split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
|
|
199
|
-
for (let i = 0; i < 3; i++) {
|
|
200
|
-
if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
|
|
201
|
-
}
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function updateHint() {
|
|
206
|
-
const pm = detectPM();
|
|
207
|
-
const script = pm === "bun" ? "bun run skills:update"
|
|
208
|
-
: pm ? `${pm} run skills:update`
|
|
209
|
-
: "sassy-skills update";
|
|
210
|
-
return script;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Non-blocking version nudge. Throttled to once a day per machine, silent when
|
|
214
|
-
// offline, and it never fails a run — a stale pin is worth mentioning, not
|
|
215
|
-
// worth blocking work over.
|
|
216
|
-
async function outdated(force = false) {
|
|
217
|
-
const cacheFile = join(tmpdir(), "sassy-skills-version-check.json");
|
|
218
|
-
const TTL = 24 * 60 * 60 * 1000;
|
|
219
|
-
let cached = null;
|
|
220
|
-
try { cached = JSON.parse(readFileSync(cacheFile, "utf8")); } catch { /* first run */ }
|
|
221
|
-
|
|
222
|
-
let latest = null;
|
|
223
|
-
const fresh = cached && typeof cached.checkedAt === "number" && Date.now() - cached.checkedAt < TTL;
|
|
224
|
-
if (fresh && !force) {
|
|
225
|
-
latest = cached.latest ?? null;
|
|
226
|
-
} else {
|
|
227
|
-
try {
|
|
228
|
-
const res = await fetch(`${REGISTRY}/${encodeURIComponent(NAME)}/latest`, {
|
|
229
|
-
signal: AbortSignal.timeout(2000),
|
|
230
|
-
headers: { accept: "application/json" },
|
|
231
|
-
});
|
|
232
|
-
const body = await res.json();
|
|
233
|
-
latest = typeof body?.version === "string" ? body.version : null;
|
|
234
|
-
} catch { latest = null; }
|
|
235
|
-
try { writeFileSync(cacheFile, JSON.stringify({ checkedAt: Date.now(), latest })); } catch { /* read-only tmp */ }
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (!latest || !newerThan(latest, VERSION)) return 0;
|
|
239
|
-
console.log(` ↑ ${NAME} ${VERSION} → ${latest} available — ${updateHint()}`);
|
|
240
|
-
return 0;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
switch (command) {
|
|
244
|
-
case "outdated": process.exit(await outdated(args.includes("--force")));
|
|
245
|
-
case "sync": process.exit(sync());
|
|
246
|
-
case "check": process.exit(check());
|
|
247
|
-
case "update": process.exit(update());
|
|
248
|
-
default:
|
|
249
|
-
die(`unknown command: ${command} (try --help)`);
|
|
250
|
-
}
|