pruff-skills 1.11.0 → 1.12.1
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 +2 -1
- package/dist/cli.mjs +457 -172
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,7 +142,7 @@ npx pruff-skills add <skill> -g -a claude-code -y
|
|
|
142
142
|
| `npx pruff-skills list` | List installed skills (alias: `ls`) |
|
|
143
143
|
| `npx pruff-skills find [query]` | Search the Pruff catalog interactively or by keyword |
|
|
144
144
|
| `npx pruff-skills remove [skills]` | Remove installed skills from agents |
|
|
145
|
-
| `npx pruff-skills update [skills]` |
|
|
145
|
+
| `npx pruff-skills update [skills]` | Show what changed upstream (modified / new / deleted), then update only that |
|
|
146
146
|
| `npx pruff-skills init [name]` | Create a new `SKILL.md` template |
|
|
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 |
|
|
@@ -155,6 +155,7 @@ Troubleshooting: if a command fails, run `npx pruff-skills doctor` and paste the
|
|
|
155
155
|
npx pruff-skills list # list installed skills
|
|
156
156
|
npx pruff-skills find logs # search the catalog by keyword
|
|
157
157
|
npx pruff-skills update -g # update global skills
|
|
158
|
+
npx pruff-skills update --dry-run # show the plan, change nothing
|
|
158
159
|
npx pruff-skills remove <skill> # remove a skill
|
|
159
160
|
```
|
|
160
161
|
|
package/dist/cli.mjs
CHANGED
|
@@ -7,7 +7,7 @@ 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 { accessSync, chmodSync, constants, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
10
|
+
import { accessSync, chmodSync, constants, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, 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";
|
|
@@ -30,6 +30,7 @@ const PROBE_TIMEOUT_MS = 2500;
|
|
|
30
30
|
const probeCache = /* @__PURE__ */ new Map();
|
|
31
31
|
let currentStep = "start";
|
|
32
32
|
let failure = null;
|
|
33
|
+
let usageError = false;
|
|
33
34
|
const tail = [];
|
|
34
35
|
function setStep(step) {
|
|
35
36
|
currentStep = step;
|
|
@@ -40,9 +41,13 @@ function recordFailure(message) {
|
|
|
40
41
|
message
|
|
41
42
|
};
|
|
42
43
|
}
|
|
44
|
+
function markUsageError() {
|
|
45
|
+
usageError = true;
|
|
46
|
+
}
|
|
43
47
|
function resetDiagnosticsState() {
|
|
44
48
|
currentStep = "start";
|
|
45
49
|
failure = null;
|
|
50
|
+
usageError = false;
|
|
46
51
|
tail.length = 0;
|
|
47
52
|
}
|
|
48
53
|
function recordedFailure() {
|
|
@@ -333,7 +338,7 @@ async function withDiagnostics(command, args, fn) {
|
|
|
333
338
|
};
|
|
334
339
|
let printed = false;
|
|
335
340
|
const emit = (exitCode, error) => {
|
|
336
|
-
if (printed) return;
|
|
341
|
+
if (printed || usageError) return;
|
|
337
342
|
printed = true;
|
|
338
343
|
const message = error ?? recordedFailure()?.message ?? lastErrorLine(tail);
|
|
339
344
|
restore();
|
|
@@ -1041,6 +1046,14 @@ async function getPluginGroupings(basePath) {
|
|
|
1041
1046
|
} catch {}
|
|
1042
1047
|
return groupings;
|
|
1043
1048
|
}
|
|
1049
|
+
const AGENTS_DIR$1 = ".agents";
|
|
1050
|
+
const SKILLS_SUBDIR = "skills";
|
|
1051
|
+
const INSTALL_EXCLUDED_FILES = new Set(["metadata.json"]);
|
|
1052
|
+
const INSTALL_EXCLUDED_DIRS = new Set([
|
|
1053
|
+
".git",
|
|
1054
|
+
"__pycache__",
|
|
1055
|
+
"__pypackages__"
|
|
1056
|
+
]);
|
|
1044
1057
|
const LOCAL_LOCK_FILE = "skills-lock.json";
|
|
1045
1058
|
const CURRENT_VERSION$1 = 1;
|
|
1046
1059
|
function getLocalLockPath(cwd) {
|
|
@@ -1077,8 +1090,14 @@ async function writeLocalLock(lock, cwd) {
|
|
|
1077
1090
|
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
1078
1091
|
}
|
|
1079
1092
|
async function computeSkillFolderHash(skillDir) {
|
|
1093
|
+
return hashSkillFiles(skillDir, false);
|
|
1094
|
+
}
|
|
1095
|
+
async function computeSkillContentHash(skillDir) {
|
|
1096
|
+
return hashSkillFiles(skillDir, true);
|
|
1097
|
+
}
|
|
1098
|
+
async function hashSkillFiles(skillDir, installedOnly) {
|
|
1080
1099
|
const files = [];
|
|
1081
|
-
await collectFiles(skillDir, skillDir, files);
|
|
1100
|
+
await collectFiles(skillDir, skillDir, files, installedOnly);
|
|
1082
1101
|
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
1083
1102
|
const hash = createHash("sha256");
|
|
1084
1103
|
for (const file of files) {
|
|
@@ -1087,14 +1106,16 @@ async function computeSkillFolderHash(skillDir) {
|
|
|
1087
1106
|
}
|
|
1088
1107
|
return hash.digest("hex");
|
|
1089
1108
|
}
|
|
1090
|
-
async function collectFiles(baseDir, currentDir, results) {
|
|
1109
|
+
async function collectFiles(baseDir, currentDir, results, installedOnly) {
|
|
1091
1110
|
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
1092
1111
|
await Promise.all(entries.map(async (entry) => {
|
|
1093
1112
|
const fullPath = join(currentDir, entry.name);
|
|
1094
1113
|
if (entry.isDirectory()) {
|
|
1095
1114
|
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
1096
|
-
|
|
1115
|
+
if (installedOnly && INSTALL_EXCLUDED_DIRS.has(entry.name)) return;
|
|
1116
|
+
await collectFiles(baseDir, fullPath, results, installedOnly);
|
|
1097
1117
|
} else if (entry.isFile()) {
|
|
1118
|
+
if (installedOnly && INSTALL_EXCLUDED_FILES.has(entry.name)) return;
|
|
1098
1119
|
const content = await readFile(fullPath);
|
|
1099
1120
|
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
1100
1121
|
results.push({
|
|
@@ -1109,6 +1130,13 @@ async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
|
1109
1130
|
lock.skills[skillName] = entry;
|
|
1110
1131
|
await writeLocalLock(lock, cwd);
|
|
1111
1132
|
}
|
|
1133
|
+
async function removeSkillFromLocalLock(skillName, cwd) {
|
|
1134
|
+
const lock = await readLocalLock(cwd);
|
|
1135
|
+
if (!(skillName in lock.skills)) return false;
|
|
1136
|
+
delete lock.skills[skillName];
|
|
1137
|
+
await writeLocalLock(lock, cwd);
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1112
1140
|
function createEmptyLocalLock() {
|
|
1113
1141
|
return {
|
|
1114
1142
|
version: CURRENT_VERSION$1,
|
|
@@ -1972,8 +2000,6 @@ function getNonUniversalAgents() {
|
|
|
1972
2000
|
function isUniversalAgent(type) {
|
|
1973
2001
|
return agents[type].skillsDir === ".agents/skills";
|
|
1974
2002
|
}
|
|
1975
|
-
const AGENTS_DIR$1 = ".agents";
|
|
1976
|
-
const SKILLS_SUBDIR = "skills";
|
|
1977
2003
|
function sanitizeName(name) {
|
|
1978
2004
|
return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
|
|
1979
2005
|
}
|
|
@@ -2152,15 +2178,9 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
2152
2178
|
};
|
|
2153
2179
|
}
|
|
2154
2180
|
}
|
|
2155
|
-
const EXCLUDE_FILES$1 = new Set(["metadata.json"]);
|
|
2156
|
-
const EXCLUDE_DIRS$1 = new Set([
|
|
2157
|
-
".git",
|
|
2158
|
-
"__pycache__",
|
|
2159
|
-
"__pypackages__"
|
|
2160
|
-
]);
|
|
2161
2181
|
const isExcluded$1 = (name, isDirectory = false) => {
|
|
2162
|
-
if (
|
|
2163
|
-
if (isDirectory &&
|
|
2182
|
+
if (INSTALL_EXCLUDED_FILES.has(name)) return true;
|
|
2183
|
+
if (isDirectory && INSTALL_EXCLUDED_DIRS.has(name)) return true;
|
|
2164
2184
|
return false;
|
|
2165
2185
|
};
|
|
2166
2186
|
async function copyDirectory(src, dest) {
|
|
@@ -3113,6 +3133,33 @@ async function dismissPrompt(promptKey) {
|
|
|
3113
3133
|
lock.dismissed[promptKey] = true;
|
|
3114
3134
|
await writeSkillLock(lock);
|
|
3115
3135
|
}
|
|
3136
|
+
const DIVERGENT_HASH = "divergent";
|
|
3137
|
+
function findInstalledSkillDirs(skillName, cwd) {
|
|
3138
|
+
const candidates = [getCanonicalPath(skillName, { cwd })];
|
|
3139
|
+
for (const [type, config] of Object.entries(agents)) {
|
|
3140
|
+
if (isUniversalAgent(type)) continue;
|
|
3141
|
+
candidates.push(join(cwd, config.skillsDir, sanitizeName(skillName)));
|
|
3142
|
+
}
|
|
3143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3144
|
+
const dirs = [];
|
|
3145
|
+
for (const dir of candidates) {
|
|
3146
|
+
if (!existsSync(join(dir, "SKILL.md"))) continue;
|
|
3147
|
+
let real = dir;
|
|
3148
|
+
try {
|
|
3149
|
+
real = realpathSync(dir);
|
|
3150
|
+
} catch {}
|
|
3151
|
+
if (seen.has(real)) continue;
|
|
3152
|
+
seen.add(real);
|
|
3153
|
+
dirs.push(dir);
|
|
3154
|
+
}
|
|
3155
|
+
return dirs;
|
|
3156
|
+
}
|
|
3157
|
+
async function hashInstalledSkill(skillName, cwd) {
|
|
3158
|
+
const dirs = findInstalledSkillDirs(skillName, cwd);
|
|
3159
|
+
if (dirs.length === 0) return null;
|
|
3160
|
+
const hashes = new Set(await Promise.all(dirs.map((dir) => computeSkillContentHash(dir))));
|
|
3161
|
+
return hashes.size === 1 ? [...hashes][0] : DIVERGENT_HASH;
|
|
3162
|
+
}
|
|
3116
3163
|
const DOWNLOAD_BASE_URL = process.env.PRUFF_SKILLS_DOWNLOAD_URL || "";
|
|
3117
3164
|
const FETCH_TIMEOUT = 1e4;
|
|
3118
3165
|
function toSkillSlug(name) {
|
|
@@ -3791,6 +3838,7 @@ async function runAdd(args, options = {}) {
|
|
|
3791
3838
|
installTipShown = true;
|
|
3792
3839
|
};
|
|
3793
3840
|
if (!source) {
|
|
3841
|
+
markUsageError();
|
|
3794
3842
|
console.log();
|
|
3795
3843
|
console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
|
|
3796
3844
|
console.log();
|
|
@@ -4255,12 +4303,14 @@ async function runAdd(args, options = {}) {
|
|
|
4255
4303
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
4256
4304
|
const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path);
|
|
4257
4305
|
const skillPathValue = skillFiles[skill.name];
|
|
4306
|
+
const installedHash = await hashInstalledSkill(skill.name, cwd);
|
|
4258
4307
|
await addSkillToLocalLock(skill.name, {
|
|
4259
4308
|
source: lockSource || parsed.url,
|
|
4260
4309
|
ref: parsed.ref,
|
|
4261
4310
|
sourceType: parsed.type,
|
|
4262
4311
|
...skillPathValue && { skillPath: skillPathValue },
|
|
4263
|
-
computedHash
|
|
4312
|
+
computedHash,
|
|
4313
|
+
...installedHash && installedHash !== DIVERGENT_HASH && { installedHash }
|
|
4264
4314
|
}, cwd);
|
|
4265
4315
|
} catch {}
|
|
4266
4316
|
}
|
|
@@ -4554,7 +4604,7 @@ async function updateAgents(options = {}) {
|
|
|
4554
4604
|
result.unchanged.push(name);
|
|
4555
4605
|
continue;
|
|
4556
4606
|
}
|
|
4557
|
-
await writeFile(dest, content);
|
|
4607
|
+
if (!options.dryRun) await writeFile(dest, content);
|
|
4558
4608
|
result.updated.push(name);
|
|
4559
4609
|
}
|
|
4560
4610
|
return result;
|
|
@@ -4756,10 +4806,10 @@ async function runRules(args) {
|
|
|
4756
4806
|
await addRules(names, options);
|
|
4757
4807
|
}
|
|
4758
4808
|
const execFileAsync$2 = promisify(execFile);
|
|
4759
|
-
const RESET$
|
|
4760
|
-
const BOLD$
|
|
4761
|
-
const DIM$
|
|
4762
|
-
const TEXT$
|
|
4809
|
+
const RESET$4 = "\x1B[0m";
|
|
4810
|
+
const BOLD$4 = "\x1B[1m";
|
|
4811
|
+
const DIM$4 = "\x1B[38;5;102m";
|
|
4812
|
+
const TEXT$3 = "\x1B[38;5;145m";
|
|
4763
4813
|
const CYAN$1 = "\x1B[36m";
|
|
4764
4814
|
const GREEN = "\x1B[32m";
|
|
4765
4815
|
const MAGENTA = "\x1B[35m";
|
|
@@ -4850,10 +4900,10 @@ const MOVE_UP = (n) => `\x1b[${n}A`;
|
|
|
4850
4900
|
const MOVE_TO_COL = (n) => `\x1b[${n}G`;
|
|
4851
4901
|
function rowBadge(skill) {
|
|
4852
4902
|
const parts = [];
|
|
4853
|
-
if (skill.category) parts.push(`${CYAN$1}${skill.category}${RESET$
|
|
4854
|
-
if (skill.installed) parts.push(`${GREEN}installed${RESET$
|
|
4903
|
+
if (skill.category) parts.push(`${CYAN$1}${skill.category}${RESET$4}`);
|
|
4904
|
+
if (skill.installed) parts.push(`${GREEN}installed${RESET$4}`);
|
|
4855
4905
|
if (parts.length === 0) return "";
|
|
4856
|
-
return ` ${DIM$
|
|
4906
|
+
return ` ${DIM$4}[${RESET$4}${parts.join(`${DIM$4} · ${RESET$4}`)}${DIM$4}]${RESET$4}`;
|
|
4857
4907
|
}
|
|
4858
4908
|
async function runSearchPrompt(kind, alreadyPicked = /* @__PURE__ */ new Set(), initialQuery = "") {
|
|
4859
4909
|
let results = [];
|
|
@@ -4871,33 +4921,33 @@ async function runSearchPrompt(kind, alreadyPicked = /* @__PURE__ */ new Set(),
|
|
|
4871
4921
|
if (lastRenderedLines > 0) process.stdout.write(MOVE_UP(lastRenderedLines) + MOVE_TO_COL(1));
|
|
4872
4922
|
process.stdout.write(CLEAR_DOWN);
|
|
4873
4923
|
const lines = [];
|
|
4874
|
-
const cursor = `${BOLD$
|
|
4875
|
-
lines.push(`${TEXT$
|
|
4924
|
+
const cursor = `${BOLD$4}_${RESET$4}`;
|
|
4925
|
+
lines.push(`${TEXT$3}Search ${noun}:${RESET$4} ${query}${cursor}`);
|
|
4876
4926
|
lines.push("");
|
|
4877
|
-
if (loading && results.length === 0) lines.push(`${DIM$
|
|
4878
|
-
else if (results.length === 0) lines.push(`${DIM$
|
|
4927
|
+
if (loading && results.length === 0) lines.push(`${DIM$4}Loading catalog…${RESET$4}`);
|
|
4928
|
+
else if (results.length === 0) lines.push(`${DIM$4}${query ? `No ${noun} match "${query}"` : `No ${noun} in catalog`}${RESET$4}`);
|
|
4879
4929
|
else {
|
|
4880
4930
|
const maxVisible = 12;
|
|
4881
4931
|
const start = Math.max(0, Math.min(selectedIndex - maxVisible + 1, results.length - maxVisible));
|
|
4882
4932
|
const from = Math.max(0, start);
|
|
4883
4933
|
const visible = results.slice(from, from + maxVisible);
|
|
4884
|
-
if (from > 0) lines.push(` ${DIM$
|
|
4934
|
+
if (from > 0) lines.push(` ${DIM$4}↑ ${from} more${RESET$4}`);
|
|
4885
4935
|
for (let i = 0; i < visible.length; i++) {
|
|
4886
4936
|
const idx = from + i;
|
|
4887
4937
|
const skill = visible[i];
|
|
4888
4938
|
const isSelected = idx === selectedIndex;
|
|
4889
|
-
const arrow = isSelected ? `${BOLD$
|
|
4890
|
-
const box = picked.has(skill.name) ? `${GREEN}${CHECKED}${RESET$
|
|
4891
|
-
const name = isSelected ? `${BOLD$
|
|
4892
|
-
const loadingIndicator = loading && idx === 0 ? ` ${DIM$
|
|
4939
|
+
const arrow = isSelected ? `${BOLD$4}>${RESET$4}` : " ";
|
|
4940
|
+
const box = picked.has(skill.name) ? `${GREEN}${CHECKED}${RESET$4}` : `${DIM$4}${UNCHECKED}${RESET$4}`;
|
|
4941
|
+
const name = isSelected ? `${BOLD$4}${skill.name}${RESET$4}` : `${TEXT$3}${skill.name}${RESET$4}`;
|
|
4942
|
+
const loadingIndicator = loading && idx === 0 ? ` ${DIM$4}...${RESET$4}` : "";
|
|
4893
4943
|
lines.push(` ${arrow} ${box} ${name}${rowBadge(skill)}${loadingIndicator}`);
|
|
4894
4944
|
}
|
|
4895
4945
|
const remaining = results.length - (from + visible.length);
|
|
4896
|
-
if (remaining > 0) lines.push(` ${DIM$
|
|
4946
|
+
if (remaining > 0) lines.push(` ${DIM$4}↓ ${remaining} more${RESET$4}`);
|
|
4897
4947
|
}
|
|
4898
4948
|
lines.push("");
|
|
4899
|
-
const count = picked.size > 0 ? ` ${GREEN}(${picked.size} selected)${RESET$
|
|
4900
|
-
lines.push(`${DIM$
|
|
4949
|
+
const count = picked.size > 0 ? ` ${GREEN}(${picked.size} selected)${RESET$4}` : "";
|
|
4950
|
+
lines.push(`${DIM$4}up/down navigate | space toggle | enter done${count}${DIM$4} | esc back${RESET$4}`);
|
|
4901
4951
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
4902
4952
|
lastRenderedLines = lines.length;
|
|
4903
4953
|
}
|
|
@@ -4982,22 +5032,22 @@ async function runSearchPrompt(kind, alreadyPicked = /* @__PURE__ */ new Set(),
|
|
|
4982
5032
|
async function runFind(args) {
|
|
4983
5033
|
const query = args.join(" ");
|
|
4984
5034
|
const isNonInteractive = !process.stdin.isTTY;
|
|
4985
|
-
const agentTip = `${DIM$
|
|
4986
|
-
${DIM$
|
|
4987
|
-
${DIM$
|
|
5035
|
+
const agentTip = `${DIM$4}Tip: if running in a coding agent, follow these steps:${RESET$4}
|
|
5036
|
+
${DIM$4} 1) npx pruff-skills find [query]${RESET$4}
|
|
5037
|
+
${DIM$4} 2) npx pruff-skills add <skill>${RESET$4}`;
|
|
4988
5038
|
if (query) {
|
|
4989
5039
|
const results = await searchSkillsAPI(query);
|
|
4990
5040
|
String(results.length);
|
|
4991
5041
|
if (results.length === 0) {
|
|
4992
|
-
console.log(`${DIM$
|
|
5042
|
+
console.log(`${DIM$4}Nothing in the catalog matches "${query}"${RESET$4}`);
|
|
4993
5043
|
return;
|
|
4994
5044
|
}
|
|
4995
|
-
console.log(`${DIM$
|
|
5045
|
+
console.log(`${DIM$4}Install a skill with${RESET$4} npx pruff-skills add <name>${DIM$4}, a subagent with${RESET$4} npx pruff-skills agents add <name>${DIM$4}, a rule with${RESET$4} npx pruff-skills rules add <name>`);
|
|
4996
5046
|
console.log();
|
|
4997
5047
|
for (const skill of results.slice(0, 10)) {
|
|
4998
|
-
const kindBadge = skill.kind === "agent" ? ` ${MAGENTA}[subagent]${RESET$
|
|
4999
|
-
console.log(`${TEXT$
|
|
5000
|
-
if (skill.description) console.log(`${DIM$
|
|
5048
|
+
const kindBadge = skill.kind === "agent" ? ` ${MAGENTA}[subagent]${RESET$4}` : skill.kind === "rule" ? ` ${BLUE$1}[rule]${RESET$4}` : ` ${CYAN$1}[skill]${RESET$4}`;
|
|
5049
|
+
console.log(`${TEXT$3}${skill.name}${RESET$4}${kindBadge}${rowBadge(skill)}`);
|
|
5050
|
+
if (skill.description) console.log(`${DIM$4}└ ${skill.description}${RESET$4}`);
|
|
5001
5051
|
console.log();
|
|
5002
5052
|
}
|
|
5003
5053
|
return;
|
|
@@ -5005,7 +5055,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5005
5055
|
if (isNonInteractive || await isRunningInAgent()) {
|
|
5006
5056
|
console.log(agentTip);
|
|
5007
5057
|
console.log();
|
|
5008
|
-
console.log(`${DIM$
|
|
5058
|
+
console.log(`${DIM$4}Usage: npx pruff-skills find <query>${RESET$4}`);
|
|
5009
5059
|
return;
|
|
5010
5060
|
}
|
|
5011
5061
|
const all = await searchSkillsAPI("");
|
|
@@ -5030,7 +5080,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5030
5080
|
const pickedByKind = new Map(KINDS.map((k) => [k.value, /* @__PURE__ */ new Set()]));
|
|
5031
5081
|
const totalPicked = () => [...pickedByKind.values()].reduce((n, set) => n + set.size, 0);
|
|
5032
5082
|
const cancelled = () => {
|
|
5033
|
-
console.log(`${DIM$
|
|
5083
|
+
console.log(`${DIM$4}Search cancelled${RESET$4}`);
|
|
5034
5084
|
console.log();
|
|
5035
5085
|
};
|
|
5036
5086
|
for (;;) {
|
|
@@ -5061,10 +5111,10 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5061
5111
|
for (const kind of KINDS) {
|
|
5062
5112
|
const names = [...pickedByKind.get(kind.value)];
|
|
5063
5113
|
if (names.length === 0) continue;
|
|
5064
|
-
console.log(`${TEXT$
|
|
5114
|
+
console.log(`${TEXT$3}${kind.label}${RESET$4} ${DIM$4}(${names.length})${RESET$4}`);
|
|
5065
5115
|
for (const name of names) {
|
|
5066
|
-
const note = all.find((s) => s.kind === kind.value && s.name === name)?.installed ? ` ${DIM$
|
|
5067
|
-
console.log(` ${DIM$
|
|
5116
|
+
const note = all.find((s) => s.kind === kind.value && s.name === name)?.installed ? ` ${DIM$4}— already installed, will reinstall${RESET$4}` : "";
|
|
5117
|
+
console.log(` ${DIM$4}·${RESET$4} ${name}${note}`);
|
|
5068
5118
|
}
|
|
5069
5119
|
}
|
|
5070
5120
|
console.log();
|
|
@@ -5091,7 +5141,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5091
5141
|
console.log();
|
|
5092
5142
|
if (skills.length > 0) {
|
|
5093
5143
|
const pkg = CATALOG_SOURCE;
|
|
5094
|
-
console.log(`${TEXT$
|
|
5144
|
+
console.log(`${TEXT$3}Installing ${BOLD$4}${skills.join(", ")}${RESET$4} from ${DIM$4}${pkg}${RESET$4}...`);
|
|
5095
5145
|
console.log();
|
|
5096
5146
|
const { source, options } = parseAddOptions([
|
|
5097
5147
|
pkg,
|
|
@@ -5103,7 +5153,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5103
5153
|
if (agents.length > 0) await addAgents(agents, {});
|
|
5104
5154
|
if (rules.length > 0) await addRules(rules, {});
|
|
5105
5155
|
console.log();
|
|
5106
|
-
console.log(`${DIM$
|
|
5156
|
+
console.log(`${DIM$4}Installed.${RESET$4} ${TEXT$3}npx pruff-skills list${RESET$4} ${DIM$4}shows what's installed.${RESET$4}`);
|
|
5107
5157
|
console.log();
|
|
5108
5158
|
}
|
|
5109
5159
|
const isCancelled = (value) => typeof value === "symbol";
|
|
@@ -5452,9 +5502,9 @@ async function runInstallFromLock(args) {
|
|
|
5452
5502
|
}
|
|
5453
5503
|
}
|
|
5454
5504
|
}
|
|
5455
|
-
const RESET$
|
|
5456
|
-
const BOLD$
|
|
5457
|
-
const DIM$
|
|
5505
|
+
const RESET$3 = "\x1B[0m";
|
|
5506
|
+
const BOLD$3 = "\x1B[1m";
|
|
5507
|
+
const DIM$3 = "\x1B[38;5;102m";
|
|
5458
5508
|
const CYAN = "\x1B[36m";
|
|
5459
5509
|
const YELLOW = "\x1B[33m";
|
|
5460
5510
|
const LIST_JSON_SCHEMA_VERSION = 1;
|
|
@@ -5491,8 +5541,8 @@ async function runList(args) {
|
|
|
5491
5541
|
const validAgents = Object.keys(agents);
|
|
5492
5542
|
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
5493
5543
|
if (invalidAgents.length > 0) {
|
|
5494
|
-
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$
|
|
5495
|
-
console.log(`${DIM$
|
|
5544
|
+
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$3}`);
|
|
5545
|
+
console.log(`${DIM$3}Valid agents: ${validAgents.join(", ")}${RESET$3}`);
|
|
5496
5546
|
process.exit(1);
|
|
5497
5547
|
}
|
|
5498
5548
|
agentFilter = options.agent;
|
|
@@ -5518,21 +5568,21 @@ async function runList(args) {
|
|
|
5518
5568
|
const cwd = process.cwd();
|
|
5519
5569
|
const scopeLabel = scope ? "Global" : "Project";
|
|
5520
5570
|
if (installedSkills.length === 0) {
|
|
5521
|
-
console.log(`${DIM$
|
|
5522
|
-
if (scope) console.log(`${DIM$
|
|
5523
|
-
else console.log(`${DIM$
|
|
5571
|
+
console.log(`${DIM$3}No ${scopeLabel.toLowerCase()} skills found.${RESET$3}`);
|
|
5572
|
+
if (scope) console.log(`${DIM$3}Try listing project skills without -g${RESET$3}`);
|
|
5573
|
+
else console.log(`${DIM$3}Try listing global skills with -g${RESET$3}`);
|
|
5524
5574
|
return;
|
|
5525
5575
|
}
|
|
5526
5576
|
function printSkill(skill, indent = false, maxNameLength = 0, maxPathLength = 0) {
|
|
5527
5577
|
const prefix = indent ? " " : "";
|
|
5528
5578
|
const shortPath = shortenPath(skill.canonicalPath, cwd);
|
|
5529
5579
|
const agentNames = skill.agents.map((a) => agents[a].displayName);
|
|
5530
|
-
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$
|
|
5580
|
+
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$3}`;
|
|
5531
5581
|
const paddedName = sanitizeMetadata(skill.name).padEnd(maxNameLength);
|
|
5532
5582
|
const paddedPath = shortPath.padEnd(maxPathLength);
|
|
5533
|
-
console.log(`${prefix}${CYAN}${paddedName}${RESET$
|
|
5583
|
+
console.log(`${prefix}${CYAN}${paddedName}${RESET$3} ${DIM$3}${paddedPath}${RESET$3} ${DIM$3}Agents:${RESET$3} ${agentInfo}`);
|
|
5534
5584
|
}
|
|
5535
|
-
console.log(`${BOLD$
|
|
5585
|
+
console.log(`${BOLD$3}${scopeLabel} Skills${RESET$3}`);
|
|
5536
5586
|
console.log();
|
|
5537
5587
|
const groupedSkills = {};
|
|
5538
5588
|
const ungroupedSkills = [];
|
|
@@ -5548,7 +5598,7 @@ async function runList(args) {
|
|
|
5548
5598
|
const sortedGroups = Object.keys(groupedSkills).sort();
|
|
5549
5599
|
for (const group of sortedGroups) {
|
|
5550
5600
|
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
5551
|
-
console.log(`${BOLD$
|
|
5601
|
+
console.log(`${BOLD$3}${title}${RESET$3}`);
|
|
5552
5602
|
const skills = groupedSkills[group];
|
|
5553
5603
|
if (skills) {
|
|
5554
5604
|
let maxNameLength = 0;
|
|
@@ -5564,7 +5614,7 @@ async function runList(args) {
|
|
|
5564
5614
|
console.log();
|
|
5565
5615
|
}
|
|
5566
5616
|
if (ungroupedSkills.length > 0) {
|
|
5567
|
-
console.log(`${BOLD$
|
|
5617
|
+
console.log(`${BOLD$3}General${RESET$3}`);
|
|
5568
5618
|
let maxNameLength = 0;
|
|
5569
5619
|
let maxPathLength = 0;
|
|
5570
5620
|
for (const skill of ungroupedSkills) {
|
|
@@ -5787,6 +5837,81 @@ function parseRemoveOptions(args) {
|
|
|
5787
5837
|
options
|
|
5788
5838
|
};
|
|
5789
5839
|
}
|
|
5840
|
+
const RESET$2 = "\x1B[0m";
|
|
5841
|
+
const BOLD$2 = "\x1B[1m";
|
|
5842
|
+
const DIM$2 = "\x1B[38;5;102m";
|
|
5843
|
+
const TEXT$2 = "\x1B[38;5;145m";
|
|
5844
|
+
function classifyTrackedSkill(h) {
|
|
5845
|
+
if (h.diskHash === null) return {
|
|
5846
|
+
status: "missing",
|
|
5847
|
+
locallyEdited: false
|
|
5848
|
+
};
|
|
5849
|
+
const divergent = h.diskHash === DIVERGENT_HASH;
|
|
5850
|
+
if (h.catalogHash && h.catalogHash === h.lockHash) {
|
|
5851
|
+
const installed = h.installedHash ?? h.catalogContentHash;
|
|
5852
|
+
return {
|
|
5853
|
+
status: "unchanged",
|
|
5854
|
+
locallyEdited: divergent || h.diskHash !== installed
|
|
5855
|
+
};
|
|
5856
|
+
}
|
|
5857
|
+
if (h.catalogContentHash && h.diskHash === h.catalogContentHash) return {
|
|
5858
|
+
status: "rebaseline",
|
|
5859
|
+
locallyEdited: false
|
|
5860
|
+
};
|
|
5861
|
+
const installed = h.installedHash ?? h.lockHash;
|
|
5862
|
+
return {
|
|
5863
|
+
status: "modified",
|
|
5864
|
+
locallyEdited: divergent || h.diskHash !== installed
|
|
5865
|
+
};
|
|
5866
|
+
}
|
|
5867
|
+
function emptyPlan() {
|
|
5868
|
+
return {
|
|
5869
|
+
modified: [],
|
|
5870
|
+
missing: [],
|
|
5871
|
+
rebaseline: [],
|
|
5872
|
+
unchanged: [],
|
|
5873
|
+
conflicts: [],
|
|
5874
|
+
localEdits: [],
|
|
5875
|
+
deleted: [],
|
|
5876
|
+
available: [],
|
|
5877
|
+
untracked: [],
|
|
5878
|
+
failedSources: []
|
|
5879
|
+
};
|
|
5880
|
+
}
|
|
5881
|
+
function addClassified(plan, entry, c) {
|
|
5882
|
+
if (c.status === "unchanged") {
|
|
5883
|
+
plan.unchanged.push(entry);
|
|
5884
|
+
if (c.locallyEdited) plan.localEdits.push(entry);
|
|
5885
|
+
return;
|
|
5886
|
+
}
|
|
5887
|
+
if (c.status === "modified" && c.locallyEdited) {
|
|
5888
|
+
plan.conflicts.push(entry);
|
|
5889
|
+
return;
|
|
5890
|
+
}
|
|
5891
|
+
plan[c.status].push(entry);
|
|
5892
|
+
}
|
|
5893
|
+
function names(entries) {
|
|
5894
|
+
return entries.map((e) => sanitizeMetadata(e.name)).join(", ");
|
|
5895
|
+
}
|
|
5896
|
+
function line(label, entries, note) {
|
|
5897
|
+
if (entries.length === 0) return;
|
|
5898
|
+
const suffix = note ? ` ${DIM$2}— ${note}${RESET$2}` : "";
|
|
5899
|
+
console.log(` ${TEXT$2}${label} (${entries.length}):${RESET$2} ${names(entries)}${suffix}`);
|
|
5900
|
+
}
|
|
5901
|
+
function printPlan(plan, opts = {}) {
|
|
5902
|
+
console.log(`${BOLD$2}Plan${RESET$2}`);
|
|
5903
|
+
line("Modified upstream", plan.modified, "will update");
|
|
5904
|
+
line("Missing on disk", plan.missing, "will reinstall");
|
|
5905
|
+
line("Modified upstream + edited locally", plan.conflicts, opts.force ? "will overwrite (--force)" : "local edits kept unless you confirm or pass --force");
|
|
5906
|
+
line("Deleted upstream", plan.deleted);
|
|
5907
|
+
line("Already current", plan.rebaseline, "lock hash refreshed, nothing reinstalled");
|
|
5908
|
+
line("New in catalog", plan.available, "not installed");
|
|
5909
|
+
line("Installed but not tracked", plan.untracked, "not in skills-lock.json");
|
|
5910
|
+
line("Edited locally", plan.localEdits, "unchanged upstream, left as is");
|
|
5911
|
+
if (plan.unchanged.length > 0) console.log(` ${DIM$2}Up to date: ${plan.unchanged.length}${RESET$2}`);
|
|
5912
|
+
for (const source of plan.failedSources) console.log(` ${DIM$2}✗ Could not read ${sanitizeMetadata(source)} — its skills were not checked${RESET$2}`);
|
|
5913
|
+
console.log();
|
|
5914
|
+
}
|
|
5790
5915
|
function formatSourceInput(sourceUrl, ref) {
|
|
5791
5916
|
if (!ref) return sourceUrl;
|
|
5792
5917
|
return `${sourceUrl}#${ref}`;
|
|
@@ -5830,6 +5955,8 @@ function parseUpdateOptions(args) {
|
|
|
5830
5955
|
for (const arg of args) if (arg === "-g" || arg === "--global") options.global = true;
|
|
5831
5956
|
else if (arg === "-p" || arg === "--project") options.project = true;
|
|
5832
5957
|
else if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
5958
|
+
else if (arg === "--dry-run") options.dryRun = true;
|
|
5959
|
+
else if (arg === "--force") options.force = true;
|
|
5833
5960
|
else if (!arg.startsWith("-")) positional.push(arg);
|
|
5834
5961
|
if (positional.length > 0) options.skills = positional;
|
|
5835
5962
|
return options;
|
|
@@ -5950,7 +6077,9 @@ async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills
|
|
|
5950
6077
|
console.log();
|
|
5951
6078
|
console.log(`${DIM$1}Warning:${RESET$1} The following skills from ${DIM$1}${source}${RESET$1} appear to have been deleted upstream:`);
|
|
5952
6079
|
for (const s of deletedSkills) console.log(` ${DIM$1}•${RESET$1} ${s}`);
|
|
5953
|
-
|
|
6080
|
+
const isNonInteractive = options.yes || !process.stdin.isTTY;
|
|
6081
|
+
if (options.dryRun) console.log(`${DIM$1}Dry run — nothing removed.${RESET$1}`);
|
|
6082
|
+
else if (isNonInteractive) console.log(`${DIM$1}Skipping deletion in non-interactive mode.${RESET$1}`);
|
|
5954
6083
|
else {
|
|
5955
6084
|
const confirmed = await ye({ message: `Would you like to remove the local copies of these deleted skills?` });
|
|
5956
6085
|
if (confirmed && !pD(confirmed)) for (const s of deletedSkills) {
|
|
@@ -6086,6 +6215,16 @@ async function updateGlobalSkills(options = {}) {
|
|
|
6086
6215
|
}
|
|
6087
6216
|
console.log(`${TEXT$1}Found ${updates.length} global update(s)${RESET$1}`);
|
|
6088
6217
|
console.log();
|
|
6218
|
+
if (options.dryRun) {
|
|
6219
|
+
for (const update of updates) console.log(` ${TEXT$1}•${RESET$1} ${sanitizeMetadata(update.name)} ${DIM$1}would update${RESET$1}`);
|
|
6220
|
+
printSkippedSkills(skipped);
|
|
6221
|
+
console.log(`${DIM$1}Dry run — nothing was changed.${RESET$1}`);
|
|
6222
|
+
return {
|
|
6223
|
+
successCount,
|
|
6224
|
+
failCount,
|
|
6225
|
+
checkedCount
|
|
6226
|
+
};
|
|
6227
|
+
}
|
|
6089
6228
|
for (const update of updates) {
|
|
6090
6229
|
const safeName = sanitizeMetadata(update.name);
|
|
6091
6230
|
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
@@ -6125,36 +6264,7 @@ async function updateGlobalSkills(options = {}) {
|
|
|
6125
6264
|
checkedCount
|
|
6126
6265
|
};
|
|
6127
6266
|
}
|
|
6128
|
-
|
|
6129
|
-
const projectSkills = await getProjectSkillsForUpdate(options.skills);
|
|
6130
|
-
let successCount = 0;
|
|
6131
|
-
let failCount = 0;
|
|
6132
|
-
let upToDateCount = 0;
|
|
6133
|
-
if (projectSkills.length === 0) {
|
|
6134
|
-
if (!options.skills) {
|
|
6135
|
-
console.log(`${DIM$1}No project skills to update.${RESET$1}`);
|
|
6136
|
-
console.log(`${DIM$1}Install project skills with${RESET$1} ${TEXT$1}npx pruff-skills add <skill>${RESET$1}`);
|
|
6137
|
-
}
|
|
6138
|
-
return {
|
|
6139
|
-
successCount,
|
|
6140
|
-
failCount,
|
|
6141
|
-
foundCount: 0,
|
|
6142
|
-
upToDateCount
|
|
6143
|
-
};
|
|
6144
|
-
}
|
|
6145
|
-
const updatable = projectSkills.filter((s) => s.entry.skillPath);
|
|
6146
|
-
const legacy = projectSkills.filter((s) => !s.entry.skillPath);
|
|
6147
|
-
if (updatable.length === 0) {
|
|
6148
|
-
console.log(`${DIM$1}No project skills can be updated in place.${RESET$1}`);
|
|
6149
|
-
printLegacyProjectSkills(legacy);
|
|
6150
|
-
return {
|
|
6151
|
-
successCount,
|
|
6152
|
-
failCount,
|
|
6153
|
-
foundCount: projectSkills.length,
|
|
6154
|
-
upToDateCount
|
|
6155
|
-
};
|
|
6156
|
-
}
|
|
6157
|
-
const cwd = process.cwd();
|
|
6267
|
+
function printUpdateTargets(cwd) {
|
|
6158
6268
|
const targetAgentNames = [];
|
|
6159
6269
|
let hasUniversal = false;
|
|
6160
6270
|
for (const [type, config] of Object.entries(agents)) if (isUniversalAgent(type)) {
|
|
@@ -6167,95 +6277,253 @@ async function updateProjectSkills(options = {}) {
|
|
|
6167
6277
|
if (hasUniversal) targetParts.push("Universal");
|
|
6168
6278
|
targetParts.push(...targetAgentNames);
|
|
6169
6279
|
if (targetParts.length > 0) console.log(`${TEXT$1}Updating for: ${targetParts.join(", ")}${RESET$1}`);
|
|
6170
|
-
|
|
6171
|
-
|
|
6280
|
+
}
|
|
6281
|
+
async function safeHash(hash, dir) {
|
|
6282
|
+
try {
|
|
6283
|
+
return await hash(dir) || "";
|
|
6284
|
+
} catch {
|
|
6285
|
+
return "";
|
|
6286
|
+
}
|
|
6287
|
+
}
|
|
6288
|
+
async function buildProjectPlan(updatable, localLock, cwd, skillFilter) {
|
|
6289
|
+
const plan = emptyPlan();
|
|
6290
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
6172
6291
|
const bySource = /* @__PURE__ */ new Map();
|
|
6173
6292
|
for (const skill of updatable) {
|
|
6174
|
-
const
|
|
6175
|
-
const existing = bySource.get(source) || [];
|
|
6293
|
+
const existing = bySource.get(skill.entry.source) || [];
|
|
6176
6294
|
existing.push(skill);
|
|
6177
|
-
bySource.set(source, existing);
|
|
6178
|
-
}
|
|
6179
|
-
const localLock = await readLocalLock();
|
|
6180
|
-
const cliEntry = join(__dirname$2, "..", "bin", "cli.mjs");
|
|
6181
|
-
if (!existsSync(cliEntry)) {
|
|
6182
|
-
console.log(`${DIM$1}✗ CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
6183
|
-
return {
|
|
6184
|
-
successCount,
|
|
6185
|
-
failCount: updatable.length,
|
|
6186
|
-
foundCount: projectSkills.length,
|
|
6187
|
-
upToDateCount
|
|
6188
|
-
};
|
|
6295
|
+
bySource.set(skill.entry.source, existing);
|
|
6189
6296
|
}
|
|
6190
6297
|
for (const [source, skillsForSource] of bySource) {
|
|
6191
6298
|
const firstEntry = skillsForSource[0].entry;
|
|
6192
6299
|
const parsedSource = parseSource(firstEntry.source);
|
|
6193
|
-
const sourceUrl = parsedSource.url;
|
|
6194
6300
|
const ref = firstEntry.ref ?? parsedSource.ref;
|
|
6195
|
-
const allLockedForSource = Object.entries(localLock.skills).filter(([_, entry]) => entry.source === source).map(([name, _]) => name);
|
|
6196
6301
|
let tempDir = null;
|
|
6197
|
-
let deletedSkills = [];
|
|
6198
|
-
const unchangedSkills = /* @__PURE__ */ new Set();
|
|
6199
6302
|
try {
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredPaths);
|
|
6303
|
+
setStep(`update:check:${source}`);
|
|
6304
|
+
tempDir = await cloneRepo$1(parsedSource.url, ref);
|
|
6305
|
+
const discovered = await discoverSkills(tempDir);
|
|
6306
|
+
const byPath = new Map(discovered.map((s) => [join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/"), s]));
|
|
6205
6307
|
for (const skill of skillsForSource) {
|
|
6206
|
-
const
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6308
|
+
const entry = {
|
|
6309
|
+
name: skill.name,
|
|
6310
|
+
source
|
|
6311
|
+
};
|
|
6312
|
+
const found = byPath.get(skill.entry.skillPath);
|
|
6313
|
+
if (!found) {
|
|
6314
|
+
plan.deleted.push(entry);
|
|
6315
|
+
continue;
|
|
6316
|
+
}
|
|
6317
|
+
const [catalogHash, catalogContentHash, diskHash] = await Promise.all([
|
|
6318
|
+
safeHash(computeSkillFolderHash, found.path),
|
|
6319
|
+
safeHash(computeSkillContentHash, found.path),
|
|
6320
|
+
hashInstalledSkill(skill.name, cwd)
|
|
6321
|
+
]);
|
|
6322
|
+
hashes.set(skill.name, {
|
|
6323
|
+
catalogHash,
|
|
6324
|
+
catalogContentHash,
|
|
6325
|
+
diskHash
|
|
6326
|
+
});
|
|
6327
|
+
addClassified(plan, entry, classifyTrackedSkill({
|
|
6328
|
+
lockHash: skill.entry.computedHash,
|
|
6329
|
+
installedHash: skill.entry.installedHash,
|
|
6330
|
+
catalogHash,
|
|
6331
|
+
catalogContentHash,
|
|
6332
|
+
diskHash
|
|
6333
|
+
}));
|
|
6334
|
+
}
|
|
6335
|
+
for (const skill of discovered) {
|
|
6336
|
+
if (skill.name in localLock.skills) continue;
|
|
6337
|
+
if (!matchesSkillFilter(skill.name, skillFilter)) continue;
|
|
6338
|
+
const entry = {
|
|
6339
|
+
name: skill.name,
|
|
6340
|
+
source
|
|
6341
|
+
};
|
|
6342
|
+
if (findInstalledSkillDirs(skill.name, cwd).length > 0) plan.untracked.push(entry);
|
|
6343
|
+
else plan.available.push(entry);
|
|
6210
6344
|
}
|
|
6211
6345
|
} catch (error) {
|
|
6212
6346
|
const reason = error instanceof Error ? error.message : String(error);
|
|
6213
|
-
console.log(`${DIM$1}✗ Failed to
|
|
6347
|
+
console.log(`${DIM$1}✗ Failed to read ${source}: ${sanitizeMetadata(reason)}${RESET$1}`);
|
|
6348
|
+
plan.failedSources.push(source);
|
|
6214
6349
|
} finally {
|
|
6215
6350
|
if (tempDir) await cleanupTempDir(tempDir);
|
|
6216
6351
|
}
|
|
6217
|
-
const remainingSkills = skillsForSource.filter((s) => !deletedSkills.includes(s.name));
|
|
6218
|
-
for (const skill of remainingSkills) {
|
|
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
|
-
}
|
|
6225
|
-
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
6226
|
-
const installUrl = formatSourceInput(skill.entry.source, skill.entry.ref);
|
|
6227
|
-
if (spawnSync(process.execPath, [
|
|
6228
|
-
cliEntry,
|
|
6229
|
-
"add",
|
|
6230
|
-
installUrl,
|
|
6231
|
-
"--skill",
|
|
6232
|
-
skill.name,
|
|
6233
|
-
"-y"
|
|
6234
|
-
], {
|
|
6235
|
-
stdio: [
|
|
6236
|
-
"inherit",
|
|
6237
|
-
"pipe",
|
|
6238
|
-
"pipe"
|
|
6239
|
-
],
|
|
6240
|
-
encoding: "utf-8",
|
|
6241
|
-
shell: process.platform === "win32"
|
|
6242
|
-
}).status === 0) {
|
|
6243
|
-
successCount++;
|
|
6244
|
-
console.log(` ${TEXT$1}✓${RESET$1} Updated ${safeName}`);
|
|
6245
|
-
} else {
|
|
6246
|
-
failCount++;
|
|
6247
|
-
console.log(` ${DIM$1}✗ Failed to update ${safeName}${RESET$1}`);
|
|
6248
|
-
}
|
|
6249
|
-
}
|
|
6250
6352
|
}
|
|
6251
|
-
printLegacyProjectSkills(legacy);
|
|
6252
6353
|
return {
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
foundCount: projectSkills.length,
|
|
6256
|
-
upToDateCount
|
|
6354
|
+
plan,
|
|
6355
|
+
hashes
|
|
6257
6356
|
};
|
|
6258
6357
|
}
|
|
6358
|
+
async function selectEntries(message, entries, hint) {
|
|
6359
|
+
if (entries.length === 0) return [];
|
|
6360
|
+
const picked = await searchMultiselect({
|
|
6361
|
+
message,
|
|
6362
|
+
items: entries.map((e) => ({
|
|
6363
|
+
value: e,
|
|
6364
|
+
label: e.name,
|
|
6365
|
+
hint: hint(e)
|
|
6366
|
+
}))
|
|
6367
|
+
});
|
|
6368
|
+
if (typeof picked === "symbol") return [];
|
|
6369
|
+
return picked;
|
|
6370
|
+
}
|
|
6371
|
+
async function applyDeletions(deleted, interactive, cwd) {
|
|
6372
|
+
if (deleted.length === 0) return;
|
|
6373
|
+
if (!interactive) {
|
|
6374
|
+
console.log(`${DIM$1}Deleted-upstream skills kept (non-interactive).${RESET$1}`);
|
|
6375
|
+
return;
|
|
6376
|
+
}
|
|
6377
|
+
const confirmed = await ye({ message: `Remove the local copies of ${deleted.length} skill(s) deleted upstream?` });
|
|
6378
|
+
if (!confirmed || pD(confirmed)) return;
|
|
6379
|
+
for (const { name } of deleted) {
|
|
6380
|
+
if (findInstalledSkillDirs(name, cwd).length > 0) {
|
|
6381
|
+
console.log(`${DIM$1}Removing${RESET$1} ${sanitizeMetadata(name)}...`);
|
|
6382
|
+
await removeCommand([name], {
|
|
6383
|
+
yes: true,
|
|
6384
|
+
global: false
|
|
6385
|
+
});
|
|
6386
|
+
}
|
|
6387
|
+
await removeSkillFromLocalLock(name, cwd);
|
|
6388
|
+
}
|
|
6389
|
+
}
|
|
6390
|
+
async function refreshLockHashes(plan, hashes, cwd) {
|
|
6391
|
+
const lock = await readLocalLock(cwd);
|
|
6392
|
+
const edited = new Set(plan.localEdits.map((e) => e.name));
|
|
6393
|
+
let changed = false;
|
|
6394
|
+
for (const { name } of plan.rebaseline) {
|
|
6395
|
+
const h = hashes.get(name);
|
|
6396
|
+
const entry = lock.skills[name];
|
|
6397
|
+
if (!h || !entry || !h.diskHash) continue;
|
|
6398
|
+
lock.skills[name] = {
|
|
6399
|
+
...entry,
|
|
6400
|
+
computedHash: h.catalogHash,
|
|
6401
|
+
installedHash: h.diskHash
|
|
6402
|
+
};
|
|
6403
|
+
changed = true;
|
|
6404
|
+
}
|
|
6405
|
+
for (const { name } of plan.unchanged) {
|
|
6406
|
+
const h = hashes.get(name);
|
|
6407
|
+
const entry = lock.skills[name];
|
|
6408
|
+
if (!h || !entry || entry.installedHash || edited.has(name) || !h.diskHash) continue;
|
|
6409
|
+
lock.skills[name] = {
|
|
6410
|
+
...entry,
|
|
6411
|
+
installedHash: h.diskHash
|
|
6412
|
+
};
|
|
6413
|
+
changed = true;
|
|
6414
|
+
}
|
|
6415
|
+
if (changed) await writeLocalLock(lock, cwd);
|
|
6416
|
+
}
|
|
6417
|
+
function installFromSource(cliEntry, installUrl, skillNames) {
|
|
6418
|
+
return spawnSync(process.execPath, [
|
|
6419
|
+
cliEntry,
|
|
6420
|
+
"add",
|
|
6421
|
+
installUrl,
|
|
6422
|
+
"--skill",
|
|
6423
|
+
...skillNames,
|
|
6424
|
+
"-y"
|
|
6425
|
+
], {
|
|
6426
|
+
stdio: [
|
|
6427
|
+
"inherit",
|
|
6428
|
+
"pipe",
|
|
6429
|
+
"pipe"
|
|
6430
|
+
],
|
|
6431
|
+
encoding: "utf-8",
|
|
6432
|
+
shell: process.platform === "win32"
|
|
6433
|
+
}).status === 0;
|
|
6434
|
+
}
|
|
6435
|
+
async function updateProjectSkills(options = {}) {
|
|
6436
|
+
const result = {
|
|
6437
|
+
successCount: 0,
|
|
6438
|
+
failCount: 0,
|
|
6439
|
+
foundCount: 0,
|
|
6440
|
+
upToDateCount: 0,
|
|
6441
|
+
installedCount: 0,
|
|
6442
|
+
rebaselinedCount: 0
|
|
6443
|
+
};
|
|
6444
|
+
const projectSkills = await getProjectSkillsForUpdate(options.skills);
|
|
6445
|
+
result.foundCount = projectSkills.length;
|
|
6446
|
+
if (projectSkills.length === 0) {
|
|
6447
|
+
if (!options.skills) {
|
|
6448
|
+
console.log(`${DIM$1}No project skills to update.${RESET$1}`);
|
|
6449
|
+
console.log(`${DIM$1}Install project skills with${RESET$1} ${TEXT$1}npx pruff-skills add <skill>${RESET$1}`);
|
|
6450
|
+
}
|
|
6451
|
+
return result;
|
|
6452
|
+
}
|
|
6453
|
+
const updatable = projectSkills.filter((s) => s.entry.skillPath);
|
|
6454
|
+
const legacy = projectSkills.filter((s) => !s.entry.skillPath);
|
|
6455
|
+
if (updatable.length === 0) {
|
|
6456
|
+
console.log(`${DIM$1}No project skills can be updated in place.${RESET$1}`);
|
|
6457
|
+
printLegacyProjectSkills(legacy);
|
|
6458
|
+
return result;
|
|
6459
|
+
}
|
|
6460
|
+
const cwd = process.cwd();
|
|
6461
|
+
printUpdateTargets(cwd);
|
|
6462
|
+
console.log(`${TEXT$1}Comparing ${updatable.length} skill(s) with their sources...${RESET$1}`);
|
|
6463
|
+
console.log();
|
|
6464
|
+
const { plan, hashes } = await buildProjectPlan(updatable, await readLocalLock(cwd), cwd, options.skills);
|
|
6465
|
+
printPlan(plan, { force: options.force });
|
|
6466
|
+
printLegacyProjectSkills(legacy);
|
|
6467
|
+
result.upToDateCount = plan.unchanged.length;
|
|
6468
|
+
if (options.dryRun) {
|
|
6469
|
+
console.log(`${DIM$1}Dry run — nothing was changed.${RESET$1}`);
|
|
6470
|
+
return result;
|
|
6471
|
+
}
|
|
6472
|
+
const interactive = !options.yes && process.stdin.isTTY === true;
|
|
6473
|
+
await applyDeletions(plan.deleted, interactive, cwd);
|
|
6474
|
+
await refreshLockHashes(plan, hashes, cwd);
|
|
6475
|
+
result.rebaselinedCount = plan.rebaseline.length;
|
|
6476
|
+
result.upToDateCount += plan.rebaseline.length;
|
|
6477
|
+
let overwrite = [];
|
|
6478
|
+
if (options.force) overwrite = plan.conflicts;
|
|
6479
|
+
else if (interactive) overwrite = await selectEntries("Changed upstream and edited locally. Overwrite which? (none selected keeps your edits)", plan.conflicts, () => "local edits will be lost");
|
|
6480
|
+
let toInstall = [];
|
|
6481
|
+
if (interactive) toInstall = await selectEntries("Install new skills from your sources?", [...plan.available, ...plan.untracked], (e) => plan.untracked.includes(e) ? "already on disk, untracked — overwrites it" : e.source);
|
|
6482
|
+
else if (plan.available.length > 0) console.log(`${DIM$1}To install new skills: ${TEXT$1}npx pruff-skills add <source> --skill <name>${RESET$1}`);
|
|
6483
|
+
const toUpdate = [
|
|
6484
|
+
...plan.modified,
|
|
6485
|
+
...plan.missing,
|
|
6486
|
+
...overwrite
|
|
6487
|
+
];
|
|
6488
|
+
if (toUpdate.length === 0 && toInstall.length === 0) return result;
|
|
6489
|
+
const cliEntry = join(__dirname$2, "..", "bin", "cli.mjs");
|
|
6490
|
+
if (!existsSync(cliEntry)) {
|
|
6491
|
+
console.log(`${DIM$1}✗ CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
6492
|
+
result.failCount += toUpdate.length + toInstall.length;
|
|
6493
|
+
return result;
|
|
6494
|
+
}
|
|
6495
|
+
const refBySource = new Map(updatable.map((s) => [s.entry.source, s.entry.ref]));
|
|
6496
|
+
const batches = /* @__PURE__ */ new Map();
|
|
6497
|
+
for (const e of [...toUpdate, ...toInstall]) {
|
|
6498
|
+
const batch = batches.get(e.source) || [];
|
|
6499
|
+
batch.push(e);
|
|
6500
|
+
batches.set(e.source, batch);
|
|
6501
|
+
}
|
|
6502
|
+
for (const [source, entries] of batches) {
|
|
6503
|
+
const label = entries.map((e) => sanitizeMetadata(e.name)).join(", ");
|
|
6504
|
+
console.log(`${TEXT$1}Installing from ${sanitizeMetadata(source)}: ${label}${RESET$1}`);
|
|
6505
|
+
setStep(`update:install:${source}`);
|
|
6506
|
+
installFromSource(cliEntry, formatSourceInput(source, refBySource.get(source)), entries.map((e) => e.name));
|
|
6507
|
+
}
|
|
6508
|
+
const after = await readLocalLock(cwd);
|
|
6509
|
+
const isNew = new Set(toInstall.map((e) => e.name));
|
|
6510
|
+
for (const e of [...toUpdate, ...toInstall]) {
|
|
6511
|
+
const safeName = sanitizeMetadata(e.name);
|
|
6512
|
+
const entry = after.skills[e.name];
|
|
6513
|
+
const expected = hashes.get(e.name)?.catalogHash;
|
|
6514
|
+
if (!(isNew.has(e.name) ? Boolean(entry) : Boolean(entry && (!expected || entry.computedHash === expected)))) {
|
|
6515
|
+
result.failCount++;
|
|
6516
|
+
console.log(` ${DIM$1}✗ Failed to ${isNew.has(e.name) ? "install" : "update"} ${safeName}${RESET$1}`);
|
|
6517
|
+
} else if (isNew.has(e.name)) {
|
|
6518
|
+
result.installedCount++;
|
|
6519
|
+
console.log(` ${TEXT$1}✓${RESET$1} Installed ${safeName}`);
|
|
6520
|
+
} else {
|
|
6521
|
+
result.successCount++;
|
|
6522
|
+
console.log(` ${TEXT$1}✓${RESET$1} Updated ${safeName}`);
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6525
|
+
return result;
|
|
6526
|
+
}
|
|
6259
6527
|
function printLegacyProjectSkills(legacy) {
|
|
6260
6528
|
if (legacy.length === 0) return;
|
|
6261
6529
|
console.log();
|
|
@@ -6269,14 +6537,18 @@ function printLegacyProjectSkills(legacy) {
|
|
|
6269
6537
|
async function syncAgentsForScope(isGlobal, options) {
|
|
6270
6538
|
const result = await updateAgents({
|
|
6271
6539
|
global: isGlobal,
|
|
6272
|
-
names: options.skills
|
|
6540
|
+
names: options.skills,
|
|
6541
|
+
dryRun: options.dryRun
|
|
6273
6542
|
});
|
|
6274
6543
|
const label = isGlobal ? "global" : "project";
|
|
6275
6544
|
if (result.catalogUnavailable) {
|
|
6276
6545
|
console.log(` ${DIM$1}✗ Could not read the subagent catalog — skipped ${label} subagents${RESET$1}`);
|
|
6277
6546
|
return result;
|
|
6278
6547
|
}
|
|
6279
|
-
for (const name of result.updated)
|
|
6548
|
+
for (const name of result.updated) {
|
|
6549
|
+
const verb = options.dryRun ? "Would sync" : "Synced";
|
|
6550
|
+
console.log(` ${TEXT$1}✓${RESET$1} ${verb} ${sanitizeMetadata(name)}`);
|
|
6551
|
+
}
|
|
6280
6552
|
for (const name of result.unchanged) console.log(` ${TEXT$1}✓${RESET$1} ${sanitizeMetadata(name)} ${DIM$1}is up to date${RESET$1}`);
|
|
6281
6553
|
for (const name of result.failed) console.log(` ${DIM$1}✗ Failed to sync ${sanitizeMetadata(name)}${RESET$1}`);
|
|
6282
6554
|
if (result.localOnly.length > 0) {
|
|
@@ -6301,6 +6573,8 @@ async function runUpdate(args = []) {
|
|
|
6301
6573
|
let totalFail = 0;
|
|
6302
6574
|
let totalFound = 0;
|
|
6303
6575
|
let totalUpToDate = 0;
|
|
6576
|
+
let totalInstalled = 0;
|
|
6577
|
+
let totalRebaselined = 0;
|
|
6304
6578
|
if (scope === "global" || scope === "both") {
|
|
6305
6579
|
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Global Skills${RESET$1}`);
|
|
6306
6580
|
setStep("update:global-skills");
|
|
@@ -6313,15 +6587,17 @@ async function runUpdate(args = []) {
|
|
|
6313
6587
|
if (scope === "project" || scope === "both") {
|
|
6314
6588
|
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Project Skills${RESET$1}`);
|
|
6315
6589
|
setStep("update:project-skills");
|
|
6316
|
-
const { successCount, failCount, foundCount, upToDateCount } = await updateProjectSkills(options);
|
|
6590
|
+
const { successCount, failCount, foundCount, upToDateCount, installedCount, rebaselinedCount } = await updateProjectSkills(options);
|
|
6317
6591
|
totalUpToDate += upToDateCount;
|
|
6592
|
+
totalInstalled += installedCount;
|
|
6593
|
+
totalRebaselined += rebaselinedCount;
|
|
6318
6594
|
totalSuccess += successCount;
|
|
6319
6595
|
totalFail += failCount;
|
|
6320
6596
|
totalFound += foundCount;
|
|
6321
6597
|
}
|
|
6322
6598
|
console.log();
|
|
6323
6599
|
console.log(`${BOLD$1}Subagents${RESET$1}`);
|
|
6324
|
-
console.log(`${DIM$1}ALL installed subagents will be updated and synced with the repo — local edits are overwritten.${RESET$1}`);
|
|
6600
|
+
console.log(options.dryRun ? `${DIM$1}Dry run — subagents are compared, not written.${RESET$1}` : `${DIM$1}ALL installed subagents will be updated and synced with the repo — local edits are overwritten.${RESET$1}`);
|
|
6325
6601
|
setStep("update:sync-agents");
|
|
6326
6602
|
const agentScopes = scope === "both" ? [true, false] : scope === "global" ? [true] : [false];
|
|
6327
6603
|
let agentsSynced = 0;
|
|
@@ -6340,8 +6616,13 @@ async function runUpdate(args = []) {
|
|
|
6340
6616
|
totalUpToDate += agentsUnchanged;
|
|
6341
6617
|
if (options.skills && totalFound === 0) console.log(`${DIM$1}No installed skills found matching: ${options.skills.join(", ")}${RESET$1}`);
|
|
6342
6618
|
console.log();
|
|
6343
|
-
|
|
6344
|
-
|
|
6619
|
+
const parts = [];
|
|
6620
|
+
if (totalSuccess > 0) parts.push(`${totalSuccess} ${options.dryRun ? "to update" : "updated"}`);
|
|
6621
|
+
if (totalInstalled > 0) parts.push(`${totalInstalled} installed`);
|
|
6622
|
+
if (totalRebaselined > 0) parts.push(`${totalRebaselined} already current (lock refreshed)`);
|
|
6623
|
+
if (totalUpToDate - totalRebaselined > 0) parts.push(`${totalUpToDate - totalRebaselined} up to date`);
|
|
6624
|
+
if (totalSuccess + totalInstalled === 0 && totalFail === 0 && totalUpToDate > 0) console.log(`${TEXT$1}✓ Everything is up to date${RESET$1} ${DIM$1}(${parts.join(" · ")})${RESET$1}`);
|
|
6625
|
+
else if (parts.length > 0) console.log(`${TEXT$1}✓ ${parts.join(" · ")}${RESET$1}`);
|
|
6345
6626
|
if (totalFail > 0) {
|
|
6346
6627
|
recordFailure(`Failed to update ${totalFail} item(s)`);
|
|
6347
6628
|
console.log(`${DIM$1}Failed to update ${totalFail} item(s)${RESET$1}`);
|
|
@@ -10236,14 +10517,17 @@ ${BOLD}Troubleshooting:${RESET}
|
|
|
10236
10517
|
add, update, rules add, agents add or onboarding fails.
|
|
10237
10518
|
|
|
10238
10519
|
${BOLD}Updates:${RESET}
|
|
10239
|
-
update [skills...]
|
|
10240
|
-
|
|
10241
|
-
|
|
10520
|
+
update [skills...] Compare installed skills with their source, print a plan
|
|
10521
|
+
(modified / new / deleted / up to date) and apply only what
|
|
10522
|
+
changed (alias: upgrade). Also syncs ALL installed subagents
|
|
10523
|
+
with the catalog, overwriting local edits.
|
|
10242
10524
|
|
|
10243
10525
|
${BOLD}Update Options:${RESET}
|
|
10244
10526
|
-g, --global Update global skills only
|
|
10245
10527
|
-p, --project Update project skills only
|
|
10246
10528
|
-y, --yes Skip scope prompt (auto-detect: project if in a project, else global)
|
|
10529
|
+
--dry-run Print the plan and change nothing
|
|
10530
|
+
--force Overwrite skills edited locally that also changed upstream
|
|
10247
10531
|
|
|
10248
10532
|
${BOLD}Project:${RESET}
|
|
10249
10533
|
restore Restore skills from skills-lock.json
|
|
@@ -10307,6 +10591,7 @@ ${BOLD}Examples:${RESET}
|
|
|
10307
10591
|
${DIM}$${RESET} pruff-skills update
|
|
10308
10592
|
${DIM}$${RESET} pruff-skills update <skill> ${DIM}# update a single skill${RESET}
|
|
10309
10593
|
${DIM}$${RESET} pruff-skills update -g ${DIM}# update global skills only${RESET}
|
|
10594
|
+
${DIM}$${RESET} pruff-skills update --dry-run ${DIM}# show what would change${RESET}
|
|
10310
10595
|
${DIM}$${RESET} pruff-skills restore ${DIM}# restore from skills-lock.json${RESET}
|
|
10311
10596
|
${DIM}$${RESET} pruff-skills init my-skill
|
|
10312
10597
|
${DIM}$${RESET} pruff-skills sync ${DIM}# sync from node_modules${RESET}
|