pruff-skills 1.8.1 → 1.10.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
@@ -147,6 +147,9 @@ npx pruff-skills add <skill> -g -a claude-code -y
147
147
  | `npx pruff-skills restore` | Reinstall everything recorded in `skills-lock.json` |
148
148
  | `npx pruff-skills sync` | Install skills found in `node_modules` into agent dirs |
149
149
  | `npx pruff-skills onboarding --db-access` | Set up personal read-only database access (team members) |
150
+ | `npx pruff-skills doctor` | Print a diagnostic block to paste when something fails |
151
+
152
+ Troubleshooting: if a command fails, run `npx pruff-skills doctor` and paste the block it prints when you ask for help.
150
153
 
151
154
  ```bash
152
155
  npx pruff-skills list # list installed skills
@@ -1,8 +1,8 @@
1
1
  import { r as __toESM } from "../rolldown-runtime.mjs";
2
2
  import { n as require_src, t as require_dist } from "./@kwsites/file-exists.mjs";
3
3
  import { t as require_dist$1 } from "./@kwsites/promise-deferred.mjs";
4
- import { Buffer as Buffer$1 } from "node:buffer";
5
4
  import { spawn } from "child_process";
5
+ import { Buffer as Buffer$1 } from "node:buffer";
6
6
  import { normalize } from "node:path";
7
7
  import { EventEmitter } from "node:events";
8
8
  var import_dist = require_dist();
package/dist/cli.mjs CHANGED
@@ -7,20 +7,378 @@ import "./_chunks/libs/@kwsites/promise-deferred.mjs";
7
7
  import { t as esm_default } from "./_chunks/libs/simple-git.mjs";
8
8
  import { t as xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
9
9
  import { t as require_dist } from "./_chunks/libs/@vercel/detect-agent.mjs";
10
- import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs";
10
+ import { accessSync, chmodSync, constants, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs";
11
11
  import { basename, delimiter, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
12
12
  import { fileURLToPath } from "url";
13
13
  import { stripVTControlCharacters } from "node:util";
14
+ import { execFile, execSync, spawn, spawnSync } from "child_process";
14
15
  import { homedir, platform, tmpdir } from "os";
15
16
  import * as readline from "readline";
16
17
  import { Writable } from "stream";
17
18
  import { promisify } from "util";
18
- import { execFile, execSync, spawn, spawnSync } from "child_process";
19
19
  import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
20
20
  import { parse } from "yaml";
21
21
  import { createHash, randomBytes } from "crypto";
22
22
  import { createHash as createHash$1 } from "node:crypto";
23
23
  import { gunzipSync, inflateRawSync } from "node:zlib";
24
+ var UserFacingError = class extends Error {};
25
+ var LockVersionError = class extends UserFacingError {};
26
+ const DIAGNOSTICS_HEADER = "Diagnóstico — copiá y pegá esto en #dev-tools o a Ramiro";
27
+ const DIAGNOSTICS_FOOTER = "Si esto falla otra vez: pruff-skills doctor";
28
+ const DEFAULT_CATALOG = "Pruff-Tech/pruff-skills";
29
+ const PROBE_TIMEOUT_MS = 2500;
30
+ const probeCache = /* @__PURE__ */ new Map();
31
+ let currentStep = "start";
32
+ let failure = null;
33
+ const tail = [];
34
+ function setStep(step) {
35
+ currentStep = step;
36
+ }
37
+ function recordFailure(message) {
38
+ failure ??= {
39
+ step: currentStep,
40
+ message
41
+ };
42
+ }
43
+ function resetDiagnosticsState() {
44
+ currentStep = "start";
45
+ failure = null;
46
+ tail.length = 0;
47
+ }
48
+ function recordedFailure() {
49
+ return failure;
50
+ }
51
+ const ANSI = /\x1b\[[0-9;?]*[A-Za-z]/g;
52
+ function redact(text, home = homedir()) {
53
+ let out = text.replace(ANSI, "");
54
+ if (home && home !== "/") out = out.split(home).join("~");
55
+ return out.replace(/(https?:\/\/)[^\s/@]+@/gi, "$1[redacted]@").replace(/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{10,}/g, "[redacted]").replace(/\bgithub_pat_[A-Za-z0-9_]{10,}/g, "[redacted]").replace(/\bAKIA[A-Z0-9]{12,}/g, "[redacted]").replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(/([?&]token=)[^&\s]+/gi, "$1[redacted]").replace(/(x-api-key["']?\s*[:=]?\s*)[^\s"',;]+/gi, "$1[redacted]").replace(/[A-Za-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,}/g, "[email]");
56
+ }
57
+ function catalogKind(source) {
58
+ if (/^[^/\s]+\/[^/\s]+$/.test(source) && !source.startsWith(".")) return "owner/repo";
59
+ if (source.startsWith(".") || isAbsolute(source) || source.startsWith("~")) return "local path";
60
+ try {
61
+ return `git URL (${new URL(source).host})`;
62
+ } catch {
63
+ const scp = source.match(/^[^@\s]+@([^:\s]+):/);
64
+ return scp ? `git URL (${scp[1]})` : "git URL";
65
+ }
66
+ }
67
+ const yn = (v) => v ? "yes" : "no";
68
+ function probe(cmd, args) {
69
+ const key = [cmd, ...args].join(" ");
70
+ const cached = probeCache.get(key);
71
+ if (cached) return cached;
72
+ const r = spawnSync(cmd, args, {
73
+ encoding: "utf-8",
74
+ timeout: PROBE_TIMEOUT_MS,
75
+ stdio: "pipe",
76
+ env: {
77
+ ...process.env,
78
+ GIT_TERMINAL_PROMPT: "0",
79
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
80
+ }
81
+ });
82
+ const result = {
83
+ ok: r.status === 0,
84
+ out: `${r.stdout ?? ""}\n${r.stderr ?? ""}`.trim()
85
+ };
86
+ probeCache.set(key, result);
87
+ return result;
88
+ }
89
+ const onPath = (cmd) => {
90
+ return yn((process.env.PATH ?? "").split(delimiter).filter(Boolean).some((dir) => {
91
+ try {
92
+ accessSync(join(dir, cmd), constants.X_OK);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }));
98
+ };
99
+ function version$2() {
100
+ try {
101
+ const dir = dirname(fileURLToPath(import.meta.url));
102
+ return JSON.parse(readFileSync(join(dir, "..", "package.json"), "utf-8")).version;
103
+ } catch {
104
+ return "unknown";
105
+ }
106
+ }
107
+ function timestamp(d = /* @__PURE__ */ new Date()) {
108
+ const pad = (n) => String(Math.trunc(Math.abs(n))).padStart(2, "0");
109
+ const off = -d.getTimezoneOffset();
110
+ const sign = off >= 0 ? "+" : "-";
111
+ return `${(/* @__PURE__ */ new Date(d.getTime() - d.getTimezoneOffset() * 6e4)).toISOString().slice(0, 19)}${sign}${pad(off / 60)}:${pad(off % 60)}`;
112
+ }
113
+ function packageManager() {
114
+ const pnpm = probe("pnpm", ["-v"]);
115
+ const npm = probe("npm", ["-v"]);
116
+ const v = (r) => r.ok && r.out.match(/\d+\.\d+\.\d+\S*/g)?.pop() || "n/a";
117
+ return `pnpm ${v(pnpm)}, npm ${v(npm)}`;
118
+ }
119
+ function ghStatus() {
120
+ if (onPath("gh") === "no") return {
121
+ summary: "n/a (gh not installed)",
122
+ loggedIn: false
123
+ };
124
+ const r = probe("gh", ["auth", "status"]);
125
+ const text = r.out;
126
+ const loggedIn = r.ok || /Logged in to/i.test(text);
127
+ const host = text.match(/Logged in to (\S+)/i)?.[1] ?? text.match(/^(\S+\.\S+)\s*$/m)?.[1];
128
+ const account = text.match(/account (\S+)/i)?.[1];
129
+ const scopes = text.match(/Token scopes:\s*(.+)/i)?.[1]?.replace(/['"]/g, "").trim();
130
+ if (!loggedIn) return {
131
+ summary: "logged_in=no",
132
+ loggedIn
133
+ };
134
+ return {
135
+ summary: redact(`logged_in=yes host=${host ?? "?"} account=${account ?? "?"} scopes=${scopes ?? "?"}`),
136
+ loggedIn
137
+ };
138
+ }
139
+ function checkCatalog(source) {
140
+ const kind = catalogKind(source);
141
+ if (kind === "local path") {
142
+ const root = resolve(source.replace(/^~(?=\/|$)/, homedir()));
143
+ if (!existsSync(root)) return {
144
+ ok: false,
145
+ detail: "fail (path does not exist)"
146
+ };
147
+ return existsSync(join(root, ".well-known", "agent-skills", "index.json")) ? {
148
+ ok: true,
149
+ detail: "ok (local path, index.json present)"
150
+ } : {
151
+ ok: false,
152
+ detail: "fail (index.json missing)"
153
+ };
154
+ }
155
+ if (kind === "owner/repo") {
156
+ const r = probe("gh", [
157
+ "api",
158
+ `repos/${source}`,
159
+ "--jq",
160
+ ".default_branch"
161
+ ]);
162
+ return r.ok ? {
163
+ ok: true,
164
+ detail: "ok (gh api)"
165
+ } : {
166
+ ok: false,
167
+ detail: `fail (gh api: ${redact(r.out.split("\n")[0] ?? "")})`
168
+ };
169
+ }
170
+ const r = probe("git", [
171
+ "ls-remote",
172
+ "--exit-code",
173
+ source,
174
+ "HEAD"
175
+ ]);
176
+ return r.ok ? {
177
+ ok: true,
178
+ detail: "ok (git ls-remote)"
179
+ } : {
180
+ ok: false,
181
+ detail: `fail (git ls-remote: ${redact(r.out.split("\n").pop() ?? "")})`
182
+ };
183
+ }
184
+ function nearestExisting(path) {
185
+ let dir = path;
186
+ while (!existsSync(dir) && dirname(dir) !== dir) dir = dirname(dir);
187
+ return dir;
188
+ }
189
+ function writable(path) {
190
+ try {
191
+ accessSync(nearestExisting(path), constants.W_OK);
192
+ return true;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+ function target(ctx) {
198
+ const global = ctx.args.includes("-g") || ctx.args.includes("--global");
199
+ if (ctx.command === "onboarding") {
200
+ const arg = ctx.args.find((a) => !a.startsWith("-"));
201
+ return {
202
+ scope: "workspace",
203
+ dir: arg ? resolve(arg.replace(/^~(?=\/|$)/, homedir())) : join(homedir(), "pruff")
204
+ };
205
+ }
206
+ if (global) return {
207
+ scope: "global (-g)",
208
+ dir: process.env.CLAUDE_CONFIG_DIR?.trim() || homedir()
209
+ };
210
+ return {
211
+ scope: "project",
212
+ dir: process.cwd()
213
+ };
214
+ }
215
+ function settingsStatus() {
216
+ const file = join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "settings.json");
217
+ if (!existsSync(file)) return "missing";
218
+ try {
219
+ JSON.parse(readFileSync(file, "utf-8"));
220
+ return "valid JSON";
221
+ } catch {
222
+ return "NOT valid JSON";
223
+ }
224
+ }
225
+ function collectDiagnostics(ctx) {
226
+ const envCatalog = process.env.PRUFF_SKILLS_CATALOG;
227
+ const source = envCatalog || DEFAULT_CATALOG;
228
+ const gh = ghStatus();
229
+ const catalog = ctx.checkCatalog ? checkCatalog(source) : null;
230
+ const t = target(ctx);
231
+ const configDir = join(homedir(), ".config", "pruff");
232
+ const failed = recordedFailure();
233
+ const errorLine = ctx.error ?? failed?.message ?? "";
234
+ return {
235
+ cli: version$2(),
236
+ node: process.version,
237
+ packageManager: packageManager(),
238
+ os: `${process.platform} ${process.arch}`,
239
+ time: timestamp(),
240
+ command: redact([
241
+ "pruff-skills",
242
+ ctx.command,
243
+ ...ctx.args
244
+ ].join(" ")),
245
+ failedStep: ctx.step ?? failed?.step ?? (errorLine ? currentStep : "none"),
246
+ error: errorLine ? redact(errorLine.split("\n")[0] ?? "").slice(0, 200) : "none",
247
+ exitCode: ctx.exitCode === void 0 ? "n/a" : String(ctx.exitCode),
248
+ catalogEnv: envCatalog ? `PRUFF_SKILLS_CATALOG set (${catalogKind(envCatalog)})` : "not set",
249
+ catalogSource: envCatalog ? catalogKind(envCatalog) : `default (${DEFAULT_CATALOG})`,
250
+ catalogCheck: catalog ? catalog.detail : "not checked",
251
+ gh: onPath("gh"),
252
+ ghAuth: gh.summary,
253
+ git: onPath("git"),
254
+ mysql: onPath("mysql"),
255
+ security: onPath("security"),
256
+ scope: t.scope,
257
+ targetDir: `${redact(t.dir)} (writable=${yn(writable(t.dir))})`,
258
+ dbReaderConfig: yn(existsSync(join(configDir, "db-reader.json"))),
259
+ dbReaderCa: yn(existsSync(join(configDir, "rds-us-east-1-bundle.pem"))),
260
+ claudeSettings: settingsStatus(),
261
+ ghLoggedIn: gh.loggedIn,
262
+ catalogOk: catalog?.ok ?? false
263
+ };
264
+ }
265
+ function renderDiagnostics(d) {
266
+ return [
267
+ "```",
268
+ ...[
269
+ DIAGNOSTICS_HEADER,
270
+ `cli: ${d.cli}`,
271
+ `node: ${d.node}`,
272
+ `package_manager: ${d.packageManager}`,
273
+ `os: ${d.os}`,
274
+ `time: ${d.time}`,
275
+ `command: ${d.command}`,
276
+ `failed_step: ${d.failedStep}`,
277
+ `error: ${d.error}`,
278
+ `exit_code: ${d.exitCode}`,
279
+ `catalog_env: ${d.catalogEnv}`,
280
+ `catalog_source: ${d.catalogSource}`,
281
+ `catalog_check: ${d.catalogCheck}`,
282
+ `gh_on_path: ${d.gh}`,
283
+ `gh_auth: ${d.ghAuth}`,
284
+ `git_on_path: ${d.git}`,
285
+ `mysql_on_path: ${d.mysql}`,
286
+ `security_on_path: ${d.security}`,
287
+ `install_scope: ${d.scope}`,
288
+ `target_dir: ${d.targetDir}`,
289
+ `db_reader_config: ${d.dbReaderConfig}`,
290
+ `db_reader_ca_bundle: ${d.dbReaderCa}`,
291
+ `claude_settings_json: ${d.claudeSettings}`,
292
+ DIAGNOSTICS_FOOTER
293
+ ],
294
+ "```"
295
+ ].join("\n");
296
+ }
297
+ function printDiagnostics(ctx) {
298
+ console.error("");
299
+ console.error(renderDiagnostics(collectDiagnostics(ctx)));
300
+ }
301
+ function lastLoggedError() {
302
+ return lastErrorLine(tail);
303
+ }
304
+ function lastErrorLine(lines) {
305
+ for (let i = lines.length - 1; i >= 0; i--) if (/✖|\bERROR\b|\bfailed\b|\bnot found\b|\bcould not\b|\bdoes not exist\b|\bcannot\b|\bnot authenticated\b/i.test(lines[i])) return lines[i];
306
+ return lines[lines.length - 1] ?? "";
307
+ }
308
+ async function withDiagnostics(command, args, fn) {
309
+ resetDiagnosticsState();
310
+ const realExit = process.exit;
311
+ const realOut = process.stdout.write.bind(process.stdout);
312
+ const realErr = process.stderr.write.bind(process.stderr);
313
+ const remember = (chunk) => {
314
+ if (typeof chunk !== "string") return;
315
+ for (const raw of chunk.replace(ANSI, "").split("\n")) {
316
+ const line = raw.replace(/^[\s│└├┌◇◆■▲●○]+/, "").trim();
317
+ if (line) tail.push(line);
318
+ }
319
+ if (tail.length > 60) tail.splice(0, tail.length - 60);
320
+ };
321
+ process.stdout.write = (chunk, ...rest) => {
322
+ remember(chunk);
323
+ return realOut(chunk, ...rest);
324
+ };
325
+ process.stderr.write = (chunk, ...rest) => {
326
+ remember(chunk);
327
+ return realErr(chunk, ...rest);
328
+ };
329
+ const restore = () => {
330
+ process.exit = realExit;
331
+ process.stdout.write = realOut;
332
+ process.stderr.write = realErr;
333
+ };
334
+ let printed = false;
335
+ const emit = (exitCode, error) => {
336
+ if (printed) return;
337
+ printed = true;
338
+ const message = error ?? recordedFailure()?.message ?? lastErrorLine(tail);
339
+ restore();
340
+ printDiagnostics({
341
+ command,
342
+ args,
343
+ exitCode,
344
+ error: message
345
+ });
346
+ };
347
+ process.exit = (code) => {
348
+ const n = Number(code ?? process.exitCode ?? 0);
349
+ if (n !== 0) emit(n);
350
+ restore();
351
+ return realExit(code);
352
+ };
353
+ try {
354
+ await fn();
355
+ restore();
356
+ if (recordedFailure()) {
357
+ emit(1);
358
+ return 1;
359
+ }
360
+ return 0;
361
+ } catch (error) {
362
+ restore();
363
+ const text = error instanceof Error ? error.message : String(error);
364
+ if (error instanceof UserFacingError) console.error(error.message);
365
+ else console.error(error instanceof Error ? error.stack ?? error.message : String(error));
366
+ emit(1, text);
367
+ return 1;
368
+ } finally {
369
+ restore();
370
+ }
371
+ }
372
+ async function runDoctor() {
373
+ resetDiagnosticsState();
374
+ const d = collectDiagnostics({
375
+ command: "doctor",
376
+ args: [],
377
+ checkCatalog: true
378
+ });
379
+ console.log(renderDiagnostics(d));
380
+ return d.ghLoggedIn && d.catalogOk ? 0 : 1;
381
+ }
24
382
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
25
383
  function getOwnerRepo(parsed) {
26
384
  if (parsed.type === "local") return null;
@@ -436,7 +794,7 @@ const CLONE_TIMEOUT_MS = (() => {
436
794
  const parsed = Number.parseInt(raw, 10);
437
795
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
438
796
  })();
439
- const execFileAsync$5 = promisify(execFile);
797
+ const execFileAsync$4 = promisify(execFile);
440
798
  var GitCloneError = class extends Error {
441
799
  url;
442
800
  isTimeout;
@@ -514,7 +872,7 @@ async function resetTempDir(dir) {
514
872
  async function tryGhClone(repo, tempDir, ref) {
515
873
  let cloneTarget = repo.slug;
516
874
  try {
517
- const { stdout, stderr } = await execFileAsync$5("gh", [
875
+ const { stdout, stderr } = await execFileAsync$4("gh", [
518
876
  "auth",
519
877
  "status",
520
878
  "-h",
@@ -531,7 +889,7 @@ async function tryGhClone(repo, tempDir, ref) {
531
889
  } catch {
532
890
  return false;
533
891
  }
534
- await execFileAsync$5("gh", [
892
+ await execFileAsync$4("gh", [
535
893
  "repo",
536
894
  "clone",
537
895
  cloneTarget,
@@ -683,8 +1041,6 @@ async function getPluginGroupings(basePath) {
683
1041
  } catch {}
684
1042
  return groupings;
685
1043
  }
686
- var UserFacingError = class extends Error {};
687
- var LockVersionError = class extends UserFacingError {};
688
1044
  const LOCAL_LOCK_FILE = "skills-lock.json";
689
1045
  const CURRENT_VERSION$1 = 1;
690
1046
  function getLocalLockPath(cwd) {
@@ -1695,6 +2051,27 @@ async function createSymlink(target, linkPath) {
1695
2051
  return false;
1696
2052
  }
1697
2053
  }
2054
+ const canonicalCopies = /* @__PURE__ */ new Map();
2055
+ async function copyCanonicalOnce(src, canonicalDir) {
2056
+ const key = `${src}\0${canonicalDir}`;
2057
+ let entry = canonicalCopies.get(key);
2058
+ if (!entry || entry.done && !existsSync(canonicalDir)) {
2059
+ const fresh = {
2060
+ done: false,
2061
+ promise: Promise.resolve()
2062
+ };
2063
+ fresh.promise = (async () => {
2064
+ await cleanAndCreateDirectory(canonicalDir);
2065
+ await copyDirectory(src, canonicalDir);
2066
+ })();
2067
+ fresh.promise.then(() => {
2068
+ fresh.done = true;
2069
+ }, () => canonicalCopies.delete(key));
2070
+ canonicalCopies.set(key, fresh);
2071
+ entry = fresh;
2072
+ }
2073
+ await entry.promise;
2074
+ }
1698
2075
  async function installSkillForAgent(skill, agentType, options = {}) {
1699
2076
  const agent = agents[agentType];
1700
2077
  const isGlobal = options.global ?? false;
@@ -1733,8 +2110,7 @@ async function installSkillForAgent(skill, agentType, options = {}) {
1733
2110
  mode: "copy"
1734
2111
  };
1735
2112
  }
1736
- await cleanAndCreateDirectory(canonicalDir);
1737
- await copyDirectory(skill.path, canonicalDir);
2113
+ await copyCanonicalOnce(skill.path, canonicalDir);
1738
2114
  if (isGlobal && isUniversalAgent(agentType)) return {
1739
2115
  success: true,
1740
2116
  path: canonicalDir,
@@ -2768,7 +3144,8 @@ async function fetchTreeBranch(ownerRepo, branch, token) {
2768
3144
  }
2769
3145
  return {
2770
3146
  tree: null,
2771
- rateLimited: response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0"
3147
+ rateLimited: response.status === 403 && response.headers.get("x-ratelimit-remaining") === "0",
3148
+ notFound: response.status === 404
2772
3149
  };
2773
3150
  } catch {
2774
3151
  return {
@@ -2793,6 +3170,7 @@ async function fetchRepoTree(ownerRepo, ref, getToken) {
2793
3170
  return null;
2794
3171
  }
2795
3172
  let rateLimited = false;
3173
+ let privateSuspected = false;
2796
3174
  for (const branch of branches) {
2797
3175
  const result = await fetchTreeBranch(ownerRepo, branch, null);
2798
3176
  if (result.tree) return result.tree;
@@ -2800,9 +3178,13 @@ async function fetchRepoTree(ownerRepo, ref, getToken) {
2800
3178
  rateLimited = true;
2801
3179
  break;
2802
3180
  }
3181
+ if (result.notFound && getToken) {
3182
+ privateSuspected = true;
3183
+ break;
3184
+ }
2803
3185
  }
2804
- if (!rateLimited || !getToken) return null;
2805
- _rateLimitedThisSession = true;
3186
+ if (!(rateLimited || privateSuspected) || !getToken) return null;
3187
+ if (rateLimited) _rateLimitedThisSession = true;
2806
3188
  const token = getToken();
2807
3189
  if (!token) return null;
2808
3190
  for (const branch of branches) {
@@ -3120,6 +3502,7 @@ async function selectAgentsInteractive(options) {
3120
3502
  });
3121
3503
  }
3122
3504
  async function handleWellKnownSkills(source, url, options, spinner) {
3505
+ setStep("add:discover-skills");
3123
3506
  spinner.start("Discovering skills from well-known endpoint...");
3124
3507
  const skills = await wellKnownProvider.fetchAllSkills(url);
3125
3508
  if (skills.length === 0) {
@@ -3194,6 +3577,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3194
3577
  }
3195
3578
  targetAgents = options.agent;
3196
3579
  } else {
3580
+ setStep("add:load-agents");
3197
3581
  spinner.start("Loading agents...");
3198
3582
  const installedAgents = await detectInstalledAgents();
3199
3583
  const totalAgents = Object.keys(agents).length;
@@ -3304,6 +3688,7 @@ async function handleWellKnownSkills(source, url, options, spinner) {
3304
3688
  }
3305
3689
  const sourceIdentifier = wellKnownProvider.getSourceIdentifier(url);
3306
3690
  const wellKnownPrivacyPromise = isSourcePrivate(sourceIdentifier).catch(() => null);
3691
+ setStep("add:install");
3307
3692
  spinner.start("Installing skills...");
3308
3693
  const results = [];
3309
3694
  for (const skill of selectedSkills) for (const agent of targetAgents) {
@@ -3437,6 +3822,7 @@ async function runAdd(args, options = {}) {
3437
3822
  let tempDir = null;
3438
3823
  try {
3439
3824
  const spinner = Y();
3825
+ setStep("add:parse-source");
3440
3826
  spinner.start("Parsing source...");
3441
3827
  const parsed = parseSource(source);
3442
3828
  spinner.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
@@ -3469,6 +3855,7 @@ async function runAdd(args, options = {}) {
3469
3855
  let skills;
3470
3856
  let blobResult = null;
3471
3857
  if (parsed.type === "local") {
3858
+ setStep("add:validate-path");
3472
3859
  spinner.start("Validating local path...");
3473
3860
  if (!existsSync(parsed.localPath)) {
3474
3861
  spinner.stop(import_picocolors.default.red("Path not found"));
@@ -3476,6 +3863,7 @@ async function runAdd(args, options = {}) {
3476
3863
  process.exit(1);
3477
3864
  }
3478
3865
  spinner.stop("Local path validated");
3866
+ setStep("add:discover-skills");
3479
3867
  spinner.start("Discovering skills...");
3480
3868
  skills = await discoverSkills(parsed.localPath, parsed.subpath, {
3481
3869
  includeInternal,
@@ -3486,6 +3874,7 @@ async function runAdd(args, options = {}) {
3486
3874
  const ownerRepo = getOwnerRepo(parsed);
3487
3875
  const owner = ownerRepo?.split("/")[0]?.toLowerCase();
3488
3876
  if (ownerRepo && owner && BLOB_ALLOWED_OWNERS.includes(owner)) {
3877
+ setStep("add:fetch-skills");
3489
3878
  spinner.start("Fetching skills...");
3490
3879
  blobResult = await tryBlobInstall(ownerRepo, {
3491
3880
  subpath: parsed.subpath,
@@ -3500,9 +3889,11 @@ async function runAdd(args, options = {}) {
3500
3889
  skills = blobResult.skills;
3501
3890
  spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3502
3891
  } else {
3892
+ setStep("add:clone");
3503
3893
  spinner.start("Cloning repository...");
3504
3894
  tempDir = await cloneRepo$1(parsed.url, parsed.ref);
3505
3895
  spinner.stop("Repository cloned");
3896
+ setStep("add:discover-skills");
3506
3897
  spinner.start("Discovering skills...");
3507
3898
  skills = await discoverSkills(tempDir, parsed.subpath, {
3508
3899
  includeInternal,
@@ -3510,9 +3901,11 @@ async function runAdd(args, options = {}) {
3510
3901
  });
3511
3902
  }
3512
3903
  } else {
3904
+ setStep("add:clone");
3513
3905
  spinner.start("Cloning repository...");
3514
3906
  tempDir = await cloneRepo$1(parsed.url, parsed.ref);
3515
3907
  spinner.stop("Repository cloned");
3908
+ setStep("add:discover-skills");
3516
3909
  spinner.start("Discovering skills...");
3517
3910
  skills = await discoverSkills(tempDir, parsed.subpath, {
3518
3911
  includeInternal,
@@ -3639,6 +4032,7 @@ async function runAdd(args, options = {}) {
3639
4032
  }
3640
4033
  targetAgents = options.agent;
3641
4034
  } else {
4035
+ setStep("add:load-agents");
3642
4036
  spinner.start("Loading agents...");
3643
4037
  const installedAgents = await detectInstalledAgents();
3644
4038
  const totalAgents = Object.keys(agents).length;
@@ -3784,6 +4178,7 @@ async function runAdd(args, options = {}) {
3784
4178
  process.exit(0);
3785
4179
  }
3786
4180
  }
4181
+ setStep("add:install");
3787
4182
  spinner.start("Installing skills...");
3788
4183
  const results = [];
3789
4184
  for (const skill of selectedSkills) for (const agent of targetAgents) {
@@ -4031,20 +4426,42 @@ function parseAddOptions(args) {
4031
4426
  options
4032
4427
  };
4033
4428
  }
4034
- const execFileAsync$4 = promisify(execFile);
4429
+ const execFileAsync$3 = promisify(execFile);
4035
4430
  const isLocalSource = (source) => source.startsWith(".") || isAbsolute(source);
4036
4431
  const isGitHubSource = (source) => /^[^/]+\/[^/]+$/.test(source);
4037
4432
  const ghEnv = () => ({
4038
4433
  ...process.env,
4039
4434
  GIT_TERMINAL_PROMPT: "0"
4040
4435
  });
4436
+ const clones = /* @__PURE__ */ new Map();
4437
+ let exitHookInstalled = false;
4438
+ const settledDirs = /* @__PURE__ */ new Set();
4439
+ function cloneOnce(source) {
4440
+ let pending = clones.get(source);
4441
+ if (!pending) {
4442
+ pending = cloneRepo$1(source);
4443
+ clones.set(source, pending);
4444
+ pending.catch(() => clones.delete(source));
4445
+ if (!exitHookInstalled) {
4446
+ exitHookInstalled = true;
4447
+ process.on("exit", () => {
4448
+ for (const dir of settledDirs) rmSync(dir, {
4449
+ recursive: true,
4450
+ force: true
4451
+ });
4452
+ });
4453
+ }
4454
+ pending.then((dir) => settledDirs.add(dir)).catch(() => {});
4455
+ }
4456
+ return pending;
4457
+ }
4041
4458
  async function fetchCatalogFile(source, rel) {
4042
4459
  if (isLocalSource(source)) {
4043
4460
  const file = join(source, rel);
4044
4461
  return existsSync(file) ? readFile(file, "utf8") : null;
4045
4462
  }
4046
4463
  if (isGitHubSource(source)) try {
4047
- const { stdout } = await execFileAsync$4("gh", [
4464
+ const { stdout } = await execFileAsync$3("gh", [
4048
4465
  "api",
4049
4466
  `repos/${source}/contents/${rel}`,
4050
4467
  "--jq",
@@ -4055,7 +4472,7 @@ async function fetchCatalogFile(source, rel) {
4055
4472
  return null;
4056
4473
  }
4057
4474
  try {
4058
- const file = join(await cloneRepo$1(source), rel);
4475
+ const file = join(await cloneOnce(source), rel);
4059
4476
  return existsSync(file) ? readFile(file, "utf8") : null;
4060
4477
  } catch {
4061
4478
  return null;
@@ -4068,7 +4485,7 @@ async function listCatalogDir(source, dir) {
4068
4485
  return null;
4069
4486
  }
4070
4487
  if (isGitHubSource(source)) try {
4071
- const { stdout } = await execFileAsync$4("gh", [
4488
+ const { stdout } = await execFileAsync$3("gh", [
4072
4489
  "api",
4073
4490
  `repos/${source}/contents/${dir}`,
4074
4491
  "--jq",
@@ -4079,7 +4496,7 @@ async function listCatalogDir(source, dir) {
4079
4496
  return null;
4080
4497
  }
4081
4498
  try {
4082
- return await readdir(join(await cloneRepo$1(source), dir));
4499
+ return await readdir(join(await cloneOnce(source), dir));
4083
4500
  } catch {
4084
4501
  return null;
4085
4502
  }
@@ -4088,7 +4505,7 @@ const claudeAgentsDir = (global) => global ? join(process.env.CLAUDE_CONFIG_DIR?
4088
4505
  const fetchAgentFile = (source, name) => fetchCatalogFile(source, `agents/${name}.md`);
4089
4506
  async function listCatalogAgents(source) {
4090
4507
  const files = await listCatalogDir(source, "agents");
4091
- return files ? files.filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3)) : null;
4508
+ return files ? files.filter((f) => f.endsWith(".md") && !f.startsWith("_")).map((f) => f.slice(0, -3)) : null;
4092
4509
  }
4093
4510
  async function listInstalledAgents(targetDir) {
4094
4511
  try {
@@ -4102,6 +4519,7 @@ async function updateAgents(options = {}) {
4102
4519
  const targetDir = claudeAgentsDir(options.global ?? false);
4103
4520
  const result = {
4104
4521
  updated: [],
4522
+ unchanged: [],
4105
4523
  failed: [],
4106
4524
  localOnly: [],
4107
4525
  available: [],
@@ -4125,12 +4543,18 @@ async function updateAgents(options = {}) {
4125
4543
  result.localOnly.push(name);
4126
4544
  continue;
4127
4545
  }
4546
+ setStep(`agents:fetch:${name}`);
4128
4547
  const content = await fetchAgentFile(source, name);
4129
4548
  if (content === null) {
4130
4549
  result.failed.push(name);
4131
4550
  continue;
4132
4551
  }
4133
- await writeFile(join(targetDir, `${sanitizeName(name)}.md`), content);
4552
+ const dest = join(targetDir, `${sanitizeName(name)}.md`);
4553
+ if (await readFile(dest, "utf8").catch(() => null) === content) {
4554
+ result.unchanged.push(name);
4555
+ continue;
4556
+ }
4557
+ await writeFile(dest, content);
4134
4558
  result.updated.push(name);
4135
4559
  }
4136
4560
  return result;
@@ -4155,6 +4579,7 @@ async function addAgents(names, options) {
4155
4579
  M.message(import_picocolors.default.dim("Browse them with `npx pruff-skills find`."));
4156
4580
  process.exit(1);
4157
4581
  }
4582
+ setStep("agents:write");
4158
4583
  await mkdir(targetDir, { recursive: true });
4159
4584
  const installed = [];
4160
4585
  for (const name of names) {
@@ -4212,7 +4637,6 @@ async function runAgents(args) {
4212
4637
  console.log(`Unknown agents command: ${sub}`);
4213
4638
  console.log(AGENTS_HELP);
4214
4639
  }
4215
- const execFileAsync$3 = promisify(execFile);
4216
4640
  const RULE_SCOPES = [
4217
4641
  "client",
4218
4642
  "server",
@@ -4223,35 +4647,9 @@ function stripFrontmatter(md) {
4223
4647
  const m = md.match(/^---\n[\s\S]*?\n---\n+/);
4224
4648
  return m ? md.slice(m[0].length) : md;
4225
4649
  }
4226
- async function fetchFromCatalog(source, rel) {
4227
- if (source.startsWith(".") || isAbsolute(source)) {
4228
- const file = join(source, rel);
4229
- return existsSync(file) ? readFile(file, "utf8") : null;
4230
- }
4231
- if (/^[^/]+\/[^/]+$/.test(source)) try {
4232
- const { stdout } = await execFileAsync$3("gh", [
4233
- "api",
4234
- `repos/${source}/contents/${rel}`,
4235
- "--jq",
4236
- ".content"
4237
- ], { env: {
4238
- ...process.env,
4239
- GIT_TERMINAL_PROMPT: "0"
4240
- } });
4241
- return Buffer.from(stdout.trim(), "base64").toString("utf8");
4242
- } catch {
4243
- return null;
4244
- }
4245
- try {
4246
- const file = join(await cloneRepo$1(source), rel);
4247
- return existsSync(file) ? readFile(file, "utf8") : null;
4248
- } catch {
4249
- return null;
4250
- }
4251
- }
4252
- const fetchRuleFile = (source, name) => fetchFromCatalog(source, `rules/${name}.md`);
4650
+ const fetchRuleFile = (source, name) => fetchCatalogFile(source, `rules/${name}.md`);
4253
4651
  async function fetchRuleIndex(source) {
4254
- const raw = await fetchFromCatalog(source, ".well-known/agent-skills/index.json");
4652
+ const raw = await fetchCatalogFile(source, ".well-known/agent-skills/index.json");
4255
4653
  if (!raw) return [];
4256
4654
  return parseCatalogIndex(raw, source).rules ?? [];
4257
4655
  }
@@ -4287,6 +4685,7 @@ async function addRules(names, options) {
4287
4685
  Ie(import_picocolors.default.cyan("pruff-skills rules"));
4288
4686
  let toInstall = names.filter(Boolean);
4289
4687
  if (options.all || options.scopes?.length) {
4688
+ setStep("rules:fetch-index");
4290
4689
  const index = await fetchRuleIndex(source);
4291
4690
  if (index.length === 0) {
4292
4691
  M.error(`Could not read the rules catalog (${source}).`);
@@ -4303,6 +4702,7 @@ async function addRules(names, options) {
4303
4702
  M.message(import_picocolors.default.dim("Browse them with `npx pruff-skills find`."));
4304
4703
  process.exit(1);
4305
4704
  }
4705
+ setStep("rules:write");
4306
4706
  await mkdir(targetDir, { recursive: true });
4307
4707
  const installed = [];
4308
4708
  for (const name of toInstall) {
@@ -4310,6 +4710,7 @@ async function addRules(names, options) {
4310
4710
  M.warn(`Skipping invalid rule name: ${import_picocolors.default.yellow(name)}`);
4311
4711
  continue;
4312
4712
  }
4713
+ setStep(`rules:fetch:${name}`);
4313
4714
  const content = await fetchRuleFile(source, name);
4314
4715
  if (!content) {
4315
4716
  M.error(`Rule ${import_picocolors.default.yellow(name)} not found in the catalog (${source}).`);
@@ -5728,6 +6129,7 @@ async function updateProjectSkills(options = {}) {
5728
6129
  const projectSkills = await getProjectSkillsForUpdate(options.skills);
5729
6130
  let successCount = 0;
5730
6131
  let failCount = 0;
6132
+ let upToDateCount = 0;
5731
6133
  if (projectSkills.length === 0) {
5732
6134
  if (!options.skills) {
5733
6135
  console.log(`${DIM$1}No project skills to update.${RESET$1}`);
@@ -5736,7 +6138,8 @@ async function updateProjectSkills(options = {}) {
5736
6138
  return {
5737
6139
  successCount,
5738
6140
  failCount,
5739
- foundCount: 0
6141
+ foundCount: 0,
6142
+ upToDateCount
5740
6143
  };
5741
6144
  }
5742
6145
  const updatable = projectSkills.filter((s) => s.entry.skillPath);
@@ -5747,7 +6150,8 @@ async function updateProjectSkills(options = {}) {
5747
6150
  return {
5748
6151
  successCount,
5749
6152
  failCount,
5750
- foundCount: projectSkills.length
6153
+ foundCount: projectSkills.length,
6154
+ upToDateCount
5751
6155
  };
5752
6156
  }
5753
6157
  const cwd = process.cwd();
@@ -5779,7 +6183,8 @@ async function updateProjectSkills(options = {}) {
5779
6183
  return {
5780
6184
  successCount,
5781
6185
  failCount: updatable.length,
5782
- foundCount: projectSkills.length
6186
+ foundCount: projectSkills.length,
6187
+ upToDateCount
5783
6188
  };
5784
6189
  }
5785
6190
  for (const [source, skillsForSource] of bySource) {
@@ -5790,12 +6195,19 @@ async function updateProjectSkills(options = {}) {
5790
6195
  const allLockedForSource = Object.entries(localLock.skills).filter(([_, entry]) => entry.source === source).map(([name, _]) => name);
5791
6196
  let tempDir = null;
5792
6197
  let deletedSkills = [];
6198
+ const unchangedSkills = /* @__PURE__ */ new Set();
5793
6199
  try {
5794
6200
  tempDir = await cloneRepo$1(sourceUrl, ref);
5795
6201
  const discoveredPaths = (await discoverSkills(tempDir)).map((s) => {
5796
6202
  return join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/");
5797
6203
  });
5798
6204
  deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredPaths);
6205
+ for (const skill of skillsForSource) {
6206
+ const { skillPath, computedHash } = skill.entry;
6207
+ if (!skillPath || !computedHash || deletedSkills.includes(skill.name)) continue;
6208
+ const currentHash = await computeSkillFolderHash(join(tempDir, dirname(skillPath))).catch(() => "");
6209
+ if (currentHash && currentHash === computedHash) unchangedSkills.add(skill.name);
6210
+ }
5799
6211
  } catch (error) {
5800
6212
  const reason = error instanceof Error ? error.message : String(error);
5801
6213
  console.log(`${DIM$1}✗ Failed to check for deleted skills from ${source}: ${sanitizeMetadata(reason)}${RESET$1}`);
@@ -5805,6 +6217,11 @@ async function updateProjectSkills(options = {}) {
5805
6217
  const remainingSkills = skillsForSource.filter((s) => !deletedSkills.includes(s.name));
5806
6218
  for (const skill of remainingSkills) {
5807
6219
  const safeName = sanitizeMetadata(skill.name);
6220
+ if (unchangedSkills.has(skill.name)) {
6221
+ upToDateCount++;
6222
+ console.log(`${TEXT$1}✓${RESET$1} ${safeName} ${DIM$1}is up to date${RESET$1}`);
6223
+ continue;
6224
+ }
5808
6225
  console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
5809
6226
  const installUrl = formatSourceInput(skill.entry.source, skill.entry.ref);
5810
6227
  if (spawnSync(process.execPath, [
@@ -5835,7 +6252,8 @@ async function updateProjectSkills(options = {}) {
5835
6252
  return {
5836
6253
  successCount,
5837
6254
  failCount,
5838
- foundCount: projectSkills.length
6255
+ foundCount: projectSkills.length,
6256
+ upToDateCount
5839
6257
  };
5840
6258
  }
5841
6259
  function printLegacyProjectSkills(legacy) {
@@ -5859,6 +6277,7 @@ async function syncAgentsForScope(isGlobal, options) {
5859
6277
  return result;
5860
6278
  }
5861
6279
  for (const name of result.updated) console.log(` ${TEXT$1}✓${RESET$1} Synced ${sanitizeMetadata(name)}`);
6280
+ for (const name of result.unchanged) console.log(` ${TEXT$1}✓${RESET$1} ${sanitizeMetadata(name)} ${DIM$1}is up to date${RESET$1}`);
5862
6281
  for (const name of result.failed) console.log(` ${DIM$1}✗ Failed to sync ${sanitizeMetadata(name)}${RESET$1}`);
5863
6282
  if (result.localOnly.length > 0) {
5864
6283
  const names = result.localOnly.map((n) => sanitizeMetadata(n)).join(", ");
@@ -5873,6 +6292,7 @@ async function syncAgentsForScope(isGlobal, options) {
5873
6292
  }
5874
6293
  async function runUpdate(args = []) {
5875
6294
  const options = parseUpdateOptions(args);
6295
+ setStep("update:resolve-scope");
5876
6296
  const scope = await resolveUpdateScope(options);
5877
6297
  if (options.skills) console.log(`${TEXT$1}Updating ${options.skills.join(", ")}...${RESET$1}`);
5878
6298
  else console.log(`${TEXT$1}Checking for skill updates...${RESET$1}`);
@@ -5880,8 +6300,10 @@ async function runUpdate(args = []) {
5880
6300
  let totalSuccess = 0;
5881
6301
  let totalFail = 0;
5882
6302
  let totalFound = 0;
6303
+ let totalUpToDate = 0;
5883
6304
  if (scope === "global" || scope === "both") {
5884
6305
  if (scope === "both" && !options.skills) console.log(`${BOLD$1}Global Skills${RESET$1}`);
6306
+ setStep("update:global-skills");
5885
6307
  const { successCount, failCount, checkedCount } = await updateGlobalSkills(options);
5886
6308
  totalSuccess += successCount;
5887
6309
  totalFail += failCount;
@@ -5890,7 +6312,9 @@ async function runUpdate(args = []) {
5890
6312
  }
5891
6313
  if (scope === "project" || scope === "both") {
5892
6314
  if (scope === "both" && !options.skills) console.log(`${BOLD$1}Project Skills${RESET$1}`);
5893
- const { successCount, failCount, foundCount } = await updateProjectSkills(options);
6315
+ setStep("update:project-skills");
6316
+ const { successCount, failCount, foundCount, upToDateCount } = await updateProjectSkills(options);
6317
+ totalUpToDate += upToDateCount;
5894
6318
  totalSuccess += successCount;
5895
6319
  totalFail += failCount;
5896
6320
  totalFound += foundCount;
@@ -5898,22 +6322,30 @@ async function runUpdate(args = []) {
5898
6322
  console.log();
5899
6323
  console.log(`${BOLD$1}Subagents${RESET$1}`);
5900
6324
  console.log(`${DIM$1}ALL installed subagents will be updated and synced with the repo — local edits are overwritten.${RESET$1}`);
6325
+ setStep("update:sync-agents");
5901
6326
  const agentScopes = scope === "both" ? [true, false] : scope === "global" ? [true] : [false];
5902
6327
  let agentsSynced = 0;
5903
6328
  let agentsFailed = 0;
6329
+ let agentsUnchanged = 0;
5904
6330
  for (const isGlobal of agentScopes) {
5905
6331
  const agentResult = await syncAgentsForScope(isGlobal, options);
5906
6332
  agentsSynced += agentResult.updated.length;
6333
+ agentsUnchanged += agentResult.unchanged.length;
5907
6334
  agentsFailed += agentResult.failed.length;
5908
6335
  }
5909
- if (agentsSynced === 0 && agentsFailed === 0) console.log(` ${DIM$1}No installed subagents to sync.${RESET$1}`);
6336
+ if (agentsSynced === 0 && agentsFailed === 0 && agentsUnchanged === 0) console.log(` ${DIM$1}No installed subagents to sync.${RESET$1}`);
5910
6337
  totalSuccess += agentsSynced;
5911
6338
  totalFail += agentsFailed;
5912
- totalFound += agentsSynced + agentsFailed;
6339
+ totalFound += agentsSynced + agentsFailed + agentsUnchanged;
6340
+ totalUpToDate += agentsUnchanged;
5913
6341
  if (options.skills && totalFound === 0) console.log(`${DIM$1}No installed skills found matching: ${options.skills.join(", ")}${RESET$1}`);
5914
6342
  console.log();
5915
6343
  if (totalSuccess > 0) console.log(`${TEXT$1}✓ Updated ${totalSuccess} item(s)${RESET$1}`);
5916
- if (totalFail > 0) console.log(`${DIM$1}Failed to update ${totalFail} item(s)${RESET$1}`);
6344
+ else if (totalUpToDate > 0 && totalFail === 0) console.log(`${TEXT$1}✓ Everything is up to date${RESET$1}`);
6345
+ if (totalFail > 0) {
6346
+ recordFailure(`Failed to update ${totalFail} item(s)`);
6347
+ console.log(`${DIM$1}Failed to update ${totalFail} item(s)${RESET$1}`);
6348
+ }
5917
6349
  String(totalSuccess + totalFail);
5918
6350
  console.log();
5919
6351
  }
@@ -6836,6 +7268,7 @@ async function installReaderSkill(workspace, note) {
6836
7268
  }
6837
7269
  async function setupDbReader({ workspace, note }) {
6838
7270
  M.step("Setting up read-only prod database access…");
7271
+ setStep("db-reader:aws-identity");
6839
7272
  const identity = aws([
6840
7273
  "sts",
6841
7274
  "get-caller-identity",
@@ -6854,6 +7287,7 @@ async function setupDbReader({ workspace, note }) {
6854
7287
  return;
6855
7288
  }
6856
7289
  const secretId = `${DB_READER_SECRET_PREFIX}${iamUser}`;
7290
+ setStep("db-reader:fetch-secret");
6857
7291
  const fetched = aws([
6858
7292
  "secretsmanager",
6859
7293
  "get-secret-value",
@@ -6885,16 +7319,19 @@ async function setupDbReader({ workspace, note }) {
6885
7319
  note(`Prod DB access not set up — ${secretId} does not hold a valid username and password.`);
6886
7320
  return;
6887
7321
  }
7322
+ setStep("db-reader:keychain");
6888
7323
  if (!storeInKeychain(secret.username, secret.password)) {
6889
7324
  note(`Prod DB access not set up — the Keychain write failed. Add it by hand: ${import_picocolors.default.cyan(`security add-generic-password -U -s ${DB_READER_SERVICE} -a ${secret.username} -w`)} (prompts for the password).`);
6890
7325
  return;
6891
7326
  }
6892
7327
  writeConfig(secret.username);
6893
7328
  M.success(`Keychain entry ${DB_READER_SERVICE} stored for ${secret.username}`);
7329
+ setStep("db-reader:ca-bundle");
6894
7330
  if (!downloadCaBundle()) {
6895
7331
  note(`Prod DB access not set up — the RDS CA bundle download failed. Run: ${import_picocolors.default.cyan(`curl -sSfo ${dbReaderCaPath()} ${RDS_CA_URL}`)}`);
6896
7332
  return;
6897
7333
  }
7334
+ setStep("db-reader:install-scripts");
6898
7335
  const installError = await installScripts();
6899
7336
  if (installError) {
6900
7337
  note(`Prod DB access not set up — ${installError}. Re-run once the catalog is reachable.`);
@@ -6902,6 +7339,7 @@ async function setupDbReader({ workspace, note }) {
6902
7339
  }
6903
7340
  M.success(`Installed ${DB_READER_SCRIPTS.join(" and ")} in ${dbReaderBinDir()}`);
6904
7341
  if (!(process.env.PATH ?? "").split(delimiter).includes(dbReaderBinDir())) M.warn(`${dbReaderBinDir()} is not on your PATH. Add ${import_picocolors.default.cyan("export PATH=\"$HOME/.local/bin:$PATH\"")} to ~/.zshrc.`);
7342
+ setStep("db-reader:verify");
6905
7343
  const verify = runVerify$1();
6906
7344
  if (!verify.ok) {
6907
7345
  note(`Prod DB access not enabled — ${import_picocolors.default.cyan("pruff-db-verify")} failed. Run ${import_picocolors.default.cyan(`${join(dbReaderBinDir(), "pruff-db-verify")} ${DB_READER_VERIFY_DATABASE}`)} and share the output with the infra owner.`);
@@ -6909,6 +7347,7 @@ async function setupDbReader({ workspace, note }) {
6909
7347
  return;
6910
7348
  }
6911
7349
  M.success("Read-only access verified");
7350
+ setStep("db-reader:settings");
6912
7351
  if (mergeAllowlist(workspace) === "invalid") note(`Add ${import_picocolors.default.cyan(DB_READER_ALLOW_ENTRY)} to permissions.allow in ${join(workspace, ".claude", "settings.json")} by hand — the file is not valid JSON.`);
6913
7352
  const hooks = await installHooks(["prod-db-guard"]);
6914
7353
  for (const failure of hooks.failed) note(`Hook ${failure.name} not installed: ${failure.reason}`);
@@ -7041,9 +7480,9 @@ function renderBlockLogoFrame(lines, palette, pos, band) {
7041
7480
  return out + RESET;
7042
7481
  }).join("\n");
7043
7482
  }
7044
- async function showBlockLogo(lines, letterCuts, { slow = false, palette = BLUES } = {}) {
7483
+ async function showBlockLogo(lines, letterCuts, { slow = false, palette = BLUES, animate = true } = {}) {
7045
7484
  console.log();
7046
- if (!canAnimate()) {
7485
+ if (!animate || !canAnimate()) {
7047
7486
  printStaticBlockLogo(lines, palette);
7048
7487
  return;
7049
7488
  }
@@ -7815,6 +8254,7 @@ function catalogInstallNames(rawIndex) {
7815
8254
  }
7816
8255
  const issues = [];
7817
8256
  function note(issue) {
8257
+ recordFailure(issue);
7818
8258
  issues.push(issue);
7819
8259
  }
7820
8260
  async function runOnboarding(args) {
@@ -7829,6 +8269,7 @@ async function runOnboarding(args) {
7829
8269
  Ie(import_picocolors.default.bold("Pruff onboarding"));
7830
8270
  issues.length = 0;
7831
8271
  if (args.includes("--db-access")) {
8272
+ setStep("db-access");
7832
8273
  await setupDbReader({
7833
8274
  workspace: await resolveWorkspace(args),
7834
8275
  note
@@ -7837,28 +8278,51 @@ async function runOnboarding(args) {
7837
8278
  Se(issues.length === 0 ? import_picocolors.default.green("Done.") : import_picocolors.default.yellow("Prod DB access is not complete — fix the items above and re-run."));
7838
8279
  return;
7839
8280
  }
8281
+ setStep("preflight");
7840
8282
  if (!await preflight()) {
8283
+ recordFailure(lastLoggedError() || "Preflight failed");
7841
8284
  Se(import_picocolors.default.yellow("Onboarding stopped — fix the items above and re-run."));
7842
8285
  return;
7843
8286
  }
8287
+ setStep("install-claude-code");
7844
8288
  installClaudeCode();
8289
+ setStep("obsidian");
7845
8290
  ensureObsidian();
8291
+ setStep("bun");
7846
8292
  ensureBun();
8293
+ setStep("workspace");
7847
8294
  const workspace = await resolveWorkspace(args);
7848
8295
  mkdirSync(workspace, { recursive: true });
7849
8296
  M.success(`Workspace ${import_picocolors.default.cyan(workspace)}`);
8297
+ setStep("clone-knowledge");
7850
8298
  cloneKnowledge(workspace);
7851
- for (const repo of MAIN_REPOS) cloneRepo(repo, workspace);
7852
- for (const repo of MAIN_REPOS) installDeps(repo, workspace);
8299
+ for (const repo of MAIN_REPOS) {
8300
+ setStep(`clone:${repo.name}`);
8301
+ cloneRepo(repo, workspace);
8302
+ }
8303
+ for (const repo of MAIN_REPOS) {
8304
+ setStep(`install-deps:${repo.name}`);
8305
+ installDeps(repo, workspace);
8306
+ }
8307
+ setStep("workspace-config");
7853
8308
  writeWorkspaceClaudeMd(workspace);
7854
8309
  writeHarnessConfig(workspace);
8310
+ setStep("install-catalog");
7855
8311
  await installCatalog(workspace);
8312
+ setStep("install-harness");
7856
8313
  await installHarness(workspace);
7857
- if (await shouldSetupDatabase(args)) await setupLocalEnvironment(join(workspace, "Pruff-server"));
7858
- if (await shouldSetupDbAccess(args)) await setupDbReader({
7859
- workspace,
7860
- note
7861
- });
8314
+ if (await shouldSetupDatabase(args)) {
8315
+ setStep("local-database");
8316
+ await setupLocalEnvironment(join(workspace, "Pruff-server"));
8317
+ }
8318
+ if (await shouldSetupDbAccess(args)) {
8319
+ setStep("db-access");
8320
+ await setupDbReader({
8321
+ workspace,
8322
+ note
8323
+ });
8324
+ }
8325
+ setStep("guidance");
7862
8326
  await showGuidance(workspace);
7863
8327
  showTeamCredentialsAsk(workspace);
7864
8328
  showIssues();
@@ -7867,6 +8331,7 @@ async function runOnboarding(args) {
7867
8331
  else Se(`${import_picocolors.default.yellow(`Finished with ${issues.length} issue${issues.length === 1 ? "" : "s"}.`)} Fix them and re-run — every step is idempotent.`);
7868
8332
  }
7869
8333
  async function preflight() {
8334
+ setStep("preflight:tools");
7870
8335
  const missing = [
7871
8336
  "git",
7872
8337
  "node",
@@ -7877,9 +8342,13 @@ async function preflight() {
7877
8342
  return false;
7878
8343
  }
7879
8344
  M.success("git · node · npm present");
8345
+ setStep("preflight:gh");
7880
8346
  if (!ensureGh()) return false;
8347
+ setStep("preflight:gh-auth");
7881
8348
  if (!await ensureGitHubAuth()) return false;
8349
+ setStep("preflight:git-credentials");
7882
8350
  if (!ensureGitCredentialHelper()) return false;
8351
+ setStep("preflight:packages-scope");
7883
8352
  ensurePackagesScope();
7884
8353
  return true;
7885
8354
  }
@@ -9609,7 +10078,10 @@ const LETTER_CUTS = [
9609
10078
  41
9610
10079
  ];
9611
10080
  async function showLogo(slow = false) {
9612
- await showBlockLogo(LOGO_LINES, LETTER_CUTS, { slow });
10081
+ await showBlockLogo(LOGO_LINES, LETTER_CUTS, {
10082
+ slow,
10083
+ animate: slow
10084
+ });
9613
10085
  }
9614
10086
  const BUILDING_LINES = [
9615
10087
  "██████╗ ██╗ ██╗██╗██╗ ██████╗ ██╗███╗ ██╗ ██████╗ ",
@@ -9757,6 +10229,12 @@ ${BOLD}Onboarding Options:${RESET}
9757
10229
  --no-db Skip the local database step
9758
10230
  --db-access Only set up read-only prod database access (needs AWS credentials)
9759
10231
 
10232
+ ${BOLD}Troubleshooting:${RESET}
10233
+ doctor Print a copy-pasteable diagnostic block (versions, GitHub auth,
10234
+ catalog reachability, install target). Exit 1 if gh is not logged
10235
+ in or the catalog does not resolve. The same block is printed when
10236
+ add, update, rules add, agents add or onboarding fails.
10237
+
9760
10238
  ${BOLD}Updates:${RESET}
9761
10239
  update [skills...] Update skills to latest versions (alias: upgrade)
9762
10240
  Also syncs ALL installed subagents with the catalog,
@@ -9948,8 +10426,7 @@ async function main() {
9948
10426
  case "add": {
9949
10427
  if (!inAgent) await showLogo();
9950
10428
  const { source: addSource, options: addOpts } = parseAddOptions(restArgs);
9951
- await runAdd(addSource, addOpts);
9952
- break;
10429
+ return withDiagnostics("add", restArgs, () => runAdd(addSource, addOpts));
9953
10430
  }
9954
10431
  case "use": {
9955
10432
  const { source: useSource, options: useOptions, errors: useErrors } = parseUseOptions(restArgs);
@@ -9959,13 +10436,11 @@ async function main() {
9959
10436
  case "agents":
9960
10437
  case "agent":
9961
10438
  if (!inAgent) await showLogo();
9962
- await runAgents(restArgs);
9963
- break;
10439
+ return withDiagnostics("agents", restArgs, () => runAgents(restArgs));
9964
10440
  case "rules":
9965
10441
  case "rule":
9966
10442
  if (!inAgent) await showLogo();
9967
- await runRules(restArgs);
9968
- break;
10443
+ return withDiagnostics("rules", restArgs, () => runRules(restArgs));
9969
10444
  case "hooks":
9970
10445
  case "hook":
9971
10446
  if (!inAgent) await showLogo();
@@ -9985,8 +10460,8 @@ async function main() {
9985
10460
  await showLogo(true);
9986
10461
  await showTagline(true);
9987
10462
  }
9988
- await runOnboarding(restArgs);
9989
- break;
10463
+ return withDiagnostics("onboarding", restArgs, () => runOnboarding(restArgs));
10464
+ case "doctor": return runDoctor();
9990
10465
  case "remove":
9991
10466
  case "rm":
9992
10467
  case "r":
@@ -10010,9 +10485,7 @@ async function main() {
10010
10485
  break;
10011
10486
  case "check":
10012
10487
  case "update":
10013
- case "upgrade":
10014
- await runUpdate(restArgs);
10015
- break;
10488
+ case "upgrade": return withDiagnostics("update", restArgs, () => runUpdate(restArgs));
10016
10489
  case "--help":
10017
10490
  case "-h":
10018
10491
  showHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruff-skills",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "description": "The official Pruff agent skills CLI",
5
5
  "type": "module",
6
6
  "bin": {