@spardutti/claude-skills 2.59.1 → 2.60.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/bin/cli.mjs +4 -3
- package/hooks/registry.mjs +5 -1
- package/hooks/ship-log.mjs +4 -3
- package/lib/frontmatter.mjs +2 -1
- package/lib/github.mjs +2 -2
- package/lib/install.mjs +12 -0
- package/lib/local.mjs +2 -2
- package/lib/manifest.mjs +10 -2
- package/lib/sync.mjs +21 -3
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -8,13 +8,13 @@ import { dirname, join, resolve } from "node:path";
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { fetchSkills, fetchCommands, fetchAgents } from "../lib/github.mjs";
|
|
10
10
|
import { promptSkillSelection, promptCommandSelection, promptRemoval } from "../lib/prompt.mjs";
|
|
11
|
-
import { installSkills, installCommands, installRequiredAgents } from "../lib/install.mjs";
|
|
11
|
+
import { installSkills, installCommands, installRequiredAgents, withRequiredCommands } from "../lib/install.mjs";
|
|
12
12
|
import { makeLocalSource } from "../lib/local.mjs";
|
|
13
13
|
import { runPostInstall } from "../lib/post-install.mjs";
|
|
14
14
|
import { runSync, runSyncAll } from "../lib/sync.mjs";
|
|
15
15
|
import {
|
|
16
16
|
readManifest, writeManifest, computeOrphans, computeRemovals, scanInstalled, removeArtifacts,
|
|
17
|
-
MANIFEST_FILE,
|
|
17
|
+
catalogIds, MANIFEST_FILE,
|
|
18
18
|
} from "../lib/manifest.mjs";
|
|
19
19
|
|
|
20
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -104,7 +104,7 @@ async function main() {
|
|
|
104
104
|
let installedAgents = [];
|
|
105
105
|
if (commands.length > 0) {
|
|
106
106
|
console.log();
|
|
107
|
-
selectedCommands = await promptCommandSelection(commands, manifest?.commands ?? []);
|
|
107
|
+
selectedCommands = withRequiredCommands(await promptCommandSelection(commands, manifest?.commands ?? []), commands);
|
|
108
108
|
if (selectedCommands.length > 0) {
|
|
109
109
|
console.log();
|
|
110
110
|
await installCommands(selectedCommands);
|
|
@@ -141,6 +141,7 @@ async function main() {
|
|
|
141
141
|
skills: selectedSkills.map((s) => s.dirName),
|
|
142
142
|
commands: selectedCommands.map((c) => c.fileName),
|
|
143
143
|
agents: installedAgents.map((a) => a.fileName),
|
|
144
|
+
known: catalogIds(catalog),
|
|
144
145
|
});
|
|
145
146
|
|
|
146
147
|
const parts = [];
|
package/hooks/registry.mjs
CHANGED
|
@@ -71,7 +71,11 @@ function stale() {
|
|
|
71
71
|
if (!syms.length) say("extend_by names no symbol — write names like `AimStrategy`");
|
|
72
72
|
const code = files.filter((f) => insideLocation(e, f) && !f.endsWith(REGISTRY_SUFFIX))
|
|
73
73
|
.map((f) => readFileSync(join(root, f), "utf8")).join("\n");
|
|
74
|
-
for (const s of syms)
|
|
74
|
+
for (const s of syms) {
|
|
75
|
+
// A plain word like `first` matches `var first = enemies[0]`; `Aim.first` only matches real use.
|
|
76
|
+
if (/^[a-z][a-z0-9]*$/.test(s)) say(`\`${s}\` is too common a word to prove use — name its owner, like \`Owner.${s}\``);
|
|
77
|
+
else if (!s.split(".").every((part) => uses(code, part))) say(`${s} from extend_by is not found in ${e.location}`);
|
|
78
|
+
}
|
|
75
79
|
}
|
|
76
80
|
if (problems.length) fail(problems);
|
|
77
81
|
}
|
package/hooks/ship-log.mjs
CHANGED
|
@@ -57,20 +57,21 @@ function finalize(dir, path, record, tag, status, reason, at) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function prOf(branch) {
|
|
60
|
-
const r = spawnSync("gh", ["pr", "list", "--head", branch, "--state", "all", "--json", "number,createdAt", "--limit", "1"], { encoding: "utf8" });
|
|
60
|
+
const r = spawnSync("gh", ["pr", "list", "--head", branch, "--state", "all", "--json", "number,createdAt,state", "--limit", "1"], { encoding: "utf8" });
|
|
61
61
|
try { return r.status === 0 ? JSON.parse(r.stdout)[0] : undefined; } catch { return undefined; }
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// Agents skip the `pr` call, so a log whose branch has a PR by now is closed at that PR.
|
|
65
65
|
// A PR older than the log was only updated by it, so that log ends at its own last event.
|
|
66
|
+
// A long-lived branch like dev keeps its merged PRs, so an old one that is not open is not this log's.
|
|
66
67
|
function closeOpened(dir, app) {
|
|
67
68
|
const open = join(dir, "open");
|
|
68
69
|
for (const f of existsSync(open) ? readdirSync(open).filter((n) => n.startsWith(`${app}__`)) : []) {
|
|
69
70
|
const path = join(open, f), record = JSON.parse(readFileSync(path, "utf8"));
|
|
70
71
|
if (!record.gate.length && !record.findings.length) continue;
|
|
71
72
|
const pr = prOf(record.branch);
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
const opened = pr && pr.createdAt >= record.started;
|
|
74
|
+
if (!pr || (!opened && pr.state !== "OPEN")) continue;
|
|
74
75
|
finalize(dir, path, Object.assign(record, { pr: pr.number }), `pr${pr.number}`,
|
|
75
76
|
opened ? "pr-opened" : "pr-updated", null, opened ? pr.createdAt : lastEvent(record));
|
|
76
77
|
}
|
package/lib/frontmatter.mjs
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
export function parseFrontmatter(content, fallbackName) {
|
|
17
17
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
18
18
|
if (!match) {
|
|
19
|
-
return { name: fallbackName, description: "", category: "General", requiresAgents: [] };
|
|
19
|
+
return { name: fallbackName, description: "", category: "General", requiresAgents: [], requiresCommands: [] };
|
|
20
20
|
}
|
|
21
21
|
const block = match[1];
|
|
22
22
|
|
|
@@ -33,6 +33,7 @@ export function parseFrontmatter(content, fallbackName) {
|
|
|
33
33
|
description: pick("description"),
|
|
34
34
|
category: pick("category") || "General",
|
|
35
35
|
requiresAgents: parseAgentList(pick("requires-agents")),
|
|
36
|
+
requiresCommands: parseAgentList(pick("requires-commands")),
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
|
package/lib/github.mjs
CHANGED
|
@@ -154,8 +154,8 @@ export function fetchCommands() {
|
|
|
154
154
|
entryFilter: (e) => e.type === "file" && e.name.endsWith(".md"),
|
|
155
155
|
buildRawUrl: (file) => `${RAW_COMMANDS_BASE}/${file.name}`,
|
|
156
156
|
mapEntry: (file, content) => {
|
|
157
|
-
const { name, description, category, requiresAgents } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
|
|
158
|
-
return { fileName: file.name, name, description, category, requiresAgents, content };
|
|
157
|
+
const { name, description, category, requiresAgents, requiresCommands } = parseFrontmatter(content, file.name.replace(/\.md$/, ""));
|
|
158
|
+
return { fileName: file.name, name, description, category, requiresAgents, requiresCommands, content };
|
|
159
159
|
},
|
|
160
160
|
});
|
|
161
161
|
}
|
package/lib/install.mjs
CHANGED
|
@@ -43,6 +43,18 @@ export function installAgents(agents, targetDir = process.cwd()) {
|
|
|
43
43
|
return installFlat(agents, "agents", targetDir);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// A command that names another in `requires-commands` breaks without it, so it comes along.
|
|
47
|
+
export function withRequiredCommands(selected, catalogCommands) {
|
|
48
|
+
const out = [...selected];
|
|
49
|
+
for (let i = 0; i < out.length; i++) {
|
|
50
|
+
for (const name of out[i].requiresCommands ?? []) {
|
|
51
|
+
const dep = catalogCommands.find((c) => c.name === name);
|
|
52
|
+
if (dep && !out.some((c) => c.fileName === dep.fileName)) out.push(dep);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
export async function installRequiredAgents(selectedCommands, availableAgents, targetDir = process.cwd()) {
|
|
47
59
|
const requiredNames = new Set(
|
|
48
60
|
selectedCommands.flatMap((c) => c.requiresAgents ?? [])
|
package/lib/local.mjs
CHANGED
|
@@ -39,8 +39,8 @@ export function makeLocalSource(root) {
|
|
|
39
39
|
for (const f of await listDir(join(root, "commands"))) {
|
|
40
40
|
if (!f.isFile() || !f.name.endsWith(".md")) continue;
|
|
41
41
|
const content = await readFile(join(root, "commands", f.name), "utf-8");
|
|
42
|
-
const { name, description, category, requiresAgents } = parseFrontmatter(content, f.name.replace(/\.md$/, ""));
|
|
43
|
-
out.push({ fileName: f.name, name, description, category, requiresAgents, content });
|
|
42
|
+
const { name, description, category, requiresAgents, requiresCommands } = parseFrontmatter(content, f.name.replace(/\.md$/, ""));
|
|
43
|
+
out.push({ fileName: f.name, name, description, category, requiresAgents, requiresCommands, content });
|
|
44
44
|
}
|
|
45
45
|
return out;
|
|
46
46
|
},
|
package/lib/manifest.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export async function readManifest(targetDir = process.cwd()) {
|
|
|
17
17
|
commands: data.commands ?? [],
|
|
18
18
|
agents: data.agents ?? [],
|
|
19
19
|
custom: data.custom ?? [],
|
|
20
|
+
known: data.known ?? null,
|
|
20
21
|
};
|
|
21
22
|
} catch {
|
|
22
23
|
return null; // no manifest — a pre-manifest project or a fresh install
|
|
@@ -25,9 +26,10 @@ export async function readManifest(targetDir = process.cwd()) {
|
|
|
25
26
|
|
|
26
27
|
// Record exactly what the CLI has installed. This is the CLI's source of truth —
|
|
27
28
|
// it never deletes anything not listed here. `custom` is the user's own gated skills, carried over untouched.
|
|
28
|
-
export async function writeManifest(targetDir, { catalogVersion, skills, commands, agents }) {
|
|
29
|
+
export async function writeManifest(targetDir, { catalogVersion, skills, commands, agents, known }) {
|
|
29
30
|
await mkdir(join(targetDir, ".claude"), { recursive: true });
|
|
30
|
-
const
|
|
31
|
+
const previous = await readManifest(targetDir);
|
|
32
|
+
const custom = previous?.custom ?? [];
|
|
31
33
|
const data = {
|
|
32
34
|
catalogVersion,
|
|
33
35
|
installedAt: new Date().toISOString(),
|
|
@@ -35,10 +37,16 @@ export async function writeManifest(targetDir, { catalogVersion, skills, command
|
|
|
35
37
|
commands: [...new Set(commands)].sort(),
|
|
36
38
|
agents: [...new Set(agents)].sort(),
|
|
37
39
|
custom,
|
|
40
|
+
known: [...new Set(known ?? previous?.known ?? [])].sort(),
|
|
38
41
|
};
|
|
39
42
|
await writeFile(manifestPath(targetDir), JSON.stringify(data, null, 2) + "\n");
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
// Every skill and command in the catalog, so the next sync can name what is new since.
|
|
46
|
+
export function catalogIds(catalog) {
|
|
47
|
+
return [...catalog.skills.map((s) => s.dirName), ...catalog.commands.map((c) => c.fileName)];
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
// Manifest entries that no longer exist in the current catalog (renamed or removed upstream).
|
|
43
51
|
export function computeOrphans(manifest, catalog) {
|
|
44
52
|
if (!manifest) return { skills: [], commands: [], agents: [] };
|
package/lib/sync.mjs
CHANGED
|
@@ -2,13 +2,27 @@ import chalk from "chalk";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { readdir } from "node:fs/promises";
|
|
4
4
|
import { join, basename } from "node:path";
|
|
5
|
-
import { installSkills, installCommands, installRequiredAgents } from "./install.mjs";
|
|
5
|
+
import { installSkills, installCommands, installRequiredAgents, withRequiredCommands } from "./install.mjs";
|
|
6
6
|
import { setupHook } from "./setup-hook.mjs";
|
|
7
7
|
import { setupClaudeMd } from "./setup-claude-md.mjs";
|
|
8
|
-
import { readManifest, writeManifest, computeOrphans, removeArtifacts, MANIFEST_FILE } from "./manifest.mjs";
|
|
8
|
+
import { readManifest, writeManifest, computeOrphans, removeArtifacts, catalogIds, MANIFEST_FILE } from "./manifest.mjs";
|
|
9
9
|
|
|
10
10
|
const SEARCH_DEPTH = 5;
|
|
11
11
|
|
|
12
|
+
// Sync installs only what was picked, so a new skill or command would otherwise go unseen.
|
|
13
|
+
function announceNew(manifest, catalog, have) {
|
|
14
|
+
if (!manifest.known) return;
|
|
15
|
+
const known = new Set(manifest.known);
|
|
16
|
+
const fresh = [
|
|
17
|
+
...catalog.skills.map((s) => ({ id: s.dirName, label: s.name, about: s.description })),
|
|
18
|
+
...catalog.commands.map((c) => ({ id: c.fileName, label: `/${c.name}`, about: c.description })),
|
|
19
|
+
].filter((x) => !known.has(x.id) && !have.has(x.id));
|
|
20
|
+
if (fresh.length === 0) return;
|
|
21
|
+
console.log();
|
|
22
|
+
for (const x of fresh) console.log(` ${chalk.cyan("new")} ${chalk.bold(x.label)} — ${x.about.split(" — ")[0].slice(0, 100)}`);
|
|
23
|
+
console.log(chalk.dim(" Run the CLI without --sync to pick any of them."));
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
// Re-install everything the manifest records (refreshed to the latest catalog),
|
|
13
27
|
// refresh hooks the project already has, and prune anything removed upstream.
|
|
14
28
|
export async function runSync(dir, catalog, version) {
|
|
@@ -25,17 +39,21 @@ export async function runSync(dir, catalog, version) {
|
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
const skills = catalog.skills.filter((s) => manifest.skills.includes(s.dirName));
|
|
28
|
-
const
|
|
42
|
+
const picked = catalog.commands.filter((c) => manifest.commands.includes(c.fileName));
|
|
43
|
+
const commands = withRequiredCommands(picked, catalog.commands);
|
|
44
|
+
for (const c of commands.slice(picked.length)) console.log(` ${chalk.green("+")} Added ${chalk.bold(`/${c.name}`)} — a command you have needs it.`);
|
|
29
45
|
if (skills.length > 0) { console.log(); await installSkills(skills, dir); }
|
|
30
46
|
if (commands.length > 0) { console.log(); await installCommands(commands, dir); }
|
|
31
47
|
const { installed } = await installRequiredAgents(commands, catalog.agents, dir);
|
|
32
48
|
if (existsSync(join(dir, ".claude", "hooks", "skill-gate.sh"))) { console.log(); await setupHook(dir); await setupClaudeMd(dir); }
|
|
49
|
+
announceNew(manifest, catalog, new Set([...skills.map((s) => s.dirName), ...commands.map((c) => c.fileName)]));
|
|
33
50
|
|
|
34
51
|
await writeManifest(dir, {
|
|
35
52
|
catalogVersion: version,
|
|
36
53
|
skills: skills.map((s) => s.dirName),
|
|
37
54
|
commands: commands.map((c) => c.fileName),
|
|
38
55
|
agents: installed.map((a) => a.fileName),
|
|
56
|
+
known: catalogIds(catalog),
|
|
39
57
|
});
|
|
40
58
|
console.log(`\n ${chalk.green("✔")} ${chalk.bold(`Synced to catalog v${version}.`)}\n`);
|
|
41
59
|
return true;
|