pruff-skills 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,6 +38,15 @@ The catalog also ships **Claude Code subagents**. `find` lists them alongside sk
38
38
  npx pruff-skills agents add <name> # → .claude/agents/ (add -g for ~/.claude/agents/)
39
39
  ```
40
40
 
41
+ It also ships **coding-convention rules** for the Pruff repos, each tagged by scope
42
+ (`client` / `server` / `common`, where `common` applies to both). `find` lists them tagged
43
+ `[rule]`. Install one by name, or a whole repo's set at once:
44
+
45
+ ```bash
46
+ npx pruff-skills rules add <name> # → .claude/rules/ (add -g for ~/.claude/rules/)
47
+ npx pruff-skills rules add --scope server # all server + common rules (client/server pull in common)
48
+ ```
49
+
41
50
  ## Use a Skill Without Installing
42
51
 
43
52
  Generate a prompt for one skill, or start a supported coding agent interactively:
@@ -1,5 +1,5 @@
1
1
  import { __toESM } from "../../rolldown-runtime.mjs";
2
- import { LD, SD, _D, dD, fD, require_picocolors, require_src } from "./core.mjs";
2
+ import { LD, RD, SD, _D, dD, fD, require_picocolors, require_src } from "./core.mjs";
3
3
  import { stripVTControlCharacters } from "node:util";
4
4
  import y from "node:process";
5
5
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
@@ -24,7 +24,29 @@ const V = ce(), u = (t, n) => V ? t : n, le = u("◆", "*"), L = u("■", "x"),
24
24
  const j = v === 0 && $, E = v === f.length - 1 && g;
25
25
  return j || E ? import_picocolors.default.dim("...") : i(p, v + l === n);
26
26
  });
27
- }, ye = (t) => {
27
+ }, he = (t) => new RD({
28
+ validate: t.validate,
29
+ placeholder: t.placeholder,
30
+ defaultValue: t.defaultValue,
31
+ initialValue: t.initialValue,
32
+ render() {
33
+ const n = `${import_picocolors.default.gray(o)}
34
+ ${b(this.state)} ${t.message}
35
+ `, r = t.placeholder ? import_picocolors.default.inverse(t.placeholder[0]) + import_picocolors.default.dim(t.placeholder.slice(1)) : import_picocolors.default.inverse(import_picocolors.default.hidden("_")), i = this.value ? this.valueWithCursor : r;
36
+ switch (this.state) {
37
+ case "error": return `${n.trim()}
38
+ ${import_picocolors.default.yellow(o)} ${i}
39
+ ${import_picocolors.default.yellow(d)} ${import_picocolors.default.yellow(this.error)}
40
+ `;
41
+ case "submit": return `${n}${import_picocolors.default.gray(o)} ${import_picocolors.default.dim(this.value || t.placeholder)}`;
42
+ case "cancel": return `${n}${import_picocolors.default.gray(o)} ${import_picocolors.default.strikethrough(import_picocolors.default.dim(this.value ?? ""))}${this.value?.trim() ? `
43
+ ${import_picocolors.default.gray(o)}` : ""}`;
44
+ default: return `${n}${import_picocolors.default.cyan(o)} ${i}
45
+ ${import_picocolors.default.cyan(d)}
46
+ `;
47
+ }
48
+ }
49
+ }).prompt(), ye = (t) => {
28
50
  const n = t.active ?? "Yes", r = t.inactive ?? "No";
29
51
  return new dD({
30
52
  active: n,
@@ -329,4 +351,4 @@ ${J}${i.trimStart()}`), r = 3 + stripVTControlCharacters(i.trimStart()).length);
329
351
  }
330
352
  };
331
353
  };
332
- export { Ie, M, Me, Se, Y, be, fe, ve, xe, ye };
354
+ export { Ie, M, Me, Se, Y, be, fe, he, ve, xe, ye };
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { __toESM } from "./_chunks/rolldown-runtime.mjs";
3
3
  import { pD, require_picocolors } from "./_chunks/libs/@clack/core.mjs";
4
- import { Ie, M, Me, Se, Y, be, fe, ve, xe, ye } from "./_chunks/libs/@clack/prompts.mjs";
4
+ import { Ie, M, Me, Se, Y, be, fe, he, ve, xe, ye } from "./_chunks/libs/@clack/prompts.mjs";
5
5
  import { esm_default } from "./_chunks/libs/simple-git.mjs";
