pruff-skills 1.10.0 → 1.12.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 +2 -1
- package/dist/cli.mjs +450 -171
- 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";
|
|
@@ -1041,6 +1041,14 @@ async function getPluginGroupings(basePath) {
|
|
|
1041
1041
|
} catch {}
|
|
1042
1042
|
return groupings;
|
|
1043
1043
|
}
|
|
1044
|
+
const AGENTS_DIR$1 = ".agents";
|
|
1045
|
+
const SKILLS_SUBDIR = "skills";
|
|
1046
|
+
const INSTALL_EXCLUDED_FILES = new Set(["metadata.json"]);
|
|
1047
|
+
const INSTALL_EXCLUDED_DIRS = new Set([
|
|
1048
|
+
".git",
|
|
1049
|
+
"__pycache__",
|
|
1050
|
+
"__pypackages__"
|
|
1051
|
+
]);
|
|
1044
1052
|
const LOCAL_LOCK_FILE = "skills-lock.json";
|
|
1045
1053
|
const CURRENT_VERSION$1 = 1;
|
|
1046
1054
|
function getLocalLockPath(cwd) {
|
|
@@ -1077,8 +1085,14 @@ async function writeLocalLock(lock, cwd) {
|
|
|
1077
1085
|
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
1078
1086
|
}
|
|
1079
1087
|
async function computeSkillFolderHash(skillDir) {
|
|
1088
|
+
return hashSkillFiles(skillDir, false);
|
|
1089
|
+
}
|
|
1090
|
+
async function computeSkillContentHash(skillDir) {
|
|
1091
|
+
return hashSkillFiles(skillDir, true);
|
|
1092
|
+
}
|
|
1093
|
+
async function hashSkillFiles(skillDir, installedOnly) {
|
|
1080
1094
|
const files = [];
|
|
1081
|
-
await collectFiles(skillDir, skillDir, files);
|
|
1095
|
+
await collectFiles(skillDir, skillDir, files, installedOnly);
|
|
1082
1096
|
files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
1083
1097
|
const hash = createHash("sha256");
|
|
1084
1098
|
for (const file of files) {
|
|
@@ -1087,14 +1101,16 @@ async function computeSkillFolderHash(skillDir) {
|
|
|
1087
1101
|
}
|
|
1088
1102
|
return hash.digest("hex");
|
|
1089
1103
|
}
|
|
1090
|
-
async function collectFiles(baseDir, currentDir, results) {
|
|
1104
|
+
async function collectFiles(baseDir, currentDir, results, installedOnly) {
|
|
1091
1105
|
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
1092
1106
|
await Promise.all(entries.map(async (entry) => {
|
|
1093
1107
|
const fullPath = join(currentDir, entry.name);
|
|
1094
1108
|
if (entry.isDirectory()) {
|
|
1095
1109
|
if (entry.name === ".git" || entry.name === "node_modules") return;
|
|
1096
|
-
|
|
1110
|
+
if (installedOnly && INSTALL_EXCLUDED_DIRS.has(entry.name)) return;
|
|
1111
|
+
await collectFiles(baseDir, fullPath, results, installedOnly);
|
|
1097
1112
|
} else if (entry.isFile()) {
|
|
1113
|
+
if (installedOnly && INSTALL_EXCLUDED_FILES.has(entry.name)) return;
|
|
1098
1114
|
const content = await readFile(fullPath);
|
|
1099
1115
|
const relativePath = relative(baseDir, fullPath).split("\\").join("/");
|
|
1100
1116
|
results.push({
|
|
@@ -1109,6 +1125,13 @@ async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
|
1109
1125
|
lock.skills[skillName] = entry;
|
|
1110
1126
|
await writeLocalLock(lock, cwd);
|
|
1111
1127
|
}
|
|
1128
|
+
async function removeSkillFromLocalLock(skillName, cwd) {
|
|
1129
|
+
const lock = await readLocalLock(cwd);
|
|
1130
|
+
if (!(skillName in lock.skills)) return false;
|
|
1131
|
+
delete lock.skills[skillName];
|
|
1132
|
+
await writeLocalLock(lock, cwd);
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1112
1135
|
function createEmptyLocalLock() {
|
|
1113
1136
|
return {
|
|
1114
1137
|
version: CURRENT_VERSION$1,
|
|
@@ -1972,8 +1995,6 @@ function getNonUniversalAgents() {
|
|
|
1972
1995
|
function isUniversalAgent(type) {
|
|
1973
1996
|
return agents[type].skillsDir === ".agents/skills";
|
|
1974
1997
|
}
|
|
1975
|
-
const AGENTS_DIR$1 = ".agents";
|
|
1976
|
-
const SKILLS_SUBDIR = "skills";
|
|
1977
1998
|
function sanitizeName(name) {
|
|
1978
1999
|
return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
|
|
1979
2000
|
}
|
|
@@ -2152,15 +2173,9 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
2152
2173
|
};
|
|
2153
2174
|
}
|
|
2154
2175
|
}
|
|
2155
|
-
const EXCLUDE_FILES$1 = new Set(["metadata.json"]);
|
|
2156
|
-
const EXCLUDE_DIRS$1 = new Set([
|
|
2157
|
-
".git",
|
|
2158
|
-
"__pycache__",
|
|
2159
|
-
"__pypackages__"
|
|
2160
|
-
]);
|
|
2161
2176
|
const isExcluded$1 = (name, isDirectory = false) => {
|
|
2162
|
-
if (
|
|
2163
|
-
if (isDirectory &&
|
|
2177
|
+
if (INSTALL_EXCLUDED_FILES.has(name)) return true;
|
|
2178
|
+
if (isDirectory && INSTALL_EXCLUDED_DIRS.has(name)) return true;
|
|
2164
2179
|
return false;
|
|
2165
2180
|
};
|
|
2166
2181
|
async function copyDirectory(src, dest) {
|
|
@@ -3113,6 +3128,33 @@ async function dismissPrompt(promptKey) {
|
|
|
3113
3128
|
lock.dismissed[promptKey] = true;
|
|
3114
3129
|
await writeSkillLock(lock);
|
|
3115
3130
|
}
|
|
3131
|
+
const DIVERGENT_HASH = "divergent";
|
|
3132
|
+
function findInstalledSkillDirs(skillName, cwd) {
|
|
3133
|
+
const candidates = [getCanonicalPath(skillName, { cwd })];
|
|
3134
|
+
for (const [type, config] of Object.entries(agents)) {
|
|
3135
|
+
if (isUniversalAgent(type)) continue;
|
|
3136
|
+
candidates.push(join(cwd, config.skillsDir, sanitizeName(skillName)));
|
|
3137
|
+
}
|
|
3138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3139
|
+
const dirs = [];
|
|
3140
|
+
for (const dir of candidates) {
|
|
3141
|
+
if (!existsSync(join(dir, "SKILL.md"))) continue;
|
|
3142
|
+
let real = dir;
|
|
3143
|
+
try {
|
|
3144
|
+
real = realpathSync(dir);
|
|
3145
|
+
} catch {}
|
|
3146
|
+
if (seen.has(real)) continue;
|
|
3147
|
+
seen.add(real);
|
|
3148
|
+
dirs.push(dir);
|
|
3149
|
+
}
|
|
3150
|
+
return dirs;
|
|
3151
|
+
}
|
|
3152
|
+
async function hashInstalledSkill(skillName, cwd) {
|
|
3153
|
+
const dirs = findInstalledSkillDirs(skillName, cwd);
|
|
3154
|
+
if (dirs.length === 0) return null;
|
|
3155
|
+
const hashes = new Set(await Promise.all(dirs.map((dir) => computeSkillContentHash(dir))));
|
|
3156
|
+
return hashes.size === 1 ? [...hashes][0] : DIVERGENT_HASH;
|
|
3157
|
+
}
|
|
3116
3158
|
const DOWNLOAD_BASE_URL = process.env.PRUFF_SKILLS_DOWNLOAD_URL || "";
|
|
3117
3159
|
const FETCH_TIMEOUT = 1e4;
|
|
3118
3160
|
function toSkillSlug(name) {
|
|
@@ -4255,12 +4297,14 @@ async function runAdd(args, options = {}) {
|
|
|
4255
4297
|
if (successfulSkillNames.has(skillDisplayName)) try {
|
|
4256
4298
|
const computedHash = blobResult && "snapshotHash" in skill ? skill.snapshotHash : await computeSkillFolderHash(skill.path);
|
|
4257
4299
|
const skillPathValue = skillFiles[skill.name];
|
|
4300
|
+
const installedHash = await hashInstalledSkill(skill.name, cwd);
|
|
4258
4301
|
await addSkillToLocalLock(skill.name, {
|
|
4259
4302
|
source: lockSource || parsed.url,
|
|
4260
4303
|
ref: parsed.ref,
|
|
4261
4304
|
sourceType: parsed.type,
|
|
4262
4305
|
...skillPathValue && { skillPath: skillPathValue },
|
|
4263
|
-
computedHash
|
|
4306
|
+
computedHash,
|
|
4307
|
+
...installedHash && installedHash !== DIVERGENT_HASH && { installedHash }
|
|
4264
4308
|
}, cwd);
|
|
4265
4309
|
} catch {}
|
|
4266
4310
|
}
|
|
@@ -4554,7 +4598,7 @@ async function updateAgents(options = {}) {
|
|
|
4554
4598
|
result.unchanged.push(name);
|
|
4555
4599
|
continue;
|
|
4556
4600
|
}
|
|
4557
|
-
await writeFile(dest, content);
|
|
4601
|
+
if (!options.dryRun) await writeFile(dest, content);
|
|
4558
4602
|
result.updated.push(name);
|
|
4559
4603
|
}
|
|
4560
4604
|
return result;
|
|
@@ -4756,10 +4800,10 @@ async function runRules(args) {
|
|
|
4756
4800
|
await addRules(names, options);
|
|
4757
4801
|
}
|
|
4758
4802
|
const execFileAsync$2 = promisify(execFile);
|
|
4759
|
-
const RESET$
|
|
4760
|
-
const BOLD$
|
|
4761
|
-
const DIM$
|
|
4762
|
-
const TEXT$
|
|
4803
|
+
const RESET$4 = "\x1B[0m";
|
|
4804
|
+
const BOLD$4 = "\x1B[1m";
|
|
4805
|
+
const DIM$4 = "\x1B[38;5;102m";
|
|
4806
|
+
const TEXT$3 = "\x1B[38;5;145m";
|
|
4763
4807
|
const CYAN$1 = "\x1B[36m";
|
|
4764
4808
|
const GREEN = "\x1B[32m";
|
|
4765
4809
|
const MAGENTA = "\x1B[35m";
|
|
@@ -4850,10 +4894,10 @@ const MOVE_UP = (n) => `\x1b[${n}A`;
|
|
|
4850
4894
|
const MOVE_TO_COL = (n) => `\x1b[${n}G`;
|
|
4851
4895
|
function rowBadge(skill) {
|
|
4852
4896
|
const parts = [];
|
|
4853
|
-
if (skill.category) parts.push(`${CYAN$1}${skill.category}${RESET$
|
|
4854
|
-
if (skill.installed) parts.push(`${GREEN}installed${RESET$
|
|
4897
|
+
if (skill.category) parts.push(`${CYAN$1}${skill.category}${RESET$4}`);
|
|
4898
|
+
if (skill.installed) parts.push(`${GREEN}installed${RESET$4}`);
|
|
4855
4899
|
if (parts.length === 0) return "";
|
|
4856
|
-
return ` ${DIM$
|
|
4900
|
+
return ` ${DIM$4}[${RESET$4}${parts.join(`${DIM$4} · ${RESET$4}`)}${DIM$4}]${RESET$4}`;
|
|
4857
4901
|
}
|
|
4858
4902
|
async function runSearchPrompt(kind, alreadyPicked = /* @__PURE__ */ new Set(), initialQuery = "") {
|
|
4859
4903
|
let results = [];
|
|
@@ -4871,33 +4915,33 @@ async function runSearchPrompt(kind, alreadyPicked = /* @__PURE__ */ new Set(),
|
|
|
4871
4915
|
if (lastRenderedLines > 0) process.stdout.write(MOVE_UP(lastRenderedLines) + MOVE_TO_COL(1));
|
|
4872
4916
|
process.stdout.write(CLEAR_DOWN);
|
|
4873
4917
|
const lines = [];
|
|
4874
|
-
const cursor = `${BOLD$
|
|
4875
|
-
lines.push(`${TEXT$
|
|
4918
|
+
const cursor = `${BOLD$4}_${RESET$4}`;
|
|
4919
|
+
lines.push(`${TEXT$3}Search ${noun}:${RESET$4} ${query}${cursor}`);
|
|
4876
4920
|
lines.push("");
|
|
4877
|
-
if (loading && results.length === 0) lines.push(`${DIM$
|
|
4878
|
-
else if (results.length === 0) lines.push(`${DIM$
|
|
4921
|
+
if (loading && results.length === 0) lines.push(`${DIM$4}Loading catalog…${RESET$4}`);
|
|
4922
|
+
else if (results.length === 0) lines.push(`${DIM$4}${query ? `No ${noun} match "${query}"` : `No ${noun} in catalog`}${RESET$4}`);
|
|
4879
4923
|
else {
|
|
4880
4924
|
const maxVisible = 12;
|
|
4881
4925
|
const start = Math.max(0, Math.min(selectedIndex - maxVisible + 1, results.length - maxVisible));
|
|
4882
4926
|
const from = Math.max(0, start);
|
|
4883
4927
|
const visible = results.slice(from, from + maxVisible);
|
|
4884
|
-
if (from > 0) lines.push(` ${DIM$
|
|
4928
|
+
if (from > 0) lines.push(` ${DIM$4}↑ ${from} more${RESET$4}`);
|
|
4885
4929
|
for (let i = 0; i < visible.length; i++) {
|
|
4886
4930
|
const idx = from + i;
|
|
4887
4931
|
const skill = visible[i];
|
|
4888
4932
|
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$
|
|
4933
|
+
const arrow = isSelected ? `${BOLD$4}>${RESET$4}` : " ";
|
|
4934
|
+
const box = picked.has(skill.name) ? `${GREEN}${CHECKED}${RESET$4}` : `${DIM$4}${UNCHECKED}${RESET$4}`;
|
|
4935
|
+
const name = isSelected ? `${BOLD$4}${skill.name}${RESET$4}` : `${TEXT$3}${skill.name}${RESET$4}`;
|
|
4936
|
+
const loadingIndicator = loading && idx === 0 ? ` ${DIM$4}...${RESET$4}` : "";
|
|
4893
4937
|
lines.push(` ${arrow} ${box} ${name}${rowBadge(skill)}${loadingIndicator}`);
|
|
4894
4938
|
}
|
|
4895
4939
|
const remaining = results.length - (from + visible.length);
|
|
4896
|
-
if (remaining > 0) lines.push(` ${DIM$
|
|
4940
|
+
if (remaining > 0) lines.push(` ${DIM$4}↓ ${remaining} more${RESET$4}`);
|
|
4897
4941
|
}
|
|
4898
4942
|
lines.push("");
|
|
4899
|
-
const count = picked.size > 0 ? ` ${GREEN}(${picked.size} selected)${RESET$
|
|
4900
|
-
lines.push(`${DIM$
|
|
4943
|
+
const count = picked.size > 0 ? ` ${GREEN}(${picked.size} selected)${RESET$4}` : "";
|
|
4944
|
+
lines.push(`${DIM$4}up/down navigate | space toggle | enter done${count}${DIM$4} | esc back${RESET$4}`);
|
|
4901
4945
|
for (const line of lines) process.stdout.write(line + "\n");
|
|
4902
4946
|
lastRenderedLines = lines.length;
|
|
4903
4947
|
}
|
|
@@ -4982,22 +5026,22 @@ async function runSearchPrompt(kind, alreadyPicked = /* @__PURE__ */ new Set(),
|
|
|
4982
5026
|
async function runFind(args) {
|
|
4983
5027
|
const query = args.join(" ");
|
|
4984
5028
|
const isNonInteractive = !process.stdin.isTTY;
|
|
4985
|
-
const agentTip = `${DIM$
|
|
4986
|
-
${DIM$
|
|
4987
|
-
${DIM$
|
|
5029
|
+
const agentTip = `${DIM$4}Tip: if running in a coding agent, follow these steps:${RESET$4}
|
|
5030
|
+
${DIM$4} 1) npx pruff-skills find [query]${RESET$4}
|
|
5031
|
+
${DIM$4} 2) npx pruff-skills add <skill>${RESET$4}`;
|
|
4988
5032
|
if (query) {
|
|
4989
5033
|
const results = await searchSkillsAPI(query);
|
|
4990
5034
|
String(results.length);
|
|
4991
5035
|
if (results.length === 0) {
|
|
4992
|
-
console.log(`${DIM$
|
|
5036
|
+
console.log(`${DIM$4}Nothing in the catalog matches "${query}"${RESET$4}`);
|
|
4993
5037
|
return;
|
|
4994
5038
|
}
|
|
4995
|
-
console.log(`${DIM$
|
|
5039
|
+
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
5040
|
console.log();
|
|
4997
5041
|
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$
|
|
5042
|
+
const kindBadge = skill.kind === "agent" ? ` ${MAGENTA}[subagent]${RESET$4}` : skill.kind === "rule" ? ` ${BLUE$1}[rule]${RESET$4}` : ` ${CYAN$1}[skill]${RESET$4}`;
|
|
5043
|
+
console.log(`${TEXT$3}${skill.name}${RESET$4}${kindBadge}${rowBadge(skill)}`);
|
|
5044
|
+
if (skill.description) console.log(`${DIM$4}└ ${skill.description}${RESET$4}`);
|
|
5001
5045
|
console.log();
|
|
5002
5046
|
}
|
|
5003
5047
|
return;
|
|
@@ -5005,7 +5049,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5005
5049
|
if (isNonInteractive || await isRunningInAgent()) {
|
|
5006
5050
|
console.log(agentTip);
|
|
5007
5051
|
console.log();
|
|
5008
|
-
console.log(`${DIM$
|
|
5052
|
+
console.log(`${DIM$4}Usage: npx pruff-skills find <query>${RESET$4}`);
|
|
5009
5053
|
return;
|
|
5010
5054
|
}
|
|
5011
5055
|
const all = await searchSkillsAPI("");
|
|
@@ -5030,7 +5074,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5030
5074
|
const pickedByKind = new Map(KINDS.map((k) => [k.value, /* @__PURE__ */ new Set()]));
|
|
5031
5075
|
const totalPicked = () => [...pickedByKind.values()].reduce((n, set) => n + set.size, 0);
|
|
5032
5076
|
const cancelled = () => {
|
|
5033
|
-
console.log(`${DIM$
|
|
5077
|
+
console.log(`${DIM$4}Search cancelled${RESET$4}`);
|
|
5034
5078
|
console.log();
|
|
5035
5079
|
};
|
|
5036
5080
|
for (;;) {
|
|
@@ -5061,10 +5105,10 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5061
5105
|
for (const kind of KINDS) {
|
|
5062
5106
|
const names = [...pickedByKind.get(kind.value)];
|
|
5063
5107
|
if (names.length === 0) continue;
|
|
5064
|
-
console.log(`${TEXT$
|
|
5108
|
+
console.log(`${TEXT$3}${kind.label}${RESET$4} ${DIM$4}(${names.length})${RESET$4}`);
|
|
5065
5109
|
for (const name of names) {
|
|
5066
|
-
const note = all.find((s) => s.kind === kind.value && s.name === name)?.installed ? ` ${DIM$
|
|
5067
|
-
console.log(` ${DIM$
|
|
5110
|
+
const note = all.find((s) => s.kind === kind.value && s.name === name)?.installed ? ` ${DIM$4}— already installed, will reinstall${RESET$4}` : "";
|
|
5111
|
+
console.log(` ${DIM$4}·${RESET$4} ${name}${note}`);
|
|
5068
5112
|
}
|
|
5069
5113
|
}
|
|
5070
5114
|
console.log();
|
|
@@ -5091,7 +5135,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5091
5135
|
console.log();
|
|
5092
5136
|
if (skills.length > 0) {
|
|
5093
5137
|
const pkg = CATALOG_SOURCE;
|
|
5094
|
-
console.log(`${TEXT$
|
|
5138
|
+
console.log(`${TEXT$3}Installing ${BOLD$4}${skills.join(", ")}${RESET$4} from ${DIM$4}${pkg}${RESET$4}...`);
|
|
5095
5139
|
console.log();
|
|
5096
5140
|
const { source, options } = parseAddOptions([
|
|
5097
5141
|
pkg,
|
|
@@ -5103,7 +5147,7 @@ ${DIM$3} 2) npx pruff-skills add <skill>${RESET$3}`;
|
|
|
5103
5147
|
if (agents.length > 0) await addAgents(agents, {});
|
|
5104
5148
|
if (rules.length > 0) await addRules(rules, {});
|
|
5105
5149
|
console.log();
|
|
5106
|
-
console.log(`${DIM$
|
|
5150
|
+
console.log(`${DIM$4}Installed.${RESET$4} ${TEXT$3}npx pruff-skills list${RESET$4} ${DIM$4}shows what's installed.${RESET$4}`);
|
|
5107
5151
|
console.log();
|
|
5108
5152
|
}
|
|
5109
5153
|
const isCancelled = (value) => typeof value === "symbol";
|
|
@@ -5452,9 +5496,9 @@ async function runInstallFromLock(args) {
|
|
|
5452
5496
|
}
|
|
5453
5497
|
}
|
|
5454
5498
|
}
|
|
5455
|
-
const RESET$
|
|
5456
|
-
const BOLD$
|
|
5457
|
-
const DIM$
|
|
5499
|
+
const RESET$3 = "\x1B[0m";
|
|
5500
|
+
const BOLD$3 = "\x1B[1m";
|
|
5501
|
+
const DIM$3 = "\x1B[38;5;102m";
|
|
5458
5502
|
const CYAN = "\x1B[36m";
|
|
5459
5503
|
const YELLOW = "\x1B[33m";
|
|
5460
5504
|
const LIST_JSON_SCHEMA_VERSION = 1;
|
|
@@ -5491,8 +5535,8 @@ async function runList(args) {
|
|
|
5491
5535
|
const validAgents = Object.keys(agents);
|
|
5492
5536
|
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
5493
5537
|
if (invalidAgents.length > 0) {
|
|
5494
|
-
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$
|
|
5495
|
-
console.log(`${DIM$
|
|
5538
|
+
console.log(`${YELLOW}Invalid agents: ${invalidAgents.join(", ")}${RESET$3}`);
|
|
5539
|
+
console.log(`${DIM$3}Valid agents: ${validAgents.join(", ")}${RESET$3}`);
|
|
5496
5540
|
process.exit(1);
|
|
5497
5541
|
}
|
|
5498
5542
|
agentFilter = options.agent;
|
|
@@ -5518,21 +5562,21 @@ async function runList(args) {
|
|
|
5518
5562
|
const cwd = process.cwd();
|
|
5519
5563
|
const scopeLabel = scope ? "Global" : "Project";
|
|
5520
5564
|
if (installedSkills.length === 0) {
|
|
5521
|
-
console.log(`${DIM$
|
|
5522
|
-
if (scope) console.log(`${DIM$
|
|
5523
|
-
else console.log(`${DIM$
|
|
5565
|
+
console.log(`${DIM$3}No ${scopeLabel.toLowerCase()} skills found.${RESET$3}`);
|
|
5566
|
+
if (scope) console.log(`${DIM$3}Try listing project skills without -g${RESET$3}`);
|
|
5567
|
+
else console.log(`${DIM$3}Try listing global skills with -g${RESET$3}`);
|
|
5524
5568
|
return;
|
|
5525
5569
|
}
|
|
5526
5570
|
function printSkill(skill, indent = false, maxNameLength = 0, maxPathLength = 0) {
|
|
5527
5571
|
const prefix = indent ? " " : "";
|
|
5528
5572
|
const shortPath = shortenPath(skill.canonicalPath, cwd);
|
|
5529
5573
|
const agentNames = skill.agents.map((a) => agents[a].displayName);
|
|
5530
|
-
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$
|
|
5574
|
+
const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$3}`;
|
|
5531
5575
|
const paddedName = sanitizeMetadata(skill.name).padEnd(maxNameLength);
|
|
5532
5576
|
const paddedPath = shortPath.padEnd(maxPathLength);
|
|
5533
|
-
console.log(`${prefix}${CYAN}${paddedName}${RESET$
|
|
5577
|
+
console.log(`${prefix}${CYAN}${paddedName}${RESET$3} ${DIM$3}${paddedPath}${RESET$3} ${DIM$3}Agents:${RESET$3} ${agentInfo}`);
|
|
5534
5578
|
}
|
|
5535
|
-
console.log(`${BOLD$
|
|
5579
|
+
console.log(`${BOLD$3}${scopeLabel} Skills${RESET$3}`);
|
|
5536
5580
|
console.log();
|
|
5537
5581
|
const groupedSkills = {};
|
|
5538
5582
|
const ungroupedSkills = [];
|
|
@@ -5548,7 +5592,7 @@ async function runList(args) {
|
|
|
5548
5592
|
const sortedGroups = Object.keys(groupedSkills).sort();
|
|
5549
5593
|
for (const group of sortedGroups) {
|
|
5550
5594
|
const title = group.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
5551
|
-
console.log(`${BOLD$
|
|
5595
|
+
console.log(`${BOLD$3}${title}${RESET$3}`);
|
|
5552
5596
|
const skills = groupedSkills[group];
|
|
5553
5597
|
if (skills) {
|
|
5554
5598
|
let maxNameLength = 0;
|
|
@@ -5564,7 +5608,7 @@ async function runList(args) {
|
|
|
5564
5608
|
console.log();
|
|
5565
5609
|
}
|
|
5566
5610
|
if (ungroupedSkills.length > 0) {
|
|
5567
|
-
console.log(`${BOLD$
|
|
5611
|
+
console.log(`${BOLD$3}General${RESET$3}`);
|
|
5568
5612
|
let maxNameLength = 0;
|
|
5569
5613
|
let maxPathLength = 0;
|
|
5570
5614
|
for (const skill of ungroupedSkills) {
|
|
@@ -5787,6 +5831,81 @@ function parseRemoveOptions(args) {
|
|
|
5787
5831
|
options
|
|
5788
5832
|
};
|
|
5789
5833
|
}
|
|
5834
|
+
const RESET$2 = "\x1B[0m";
|
|
5835
|
+
const BOLD$2 = "\x1B[1m";
|
|
5836
|
+
const DIM$2 = "\x1B[38;5;102m";
|
|
5837
|
+
const TEXT$2 = "\x1B[38;5;145m";
|
|
5838
|
+
function classifyTrackedSkill(h) {
|
|
5839
|
+
if (h.diskHash === null) return {
|
|
5840
|
+
status: "missing",
|
|
5841
|
+
locallyEdited: false
|
|
5842
|
+
};
|
|
5843
|
+
const divergent = h.diskHash === DIVERGENT_HASH;
|
|
5844
|
+
if (h.catalogHash && h.catalogHash === h.lockHash) {
|
|
5845
|
+
const installed = h.installedHash ?? h.catalogContentHash;
|
|
5846
|
+
return {
|
|
5847
|
+
status: "unchanged",
|
|
5848
|
+
locallyEdited: divergent || h.diskHash !== installed
|
|
5849
|
+
};
|
|
5850
|
+
}
|
|
5851
|
+
if (h.catalogContentHash && h.diskHash === h.catalogContentHash) return {
|
|
5852
|
+
status: "rebaseline",
|
|
5853
|
+
locallyEdited: false
|
|
5854
|
+
};
|
|
5855
|
+
const installed = h.installedHash ?? h.lockHash;
|
|
5856
|
+
return {
|
|
5857
|
+
status: "modified",
|
|
5858
|
+
locallyEdited: divergent || h.diskHash !== installed
|
|
5859
|
+
};
|
|
5860
|
+
}
|
|
5861
|
+
function emptyPlan() {
|
|
5862
|
+
return {
|
|
5863
|
+
modified: [],
|
|
5864
|
+
missing: [],
|
|
5865
|
+
rebaseline: [],
|
|
5866
|
+
unchanged: [],
|
|
5867
|
+
conflicts: [],
|
|
5868
|
+
localEdits: [],
|
|
5869
|
+
deleted: [],
|
|
5870
|
+
available: [],
|
|
5871
|
+
untracked: [],
|
|
5872
|
+
failedSources: []
|
|
5873
|
+
};
|
|
5874
|
+
}
|
|
5875
|
+
function addClassified(plan, entry, c) {
|
|
5876
|
+
if (c.status === "unchanged") {
|
|
5877
|
+
plan.unchanged.push(entry);
|
|
5878
|
+
if (c.locallyEdited) plan.localEdits.push(entry);
|
|
5879
|
+
return;
|
|
5880
|
+
}
|
|
5881
|
+
if (c.status === "modified" && c.locallyEdited) {
|
|
5882
|
+
plan.conflicts.push(entry);
|
|
5883
|
+
return;
|
|
5884
|
+
}
|
|
5885
|
+
plan[c.status].push(entry);
|
|
5886
|
+
}
|
|
5887
|
+
function names(entries) {
|
|
5888
|
+
return entries.map((e) => sanitizeMetadata(e.name)).join(", ");
|
|
5889
|
+
}
|
|
5890
|
+
function line(label, entries, note) {
|
|
5891
|
+
if (entries.length === 0) return;
|
|
5892
|
+
const suffix = note ? ` ${DIM$2}— ${note}${RESET$2}` : "";
|
|
5893
|
+
console.log(` ${TEXT$2}${label} (${entries.length}):${RESET$2} ${names(entries)}${suffix}`);
|
|
5894
|
+
}
|
|
5895
|
+
function printPlan(plan, opts = {}) {
|
|
5896
|
+
console.log(`${BOLD$2}Plan${RESET$2}`);
|
|
5897
|
+
line("Modified upstream", plan.modified, "will update");
|
|
5898
|
+
line("Missing on disk", plan.missing, "will reinstall");
|
|
5899
|
+
line("Modified upstream + edited locally", plan.conflicts, opts.force ? "will overwrite (--force)" : "local edits kept unless you confirm or pass --force");
|
|
5900
|
+
line("Deleted upstream", plan.deleted);
|
|
5901
|
+
line("Already current", plan.rebaseline, "lock hash refreshed, nothing reinstalled");
|
|
5902
|
+
line("New in catalog", plan.available, "not installed");
|
|
5903
|
+
line("Installed but not tracked", plan.untracked, "not in skills-lock.json");
|
|
5904
|
+
line("Edited locally", plan.localEdits, "unchanged upstream, left as is");
|
|
5905
|
+
if (plan.unchanged.length > 0) console.log(` ${DIM$2}Up to date: ${plan.unchanged.length}${RESET$2}`);
|
|
5906
|
+
for (const source of plan.failedSources) console.log(` ${DIM$2}✗ Could not read ${sanitizeMetadata(source)} — its skills were not checked${RESET$2}`);
|
|
5907
|
+
console.log();
|
|
5908
|
+
}
|
|
5790
5909
|
function formatSourceInput(sourceUrl, ref) {
|
|
5791
5910
|
if (!ref) return sourceUrl;
|
|
5792
5911
|
return `${sourceUrl}#${ref}`;
|
|
@@ -5830,6 +5949,8 @@ function parseUpdateOptions(args) {
|
|
|
5830
5949
|
for (const arg of args) if (arg === "-g" || arg === "--global") options.global = true;
|
|
5831
5950
|
else if (arg === "-p" || arg === "--project") options.project = true;
|
|
5832
5951
|
else if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
5952
|
+
else if (arg === "--dry-run") options.dryRun = true;
|
|
5953
|
+
else if (arg === "--force") options.force = true;
|
|
5833
5954
|
else if (!arg.startsWith("-")) positional.push(arg);
|
|
5834
5955
|
if (positional.length > 0) options.skills = positional;
|
|
5835
5956
|
return options;
|
|
@@ -5950,7 +6071,9 @@ async function checkAndPromptForDeletions(source, allLockedForSource, lockSkills
|
|
|
5950
6071
|
console.log();
|
|
5951
6072
|
console.log(`${DIM$1}Warning:${RESET$1} The following skills from ${DIM$1}${source}${RESET$1} appear to have been deleted upstream:`);
|
|
5952
6073
|
for (const s of deletedSkills) console.log(` ${DIM$1}•${RESET$1} ${s}`);
|
|
5953
|
-
|
|
6074
|
+
const isNonInteractive = options.yes || !process.stdin.isTTY;
|
|
6075
|
+
if (options.dryRun) console.log(`${DIM$1}Dry run — nothing removed.${RESET$1}`);
|
|
6076
|
+
else if (isNonInteractive) console.log(`${DIM$1}Skipping deletion in non-interactive mode.${RESET$1}`);
|
|
5954
6077
|
else {
|
|
5955
6078
|
const confirmed = await ye({ message: `Would you like to remove the local copies of these deleted skills?` });
|
|
5956
6079
|
if (confirmed && !pD(confirmed)) for (const s of deletedSkills) {
|
|
@@ -6086,6 +6209,16 @@ async function updateGlobalSkills(options = {}) {
|
|
|
6086
6209
|
}
|
|
6087
6210
|
console.log(`${TEXT$1}Found ${updates.length} global update(s)${RESET$1}`);
|
|
6088
6211
|
console.log();
|
|
6212
|
+
if (options.dryRun) {
|
|
6213
|
+
for (const update of updates) console.log(` ${TEXT$1}•${RESET$1} ${sanitizeMetadata(update.name)} ${DIM$1}would update${RESET$1}`);
|
|
6214
|
+
printSkippedSkills(skipped);
|
|
6215
|
+
console.log(`${DIM$1}Dry run — nothing was changed.${RESET$1}`);
|
|
6216
|
+
return {
|
|
6217
|
+
successCount,
|
|
6218
|
+
failCount,
|
|
6219
|
+
checkedCount
|
|
6220
|
+
};
|
|
6221
|
+
}
|
|
6089
6222
|
for (const update of updates) {
|
|
6090
6223
|
const safeName = sanitizeMetadata(update.name);
|
|
6091
6224
|
console.log(`${TEXT$1}Updating ${safeName}...${RESET$1}`);
|
|
@@ -6125,36 +6258,7 @@ async function updateGlobalSkills(options = {}) {
|
|
|
6125
6258
|
checkedCount
|
|
6126
6259
|
};
|
|
6127
6260
|
}
|
|
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();
|
|
6261
|
+
function printUpdateTargets(cwd) {
|
|
6158
6262
|
const targetAgentNames = [];
|
|
6159
6263
|
let hasUniversal = false;
|
|
6160
6264
|
for (const [type, config] of Object.entries(agents)) if (isUniversalAgent(type)) {
|
|
@@ -6167,95 +6271,253 @@ async function updateProjectSkills(options = {}) {
|
|
|
6167
6271
|
if (hasUniversal) targetParts.push("Universal");
|
|
6168
6272
|
targetParts.push(...targetAgentNames);
|
|
6169
6273
|
if (targetParts.length > 0) console.log(`${TEXT$1}Updating for: ${targetParts.join(", ")}${RESET$1}`);
|
|
6170
|
-
|
|
6171
|
-
|
|
6274
|
+
}
|
|
6275
|
+
async function safeHash(hash, dir) {
|
|
6276
|
+
try {
|
|
6277
|
+
return await hash(dir) || "";
|
|
6278
|
+
} catch {
|
|
6279
|
+
return "";
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
async function buildProjectPlan(updatable, localLock, cwd, skillFilter) {
|
|
6283
|
+
const plan = emptyPlan();
|
|
6284
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
6172
6285
|
const bySource = /* @__PURE__ */ new Map();
|
|
6173
6286
|
for (const skill of updatable) {
|
|
6174
|
-
const
|
|
6175
|
-
const existing = bySource.get(source) || [];
|
|
6287
|
+
const existing = bySource.get(skill.entry.source) || [];
|
|
6176
6288
|
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
|
-
};
|
|
6289
|
+
bySource.set(skill.entry.source, existing);
|
|
6189
6290
|
}
|
|
6190
6291
|
for (const [source, skillsForSource] of bySource) {
|
|
6191
6292
|
const firstEntry = skillsForSource[0].entry;
|
|
6192
6293
|
const parsedSource = parseSource(firstEntry.source);
|
|
6193
|
-
const sourceUrl = parsedSource.url;
|
|
6194
6294
|
const ref = firstEntry.ref ?? parsedSource.ref;
|
|
6195
|
-
const allLockedForSource = Object.entries(localLock.skills).filter(([_, entry]) => entry.source === source).map(([name, _]) => name);
|
|
6196
6295
|
let tempDir = null;
|
|
6197
|
-
let deletedSkills = [];
|
|
6198
|
-
const unchangedSkills = /* @__PURE__ */ new Set();
|
|
6199
6296
|
try {
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredPaths);
|
|
6297
|
+
setStep(`update:check:${source}`);
|
|
6298
|
+
tempDir = await cloneRepo$1(parsedSource.url, ref);
|
|
6299
|
+
const discovered = await discoverSkills(tempDir);
|
|
6300
|
+
const byPath = new Map(discovered.map((s) => [join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/"), s]));
|
|
6205
6301
|
for (const skill of skillsForSource) {
|
|
6206
|
-
const
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6302
|
+
const entry = {
|
|
6303
|
+
name: skill.name,
|
|
6304
|
+
source
|
|
6305
|
+
};
|
|
6306
|
+
const found = byPath.get(skill.entry.skillPath);
|
|
6307
|
+
if (!found) {
|
|
6308
|
+
plan.deleted.push(entry);
|
|
6309
|
+
continue;
|
|
6310
|
+
}
|
|
6311
|
+
const [catalogHash, catalogContentHash, diskHash] = await Promise.all([
|
|
6312
|
+
safeHash(computeSkillFolderHash, found.path),
|
|
6313
|
+
safeHash(computeSkillContentHash, found.path),
|
|
6314
|
+
hashInstalledSkill(skill.name, cwd)
|
|
6315
|
+
]);
|
|
6316
|
+
hashes.set(skill.name, {
|
|
6317
|
+
catalogHash,
|
|
6318
|
+
catalogContentHash,
|
|
6319
|
+
diskHash
|
|
6320
|
+
});
|
|
6321
|
+
addClassified(plan, entry, classifyTrackedSkill({
|
|
6322
|
+
lockHash: skill.entry.computedHash,
|
|
6323
|
+
installedHash: skill.entry.installedHash,
|
|
6324
|
+
catalogHash,
|
|
6325
|
+
catalogContentHash,
|
|
6326
|
+
diskHash
|
|
6327
|
+
}));
|
|
6328
|
+
}
|
|
6329
|
+
for (const skill of discovered) {
|
|
6330
|
+
if (skill.name in localLock.skills) continue;
|
|
6331
|
+
if (!matchesSkillFilter(skill.name, skillFilter)) continue;
|
|
6332
|
+
const entry = {
|
|
6333
|
+
name: skill.name,
|
|
6334
|
+
source
|
|
6335
|
+
};
|
|
6336
|
+
if (findInstalledSkillDirs(skill.name, cwd).length > 0) plan.untracked.push(entry);
|
|
6337
|
+
else plan.available.push(entry);
|
|
6210
6338
|
}
|
|
6211
6339
|
} catch (error) {
|
|
6212
6340
|
const reason = error instanceof Error ? error.message : String(error);
|
|
6213
|
-
console.log(`${DIM$1}✗ Failed to
|
|
6341
|
+
console.log(`${DIM$1}✗ Failed to read ${source}: ${sanitizeMetadata(reason)}${RESET$1}`);
|
|
6342
|
+
plan.failedSources.push(source);
|
|
6214
6343
|
} finally {
|
|
6215
6344
|
if (tempDir) await cleanupTempDir(tempDir);
|
|
6216
6345
|
}
|
|
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
6346
|
}
|
|
6251
|
-
printLegacyProjectSkills(legacy);
|
|
6252
6347
|
return {
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
foundCount: projectSkills.length,
|
|
6256
|
-
upToDateCount
|
|
6348
|
+
plan,
|
|
6349
|
+
hashes
|
|
6257
6350
|
};
|
|
6258
6351
|
}
|
|
6352
|
+
async function selectEntries(message, entries, hint) {
|
|
6353
|
+
if (entries.length === 0) return [];
|
|
6354
|
+
const picked = await searchMultiselect({
|
|
6355
|
+
message,
|
|
6356
|
+
items: entries.map((e) => ({
|
|
6357
|
+
value: e,
|
|
6358
|
+
label: e.name,
|
|
6359
|
+
hint: hint(e)
|
|
6360
|
+
}))
|
|
6361
|
+
});
|
|
6362
|
+
if (typeof picked === "symbol") return [];
|
|
6363
|
+
return picked;
|
|
6364
|
+
}
|
|
6365
|
+
async function applyDeletions(deleted, interactive, cwd) {
|
|
6366
|
+
if (deleted.length === 0) return;
|
|
6367
|
+
if (!interactive) {
|
|
6368
|
+
console.log(`${DIM$1}Deleted-upstream skills kept (non-interactive).${RESET$1}`);
|
|
6369
|
+
return;
|
|
6370
|
+
}
|
|
6371
|
+
const confirmed = await ye({ message: `Remove the local copies of ${deleted.length} skill(s) deleted upstream?` });
|
|
6372
|
+
if (!confirmed || pD(confirmed)) return;
|
|
6373
|
+
for (const { name } of deleted) {
|
|
6374
|
+
if (findInstalledSkillDirs(name, cwd).length > 0) {
|
|
6375
|
+
console.log(`${DIM$1}Removing${RESET$1} ${sanitizeMetadata(name)}...`);
|
|
6376
|
+
await removeCommand([name], {
|
|
6377
|
+
yes: true,
|
|
6378
|
+
global: false
|
|
6379
|
+
});
|
|
6380
|
+
}
|
|
6381
|
+
await removeSkillFromLocalLock(name, cwd);
|
|
6382
|
+
}
|
|
6383
|
+
}
|
|
6384
|
+
async function refreshLockHashes(plan, hashes, cwd) {
|
|
6385
|
+
const lock = await readLocalLock(cwd);
|
|
6386
|
+
const edited = new Set(plan.localEdits.map((e) => e.name));
|
|
6387
|
+
let changed = false;
|
|
6388
|
+
for (const { name } of plan.rebaseline) {
|
|
6389
|
+
const h = hashes.get(name);
|
|
6390
|
+
const entry = lock.skills[name];
|
|
6391
|
+
if (!h || !entry || !h.diskHash) continue;
|
|
6392
|
+
lock.skills[name] = {
|
|
6393
|
+
...entry,
|
|
6394
|
+
computedHash: h.catalogHash,
|
|
6395
|
+
installedHash: h.diskHash
|
|
6396
|
+
};
|
|
6397
|
+
changed = true;
|
|
6398
|
+
}
|
|
6399
|
+
for (const { name } of plan.unchanged) {
|
|
6400
|
+
const h = hashes.get(name);
|
|
6401
|
+
const entry = lock.skills[name];
|
|
6402
|
+
if (!h || !entry || entry.installedHash || edited.has(name) || !h.diskHash) continue;
|
|
6403
|
+
lock.skills[name] = {
|
|
6404
|
+
...entry,
|
|
6405
|
+
installedHash: h.diskHash
|
|
6406
|
+
};
|
|
6407
|
+
changed = true;
|
|
6408
|
+
}
|
|
6409
|
+
if (changed) await writeLocalLock(lock, cwd);
|
|
6410
|
+
}
|
|
6411
|
+
function installFromSource(cliEntry, installUrl, skillNames) {
|
|
6412
|
+
return spawnSync(process.execPath, [
|
|
6413
|
+
cliEntry,
|
|
6414
|
+
"add",
|
|
6415
|
+
installUrl,
|
|
6416
|
+
"--skill",
|
|
6417
|
+
...skillNames,
|
|
6418
|
+
"-y"
|
|
6419
|
+
], {
|
|
6420
|
+
stdio: [
|
|
6421
|
+
"inherit",
|
|
6422
|
+
"pipe",
|
|
6423
|
+
"pipe"
|
|
6424
|
+
],
|
|
6425
|
+
encoding: "utf-8",
|
|
6426
|
+
shell: process.platform === "win32"
|
|
6427
|
+
}).status === 0;
|
|
6428
|
+
}
|
|
6429
|
+
async function updateProjectSkills(options = {}) {
|
|
6430
|
+
const result = {
|
|
6431
|
+
successCount: 0,
|
|
6432
|
+
failCount: 0,
|
|
6433
|
+
foundCount: 0,
|
|
6434
|
+
upToDateCount: 0,
|
|
6435
|
+
installedCount: 0,
|
|
6436
|
+
rebaselinedCount: 0
|
|
6437
|
+
};
|
|
6438
|
+
const projectSkills = await getProjectSkillsForUpdate(options.skills);
|
|
6439
|
+
result.foundCount = projectSkills.length;
|
|
6440
|
+
if (projectSkills.length === 0) {
|
|
6441
|
+
if (!options.skills) {
|
|
6442
|
+
console.log(`${DIM$1}No project skills to update.${RESET$1}`);
|
|
6443
|
+
console.log(`${DIM$1}Install project skills with${RESET$1} ${TEXT$1}npx pruff-skills add <skill>${RESET$1}`);
|
|
6444
|
+
}
|
|
6445
|
+
return result;
|
|
6446
|
+
}
|
|
6447
|
+
const updatable = projectSkills.filter((s) => s.entry.skillPath);
|
|
6448
|
+
const legacy = projectSkills.filter((s) => !s.entry.skillPath);
|
|
6449
|
+
if (updatable.length === 0) {
|
|
6450
|
+
console.log(`${DIM$1}No project skills can be updated in place.${RESET$1}`);
|
|
6451
|
+
printLegacyProjectSkills(legacy);
|
|
6452
|
+
return result;
|
|
6453
|
+
}
|
|
6454
|
+
const cwd = process.cwd();
|
|
6455
|
+
printUpdateTargets(cwd);
|
|
6456
|
+
console.log(`${TEXT$1}Comparing ${updatable.length} skill(s) with their sources...${RESET$1}`);
|
|
6457
|
+
console.log();
|
|
6458
|
+
const { plan, hashes } = await buildProjectPlan(updatable, await readLocalLock(cwd), cwd, options.skills);
|
|
6459
|
+
printPlan(plan, { force: options.force });
|
|
6460
|
+
printLegacyProjectSkills(legacy);
|
|
6461
|
+
result.upToDateCount = plan.unchanged.length;
|
|
6462
|
+
if (options.dryRun) {
|
|
6463
|
+
console.log(`${DIM$1}Dry run — nothing was changed.${RESET$1}`);
|
|
6464
|
+
return result;
|
|
6465
|
+
}
|
|
6466
|
+
const interactive = !options.yes && process.stdin.isTTY === true;
|
|
6467
|
+
await applyDeletions(plan.deleted, interactive, cwd);
|
|
6468
|
+
await refreshLockHashes(plan, hashes, cwd);
|
|
6469
|
+
result.rebaselinedCount = plan.rebaseline.length;
|
|
6470
|
+
result.upToDateCount += plan.rebaseline.length;
|
|
6471
|
+
let overwrite = [];
|
|
6472
|
+
if (options.force) overwrite = plan.conflicts;
|
|
6473
|
+
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");
|
|
6474
|
+
let toInstall = [];
|
|
6475
|
+
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);
|
|
6476
|
+
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}`);
|
|
6477
|
+
const toUpdate = [
|
|
6478
|
+
...plan.modified,
|
|
6479
|
+
...plan.missing,
|
|
6480
|
+
...overwrite
|
|
6481
|
+
];
|
|
6482
|
+
if (toUpdate.length === 0 && toInstall.length === 0) return result;
|
|
6483
|
+
const cliEntry = join(__dirname$2, "..", "bin", "cli.mjs");
|
|
6484
|
+
if (!existsSync(cliEntry)) {
|
|
6485
|
+
console.log(`${DIM$1}✗ CLI entrypoint not found at ${cliEntry}${RESET$1}`);
|
|
6486
|
+
result.failCount += toUpdate.length + toInstall.length;
|
|
6487
|
+
return result;
|
|
6488
|
+
}
|
|
6489
|
+
const refBySource = new Map(updatable.map((s) => [s.entry.source, s.entry.ref]));
|
|
6490
|
+
const batches = /* @__PURE__ */ new Map();
|
|
6491
|
+
for (const e of [...toUpdate, ...toInstall]) {
|
|
6492
|
+
const batch = batches.get(e.source) || [];
|
|
6493
|
+
batch.push(e);
|
|
6494
|
+
batches.set(e.source, batch);
|
|
6495
|
+
}
|
|
6496
|
+
for (const [source, entries] of batches) {
|
|
6497
|
+
const label = entries.map((e) => sanitizeMetadata(e.name)).join(", ");
|
|
6498
|
+
console.log(`${TEXT$1}Installing from ${sanitizeMetadata(source)}: ${label}${RESET$1}`);
|
|
6499
|
+
setStep(`update:install:${source}`);
|
|
6500
|
+
installFromSource(cliEntry, formatSourceInput(source, refBySource.get(source)), entries.map((e) => e.name));
|
|
6501
|
+
}
|
|
6502
|
+
const after = await readLocalLock(cwd);
|
|
6503
|
+
const isNew = new Set(toInstall.map((e) => e.name));
|
|
6504
|
+
for (const e of [...toUpdate, ...toInstall]) {
|
|
6505
|
+
const safeName = sanitizeMetadata(e.name);
|
|
6506
|
+
const entry = after.skills[e.name];
|
|
6507
|
+
const expected = hashes.get(e.name)?.catalogHash;
|
|
6508
|
+
if (!(isNew.has(e.name) ? Boolean(entry) : Boolean(entry && (!expected || entry.computedHash === expected)))) {
|
|
6509
|
+
result.failCount++;
|
|
6510
|
+
console.log(` ${DIM$1}✗ Failed to ${isNew.has(e.name) ? "install" : "update"} ${safeName}${RESET$1}`);
|
|
6511
|
+
} else if (isNew.has(e.name)) {
|
|
6512
|
+
result.installedCount++;
|
|
6513
|
+
console.log(` ${TEXT$1}✓${RESET$1} Installed ${safeName}`);
|
|
6514
|
+
} else {
|
|
6515
|
+
result.successCount++;
|
|
6516
|
+
console.log(` ${TEXT$1}✓${RESET$1} Updated ${safeName}`);
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
return result;
|
|
6520
|
+
}
|
|
6259
6521
|
function printLegacyProjectSkills(legacy) {
|
|
6260
6522
|
if (legacy.length === 0) return;
|
|
6261
6523
|
console.log();
|
|
@@ -6269,14 +6531,18 @@ function printLegacyProjectSkills(legacy) {
|
|
|
6269
6531
|
async function syncAgentsForScope(isGlobal, options) {
|
|
6270
6532
|
const result = await updateAgents({
|
|
6271
6533
|
global: isGlobal,
|
|
6272
|
-
names: options.skills
|
|
6534
|
+
names: options.skills,
|
|
6535
|
+
dryRun: options.dryRun
|
|
6273
6536
|
});
|
|
6274
6537
|
const label = isGlobal ? "global" : "project";
|
|
6275
6538
|
if (result.catalogUnavailable) {
|
|
6276
6539
|
console.log(` ${DIM$1}✗ Could not read the subagent catalog — skipped ${label} subagents${RESET$1}`);
|
|
6277
6540
|
return result;
|
|
6278
6541
|
}
|
|
6279
|
-
for (const name of result.updated)
|
|
6542
|
+
for (const name of result.updated) {
|
|
6543
|
+
const verb = options.dryRun ? "Would sync" : "Synced";
|
|
6544
|
+
console.log(` ${TEXT$1}✓${RESET$1} ${verb} ${sanitizeMetadata(name)}`);
|
|
6545
|
+
}
|
|
6280
6546
|
for (const name of result.unchanged) console.log(` ${TEXT$1}✓${RESET$1} ${sanitizeMetadata(name)} ${DIM$1}is up to date${RESET$1}`);
|
|
6281
6547
|
for (const name of result.failed) console.log(` ${DIM$1}✗ Failed to sync ${sanitizeMetadata(name)}${RESET$1}`);
|
|
6282
6548
|
if (result.localOnly.length > 0) {
|
|
@@ -6301,6 +6567,8 @@ async function runUpdate(args = []) {
|
|
|
6301
6567
|
let totalFail = 0;
|
|
6302
6568
|
let totalFound = 0;
|
|
6303
6569
|
let totalUpToDate = 0;
|
|
6570
|
+
let totalInstalled = 0;
|
|
6571
|
+
let totalRebaselined = 0;
|
|
6304
6572
|
if (scope === "global" || scope === "both") {
|
|
6305
6573
|
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Global Skills${RESET$1}`);
|
|
6306
6574
|
setStep("update:global-skills");
|
|
@@ -6313,15 +6581,17 @@ async function runUpdate(args = []) {
|
|
|
6313
6581
|
if (scope === "project" || scope === "both") {
|
|
6314
6582
|
if (scope === "both" && !options.skills) console.log(`${BOLD$1}Project Skills${RESET$1}`);
|
|
6315
6583
|
setStep("update:project-skills");
|
|
6316
|
-
const { successCount, failCount, foundCount, upToDateCount } = await updateProjectSkills(options);
|
|
6584
|
+
const { successCount, failCount, foundCount, upToDateCount, installedCount, rebaselinedCount } = await updateProjectSkills(options);
|
|
6317
6585
|
totalUpToDate += upToDateCount;
|
|
6586
|
+
totalInstalled += installedCount;
|
|
6587
|
+
totalRebaselined += rebaselinedCount;
|
|
6318
6588
|
totalSuccess += successCount;
|
|
6319
6589
|
totalFail += failCount;
|
|
6320
6590
|
totalFound += foundCount;
|
|
6321
6591
|
}
|
|
6322
6592
|
console.log();
|
|
6323
6593
|
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}`);
|
|
6594
|
+
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
6595
|
setStep("update:sync-agents");
|
|
6326
6596
|
const agentScopes = scope === "both" ? [true, false] : scope === "global" ? [true] : [false];
|
|
6327
6597
|
let agentsSynced = 0;
|
|
@@ -6340,8 +6610,13 @@ async function runUpdate(args = []) {
|
|
|
6340
6610
|
totalUpToDate += agentsUnchanged;
|
|
6341
6611
|
if (options.skills && totalFound === 0) console.log(`${DIM$1}No installed skills found matching: ${options.skills.join(", ")}${RESET$1}`);
|
|
6342
6612
|
console.log();
|
|
6343
|
-
|
|
6344
|
-
|
|
6613
|
+
const parts = [];
|
|
6614
|
+
if (totalSuccess > 0) parts.push(`${totalSuccess} ${options.dryRun ? "to update" : "updated"}`);
|
|
6615
|
+
if (totalInstalled > 0) parts.push(`${totalInstalled} installed`);
|
|
6616
|
+
if (totalRebaselined > 0) parts.push(`${totalRebaselined} already current (lock refreshed)`);
|
|
6617
|
+
if (totalUpToDate - totalRebaselined > 0) parts.push(`${totalUpToDate - totalRebaselined} up to date`);
|
|
6618
|
+
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}`);
|
|
6619
|
+
else if (parts.length > 0) console.log(`${TEXT$1}✓ ${parts.join(" · ")}${RESET$1}`);
|
|
6345
6620
|
if (totalFail > 0) {
|
|
6346
6621
|
recordFailure(`Failed to update ${totalFail} item(s)`);
|
|
6347
6622
|
console.log(`${DIM$1}Failed to update ${totalFail} item(s)${RESET$1}`);
|
|
@@ -10236,14 +10511,17 @@ ${BOLD}Troubleshooting:${RESET}
|
|
|
10236
10511
|
add, update, rules add, agents add or onboarding fails.
|
|
10237
10512
|
|
|
10238
10513
|
${BOLD}Updates:${RESET}
|
|
10239
|
-
update [skills...]
|
|
10240
|
-
|
|
10241
|
-
|
|
10514
|
+
update [skills...] Compare installed skills with their source, print a plan
|
|
10515
|
+
(modified / new / deleted / up to date) and apply only what
|
|
10516
|
+
changed (alias: upgrade). Also syncs ALL installed subagents
|
|
10517
|
+
with the catalog, overwriting local edits.
|
|
10242
10518
|
|
|
10243
10519
|
${BOLD}Update Options:${RESET}
|
|
10244
10520
|
-g, --global Update global skills only
|
|
10245
10521
|
-p, --project Update project skills only
|
|
10246
10522
|
-y, --yes Skip scope prompt (auto-detect: project if in a project, else global)
|
|
10523
|
+
--dry-run Print the plan and change nothing
|
|
10524
|
+
--force Overwrite skills edited locally that also changed upstream
|
|
10247
10525
|
|
|
10248
10526
|
${BOLD}Project:${RESET}
|
|
10249
10527
|
restore Restore skills from skills-lock.json
|
|
@@ -10307,6 +10585,7 @@ ${BOLD}Examples:${RESET}
|
|
|
10307
10585
|
${DIM}$${RESET} pruff-skills update
|
|
10308
10586
|
${DIM}$${RESET} pruff-skills update <skill> ${DIM}# update a single skill${RESET}
|
|
10309
10587
|
${DIM}$${RESET} pruff-skills update -g ${DIM}# update global skills only${RESET}
|
|
10588
|
+
${DIM}$${RESET} pruff-skills update --dry-run ${DIM}# show what would change${RESET}
|
|
10310
10589
|
${DIM}$${RESET} pruff-skills restore ${DIM}# restore from skills-lock.json${RESET}
|
|
10311
10590
|
${DIM}$${RESET} pruff-skills init my-skill
|
|
10312
10591
|
${DIM}$${RESET} pruff-skills sync ${DIM}# sync from node_modules${RESET}
|