@spardutti/claude-skills 2.33.7 → 2.34.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 CHANGED
@@ -169,7 +169,13 @@ Every install writes a manifest at `.claude/.claude-skills.json` recording what
169
169
  npx @spardutti/claude-skills --sync
170
170
  ```
171
171
 
172
- `--sync` refreshes every tracked item to the latest catalog and prunes stale ones in one shot — no menu. For a project that predates the manifest, the first normal run offers a one-time cleanup of `.claude/` content no longer in the catalog.
172
+ `--sync` refreshes every tracked item, and any hooks the project already has, to the latest catalog and prunes stale ones in one shot — no menu. For a project that predates the manifest, the first normal run offers a one-time cleanup of `.claude/` content no longer in the catalog.
173
+
174
+ `--sync-all[=dir]` does the same for every project under `dir` (default: your home folder) that has a manifest, fetching the catalog once:
175
+
176
+ ```bash
177
+ npx @spardutti/claude-skills --sync-all
178
+ ```
173
179
 
174
180
  `--local[=path]` reads the catalog from a working copy instead of GitHub, so an unreleased change can be installed and tried without publishing it first:
175
181
 
package/bin/cli.mjs CHANGED
@@ -4,12 +4,14 @@ import { confirm } from "@inquirer/prompts";
4
4
  import chalk from "chalk";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { fileURLToPath } from "node:url";
7
- import { dirname, join } from "node:path";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { homedir } from "node:os";
8
9
  import { fetchSkills, fetchCommands, fetchAgents } from "../lib/github.mjs";
9
10
  import { promptSkillSelection, promptCommandSelection, promptRemoval } from "../lib/prompt.mjs";
10
11
  import { installSkills, installCommands, installRequiredAgents } from "../lib/install.mjs";
11
12
  import { makeLocalSource } from "../lib/local.mjs";
12
13
  import { runPostInstall } from "../lib/post-install.mjs";
14
+ import { runSync, runSyncAll } from "../lib/sync.mjs";
13
15
  import {
14
16
  readManifest, writeManifest, computeOrphans, computeRemovals, scanInstalled, removeArtifacts,
15
17
  MANIFEST_FILE,
@@ -19,37 +21,11 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
19
21
  const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
20
22
  const CWD = process.cwd();
21
23
 
22
- // `--sync`: re-install everything the manifest records (refreshed to the latest
23
- // catalog) and prune anything removed upstream — no interactive menu.
24
- async function runSync(manifest, catalog) {
25
- if (!manifest) {
26
- console.log(` ${chalk.yellow("Nothing to sync")} — no manifest in this project. Run without --sync first.\n`);
27
- return;
28
- }
29
- const orphans = computeOrphans(manifest, catalog);
30
- const orphanCount = orphans.skills.length + orphans.commands.length + orphans.agents.length;
31
- if (orphanCount > 0) {
32
- await removeArtifacts(CWD, orphans);
33
- console.log(` ${chalk.green("✔")} Pruned ${orphanCount} item(s) removed from the catalog.`);
34
- }
35
-
36
- const skills = catalog.skills.filter((s) => manifest.skills.includes(s.dirName));
37
- const commands = catalog.commands.filter((c) => manifest.commands.includes(c.fileName));
38
- if (skills.length > 0) { console.log(); await installSkills(skills); }
39
- if (commands.length > 0) { console.log(); await installCommands(commands); }
40
- const { installed } = await installRequiredAgents(commands, catalog.agents, CWD);
41
-
42
- await writeManifest(CWD, {
43
- catalogVersion: pkg.version,
44
- skills: skills.map((s) => s.dirName),
45
- commands: commands.map((c) => c.fileName),
46
- agents: installed.map((a) => a.fileName),
47
- });
48
- console.log(`\n ${chalk.green("✔")} ${chalk.bold(`Synced to catalog v${pkg.version}.`)}\n`);
49
- }
50
-
51
24
  async function main() {
52
25
  const isSync = process.argv.includes("--sync");
26
+ // --sync-all[=dir] syncs every project under dir (default: home) from one catalog fetch.
27
+ const syncAllArg = process.argv.find((a) => a === "--sync-all" || a.startsWith("--sync-all="));
28
+ const syncAllRoot = syncAllArg ? resolve(syncAllArg.split("=")[1] || homedir()) : null;
53
29
 
54
30
  // --local[=path] reads the catalog from a working copy instead of GitHub, so an
55
31
  // unreleased change can be installed and tried without publishing it first.
@@ -69,10 +45,10 @@ async function main() {
69
45
  fetchers.fetchSkills(), fetchers.fetchCommands(), fetchers.fetchAgents(),
70
46
  ]);
71
47
  const catalog = { skills, commands, agents };
48
+ if (syncAllRoot) return runSyncAll(syncAllRoot, catalog, pkg.version);
49
+ if (isSync) return runSync(CWD, catalog, pkg.version);
72
50
  const manifest = await readManifest(CWD);
73
51
 
74
- if (isSync) return runSync(manifest, catalog);
75
-
76
52
  // --- Prune items renamed or removed from the catalog upstream ---
77
53
  const orphans = computeOrphans(manifest, catalog);
78
54
  const orphanNames = [...orphans.skills, ...orphans.commands, ...orphans.agents];
package/lib/sync.mjs ADDED
@@ -0,0 +1,81 @@
1
+ import chalk from "chalk";
2
+ import { existsSync } from "node:fs";
3
+ import { readdir } from "node:fs/promises";
4
+ import { join, basename } from "node:path";
5
+ import { installSkills, installCommands, installRequiredAgents } from "./install.mjs";
6
+ import { setupHook } from "./setup-hook.mjs";
7
+ import { readManifest, writeManifest, computeOrphans, removeArtifacts, MANIFEST_FILE } from "./manifest.mjs";
8
+
9
+ const SEARCH_DEPTH = 5;
10
+
11
+ // Re-install everything the manifest records (refreshed to the latest catalog),
12
+ // refresh hooks the project already has, and prune anything removed upstream.
13
+ export async function runSync(dir, catalog, version) {
14
+ const manifest = await readManifest(dir);
15
+ if (!manifest) {
16
+ console.log(` ${chalk.yellow("Nothing to sync")} — no manifest in this project. Run without --sync first.\n`);
17
+ return false;
18
+ }
19
+ const orphans = computeOrphans(manifest, catalog);
20
+ const orphanCount = orphans.skills.length + orphans.commands.length + orphans.agents.length;
21
+ if (orphanCount > 0) {
22
+ await removeArtifacts(dir, orphans);
23
+ console.log(` ${chalk.green("✔")} Pruned ${orphanCount} item(s) removed from the catalog.`);
24
+ }
25
+
26
+ const skills = catalog.skills.filter((s) => manifest.skills.includes(s.dirName));
27
+ const commands = catalog.commands.filter((c) => manifest.commands.includes(c.fileName));
28
+ if (skills.length > 0) { console.log(); await installSkills(skills, dir); }
29
+ if (commands.length > 0) { console.log(); await installCommands(commands, dir); }
30
+ const { installed } = await installRequiredAgents(commands, catalog.agents, dir);
31
+ if (existsSync(join(dir, ".claude", "hooks", "skill-gate.sh"))) { console.log(); await setupHook(dir); }
32
+
33
+ await writeManifest(dir, {
34
+ catalogVersion: version,
35
+ skills: skills.map((s) => s.dirName),
36
+ commands: commands.map((c) => c.fileName),
37
+ agents: installed.map((a) => a.fileName),
38
+ });
39
+ console.log(`\n ${chalk.green("✔")} ${chalk.bold(`Synced to catalog v${version}.`)}\n`);
40
+ return true;
41
+ }
42
+
43
+ export async function findProjects(root, depth = SEARCH_DEPTH) {
44
+ if (existsSync(join(root, ".claude", MANIFEST_FILE))) return [root];
45
+ if (depth === 0) return [];
46
+ let entries;
47
+ try {
48
+ entries = await readdir(root, { withFileTypes: true });
49
+ } catch {
50
+ return [];
51
+ }
52
+ const found = [];
53
+ for (const e of entries) {
54
+ if (!e.isDirectory() || e.name.startsWith(".") || e.name === "node_modules") continue;
55
+ found.push(...(await findProjects(join(root, e.name), depth - 1)));
56
+ }
57
+ return found;
58
+ }
59
+
60
+ export async function runSyncAll(root, catalog, version) {
61
+ const projects = await findProjects(root);
62
+ if (projects.length === 0) {
63
+ console.log(` ${chalk.yellow("No projects found")} under ${root} with .claude/${MANIFEST_FILE}.\n`);
64
+ return;
65
+ }
66
+ const failed = [];
67
+ for (const dir of projects) {
68
+ console.log(chalk.bold.cyan(`── ${dir}`));
69
+ try {
70
+ await runSync(dir, catalog, version);
71
+ } catch (err) {
72
+ failed.push(basename(dir));
73
+ console.log(` ${chalk.red("Error:")} ${err.message}\n`);
74
+ }
75
+ }
76
+ const ok = projects.length - failed.length;
77
+ console.log(` ${chalk.green("✔")} ${chalk.bold(`${ok} of ${projects.length} project(s) synced to v${version}.`)}`);
78
+ if (failed.length > 0) console.log(` ${chalk.red("✘")} Failed: ${failed.join(", ")}`);
79
+ console.log();
80
+ if (failed.length > 0) process.exitCode = 1;
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spardutti/claude-skills",
3
- "version": "2.33.7",
3
+ "version": "2.34.0",
4
4
  "description": "Guardrails for Claude Code — best-practice skills, planning commands, and delivery gates.",
5
5
  "type": "module",
6
6
  "repository": {