6
6
  import { xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
7
7
  import { require_dist } from "./_chunks/libs/@vercel/detect-agent.mjs";
@@ -16,7 +16,7 @@ import { promisify } from "util";
16
16
  import { execFile, execSync, spawn, spawnSync } from "child_process";
17
17
  import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
18
18
  import { parse } from "yaml";
19
- import { createHash } from "crypto";
19
+ import { createHash, randomBytes } from "crypto";
20
20
  import { createHash as createHash$1 } from "node:crypto";
21
21
  import { gunzipSync, inflateRawSync } from "node:zlib";
22
22
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
@@ -434,7 +434,7 @@ const CLONE_TIMEOUT_MS = (() => {
434
434
  const parsed = Number.parseInt(raw, 10);
435
435
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
436
436
  })();
437
- const execFileAsync$2 = promisify(execFile);
437
+ const execFileAsync$3 = promisify(execFile);
438
438
  var GitCloneError = class extends Error {
439
439
  url;
440
440
  isTimeout;
@@ -513,7 +513,7 @@ async function resetTempDir(dir) {
513
513
  async function tryGhClone(repo, tempDir, ref) {
514
514
  let cloneTarget = repo.slug;
515
515
  try {
516
- const { stdout, stderr } = await execFileAsync$2("gh", [
516
+ const { stdout, stderr } = await execFileAsync$3("gh", [
517
517
  "auth",
518
518
  "status",
519
519
  "-h",
@@ -530,7 +530,7 @@ async function tryGhClone(repo, tempDir, ref) {
530
530
  } catch {
531
531
  return false;
532
532
  }
533
- await execFileAsync$2("gh", [
533
+ await execFileAsync$3("gh", [
534
534
  "repo",
535
535
  "clone",
536
536
  cloneTarget,
@@ -555,7 +555,7 @@ function buildGitHubAuthError(url, repo, message) {
555
555
  if (repo) return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - Retry with SSH: npx pruff-skills add ${repo.sshUrl}\n - Check access with: gh auth status -h github.com or ssh -T git@github.com`;
556
556
  return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`;
557
557
  }
558
- async function cloneRepo(url, ref) {
558
+ async function cloneRepo$1(url, ref) {
559
559
  const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
560
560
  const cloneOptions = ref ? [
561
561
  "--depth",
@@ -3467,7 +3467,7 @@ async function runAdd(args, options = {}) {
3467
3467
  spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
3468
3468
  } else {
3469
3469
  spinner.start("Cloning repository...");
3470
- tempDir = await cloneRepo(parsed.url, parsed.ref);
3470
+ tempDir = await cloneRepo$1(parsed.url, parsed.ref);
3471
3471
  spinner.stop("Repository cloned");
3472
3472
  spinner.start("Discovering skills...");
3473
3473
  skills = await discoverSkills(tempDir, parsed.subpath, {
@@ -3477,7 +3477,7 @@ async function runAdd(args, options = {}) {
3477
3477
  }
3478
3478
  } else {
3479
3479
  spinner.start("Cloning repository...");
3480
- tempDir = await cloneRepo(parsed.url, parsed.ref);
3480
+ tempDir = await cloneRepo$1(parsed.url, parsed.ref);
3481
3481
  spinner.stop("Repository cloned");
3482
3482
  spinner.start("Discovering skills...");
3483
3483
  skills = await discoverSkills(tempDir, parsed.subpath, {
@@ -3997,7 +3997,7 @@ function parseAddOptions(args) {
3997
3997
  options
3998
3998
  };
3999
3999
  }
4000
- const execFileAsync$1 = promisify(execFile);
4000
+ const execFileAsync$2 = promisify(execFile);
4001
4001
  const claudeAgentsDir = (global) => global ? join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "agents") : join(process.cwd(), ".claude", "agents");
4002
4002
  async function fetchAgentFile(source, name) {
4003
4003
  const rel = `agents/${name}.md`;
@@ -4006,7 +4006,7 @@ async function fetchAgentFile(source, name) {
4006
4006
  return existsSync(file) ? readFile(file, "utf8") : null;
4007
4007
  }
4008
4008
  if (/^[^/]+\/[^/]+$/.test(source)) try {
4009
- const { stdout } = await execFileAsync$1("gh", [
4009
+ const { stdout } = await execFileAsync$2("gh", [
4010
4010
  "api",
4011
4011
  `repos/${source}/contents/${rel}`,
4012
4012
  "--jq",
@@ -4020,7 +4020,7 @@ async function fetchAgentFile(source, name) {
4020
4020
  return null;
4021
4021
  }
4022
4022
  try {
4023
- const file = join(await cloneRepo(source), rel);
4023
+ const file = join(await cloneRepo$1(source), rel);
4024
4024
  return existsSync(file) ? readFile(file, "utf8") : null;
4025
4025
  } catch {
4026
4026
  return null;
@@ -4103,6 +4103,152 @@ async function runAgents(args) {
4103
4103
  console.log(`Unknown agents command: ${sub}`);
4104
4104
  console.log(AGENTS_HELP);
4105
4105
  }
4106
+ const execFileAsync$1 = promisify(execFile);
4107
+ const RULE_SCOPES = [
4108
+ "client",
4109
+ "server",
4110
+ "common"
4111
+ ];
4112
+ const claudeRulesDir = (global) => global ? join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "rules") : join(process.cwd(), ".claude", "rules");
4113
+ function stripFrontmatter(md) {
4114
+ const m = md.match(/^---\n[\s\S]*?\n---\n+/);
4115
+ return m ? md.slice(m[0].length) : md;
4116
+ }
4117
+ async function fetchFromCatalog(source, rel) {
4118
+ if (source.startsWith(".") || isAbsolute(source)) {
4119
+ const file = join(source, rel);
4120
+ return existsSync(file) ? readFile(file, "utf8") : null;
4121
+ }
4122
+ if (/^[^/]+\/[^/]+$/.test(source)) try {
4123
+ const { stdout } = await execFileAsync$1("gh", [
4124
+ "api",
4125
+ `repos/${source}/contents/${rel}`,
4126
+ "--jq",
4127
+ ".content"
4128
+ ], { env: {
4129
+ ...process.env,
4130
+ GIT_TERMINAL_PROMPT: "0"
4131
+ } });
4132
+ return Buffer.from(stdout.trim(), "base64").toString("utf8");
4133
+ } catch {
4134
+ return null;
4135
+ }
4136
+ try {
4137
+ const file = join(await cloneRepo$1(source), rel);
4138
+ return existsSync(file) ? readFile(file, "utf8") : null;
4139
+ } catch {
4140
+ return null;
4141
+ }
4142
+ }
4143
+ const fetchRuleFile = (source, name) => fetchFromCatalog(source, `rules/${name}.md`);
4144
+ async function fetchRuleIndex(source) {
4145
+ const raw = await fetchFromCatalog(source, ".well-known/agent-skills/index.json");
4146
+ if (!raw) return [];
4147
+ try {
4148
+ return JSON.parse(raw).rules ?? [];
4149
+ } catch {
4150
+ return [];
4151
+ }
4152
+ }
4153
+ function expandScopes(scopes) {
4154
+ const set = new Set(scopes);
4155
+ if (set.has("client") || set.has("server")) set.add("common");
4156
+ return set;
4157
+ }
4158
+ function parseRulesOptions(args) {
4159
+ const names = [];
4160
+ const options = {};
4161
+ const pushScope = (value) => {
4162
+ if (value && RULE_SCOPES.includes(value)) (options.scopes ??= []).push(value);
4163
+ else M.warn(`Ignoring invalid --scope value: ${import_picocolors.default.yellow(value ?? "(missing)")}`);
4164
+ };
4165
+ for (let i = 0; i < args.length; i++) {
4166
+ const arg = args[i];
4167
+ if (arg === "-g" || arg === "--global") options.global = true;
4168
+ else if (arg === "-y" || arg === "--yes") options.yes = true;
4169
+ else if (arg === "--all") options.all = true;
4170
+ else if (arg === "-s" || arg === "--scope") pushScope(args[++i]);
4171
+ else if (arg.startsWith("--scope=")) pushScope(arg.slice(8));
4172
+ else if (!arg.startsWith("-")) names.push(arg);
4173
+ }
4174
+ return {
4175
+ names,
4176
+ options
4177
+ };
4178
+ }
4179
+ async function addRules(names, options) {
4180
+ const source = PRUFF_CATALOG;
4181
+ const targetDir = claudeRulesDir(options.global ?? false);
4182
+ Ie(import_picocolors.default.cyan("pruff-skills rules"));
4183
+ let toInstall = names.filter(Boolean);
4184
+ if (options.all || options.scopes?.length) {
4185
+ const index = await fetchRuleIndex(source);
4186
+ if (index.length === 0) {
4187
+ M.error(`Could not read the rules catalog (${source}).`);
4188
+ process.exit(1);
4189
+ }
4190
+ const wanted = options.all ? null : expandScopes(options.scopes ?? []);
4191
+ const matched = index.filter((r) => wanted ? !!r.scope && wanted.has(r.scope) : true).map((r) => r.name);
4192
+ toInstall = Array.from(new Set([...toInstall, ...matched]));
4193
+ }
4194
+ if (toInstall.length === 0) {
4195
+ M.error("Specify a rule name, a --scope, or --all.");
4196
+ M.message(import_picocolors.default.dim("e.g. `pruff-skills rules add service-patterns`"));
4197
+ M.message(import_picocolors.default.dim("or `pruff-skills rules add --scope server`"));
4198
+ M.message(import_picocolors.default.dim("Browse them with `npx pruff-skills find`."));
4199
+ process.exit(1);
4200
+ }
4201
+ await mkdir(targetDir, { recursive: true });
4202
+ const installed = [];
4203
+ for (const name of toInstall) {
4204
+ if (!isBareSkillName(name)) {
4205
+ M.warn(`Skipping invalid rule name: ${import_picocolors.default.yellow(name)}`);
4206
+ continue;
4207
+ }
4208
+ const content = await fetchRuleFile(source, name);
4209
+ if (!content) {
4210
+ M.error(`Rule ${import_picocolors.default.yellow(name)} not found in the catalog (${source}).`);
4211
+ continue;
4212
+ }
4213
+ const dest = join(targetDir, `${sanitizeName(name)}.md`);
4214
+ await writeFile(dest, stripFrontmatter(content));
4215
+ installed.push(dest);
4216
+ M.success(`${import_picocolors.default.green(name)} → ${dest}`);
4217
+ }
4218
+ if (installed.length === 0) {
4219
+ Se(import_picocolors.default.red("No rules installed."));
4220
+ process.exit(1);
4221
+ }
4222
+ Se(`Installed ${installed.length} rule${installed.length === 1 ? "" : "s"} into ${import_picocolors.default.dim(targetDir)}.`);
4223
+ }
4224
+ const RULES_HELP = `${import_picocolors.default.bold("Usage:")} pruff-skills rules <command> [options]
4225
+
4226
+ ${import_picocolors.default.bold("Commands:")}
4227
+ add <name...> Install one or more rules into .claude/rules/
4228
+
4229
+ ${import_picocolors.default.bold("Options:")}
4230
+ -s, --scope <s> Install every rule in a scope: client | server | common
4231
+ (client/server also pull in common). Repeatable.
4232
+ --all Install every rule in the catalog
4233
+ -g, --global Install to ~/.claude/rules/ instead of the project
4234
+ -y, --yes Skip prompts
4235
+
4236
+ ${import_picocolors.default.bold("Examples:")}
4237
+ pruff-skills rules add service-patterns bigint-handling
4238
+ pruff-skills rules add --scope server ${import_picocolors.default.dim("# all server + common rules")}
4239
+ pruff-skills rules add --scope client -g
4240
+ pruff-skills rules add --all
4241
+
4242
+ Browse available rules with ${import_picocolors.default.cyan("npx pruff-skills find")}.`;
4243
+ async function runRules(args) {
4244
+ const sub = args[0];
4245
+ if (!sub || sub === "--help" || sub === "-h") {
4246
+ console.log(RULES_HELP);
4247
+ return;
4248
+ }
4249
+ const { names, options } = parseRulesOptions(sub === "add" || sub === "a" || sub === "install" || sub === "i" ? args.slice(1) : args);
4250
+ await addRules(names, options);
4251
+ }
4106
4252
  const execFileAsync = promisify(execFile);
4107
4253
  const RESET$3 = "\x1B[0m";
4108
4254
  const BOLD$3 = "\x1B[1m";
@@ -4111,6 +4257,7 @@ const TEXT$2 = "\x1B[38;5;145m";
4111
4257
  const CYAN$1 = "\x1B[36m";
4112
4258
  const GREEN = "\x1B[32m";
4113
4259
  const MAGENTA = "\x1B[35m";
4260
+ const BLUE$1 = "\x1B[38;2;110;139;255m";
4114
4261
  const CATALOG_SOURCE = process.env.PRUFF_SKILLS_CATALOG || "Pruff-Tech/pruff-skills";
4115
4262
  async function fetchCatalogIndex(source) {
4116
4263
  const rel = ".well-known/agent-skills/index.json";
@@ -4149,6 +4296,10 @@ function agentInstalled(name) {
4149
4296
  const globalClaude = process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
4150
4297
  return existsSync(join(process.cwd(), ".claude", "agents", `${name}.md`)) || existsSync(join(globalClaude, "agents", `${name}.md`));
4151
4298
  }
4299
+ function ruleInstalled(name) {
4300
+ const globalClaude = process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
4301
+ return existsSync(join(process.cwd(), ".claude", "rules", `${name}.md`)) || existsSync(join(globalClaude, "rules", `${name}.md`));
4302
+ }
4152
4303
  let catalogCache = null;
4153
4304
  async function loadCatalog() {
4154
4305
  if (catalogCache) return catalogCache;
@@ -4157,6 +4308,8 @@ async function loadCatalog() {
4157
4308
  const installedSkills = await installedSkillNames();
4158
4309
  const toEntry = (e, kind) => {
4159
4310
  const name = sanitizeMetadata(e.name);
4311
+ const badge = kind === "rule" ? e.scope : e.category;
4312
+ const installed = kind === "agent" ? agentInstalled(name) : kind === "rule" ? ruleInstalled(name) : installedSkills.has(name);
4160
4313
  return {
4161
4314
  name,
4162
4315
  slug: name,
@@ -4164,11 +4317,15 @@ async function loadCatalog() {
4164
4317
  installs: 0,
4165
4318
  description: sanitizeMetadata(e.description || ""),
4166
4319
  kind,
4167
- category: e.category ? sanitizeMetadata(e.category) : void 0,
4168
- installed: kind === "agent" ? agentInstalled(name) : installedSkills.has(name)
4320
+ category: badge ? sanitizeMetadata(badge) : void 0,
4321
+ installed
4169
4322
  };
4170
4323
  };
4171
- catalogCache = [...(index.skills ?? []).map((e) => toEntry(e, "skill")), ...(index.agents ?? []).map((e) => toEntry(e, "agent"))];
4324
+ catalogCache = [
4325
+ ...(index.skills ?? []).map((e) => toEntry(e, "skill")),
4326
+ ...(index.agents ?? []).map((e) => toEntry(e, "agent")),
4327
+ ...(index.rules ?? []).map((e) => toEntry(e, "rule"))
4328
+ ];
4172
4329
  } catch {
4173
4330
  catalogCache = [];
4174
4331
  }
@@ -4199,7 +4356,7 @@ async function runSearchPrompt(kind, initialQuery = "") {
4199
4356
  let query = initialQuery;
4200
4357
  let loading = false;
4201
4358
  let lastRenderedLines = 0;
4202
- const noun = kind === "agent" ? "subagents" : "skills";
4359
+ const noun = kind === "agent" ? "subagents" : kind === "rule" ? "rules" : "skills";
4203
4360
  if (process.stdin.isTTY) process.stdin.setRawMode(true);
4204
4361
  readline.emitKeypressEvents(process.stdin);
4205
4362
  process.stdin.resume();
@@ -4316,10 +4473,10 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
4316
4473
  console.log(`${DIM$3}Nothing in the catalog matches "${query}"${RESET$3}`);
4317
4474
  return;
4318
4475
  }
4319
- console.log(`${DIM$3}Install a skill with${RESET$3} npx pruff-skills add <name>${DIM$3}, a subagent with${RESET$3} npx pruff-skills agents add <name>`);
4476
+ console.log(`${DIM$3}Install a skill with${RESET$3} npx pruff-skills add <name>${DIM$3}, a subagent with${RESET$3} npx pruff-skills agents add <name>${DIM$3}, a rule with${RESET$3} npx pruff-skills rules add <name>`);
4320
4477
  console.log();
4321
4478
  for (const skill of results.slice(0, 10)) {
4322
- const kindBadge = skill.kind === "agent" ? ` ${MAGENTA}[subagent]${RESET$3}` : ` ${CYAN$1}[skill]${RESET$3}`;
4479
+ const kindBadge = skill.kind === "agent" ? ` ${MAGENTA}[subagent]${RESET$3}` : skill.kind === "rule" ? ` ${BLUE$1}[rule]${RESET$3}` : ` ${CYAN$1}[skill]${RESET$3}`;
4323
4480
  console.log(`${TEXT$2}${skill.name}${RESET$3}${kindBadge}${rowBadge(skill)}`);
4324
4481
  if (skill.description) console.log(`${DIM$3}└ ${skill.description}${RESET$3}`);
4325
4482
  console.log();
@@ -4335,19 +4492,28 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
4335
4492
  const all = await searchSkillsAPI("");
4336
4493
  const skillCount = all.filter((s) => s.kind === "skill").length;
4337
4494
  const agentCount = all.filter((s) => s.kind === "agent").length;
4495
+ const ruleCount = all.filter((s) => s.kind === "rule").length;
4338
4496
  let selected = "back";
4339
4497
  while (selected === "back") {
4340
4498
  const kindChoice = await ve({
4341
4499
  message: "What are you looking for?",
4342
- options: [{
4343
- value: "skill",
4344
- label: "Skills",
4345
- hint: `${skillCount} available`
4346
- }, {
4347
- value: "agent",
4348
- label: "Subagents",
4349
- hint: `${agentCount} available`
4350
- }]
4500
+ options: [
4501
+ {
4502
+ value: "skill",
4503
+ label: "Skills",
4504
+ hint: `${skillCount} available`
4505
+ },
4506
+ {
4507
+ value: "agent",
4508
+ label: "Subagents",
4509
+ hint: `${agentCount} available`
4510
+ },
4511
+ {
4512
+ value: "rule",
4513
+ label: "Rules",
4514
+ hint: `${ruleCount} available`
4515
+ }
4516
+ ]
4351
4517
  });
4352
4518
  if (pD(kindChoice)) {
4353
4519
  console.log(`${DIM$3}Search cancelled${RESET$3}`);
@@ -4365,6 +4531,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
4365
4531
  const skillName = selected.name;
4366
4532
  console.log();
4367
4533
  if (selected.kind === "agent") await addAgents([skillName], {});
4534
+ else if (selected.kind === "rule") await addRules([skillName], {});
4368
4535
  else {
4369
4536
  const pkg = selected.source || selected.slug;
4370
4537
  console.log(`${TEXT$2}Installing ${BOLD$3}${skillName}${RESET$3} from ${DIM$3}${pkg}${RESET$3}...`);
@@ -5299,7 +5466,7 @@ async function updateGlobalSkills(options = {}) {
5299
5466
  }
5300
5467
  continue;
5301
5468
  }
5302
- tempDir = await cloneRepo(sourceUrl, firstEntry.ref);
5469
+ tempDir = await cloneRepo$1(sourceUrl, firstEntry.ref);
5303
5470
  const discoveredPaths = (await discoverSkills(tempDir)).map((skill) => {
5304
5471
  return join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/");
5305
5472
  });
@@ -5455,7 +5622,7 @@ async function updateProjectSkills(options = {}) {
5455
5622
  let tempDir = null;
5456
5623
  let deletedSkills = [];
5457
5624
  try {
5458
- tempDir = await cloneRepo(sourceUrl, ref);
5625
+ tempDir = await cloneRepo$1(sourceUrl, ref);
5459
5626
  const discoveredPaths = (await discoverSkills(tempDir)).map((s) => {
5460
5627
  return join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/");
5461
5628
  });
@@ -5674,14 +5841,14 @@ async function runUse(sourceArgs, options = {}, parseErrors = []) {
5674
5841
  });
5675
5842
  if (blobResult) skills = blobResult.skills;
5676
5843
  else {
5677
- cloneTempDir = await cloneRepo(parsed.url, parsed.ref);
5844
+ cloneTempDir = await cloneRepo$1(parsed.url, parsed.ref);
5678
5845
  skills = await discoverSkills(cloneTempDir, parsed.subpath, {
5679
5846
  includeInternal,
5680
5847
  fullDepth: options.fullDepth
5681
5848
  });
5682
5849
  }
5683
5850
  } else {
5684
- cloneTempDir = await cloneRepo(parsed.url, parsed.ref);
5851
+ cloneTempDir = await cloneRepo$1(parsed.url, parsed.ref);
5685
5852
  skills = await discoverSkills(cloneTempDir, parsed.subpath, {
5686
5853
  includeInternal,
5687
5854
  fullDepth: options.fullDepth
@@ -5904,6 +6071,291 @@ function fail(message) {
5904
6071
  process.exit(1);
5905
6072
  }
5906
6073
  var UseCommandError = class extends Error {};
6074
+ const RESET = "\x1B[0m";
6075
+ const DIM = "\x1B[38;5;102m";
6076
+ const BLUES = [
6077
+ "\x1B[38;2;110;139;255m",
6078
+ "\x1B[38;2;91;118;249m",
6079
+ "\x1B[38;2;72;97;242m",
6080
+ "\x1B[38;2;53;77;236m",
6081
+ "\x1B[38;2;34;56;229m",
6082
+ "\x1B[38;2;15;35;223m"
6083
+ ];
6084
+ const SWEEP = "\x1B[38;2;214;226;255m";
6085
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
6086
+ function canAnimate() {
6087
+ return !!process.stdout.isTTY && !process.env.CI && !process.env.NO_COLOR && !process.env.PRUFF_SKILLS_NO_ANIM;
6088
+ }
6089
+ function columnColor(col, width) {
6090
+ return BLUES[Math.min(BLUES.length - 1, Math.floor(col / Math.max(1, width - 1) * BLUES.length))];
6091
+ }
6092
+ function renderSweepLine(chars, indent, pos, band) {
6093
+ let out = indent;
6094
+ let mode = "";
6095
+ for (let c = 0; c < chars.length; c++) {
6096
+ const color = c >= pos && c < pos + band ? SWEEP : columnColor(c, chars.length);
6097
+ if (color !== mode) {
6098
+ out += color;
6099
+ mode = color;
6100
+ }
6101
+ out += chars[c];
6102
+ }
6103
+ return out + RESET;
6104
+ }
6105
+ async function sweepLine(text, { slow = false, indent = " " } = {}) {
6106
+ const chars = Array.from(text);
6107
+ const width = chars.length;
6108
+ const band = 5;
6109
+ const step = slow ? 2 : 3;
6110
+ const ms = slow ? 42 : 24;
6111
+ if (!canAnimate()) {
6112
+ console.log(renderSweepLine(chars, indent, -1, 0));
6113
+ return;
6114
+ }
6115
+ process.stdout.write("\x1B[?25l");
6116
+ try {
6117
+ let first = true;
6118
+ for (let pos = -5; pos <= width; pos += step) {
6119
+ if (!first) process.stdout.write("\x1B[1A");
6120
+ process.stdout.write("\r" + renderSweepLine(chars, indent, pos, band) + "\n");
6121
+ first = false;
6122
+ await sleep(ms);
6123
+ }
6124
+ process.stdout.write("\x1B[1A\r" + renderSweepLine(chars, indent, -1, 0) + "\n");
6125
+ } finally {
6126
+ process.stdout.write("\x1B[?25h");
6127
+ }
6128
+ }
6129
+ const CLAUDE_CODE_PKG = "@anthropic-ai/claude-code";
6130
+ const KNOWLEDGE_REPO = "https://github.com/Pruff-Tech/pruff-knowledge";
6131
+ const KNOWLEDGE_DIR = "Pruff Knowledge";
6132
+ const DEFAULT_WORKSPACE = join(homedir(), "pruff");
6133
+ const MAIN_REPOS = [{
6134
+ name: "Pruff-server",
6135
+ url: "https://github.com/Pruff-Tech/Pruff-server"
6136
+ }, {
6137
+ name: "Pruff-Client",
6138
+ url: "https://github.com/Pruff-Tech/Pruff-Client"
6139
+ }];
6140
+ const CORE_SKILLS = [
6141
+ "commit",
6142
+ "create-pr",
6143
+ "linear",
6144
+ "start-issue",
6145
+ "handoff",
6146
+ "pruff-logs",
6147
+ "pruff-pr-review",
6148
+ "review-pr-comments",
6149
+ "react-doctor"
6150
+ ];
6151
+ const CORE_AGENTS = [
6152
+ "codebase-navigator",
6153
+ "db-query-reviewer",
6154
+ "pruff-logs-investigator"
6155
+ ];
6156
+ async function runOnboarding(args) {
6157
+ if (process.platform !== "darwin") {
6158
+ M.error(`${import_picocolors.default.bold("onboarding")} currently supports macOS only (detected ${import_picocolors.default.cyan(process.platform)}).`);
6159
+ return;
6160
+ }
6161
+ Ie(import_picocolors.default.bold("Pruff onboarding"));
6162
+ installClaudeCode();
6163
+ ensureObsidian();
6164
+ const workspace = await resolveWorkspace(args);
6165
+ mkdirSync(workspace, { recursive: true });
6166
+ M.success(`Workspace ${import_picocolors.default.cyan(workspace)}`);
6167
+ cloneKnowledge(workspace);
6168
+ for (const repo of MAIN_REPOS) cloneRepo(repo.name, repo.url, workspace);
6169
+ await installCatalog(workspace);
6170
+ await showGuidance(workspace);
6171
+ Se(`${import_picocolors.default.green("Done.")} Welcome to Pruff — happy building.`);
6172
+ }
6173
+ function has(cmd) {
6174
+ return spawnSync("command", ["-v", cmd], {
6175
+ shell: true,
6176
+ stdio: "ignore"
6177
+ }).status === 0;
6178
+ }
6179
+ function run(cmd, args, cwd) {
6180
+ return spawnSync(cmd, args, {
6181
+ stdio: "inherit",
6182
+ cwd
6183
+ }).status === 0;
6184
+ }
6185
+ function expandHome(input) {
6186
+ return input.startsWith("~") ? join(homedir(), input.slice(1)) : input;
6187
+ }
6188
+ function installClaudeCode() {
6189
+ if (has("claude")) {
6190
+ M.success("Claude Code already installed");
6191
+ return;
6192
+ }
6193
+ const s = Y();
6194
+ s.start("Installing Claude Code…");
6195
+ if (run("npm", [
6196
+ "install",
6197
+ "-g",
6198
+ CLAUDE_CODE_PKG
6199
+ ])) s.stop("Claude Code installed");
6200
+ else {
6201
+ s.stop(import_picocolors.default.yellow("Claude Code install failed"));
6202
+ M.warn(`Install it manually: ${import_picocolors.default.cyan(`npm i -g ${CLAUDE_CODE_PKG}`)}`);
6203
+ }
6204
+ }
6205
+ function ensureObsidian() {
6206
+ if (existsSync("/Applications/Obsidian.app")) {
6207
+ M.success("Obsidian already installed");
6208
+ return;
6209
+ }
6210
+ if (!has("brew")) {
6211
+ M.warn(`Obsidian not found and Homebrew is unavailable.\n Install it from ${import_picocolors.default.cyan("https://obsidian.md")} and re-run.`);
6212
+ return;
6213
+ }
6214
+ const s = Y();
6215
+ s.start("Installing Obsidian (brew)…");
6216
+ if (run("brew", [
6217
+ "install",
6218
+ "--cask",
6219
+ "obsidian"
6220
+ ])) s.stop("Obsidian installed");
6221
+ else {
6222
+ s.stop(import_picocolors.default.yellow("Obsidian install failed"));
6223
+ M.warn(`Install manually: ${import_picocolors.default.cyan("brew install --cask obsidian")}`);
6224
+ }
6225
+ }
6226
+ async function resolveWorkspace(args) {
6227
+ const fromArg = args.find((a) => !a.startsWith("-"));
6228
+ if (fromArg) return expandHome(fromArg);
6229
+ if (!process.stdout.isTTY) return DEFAULT_WORKSPACE;
6230
+ const answer = await he({
6231
+ message: "Where should the Pruff workspace live?",
6232
+ placeholder: DEFAULT_WORKSPACE,
6233
+ defaultValue: DEFAULT_WORKSPACE
6234
+ });
6235
+ if (pD(answer)) {
6236
+ xe("Onboarding cancelled.");
6237
+ process.exit(0);
6238
+ }
6239
+ return expandHome((answer || DEFAULT_WORKSPACE).trim());
6240
+ }
6241
+ function cloneKnowledge(workspace) {
6242
+ const vaultPath = join(workspace, KNOWLEDGE_DIR);
6243
+ const s = Y();
6244
+ if (existsSync(join(vaultPath, ".git"))) {
6245
+ s.start("Updating Pruff knowledge…");
6246
+ const ok = run("git", [
6247
+ "-C",
6248
+ vaultPath,
6249
+ "pull",
6250
+ "--ff-only"
6251
+ ]);
6252
+ s.stop(ok ? "Pruff knowledge up to date" : import_picocolors.default.yellow("git pull failed (keeping local copy)"));
6253
+ registerVault(vaultPath);
6254
+ return;
6255
+ }
6256
+ s.start("Cloning Pruff knowledge…");
6257
+ if (run("git", [
6258
+ "clone",
6259
+ KNOWLEDGE_REPO,
6260
+ vaultPath
6261
+ ])) {
6262
+ s.stop(`Cloned knowledge to ${import_picocolors.default.dim(vaultPath)}`);
6263
+ registerVault(vaultPath);
6264
+ return;
6265
+ }
6266
+ s.stop(import_picocolors.default.yellow("Knowledge clone failed"));
6267
+ M.warn(`Could not clone ${import_picocolors.default.cyan(KNOWLEDGE_REPO)}.\n It's private — authenticate first with ${import_picocolors.default.cyan("gh auth login")} (or an SSH key), then re-run.`);
6268
+ }
6269
+ function cloneRepo(name, url, workspace) {
6270
+ const target = join(workspace, name);
6271
+ if (existsSync(join(target, ".git"))) {
6272
+ M.success(`${name} already cloned`);
6273
+ return;
6274
+ }
6275
+ const s = Y();
6276
+ s.start(`Cloning ${name}…`);
6277
+ if (run("git", [
6278
+ "clone",
6279
+ url,
6280
+ target
6281
+ ], workspace)) s.stop(`Cloned ${import_picocolors.default.cyan(name)}`);
6282
+ else {
6283
+ s.stop(import_picocolors.default.yellow(`${name} clone failed`));
6284
+ M.warn(`Could not clone ${name}. Authenticate with ${import_picocolors.default.cyan("gh auth login")} then run:\n ${import_picocolors.default.dim(`git clone ${url}`)} (inside ${workspace})`);
6285
+ }
6286
+ }
6287
+ async function installCatalog(workspace) {
6288
+ const prevCwd = process.cwd();
6289
+ M.step("Installing Pruff rules, agents and skills into the workspace…");
6290
+ try {
6291
+ process.chdir(workspace);
6292
+ await runRules(["add", "--all"]);
6293
+ await runAgents(["add", ...CORE_AGENTS]);
6294
+ await runAdd([PRUFF_CATALOG], {
6295
+ skill: CORE_SKILLS,
6296
+ agent: ["claude-code"],
6297
+ yes: true
6298
+ });
6299
+ } catch (error) {
6300
+ M.warn(`Catalog install hit an error: ${error instanceof Error ? error.message : String(error)}\n Re-run once authenticated: ${import_picocolors.default.cyan("npx pruff-skills onboarding")}`);
6301
+ } finally {
6302
+ process.chdir(prevCwd);
6303
+ }
6304
+ }
6305
+ async function showGuidance(workspace) {
6306
+ console.log();
6307
+ await sweepLine("W O R K S P A C E R E A D Y", { slow: true });
6308
+ console.log();
6309
+ const line = (s) => console.log(` ${s}`);
6310
+ line(import_picocolors.default.bold("Recommended structure") + import_picocolors.default.dim(" (keep everything under one parent)"));
6311
+ line("");
6312
+ line(`${import_picocolors.default.cyan(workspace)}/`);
6313
+ const tree = [
6314
+ [".claude/", "rules · agents · skills (apply to every repo below)"],
6315
+ [`${KNOWLEDGE_DIR}/`, "Obsidian vault — open it in Obsidian"],
6316
+ ["Pruff-server/", "backend"],
6317
+ ["Pruff-Client/", "frontend"],
6318
+ ["sls-*/", "serverless services — clone as you need them"]
6319
+ ];
6320
+ const labelWidth = Math.max(...tree.map(([label]) => label.length)) + 2;
6321
+ tree.forEach(([label, desc], i) => {
6322
+ const connector = i === tree.length - 1 ? "└──" : "├──";
6323
+ line(`${import_picocolors.default.dim(connector)} ${label.padEnd(labelWidth)}${import_picocolors.default.dim(desc)}`);
6324
+ });
6325
+ line("");
6326
+ line(import_picocolors.default.bold("Next steps"));
6327
+ line(`${import_picocolors.default.dim("1.")} Open ${import_picocolors.default.bold("Obsidian")} → the ${import_picocolors.default.cyan(KNOWLEDGE_DIR)} vault is registered and ready.`);
6328
+ line(`${import_picocolors.default.dim("2.")} Start Claude Code from ${import_picocolors.default.cyan(workspace)} so the shared ${import_picocolors.default.dim(".claude/")} applies everywhere.`);
6329
+ line(`${import_picocolors.default.dim("3.")} Backend: ${import_picocolors.default.dim("cd Pruff-server && npm i && npm run local")}`);
6330
+ line(`${import_picocolors.default.dim("4.")} Frontend: ${import_picocolors.default.dim("cd Pruff-Client && yarn && yarn start-local")}`);
6331
+ line(`${import_picocolors.default.dim("5.")} Clone any serverless service into this folder with ${import_picocolors.default.dim("git clone <repo>")}.`);
6332
+ console.log();
6333
+ }
6334
+ function registerVault(vaultPath) {
6335
+ const dir = join(homedir(), "Library", "Application Support", "obsidian");
6336
+ const configPath = join(dir, "obsidian.json");
6337
+ let config = { vaults: {} };
6338
+ if (existsSync(configPath)) try {
6339
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
6340
+ if (!config.vaults) config.vaults = {};
6341
+ } catch {
6342
+ M.warn(`Could not parse ${import_picocolors.default.dim(configPath)} — leaving it untouched.`);
6343
+ return;
6344
+ }
6345
+ else mkdirSync(dir, { recursive: true });
6346
+ if (Object.values(config.vaults).some((v) => v.path === vaultPath)) {
6347
+ M.success("Vault already registered in Obsidian");
6348
+ return;
6349
+ }
6350
+ const id = randomBytes(8).toString("hex");
6351
+ config.vaults[id] = {
6352
+ path: vaultPath,
6353
+ ts: Date.now(),
6354
+ open: true
6355
+ };
6356
+ writeFileSync(configPath, JSON.stringify(config), "utf-8");
6357
+ M.success("Registered vault in Obsidian");
6358
+ }
5907
6359
  const __dirname = dirname(fileURLToPath(import.meta.url));
5908
6360
  function getVersion() {
5909
6361
  try {
@@ -5914,9 +6366,7 @@ function getVersion() {
5914
6366
  }
5915
6367
  }
5916
6368
  const VERSION = getVersion();
5917
- const RESET = "\x1B[0m";
5918
6369
  const BOLD = "\x1B[1m";
5919
- const DIM = "\x1B[38;5;102m";
5920
6370
  const TEXT = "\x1B[38;5;145m";
5921
6371
  const BLUE = "\x1B[38;2;110;139;255m";
5922
6372
  const LOGO_LINES = [
@@ -5927,16 +6377,6 @@ const LOGO_LINES = [
5927
6377
  "██║ ██║ ██║╚██████╔╝██║ ██║ ",
5928
6378
  "╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ "
5929
6379
  ];
5930
- const BLUES = [
5931
- "\x1B[38;2;110;139;255m",
5932
- "\x1B[38;2;91;118;249m",
5933
- "\x1B[38;2;72;97;242m",
5934
- "\x1B[38;2;53;77;236m",
5935
- "\x1B[38;2;34;56;229m",
5936
- "\x1B[38;2;15;35;223m"
5937
- ];
5938
- const SWEEP = "\x1B[38;2;214;226;255m";
5939
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5940
6380
  function printStaticLogo() {
5941
6381
  LOGO_LINES.forEach((line, i) => {
5942
6382
  console.log(`${BLUES[i]}${line}${RESET}`);
@@ -5972,7 +6412,7 @@ function renderLogoFrame(pos, band) {
5972
6412
  return out + RESET;
5973
6413
  }).join("\n");
5974
6414
  }
5975
- async function showLogo() {
6415
+ async function showLogo(slow = false) {
5976
6416
  console.log();
5977
6417
  if (!(!!process.stdout.isTTY && !process.env.CI && !process.env.NO_COLOR && !process.env.PRUFF_SKILLS_NO_ANIM)) {
5978
6418
  printStaticLogo();
@@ -5981,6 +6421,9 @@ async function showLogo() {
5981
6421
  const rows = LOGO_LINES.length;
5982
6422
  const width = Math.max(...LOGO_LINES.map((l) => Array.from(l).length));
5983
6423
  const band = 7;
6424
+ const revealMs = slow ? 190 : 90;
6425
+ const step = slow ? 3 : 4;
6426
+ const sweepMs = slow ? 34 : 18;
5984
6427
  process.stdout.write("\x1B[?25l");
5985
6428
  try {
5986
6429
  let first = true;
@@ -5988,15 +6431,75 @@ async function showLogo() {
5988
6431
  if (!first) process.stdout.write(`\x1b[${rows}A`);
5989
6432
  process.stdout.write("\r" + renderLogoReveal(cut, width) + "\n");
5990
6433
  first = false;
5991
- await sleep(90);
6434
+ await sleep(revealMs);
5992
6435
  }
5993
- for (let pos = -7; pos <= width; pos += 4) {
6436
+ for (let pos = -7; pos <= width; pos += step) {
5994
6437
  process.stdout.write(`\x1b[${rows}A`);
5995
6438
  process.stdout.write("\r" + renderLogoFrame(pos, band) + "\n");
5996
- await sleep(18);
6439
+ await sleep(sweepMs);
5997
6440
  }
5998
6441
  process.stdout.write(`\x1b[${rows}A\r`);
5999
6442
  process.stdout.write(LOGO_LINES.map((line, i) => `${BLUES[i]}${line}${RESET}`).join("\n") + "\n");
6443
+ if (slow) await sleep(500);
6444
+ } finally {
6445
+ process.stdout.write("\x1B[?25h");
6446
+ }
6447
+ }
6448
+ const TAGLINE_BLOCKS = [[
6449
+ "██████╗ ██╗ ██╗██╗██╗ ██████╗ ██╗███╗ ██╗ ██████╗ ",
6450
+ "██╔══██╗██║ ██║██║██║ ██╔══██╗██║████╗ ██║██╔════╝ ",
6451
+ "██████╔╝██║ ██║██║██║ ██║ ██║██║██╔██╗ ██║██║ ███╗",
6452
+ "██╔══██╗██║ ██║██║██║ ██║ ██║██║██║╚██╗██║██║ ██║",
6453
+ "██████╔╝╚██████╔╝██║███████╗██████╔╝██║██║ ╚████║╚██████╔╝",
6454
+ "╚═════╝ ╚═════╝ ╚═╝╚══════╝╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝ "
6455
+ ], [
6456
+ "███████╗██╗███╗ ██╗████████╗███████╗ ██████╗██╗ ██╗███████╗",
6457
+ "██╔════╝██║████╗ ██║╚══██╔══╝██╔════╝██╔════╝██║ ██║██╔════╝",
6458
+ "█████╗ ██║██╔██╗ ██║ ██║ █████╗ ██║ ███████║███████╗",
6459
+ "██╔══╝ ██║██║╚██╗██║ ██║ ██╔══╝ ██║ ██╔══██║╚════██║",
6460
+ "██║ ██║██║ ╚████║ ██║ ███████╗╚██████╗██║ ██║███████║",
6461
+ "╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝"
6462
+ ]];
6463
+ const TAGLINE_INDENT = " ";
6464
+ function renderTaglineRow(line, row, pos, band) {
6465
+ const chars = Array.from(line);
6466
+ let out = TAGLINE_INDENT;
6467
+ let mode = "";
6468
+ for (let c = 0; c < chars.length; c++) {
6469
+ const color = c >= pos && c < pos + band ? SWEEP : BLUES[row];
6470
+ if (color !== mode) {
6471
+ out += color;
6472
+ mode = color;
6473
+ }
6474
+ out += chars[c];
6475
+ }
6476
+ return out + RESET;
6477
+ }
6478
+ function renderTaglineFrame(pos, band) {
6479
+ return TAGLINE_BLOCKS.map((block) => block.map((line, row) => renderTaglineRow(line, row, pos, band)).join("\n")).join("\n");
6480
+ }
6481
+ async function showTagline(slow = false) {
6482
+ console.log();
6483
+ const rows = TAGLINE_BLOCKS.reduce((n, b) => n + b.length, 0);
6484
+ const width = Math.max(...TAGLINE_BLOCKS.flat().map((l) => Array.from(l).length));
6485
+ const band = 7;
6486
+ const step = slow ? 3 : 4;
6487
+ const sweepMs = slow ? 34 : 18;
6488
+ if (!(!!process.stdout.isTTY && !process.env.CI && !process.env.NO_COLOR && !process.env.PRUFF_SKILLS_NO_ANIM)) {
6489
+ console.log(renderTaglineFrame(-1, 0));
6490
+ return;
6491
+ }
6492
+ process.stdout.write("\x1B[?25l");
6493
+ try {
6494
+ let first = true;
6495
+ for (let pos = -7; pos <= width; pos += step) {
6496
+ if (!first) process.stdout.write(`\x1b[${rows}A`);
6497
+ process.stdout.write("\r" + renderTaglineFrame(pos, band) + "\n");
6498
+ first = false;
6499
+ await sleep(sweepMs);
6500
+ }
6501
+ process.stdout.write(`\x1b[${rows}A\r` + renderTaglineFrame(-1, 0) + "\n");
6502
+ if (slow) await sleep(500);
6000
6503
  } finally {
6001
6504
  process.stdout.write("\x1B[?25h");
6002
6505
  }
@@ -6012,6 +6515,7 @@ async function showBanner() {
6012
6515
  console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills list${RESET} ${DIM}List installed skills${RESET}`);
6013
6516
  console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills find ${DIM}[query]${RESET} ${DIM}Search the Pruff catalog${RESET}`);
6014
6517
  console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills agents add ${DIM}<name>${RESET} ${DIM}Add a Pruff subagent${RESET}`);
6518
+ console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills rules add ${DIM}<name>${RESET} ${DIM}Add a Pruff rule (.claude/rules/)${RESET}`);
6015
6519
  console.log();
6016
6520
  console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills update${RESET} ${DIM}Update installed skills${RESET}`);
6017
6521
  console.log();
@@ -6039,6 +6543,15 @@ ${BOLD}Manage Skills:${RESET}
6039
6543
  ${BOLD}Subagents:${RESET}
6040
6544
  agents add <name> Install a Claude Code subagent into .claude/agents/ (-g for global)
6041
6545
 
6546
+ ${BOLD}Rules:${RESET}
6547
+ rules add <name> Install a coding-convention rule into .claude/rules/ (-g for global)
6548
+ rules add --scope <client|server|common>
6549
+ Install a whole repo's rule set (client/server also pull in common)
6550
+
6551
+ ${BOLD}Onboarding:${RESET}
6552
+ onboarding Set up a new machine (macOS): install Claude Code,
6553
+ install Obsidian, and clone the Pruff knowledge vault
6554
+
6042
6555
  ${BOLD}Updates:${RESET}
6043
6556
  update [skills...] Update skills to latest versions (alias: upgrade)
6044
6557
 
@@ -6239,6 +6752,19 @@ async function main() {
6239
6752
  if (!inAgent) await showLogo();
6240
6753
  await runAgents(restArgs);
6241
6754
  break;
6755
+ case "rules":
6756
+ case "rule":
6757
+ if (!inAgent) await showLogo();
6758
+ await runRules(restArgs);
6759
+ break;
6760
+ case "onboarding":
6761
+ case "onboard":
6762
+ if (!inAgent) {
6763
+ await showLogo(true);
6764
+ await showTagline(true);
6765
+ }
6766
+ await runOnboarding(restArgs);
6767
+ break;
6242
6768
  case "remove":
6243
6769
  case "rm":
6244
6770
  case "r":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruff-skills",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "The official Pruff agent skills CLI",
5
5
  "type": "module",
6
6
  "bin": {