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 +9 -0
- package/dist/_chunks/libs/@clack/prompts.mjs +25 -3
- package/dist/cli.mjs +572 -46
- package/package.json +1 -1
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
|
-
},
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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:
|
|
4168
|
-
installed
|
|
4320
|
+
category: badge ? sanitizeMetadata(badge) : void 0,
|
|
4321
|
+
installed
|
|
4169
4322
|
};
|
|
4170
4323
|
};
|
|
4171
|
-
catalogCache = [
|
|
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
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
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(
|
|
6434
|
+
await sleep(revealMs);
|
|
5992
6435
|
}
|
|
5993
|
-
for (let pos = -7; pos <= width; pos +=
|
|
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(
|
|
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":
|