skillpass 0.3.0 → 0.4.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/LICENSE +21 -0
- package/README.md +15 -5
- package/dist/cli.js +329 -266
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Brad Traversy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -20,10 +20,10 @@ Requires Node 20 or newer. No runtime dependencies.
|
|
|
20
20
|
## Commands
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
skillpass search [query] [--target <tool>] [--category <slug>] [--packs] [--json]
|
|
23
|
+
skillpass search [query] [--ai] [--target <tool>] [--category <slug>] [--packs] [--json]
|
|
24
24
|
skillpass scan <path> [--json]
|
|
25
25
|
skillpass report <slug>[@version] [--json]
|
|
26
|
-
skillpass add <slug>[@version] [--target <tool
|
|
26
|
+
skillpass add <slug>[@version] [--target <tool>... [--global] | --dir <path>] [--yes]
|
|
27
27
|
skillpass remove <slug> [--target <tool> [--global] | --dir <path>]
|
|
28
28
|
skillpass update <slug>[@version] [--target <tool> [--global]] [--yes]
|
|
29
29
|
skillpass outdated
|
|
@@ -38,6 +38,10 @@ skillpass --version | --help
|
|
|
38
38
|
`--target claude-code`, `--category security-review`, or `--packs` for
|
|
39
39
|
multi-skill workflow packs.
|
|
40
40
|
|
|
41
|
+
`skillpass search --ai "turn a video into an article"` searches by meaning
|
|
42
|
+
instead of by keyword, through the directory's free AI search. Results come
|
|
43
|
+
back in relevance order and the same filters apply.
|
|
44
|
+
|
|
41
45
|
### Inspect
|
|
42
46
|
|
|
43
47
|
`skillpass report <slug>` prints the version's Skill Passport before you
|
|
@@ -52,9 +56,15 @@ risk, then downloads the validated snapshot and re-verifies every byte against
|
|
|
52
56
|
the pinned source hash before anything touches disk. Installs are atomic - a
|
|
53
57
|
failed write leaves nothing behind.
|
|
54
58
|
|
|
55
|
-
- `--target
|
|
56
|
-
|
|
57
|
-
-
|
|
59
|
+
- `--target <tool>` installs into the tool's skills folder, or its user-level
|
|
60
|
+
folder with `--global`:
|
|
61
|
+
- `claude-code`: `.claude/skills/` and `~/.claude/skills/`
|
|
62
|
+
- `agents`: the shared `.agents/skills/` and `~/.agents/skills/` that
|
|
63
|
+
`codex`, `cursor`, `windsurf`, `github-copilot`, `gemini-cli`, and
|
|
64
|
+
`opencode` all read; any of those names resolves to the same folders
|
|
65
|
+
- `cline`: `.cline/skills/` and `~/.cline/skills/`
|
|
66
|
+
- Repeat `--target` to install into several tools in one pass with one
|
|
67
|
+
pre-flight; targets that share a folder install once
|
|
58
68
|
- `--dir <path>` installs anywhere
|
|
59
69
|
- No flags in a terminal: an interactive picker
|
|
60
70
|
|
package/dist/cli.js
CHANGED
|
@@ -492,16 +492,7 @@ function unzipSync(data, opts) {
|
|
|
492
492
|
}
|
|
493
493
|
|
|
494
494
|
// src/add.ts
|
|
495
|
-
import {
|
|
496
|
-
existsSync as existsSync2,
|
|
497
|
-
mkdirSync as mkdirSync2,
|
|
498
|
-
readdirSync as readdirSync3,
|
|
499
|
-
renameSync,
|
|
500
|
-
rmdirSync,
|
|
501
|
-
rmSync,
|
|
502
|
-
statSync as statSync3,
|
|
503
|
-
writeFileSync as writeFileSync2
|
|
504
|
-
} from "node:fs";
|
|
495
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync3, renameSync, rmdirSync, rmSync, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
505
496
|
import { dirname, isAbsolute, join as join4, resolve as resolve2 } from "node:path";
|
|
506
497
|
|
|
507
498
|
// ../validator/src/load.ts
|
|
@@ -15348,14 +15339,7 @@ var aiReviewSchema = external_exports.object({
|
|
|
15348
15339
|
|
|
15349
15340
|
// ../skill-schema/src/submission.ts
|
|
15350
15341
|
var SUBMISSION_SOURCE_TYPES = ["github_url", "zip"];
|
|
15351
|
-
var SUBMISSION_STATUSES = [
|
|
15352
|
-
"draft",
|
|
15353
|
-
"validating",
|
|
15354
|
-
"passed",
|
|
15355
|
-
"warning",
|
|
15356
|
-
"failed",
|
|
15357
|
-
"published"
|
|
15358
|
-
];
|
|
15342
|
+
var SUBMISSION_STATUSES = ["draft", "validating", "passed", "warning", "failed", "published"];
|
|
15359
15343
|
var submissionSourceTypeSchema = external_exports.enum(SUBMISSION_SOURCE_TYPES);
|
|
15360
15344
|
var submissionStatusSchema = external_exports.enum(SUBMISSION_STATUSES);
|
|
15361
15345
|
var MAX_ZIP_BYTES = 10 * 1024 * 1024;
|
|
@@ -15716,6 +15700,13 @@ function readManifest(files) {
|
|
|
15716
15700
|
return { state: "ok", data: result.data };
|
|
15717
15701
|
}
|
|
15718
15702
|
var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
|
|
15703
|
+
function readDeclaredVersion(frontmatter) {
|
|
15704
|
+
const top = /^version:\s*["']?([^"'\s]+)["']?\s*$/m.exec(frontmatter);
|
|
15705
|
+
if (top) return top[1];
|
|
15706
|
+
const block = /^metadata:\s*\n((?:[ \t]+\S.*(?:\n|$))*)/m.exec(frontmatter);
|
|
15707
|
+
const nested = block && /^[ \t]+version:\s*["']?([^"'\s]+)["']?\s*$/m.exec(block[1]);
|
|
15708
|
+
return nested ? nested[1] : void 0;
|
|
15709
|
+
}
|
|
15719
15710
|
function readSkillMeta(content) {
|
|
15720
15711
|
const out = {};
|
|
15721
15712
|
const fm = FRONTMATTER_RE.exec(content);
|
|
@@ -15743,6 +15734,8 @@ function readSkillMeta(content) {
|
|
|
15743
15734
|
out[key] = parts.join(" ").replace(/^["']|["']$/g, "");
|
|
15744
15735
|
}
|
|
15745
15736
|
}
|
|
15737
|
+
const version2 = readDeclaredVersion(fm[1]);
|
|
15738
|
+
if (version2) out.version = version2;
|
|
15746
15739
|
}
|
|
15747
15740
|
if (!out.description) {
|
|
15748
15741
|
const prose = firstParagraph(body);
|
|
@@ -15792,6 +15785,7 @@ function inferManifest(files, fallbackName) {
|
|
|
15792
15785
|
return {
|
|
15793
15786
|
schemaVersion: "0.1",
|
|
15794
15787
|
name,
|
|
15788
|
+
...meta3.version && SEMVER_RE.test(meta3.version) ? { version: meta3.version } : {},
|
|
15795
15789
|
description: meta3.description ?? name,
|
|
15796
15790
|
targets: inferTargets(files),
|
|
15797
15791
|
permissions,
|
|
@@ -15826,9 +15820,7 @@ function readmeProse(files) {
|
|
|
15826
15820
|
return readme ? firstParagraph(readme.content) : void 0;
|
|
15827
15821
|
}
|
|
15828
15822
|
function inferNestedManifest(files, fallbackName) {
|
|
15829
|
-
const members = collectMemberFiles(files).sort(
|
|
15830
|
-
(a, b) => a.folder < b.folder ? -1 : a.folder > b.folder ? 1 : 0
|
|
15831
|
-
);
|
|
15823
|
+
const members = collectMemberFiles(files).sort((a, b) => a.folder < b.folder ? -1 : a.folder > b.folder ? 1 : 0);
|
|
15832
15824
|
if (members.length === 0) {
|
|
15833
15825
|
return null;
|
|
15834
15826
|
}
|
|
@@ -15841,10 +15833,7 @@ function inferNestedManifest(files, fallbackName) {
|
|
|
15841
15833
|
let entryName = slugForSkill(meta3.name ?? member.folder);
|
|
15842
15834
|
if (used.has(entryName)) entryName = slugForSkill(member.folder);
|
|
15843
15835
|
used.add(entryName);
|
|
15844
|
-
const targets = member.plainPath ? inferTargets(files) : [
|
|
15845
|
-
...member.claudePath ? ["claude-code"] : [],
|
|
15846
|
-
...member.agentsPath ? ["codex"] : []
|
|
15847
|
-
];
|
|
15836
|
+
const targets = member.plainPath ? inferTargets(files) : [...member.claudePath ? ["claude-code"] : [], ...member.agentsPath ? ["codex"] : []];
|
|
15848
15837
|
for (const target of targets) packTargets.add(target);
|
|
15849
15838
|
return {
|
|
15850
15839
|
name: entryName,
|
|
@@ -16031,10 +16020,12 @@ var CREDENTIAL_PATTERNS = [
|
|
|
16031
16020
|
},
|
|
16032
16021
|
{
|
|
16033
16022
|
code: "credential-harvesting",
|
|
16034
|
-
// The
|
|
16035
|
-
//
|
|
16023
|
+
// The verb has to sit where an instruction puts it (line or sentence
|
|
16024
|
+
// start, or after then/and/to/should/must): narrative like "timing leaks
|
|
16025
|
+
// secret bits" has a noun subject and does not fire. The ::/. lookbehinds
|
|
16026
|
+
// skip code identifiers (`Box::leak(secret)`, `.leak(`).
|
|
16036
16027
|
pattern: new RegExp(
|
|
16037
|
-
String.raw`(?<!\b(?:never|not|don'?t|avoids?|prevents?|stops?)\s)(?<!::)(?<!\.)
|
|
16028
|
+
String.raw`(?:^[-*\d.)\s]*|[.:;!?]\s+|\b(?:then|and|or|to|should|must|now|please)\s+)(?<!\b(?:never|not|don'?t|avoids?|prevents?|stops?)\s)(?<!::)(?<!\.)(?:exfiltrate|steal|harvest|dump|leak)s?\b[^.\n]{0,60}?${SECRET_NOUN}`,
|
|
16038
16029
|
"i"
|
|
16039
16030
|
),
|
|
16040
16031
|
message: "matches a credential-exfiltration phrase"
|
|
@@ -16048,14 +16039,28 @@ var CREDENTIAL_PATTERNS = [
|
|
|
16048
16039
|
message: "matches a credential-file network-transmission phrase"
|
|
16049
16040
|
}
|
|
16050
16041
|
];
|
|
16051
|
-
var advisory = (rows) => rows.map((row2) => ({ ...row2, severity: "warning" }));
|
|
16042
|
+
var advisory = (rows, mentionAware = false) => rows.map((row2) => ({ ...row2, severity: "warning", mentionAware }));
|
|
16052
16043
|
var ALL_ROWS = [
|
|
16053
16044
|
...SECRET_PATTERNS,
|
|
16054
|
-
...advisory(INJECTION_PATTERNS),
|
|
16045
|
+
...advisory(INJECTION_PATTERNS, true),
|
|
16055
16046
|
...advisory(DANGEROUS_PATTERNS),
|
|
16056
16047
|
...advisory(MALWARE_PATTERNS),
|
|
16057
|
-
...advisory(CREDENTIAL_PATTERNS)
|
|
16048
|
+
...advisory(CREDENTIAL_PATTERNS, true)
|
|
16058
16049
|
];
|
|
16050
|
+
var MENTION_LEAD_RE = /\b(?:flags?|flagged|detects?|detecting|look(?:ing)?\s+for|watch\s+for|check(?:ing)?\s+for|signs?\s+of|indicators?\s+of|evidence\s+of|red\s+flags?|examples?|e\.g\.|such\s+as|attempts?\s+to|designed\s+to|(?:code|scripts?|files?|comments?|text|instructions?)\s+that)\b/i;
|
|
16051
|
+
var TRANSMIT_VERB_RE = /\b(?:post|send|curl|transmit|exfiltrate)s?\b/i;
|
|
16052
|
+
function matchAnchor(line, match) {
|
|
16053
|
+
if (match[0]) return match.index;
|
|
16054
|
+
const verb = line.search(TRANSMIT_VERB_RE);
|
|
16055
|
+
return verb < 0 ? line.length : verb;
|
|
16056
|
+
}
|
|
16057
|
+
function isMention(line, anchor, inFence) {
|
|
16058
|
+
if (inFence || line.trimStart().startsWith("|")) return true;
|
|
16059
|
+
const before = line.slice(0, anchor);
|
|
16060
|
+
const inside = (mark) => (before.split(mark).length - 1) % 2 === 1;
|
|
16061
|
+
return inside('"') || inside("`") || MENTION_LEAD_RE.test(before);
|
|
16062
|
+
}
|
|
16063
|
+
var FENCE_RE = /^\s*(?:```|~~~)/;
|
|
16059
16064
|
var DOCUMENTED_PLACEHOLDERS = ["AKIAIOSFODNN7EXAMPLE", "AKIAI44QH8DHBEXAMPLE"];
|
|
16060
16065
|
var withoutPlaceholders = (line) => DOCUMENTED_PLACEHOLDERS.reduce((l, p) => l.replaceAll(p, " "), line);
|
|
16061
16066
|
var REDACTIONS = ALL_ROWS.filter((row2) => row2.redact).map(
|
|
@@ -16065,9 +16070,17 @@ var redactLine = (line) => REDACTIONS.reduce((l, re) => l.replace(re, "[redacted
|
|
|
16065
16070
|
var contentRule = (pkg) => {
|
|
16066
16071
|
const findings = [];
|
|
16067
16072
|
for (const file2 of pkg.files) {
|
|
16073
|
+
let inFence = false;
|
|
16068
16074
|
file2.content.split("\n").forEach((rawLine, i2) => {
|
|
16075
|
+
if (FENCE_RE.test(rawLine)) {
|
|
16076
|
+
inFence = !inFence;
|
|
16077
|
+
return;
|
|
16078
|
+
}
|
|
16069
16079
|
const line = withoutPlaceholders(rawLine);
|
|
16070
|
-
const hits = ALL_ROWS.filter((row2) =>
|
|
16080
|
+
const hits = ALL_ROWS.filter((row2) => {
|
|
16081
|
+
const match = row2.pattern.exec(line);
|
|
16082
|
+
return match !== null && !(row2.mentionAware && isMention(line, matchAnchor(line, match), inFence));
|
|
16083
|
+
});
|
|
16071
16084
|
if (hits.length === 0) return;
|
|
16072
16085
|
const snippet = redactLine(line).trim();
|
|
16073
16086
|
for (const row2 of hits) {
|
|
@@ -16225,10 +16238,23 @@ async function getParsed(fetchImpl, url2, schema) {
|
|
|
16225
16238
|
try {
|
|
16226
16239
|
body = await res.json();
|
|
16227
16240
|
} catch {
|
|
16228
|
-
return {
|
|
16241
|
+
return {
|
|
16242
|
+
ok: false,
|
|
16243
|
+
notFound: false,
|
|
16244
|
+
message: `unexpected response from the API (${res.status})`,
|
|
16245
|
+
status: res.status
|
|
16246
|
+
};
|
|
16229
16247
|
}
|
|
16230
16248
|
if (typeof body !== "object" || body === null || !("success" in body) || body.success !== true) {
|
|
16231
|
-
|
|
16249
|
+
const error51 = body?.error;
|
|
16250
|
+
const apiError = typeof error51 === "string" ? { apiError: error51 } : {};
|
|
16251
|
+
return res.status === 404 ? { ok: false, notFound: true, message: "not found", status: 404, ...apiError } : {
|
|
16252
|
+
ok: false,
|
|
16253
|
+
notFound: false,
|
|
16254
|
+
message: `the API returned an error (${res.status})`,
|
|
16255
|
+
status: res.status,
|
|
16256
|
+
...apiError
|
|
16257
|
+
};
|
|
16232
16258
|
}
|
|
16233
16259
|
const parsed = schema.safeParse(body.data);
|
|
16234
16260
|
if (!parsed.success) {
|
|
@@ -16458,10 +16484,7 @@ function renderPreflightReport(detail, preflight, st = PLAIN) {
|
|
|
16458
16484
|
...renderDiff(preflight.diff)
|
|
16459
16485
|
];
|
|
16460
16486
|
if (preflight.blocked) {
|
|
16461
|
-
lines.push(
|
|
16462
|
-
"",
|
|
16463
|
-
st.red(`BLOCKED: ${preflight.blockedReason ?? "this version cannot be downloaded"}`)
|
|
16464
|
-
);
|
|
16487
|
+
lines.push("", st.red(`BLOCKED: ${preflight.blockedReason ?? "this version cannot be downloaded"}`));
|
|
16465
16488
|
}
|
|
16466
16489
|
return lines;
|
|
16467
16490
|
}
|
|
@@ -16484,37 +16507,39 @@ function renderDiff(diff) {
|
|
|
16484
16507
|
// src/targets.ts
|
|
16485
16508
|
import { homedir } from "node:os";
|
|
16486
16509
|
import { join as join3, resolve } from "node:path";
|
|
16487
|
-
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
|
|
16510
|
+
var SHARED = { project: join3(".agents", "skills"), global: join3(".agents", "skills"), layout: "codex" };
|
|
16511
|
+
var INSTALL_TOOLS = {
|
|
16512
|
+
"claude-code": { project: join3(".claude", "skills"), global: join3(".claude", "skills"), layout: "claude-code" },
|
|
16513
|
+
agents: SHARED,
|
|
16514
|
+
codex: SHARED,
|
|
16515
|
+
cursor: SHARED,
|
|
16516
|
+
windsurf: SHARED,
|
|
16517
|
+
"github-copilot": SHARED,
|
|
16518
|
+
"gemini-cli": SHARED,
|
|
16519
|
+
opencode: SHARED,
|
|
16520
|
+
cline: { project: join3(".cline", "skills"), global: join3(".cline", "skills"), layout: "codex" }
|
|
16521
|
+
};
|
|
16522
|
+
var INSTALL_TOOL_NAMES = Object.keys(INSTALL_TOOLS);
|
|
16523
|
+
function lookupTool(name) {
|
|
16524
|
+
return Object.hasOwn(INSTALL_TOOLS, name) ? INSTALL_TOOLS[name] : void 0;
|
|
16525
|
+
}
|
|
16526
|
+
var MAPPED_TARGETS = TARGETS.filter((t) => lookupTool(t) !== void 0);
|
|
16527
|
+
function layoutTarget(tool) {
|
|
16528
|
+
return lookupTool(tool)?.layout;
|
|
16529
|
+
}
|
|
16530
|
+
function declaresTool(declared, tool) {
|
|
16531
|
+
const layout = layoutTarget(tool);
|
|
16532
|
+
return declared.includes(tool) || layout !== void 0 && declared.includes(layout);
|
|
16492
16533
|
}
|
|
16493
|
-
var MAPPED_TARGETS = Object.keys(installAreas());
|
|
16494
16534
|
function resolveTargetArea(target, global = false, home) {
|
|
16495
|
-
|
|
16496
|
-
|
|
16497
|
-
|
|
16498
|
-
|
|
16499
|
-
};
|
|
16500
|
-
}
|
|
16501
|
-
const area = installAreas(home)[target];
|
|
16502
|
-
if (!area) {
|
|
16503
|
-
return {
|
|
16504
|
-
ok: false,
|
|
16505
|
-
message: `${target} has no standard skills folder yet; use --dir to pick a location`
|
|
16506
|
-
};
|
|
16507
|
-
}
|
|
16508
|
-
if (global) {
|
|
16509
|
-
if (!area.global) {
|
|
16510
|
-
return {
|
|
16511
|
-
ok: false,
|
|
16512
|
-
message: `${target} has no user-level skills folder; install per project instead`
|
|
16513
|
-
};
|
|
16535
|
+
const tool = lookupTool(target);
|
|
16536
|
+
if (!tool) {
|
|
16537
|
+
if (TARGETS.includes(target)) {
|
|
16538
|
+
return { ok: false, message: `${target} has no standard skills folder yet; use --dir to pick a location` };
|
|
16514
16539
|
}
|
|
16515
|
-
return { ok:
|
|
16540
|
+
return { ok: false, message: `unknown target "${target}" (known tools: ${INSTALL_TOOL_NAMES.join(", ")})` };
|
|
16516
16541
|
}
|
|
16517
|
-
return { ok: true, dir:
|
|
16542
|
+
return { ok: true, dir: global ? join3(home ?? homedir(), tool.global) : tool.project };
|
|
16518
16543
|
}
|
|
16519
16544
|
function resolveTargetDir(target, slug, global = false, home) {
|
|
16520
16545
|
const area = resolveTargetArea(target, global, home);
|
|
@@ -16524,24 +16549,48 @@ function mappableDeclaredTargets(declared) {
|
|
|
16524
16549
|
return declared.filter((t) => MAPPED_TARGETS.includes(t));
|
|
16525
16550
|
}
|
|
16526
16551
|
function knownAreas(cwd, home) {
|
|
16527
|
-
const areas =
|
|
16528
|
-
const
|
|
16529
|
-
|
|
16530
|
-
|
|
16531
|
-
|
|
16532
|
-
|
|
16533
|
-
|
|
16534
|
-
|
|
16535
|
-
|
|
16536
|
-
|
|
16537
|
-
|
|
16538
|
-
|
|
16539
|
-
|
|
16540
|
-
|
|
16541
|
-
|
|
16552
|
+
const areas = /* @__PURE__ */ new Map();
|
|
16553
|
+
for (const [name, tool] of Object.entries(INSTALL_TOOLS)) {
|
|
16554
|
+
const scopes = [
|
|
16555
|
+
{ global: false, display: tool.project, dir: resolve(cwd, tool.project) },
|
|
16556
|
+
{ global: true, display: join3(home ?? homedir(), tool.global), dir: join3(home ?? homedir(), tool.global) }
|
|
16557
|
+
];
|
|
16558
|
+
for (const scope of scopes) {
|
|
16559
|
+
const existing = areas.get(scope.dir);
|
|
16560
|
+
if (existing) {
|
|
16561
|
+
if (!existing.tools.includes(name)) existing.tools.push(name);
|
|
16562
|
+
existing.project ||= !scope.global;
|
|
16563
|
+
existing.global ||= scope.global;
|
|
16564
|
+
existing.label = areaLabel(existing);
|
|
16565
|
+
continue;
|
|
16566
|
+
}
|
|
16567
|
+
const area = {
|
|
16568
|
+
tools: [name],
|
|
16569
|
+
layout: tool.layout,
|
|
16570
|
+
project: !scope.global,
|
|
16571
|
+
global: scope.global,
|
|
16572
|
+
label: "",
|
|
16573
|
+
dir: scope.dir
|
|
16574
|
+
};
|
|
16575
|
+
area.label = areaLabel(area, scope.display);
|
|
16576
|
+
areas.set(scope.dir, area);
|
|
16542
16577
|
}
|
|
16543
16578
|
}
|
|
16544
|
-
return areas;
|
|
16579
|
+
return [...areas.values()];
|
|
16580
|
+
}
|
|
16581
|
+
function areaLabel(area, display) {
|
|
16582
|
+
const shown = display ?? area.label.slice(0, area.label.indexOf(" ("));
|
|
16583
|
+
const scopes = [...area.project ? ["project"] : [], ...area.global ? ["user"] : []].join(", ");
|
|
16584
|
+
return `${shown} (${scopes}) - ${area.tools.join(", ")}`;
|
|
16585
|
+
}
|
|
16586
|
+
function resolveArea(target, global, cwd, home) {
|
|
16587
|
+
const resolved = resolveTargetArea(target, global, home);
|
|
16588
|
+
if (!resolved.ok) {
|
|
16589
|
+
return resolved;
|
|
16590
|
+
}
|
|
16591
|
+
const dir = resolve(cwd, resolved.dir);
|
|
16592
|
+
const area = knownAreas(cwd, home).find((a) => a.dir === dir);
|
|
16593
|
+
return area ? { ok: true, area } : { ok: false, message: `no known skills area for ${target}` };
|
|
16545
16594
|
}
|
|
16546
16595
|
|
|
16547
16596
|
// src/add.ts
|
|
@@ -16623,54 +16672,26 @@ function writeTree(files, target) {
|
|
|
16623
16672
|
throw err2;
|
|
16624
16673
|
}
|
|
16625
16674
|
}
|
|
16626
|
-
function
|
|
16627
|
-
const
|
|
16628
|
-
|
|
16629
|
-
|
|
16630
|
-
|
|
16631
|
-
];
|
|
16632
|
-
|
|
16633
|
-
|
|
16634
|
-
|
|
16635
|
-
|
|
16636
|
-
|
|
16637
|
-
|
|
16638
|
-
const global = resolveTargetDir(tool, slug, true);
|
|
16639
|
-
if (global.ok) {
|
|
16640
|
-
choices.push({
|
|
16641
|
-
label: `${tool} user-level skills folder (${global.dir})${note}`,
|
|
16642
|
-
dir: global.dir
|
|
16643
|
-
});
|
|
16644
|
-
}
|
|
16645
|
-
}
|
|
16675
|
+
function orderedAreas(declared, cwd, home) {
|
|
16676
|
+
const areas = knownAreas(cwd, home).map((area) => ({
|
|
16677
|
+
...area,
|
|
16678
|
+
declared: area.tools.some((tool) => declaresTool(declared, tool))
|
|
16679
|
+
}));
|
|
16680
|
+
return [...areas.filter((a) => a.declared), ...areas.filter((a) => !a.declared)];
|
|
16681
|
+
}
|
|
16682
|
+
function installChoices(declared, slug, cwd, home) {
|
|
16683
|
+
const choices = orderedAreas(declared, cwd, home).map((area) => ({
|
|
16684
|
+
label: `${area.label}${area.declared ? "" : " - not declared by this skill"}`,
|
|
16685
|
+
dir: join4(area.dir, slug)
|
|
16686
|
+
}));
|
|
16646
16687
|
choices.push({ label: `current directory (./${slug})`, dir: slug });
|
|
16647
16688
|
return choices;
|
|
16648
16689
|
}
|
|
16649
|
-
function packChoices(declared, slug, count) {
|
|
16650
|
-
const choices =
|
|
16651
|
-
|
|
16652
|
-
|
|
16653
|
-
|
|
16654
|
-
];
|
|
16655
|
-
for (const tool of ordered) {
|
|
16656
|
-
const note = declared.includes(tool) ? "" : " - not declared by this pack";
|
|
16657
|
-
const project = resolveTargetArea(tool);
|
|
16658
|
-
if (project.ok) {
|
|
16659
|
-
choices.push({
|
|
16660
|
-
label: `${tool} skills folder (${project.dir}) - ${count} skills${note}`,
|
|
16661
|
-
tool,
|
|
16662
|
-
global: false
|
|
16663
|
-
});
|
|
16664
|
-
}
|
|
16665
|
-
const global = resolveTargetArea(tool, true);
|
|
16666
|
-
if (global.ok) {
|
|
16667
|
-
choices.push({
|
|
16668
|
-
label: `${tool} user-level skills folder (${global.dir}) - ${count} skills${note}`,
|
|
16669
|
-
tool,
|
|
16670
|
-
global: true
|
|
16671
|
-
});
|
|
16672
|
-
}
|
|
16673
|
-
}
|
|
16690
|
+
function packChoices(declared, slug, count, cwd, home) {
|
|
16691
|
+
const choices = orderedAreas(declared, cwd, home).map((area) => ({
|
|
16692
|
+
label: `${area.label} - ${count} skills${area.declared ? "" : " - not declared by this pack"}`,
|
|
16693
|
+
area
|
|
16694
|
+
}));
|
|
16674
16695
|
choices.push({ label: `current directory (./${slug}, raw pack source)` });
|
|
16675
16696
|
return choices;
|
|
16676
16697
|
}
|
|
@@ -16685,23 +16706,20 @@ async function promptIndex(ask, count, push) {
|
|
|
16685
16706
|
}
|
|
16686
16707
|
async function choosePackLocation(ctx, detail, memberCount, prompt) {
|
|
16687
16708
|
if (prompt) {
|
|
16688
|
-
const choices = packChoices(detail.targets, ctx.slug, memberCount);
|
|
16709
|
+
const choices = packChoices(detail.targets, ctx.slug, memberCount, ctx.cwd, ctx.home);
|
|
16689
16710
|
ctx.push("", "Install location:");
|
|
16690
16711
|
choices.forEach((choice, i2) => ctx.push(` ${i2 + 1}) ${choice.label}`));
|
|
16691
16712
|
const chosen = choices[await promptIndex(prompt, choices.length, ctx.push)];
|
|
16692
|
-
return chosen.
|
|
16713
|
+
return chosen.area ? { area: chosen.area } : { dir: ctx.slug };
|
|
16693
16714
|
}
|
|
16694
16715
|
const mappable = mappableDeclaredTargets(detail.targets);
|
|
16695
16716
|
if (mappable.length > 0) {
|
|
16696
|
-
ctx.push(
|
|
16697
|
-
"",
|
|
16698
|
-
`tip: --target ${mappable[0]} installs the ${memberCount} skills into the tool's skills folder`
|
|
16699
|
-
);
|
|
16717
|
+
ctx.push("", `tip: --target ${mappable[0]} installs the ${memberCount} skills into the tool's skills folder`);
|
|
16700
16718
|
}
|
|
16701
16719
|
return { dir: ctx.slug };
|
|
16702
16720
|
}
|
|
16703
16721
|
async function chooseSingleLocation(ctx, detail, prompt) {
|
|
16704
|
-
const choices = installChoices(detail.targets, ctx.slug);
|
|
16722
|
+
const choices = installChoices(detail.targets, ctx.slug, ctx.cwd, ctx.home);
|
|
16705
16723
|
if (prompt && choices.length > 1) {
|
|
16706
16724
|
ctx.push("", "Install location:");
|
|
16707
16725
|
choices.forEach((choice, i2) => ctx.push(` ${i2 + 1}) ${choice.label}`));
|
|
@@ -16713,20 +16731,15 @@ async function chooseSingleLocation(ctx, detail, prompt) {
|
|
|
16713
16731
|
}
|
|
16714
16732
|
return ctx.slug;
|
|
16715
16733
|
}
|
|
16716
|
-
async function installPack(ctx, members,
|
|
16734
|
+
async function installPack(ctx, members, targets) {
|
|
16717
16735
|
const { push, done, slug, preflight } = ctx;
|
|
16718
|
-
const
|
|
16719
|
-
|
|
16720
|
-
|
|
16736
|
+
const resolved = targets.map(({ area, tool }) => ({ area, tool, ...resolvePackMembers(members, area.layout) }));
|
|
16737
|
+
const unsupported = resolved.find((r) => r.installs.length === 0);
|
|
16738
|
+
if (unsupported) {
|
|
16739
|
+
push("", `error: none of this pack's skills support ${unsupported.tool}`);
|
|
16721
16740
|
return done(2);
|
|
16722
16741
|
}
|
|
16723
|
-
const
|
|
16724
|
-
if (installs.length === 0) {
|
|
16725
|
-
push("", `error: none of this pack's skills support ${tool}`);
|
|
16726
|
-
return done(2);
|
|
16727
|
-
}
|
|
16728
|
-
const areaAbs = resolve2(ctx.cwd, area.dir);
|
|
16729
|
-
const conflicts = installs.map((m) => join4(areaAbs, m.name)).filter(isOccupied);
|
|
16742
|
+
const conflicts = resolved.flatMap((r) => r.installs.map((m) => join4(r.area.dir, m.name))).filter(isOccupied);
|
|
16730
16743
|
if (conflicts.length > 0) {
|
|
16731
16744
|
push(
|
|
16732
16745
|
"",
|
|
@@ -16740,41 +16753,47 @@ async function installPack(ctx, members, tool, global) {
|
|
|
16740
16753
|
push("", `error: ${download.message}`);
|
|
16741
16754
|
return done(2);
|
|
16742
16755
|
}
|
|
16743
|
-
const
|
|
16744
|
-
const missing = plans.find((p) => p.files.length === 0);
|
|
16756
|
+
const planned = resolved.map((r) => ({ ...r, plans: planMembers(r.installs, download.files) }));
|
|
16757
|
+
const missing = planned.flatMap((r) => r.plans).find((p) => p.files.length === 0);
|
|
16745
16758
|
if (missing) {
|
|
16746
16759
|
push("", `error: the snapshot has no files for "${missing.name}"; nothing was installed`);
|
|
16747
16760
|
return done(2);
|
|
16748
16761
|
}
|
|
16749
|
-
for (const
|
|
16750
|
-
|
|
16762
|
+
for (const r of planned) {
|
|
16763
|
+
for (const name of r.skipped) {
|
|
16764
|
+
push("", `note: ${name} does not support ${r.tool}; skipped`);
|
|
16765
|
+
}
|
|
16751
16766
|
}
|
|
16752
|
-
const
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16767
|
+
const landed = [];
|
|
16768
|
+
const describe3 = (dir, names) => planned.length > 1 ? `${dir}: ${names.join(", ")}` : names.join(", ");
|
|
16769
|
+
for (const r of planned) {
|
|
16770
|
+
const written = [];
|
|
16771
|
+
try {
|
|
16772
|
+
for (const plan of r.plans) {
|
|
16773
|
+
writeTree(plan.files, join4(r.area.dir, plan.name));
|
|
16774
|
+
written.push(plan.name);
|
|
16775
|
+
recordReceipt(r.area.dir, plan.name, receiptFor(preflight, slug));
|
|
16776
|
+
}
|
|
16777
|
+
} catch {
|
|
16778
|
+
const before = [...landed, ...written.length > 0 ? [describe3(r.area.dir, written)] : []];
|
|
16779
|
+
const where = planned.length > 1 ? ` in ${r.area.dir}` : "";
|
|
16780
|
+
push(
|
|
16781
|
+
"",
|
|
16782
|
+
`error: could not write ${r.plans[written.length].name}${where}; installed before the failure: ${before.join("; ") || "none"}`
|
|
16783
|
+
);
|
|
16784
|
+
return done(2);
|
|
16758
16785
|
}
|
|
16759
|
-
|
|
16760
|
-
push(
|
|
16761
|
-
"",
|
|
16762
|
-
`error: could not write ${plans[written.length].name}; installed before the failure: ${written.join(", ") || "none"}`
|
|
16763
|
-
);
|
|
16764
|
-
return done(2);
|
|
16786
|
+
landed.push(describe3(r.area.dir, written));
|
|
16787
|
+
push("", `Installed ${written.length} skills to ${r.area.dir}`, ` ${written.join(", ")}`);
|
|
16765
16788
|
}
|
|
16766
|
-
push(
|
|
16767
|
-
"",
|
|
16768
|
-
`Installed ${written.length} skills to ${areaAbs}`,
|
|
16769
|
-
` ${written.join(", ")}`,
|
|
16770
|
-
"Source hash verified against the Skill Passport."
|
|
16771
|
-
);
|
|
16789
|
+
push("Source hash verified against the Skill Passport.");
|
|
16772
16790
|
return done(0);
|
|
16773
16791
|
}
|
|
16774
|
-
async function installSingle(ctx,
|
|
16792
|
+
async function installSingle(ctx, targetDirs) {
|
|
16775
16793
|
const { push, done, slug, preflight } = ctx;
|
|
16776
|
-
const
|
|
16777
|
-
|
|
16794
|
+
const targets = targetDirs.map((dir) => resolve2(ctx.cwd, dir));
|
|
16795
|
+
for (const target of targets) {
|
|
16796
|
+
if (!existsSync2(target)) continue;
|
|
16778
16797
|
if (!statSync3(target).isDirectory()) {
|
|
16779
16798
|
push("", `error: target ${target} already exists and is not a directory`);
|
|
16780
16799
|
return done(2);
|
|
@@ -16789,33 +16808,40 @@ async function installSingle(ctx, targetDir) {
|
|
|
16789
16808
|
push("", `error: ${download.message}`);
|
|
16790
16809
|
return done(2);
|
|
16791
16810
|
}
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16811
|
+
const areas = knownAreas(ctx.cwd, ctx.home);
|
|
16812
|
+
const landed = [];
|
|
16813
|
+
for (const target of targets) {
|
|
16814
|
+
try {
|
|
16815
|
+
writeTree(download.files, target);
|
|
16816
|
+
} catch {
|
|
16817
|
+
push(
|
|
16818
|
+
"",
|
|
16819
|
+
landed.length === 0 ? "error: could not write the install; nothing was installed" : `error: could not write ${target}; installed before the failure: ${landed.join(", ")}`
|
|
16820
|
+
);
|
|
16821
|
+
return done(2);
|
|
16822
|
+
}
|
|
16823
|
+
landed.push(target);
|
|
16824
|
+
const area = areas.find((a) => a.dir === dirname(target));
|
|
16825
|
+
if (area) {
|
|
16826
|
+
recordReceipt(area.dir, slug, receiptFor(preflight));
|
|
16827
|
+
}
|
|
16828
|
+
push("", `Installed ${download.files.length} file(s) to ${target}`);
|
|
16801
16829
|
}
|
|
16802
|
-
push(
|
|
16803
|
-
"",
|
|
16804
|
-
`Installed ${download.files.length} file(s) to ${target}`,
|
|
16805
|
-
"Source hash verified against the Skill Passport."
|
|
16806
|
-
);
|
|
16830
|
+
push("Source hash verified against the Skill Passport.");
|
|
16807
16831
|
return done(0);
|
|
16808
16832
|
}
|
|
16809
16833
|
async function runAdd(ref, opts = {}) {
|
|
16810
16834
|
const { push, done } = createOutput(opts.emit);
|
|
16811
|
-
|
|
16835
|
+
const tools = [...new Set([opts.target ?? []].flat())];
|
|
16836
|
+
if (tools.length > 0 && opts.dir) {
|
|
16812
16837
|
push(TARGET_OR_DIR);
|
|
16813
16838
|
return done(2);
|
|
16814
16839
|
}
|
|
16815
|
-
if (opts.global &&
|
|
16840
|
+
if (opts.global && tools.length === 0) {
|
|
16816
16841
|
push(GLOBAL_NEEDS_TARGET);
|
|
16817
16842
|
return done(2);
|
|
16818
16843
|
}
|
|
16844
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
16819
16845
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
16820
16846
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
16821
16847
|
const fetched = await fetchPreflight(fetchImpl, apiUrl, ref);
|
|
@@ -16830,21 +16856,21 @@ async function runAdd(ref, opts = {}) {
|
|
|
16830
16856
|
}
|
|
16831
16857
|
const members = detail.packMembers ?? [];
|
|
16832
16858
|
const isPack = members.length > 0;
|
|
16833
|
-
|
|
16834
|
-
|
|
16835
|
-
|
|
16836
|
-
const resolved = isPack ? resolveTargetArea(opts.target, opts.global) : resolveTargetDir(opts.target, slug, opts.global);
|
|
16859
|
+
const targets = [];
|
|
16860
|
+
for (const tool of tools) {
|
|
16861
|
+
const resolved = resolveArea(tool, opts.global ?? false, cwd, opts.home);
|
|
16837
16862
|
if (!resolved.ok) {
|
|
16838
16863
|
push("", `error: ${resolved.message}`);
|
|
16839
16864
|
return done(2);
|
|
16840
16865
|
}
|
|
16841
|
-
|
|
16842
|
-
|
|
16843
|
-
|
|
16844
|
-
|
|
16866
|
+
const shared = targets.find((t) => t.area.dir === resolved.area.dir);
|
|
16867
|
+
if (shared) {
|
|
16868
|
+
push("", `note: ${tool} and ${shared.tool} share ${resolved.area.dir}; installing once`);
|
|
16869
|
+
continue;
|
|
16845
16870
|
}
|
|
16846
|
-
|
|
16847
|
-
|
|
16871
|
+
targets.push({ area: resolved.area, tool });
|
|
16872
|
+
if (!declaresTool(detail.targets, tool)) {
|
|
16873
|
+
push("", `warning: this skill does not declare ${tool} as a target`);
|
|
16848
16874
|
}
|
|
16849
16875
|
}
|
|
16850
16876
|
const confirmed = await confirmRisk(
|
|
@@ -16855,30 +16881,28 @@ async function runAdd(ref, opts = {}) {
|
|
|
16855
16881
|
push
|
|
16856
16882
|
);
|
|
16857
16883
|
if (!confirmed) return done(2);
|
|
16858
|
-
const ctx = {
|
|
16859
|
-
|
|
16860
|
-
|
|
16861
|
-
|
|
16862
|
-
|
|
16863
|
-
|
|
16864
|
-
|
|
16865
|
-
|
|
16866
|
-
|
|
16867
|
-
if (isPack && pack === void 0 && targetDir === void 0) {
|
|
16868
|
-
const location = await choosePackLocation(ctx, detail, members.length, opts.promptImpl);
|
|
16869
|
-
if ("tool" in location) pack = location;
|
|
16870
|
-
else targetDir = location.dir;
|
|
16871
|
-
}
|
|
16872
|
-
if (pack !== void 0) {
|
|
16873
|
-
return installPack(ctx, members, pack.tool, pack.global);
|
|
16874
|
-
}
|
|
16875
|
-
if (isPack && opts.dir) {
|
|
16884
|
+
const ctx = { push, done, fetchImpl, apiUrl, cwd, home: opts.home, slug, preflight };
|
|
16885
|
+
if (isPack) {
|
|
16886
|
+
if (targets.length > 0) {
|
|
16887
|
+
return installPack(ctx, members, targets);
|
|
16888
|
+
}
|
|
16889
|
+
if (opts.dir === void 0) {
|
|
16890
|
+
const location = await choosePackLocation(ctx, detail, members.length, opts.promptImpl);
|
|
16891
|
+
return "area" in location ? installPack(ctx, members, [{ area: location.area, tool: location.area.tools[0] }]) : installSingle(ctx, [location.dir]);
|
|
16892
|
+
}
|
|
16876
16893
|
push(
|
|
16877
16894
|
"",
|
|
16878
16895
|
`note: --dir installs the raw pack source; --target <tool> installs the ${members.length} skills individually`
|
|
16879
16896
|
);
|
|
16897
|
+
return installSingle(ctx, [opts.dir]);
|
|
16880
16898
|
}
|
|
16881
|
-
|
|
16899
|
+
if (targets.length > 0) {
|
|
16900
|
+
return installSingle(
|
|
16901
|
+
ctx,
|
|
16902
|
+
targets.map((t) => join4(t.area.dir, slug))
|
|
16903
|
+
);
|
|
16904
|
+
}
|
|
16905
|
+
return installSingle(ctx, [opts.dir ?? await chooseSingleLocation(ctx, detail, opts.promptImpl)]);
|
|
16882
16906
|
}
|
|
16883
16907
|
|
|
16884
16908
|
// src/list.ts
|
|
@@ -16923,11 +16947,7 @@ async function identifyByHash(fetchImpl, apiUrl, slug, dir) {
|
|
|
16923
16947
|
} catch {
|
|
16924
16948
|
return void 0;
|
|
16925
16949
|
}
|
|
16926
|
-
const detail = await getParsed(
|
|
16927
|
-
fetchImpl,
|
|
16928
|
-
`${apiUrl}/skills/${encodeURIComponent(slug)}`,
|
|
16929
|
-
publicSkillDetailSchema
|
|
16930
|
-
);
|
|
16950
|
+
const detail = await getParsed(fetchImpl, `${apiUrl}/skills/${encodeURIComponent(slug)}`, publicSkillDetailSchema);
|
|
16931
16951
|
if (!detail.ok) {
|
|
16932
16952
|
return void 0;
|
|
16933
16953
|
}
|
|
@@ -16972,9 +16992,7 @@ async function runOutdated(opts = {}) {
|
|
|
16972
16992
|
}
|
|
16973
16993
|
seenPacks.add(receipt.pack.slug);
|
|
16974
16994
|
tracked += 1;
|
|
16975
|
-
const members = Object.values(receipts).filter(
|
|
16976
|
-
(r) => r.pack?.slug === receipt.pack?.slug
|
|
16977
|
-
).length;
|
|
16995
|
+
const members = Object.values(receipts).filter((r) => r.pack?.slug === receipt.pack?.slug).length;
|
|
16978
16996
|
const latest = latestBySlug.get(receipt.pack.slug)?.version;
|
|
16979
16997
|
if (latest === void 0) {
|
|
16980
16998
|
rows.push(` ${receipt.pack.slug} ${receipt.pack.version} not in the directory`);
|
|
@@ -16982,9 +17000,7 @@ async function runOutdated(opts = {}) {
|
|
|
16982
17000
|
rows.push(` ${receipt.pack.slug} ${receipt.pack.version} current (pack, ${members} skills)`);
|
|
16983
17001
|
} else {
|
|
16984
17002
|
outdatedCount += 1;
|
|
16985
|
-
rows.push(
|
|
16986
|
-
` ${receipt.pack.slug} ${receipt.pack.version} -> ${latest} (pack, ${members} skills)`
|
|
16987
|
-
);
|
|
17003
|
+
rows.push(` ${receipt.pack.slug} ${receipt.pack.version} -> ${latest} (pack, ${members} skills)`);
|
|
16988
17004
|
}
|
|
16989
17005
|
continue;
|
|
16990
17006
|
}
|
|
@@ -17060,7 +17076,7 @@ function runRemove(slug, opts = {}) {
|
|
|
17060
17076
|
if (!opts.dir) {
|
|
17061
17077
|
let areas = knownAreas(cwd, opts.home);
|
|
17062
17078
|
if (opts.target) {
|
|
17063
|
-
areas = areas.filter((a) => a.
|
|
17079
|
+
areas = areas.filter((a) => a.tools.includes(opts.target) && (opts.global ? a.global : a.project));
|
|
17064
17080
|
}
|
|
17065
17081
|
const packHits = areas.map((area) => ({
|
|
17066
17082
|
area,
|
|
@@ -17082,10 +17098,7 @@ function runRemove(slug, opts = {}) {
|
|
|
17082
17098
|
removeReceipt(area.dir, name);
|
|
17083
17099
|
}
|
|
17084
17100
|
return {
|
|
17085
|
-
lines: [
|
|
17086
|
-
`Removed pack ${slug} (${members.length} skills) from ${area.dir}`,
|
|
17087
|
-
` ${members.sort().join(", ")}`
|
|
17088
|
-
],
|
|
17101
|
+
lines: [`Removed pack ${slug} (${members.length} skills) from ${area.dir}`, ` ${members.sort().join(", ")}`],
|
|
17089
17102
|
exitCode: 0
|
|
17090
17103
|
};
|
|
17091
17104
|
}
|
|
@@ -17100,9 +17113,7 @@ function runRemove(slug, opts = {}) {
|
|
|
17100
17113
|
} else if (opts.dir) {
|
|
17101
17114
|
dir = resolve3(cwd, opts.dir);
|
|
17102
17115
|
} else {
|
|
17103
|
-
const found = candidateDirs(slug, cwd, opts.home).filter(
|
|
17104
|
-
(candidate) => existsSync4(candidate)
|
|
17105
|
-
);
|
|
17116
|
+
const found = candidateDirs(slug, cwd, opts.home).filter((candidate) => existsSync4(candidate));
|
|
17106
17117
|
if (found.length === 0) {
|
|
17107
17118
|
return { lines: [`error: ${slug} is not installed in any known location`], exitCode: 2 };
|
|
17108
17119
|
}
|
|
@@ -17221,25 +17232,23 @@ async function runSearch(opts = {}) {
|
|
|
17221
17232
|
}
|
|
17222
17233
|
if (opts.category && !CATEGORY_SLUGS.includes(opts.category)) {
|
|
17223
17234
|
return {
|
|
17224
|
-
lines: [
|
|
17225
|
-
`error: unknown category "${opts.category}" (known categories: ${CATEGORY_SLUGS.join(", ")})`
|
|
17226
|
-
],
|
|
17235
|
+
lines: [`error: unknown category "${opts.category}" (known categories: ${CATEGORY_SLUGS.join(", ")})`],
|
|
17227
17236
|
exitCode: 2
|
|
17228
17237
|
};
|
|
17229
17238
|
}
|
|
17230
17239
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
17231
17240
|
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
17241
|
+
if (opts.ai) {
|
|
17242
|
+
return runAiSearch(opts, fetchImpl, apiUrl);
|
|
17243
|
+
}
|
|
17232
17244
|
const fetched = await getParsed(fetchImpl, `${apiUrl}/skills`, publicSkillListSchema);
|
|
17233
17245
|
if (!fetched.ok) {
|
|
17234
17246
|
return { lines: [`error: ${fetched.message}`], exitCode: 2 };
|
|
17235
17247
|
}
|
|
17236
17248
|
const query = (opts.query ?? "").trim().toLowerCase();
|
|
17237
|
-
const matches = fetched.data.filter(
|
|
17238
|
-
|
|
17239
|
-
|
|
17240
|
-
if (opts.packs && (skill.packSkills?.length ?? 0) === 0) return false;
|
|
17241
|
-
return query === "" || matchesQuery(skill, query);
|
|
17242
|
-
});
|
|
17249
|
+
const matches = fetched.data.filter(
|
|
17250
|
+
(skill) => matchesFilters(skill, opts) && (query === "" || matchesQuery(skill, query))
|
|
17251
|
+
);
|
|
17243
17252
|
if (opts.json) {
|
|
17244
17253
|
return { lines: [JSON.stringify(matches, null, 2)], exitCode: 0 };
|
|
17245
17254
|
}
|
|
@@ -17251,9 +17260,53 @@ async function runSearch(opts = {}) {
|
|
|
17251
17260
|
lines: [
|
|
17252
17261
|
...renderRows(matches, opts.width, st),
|
|
17253
17262
|
"",
|
|
17254
|
-
st.dim(
|
|
17255
|
-
|
|
17256
|
-
|
|
17263
|
+
st.dim(`${matches.length} of ${fetched.data.length} skills - skillpass report <slug> shows the passport`)
|
|
17264
|
+
],
|
|
17265
|
+
exitCode: 0
|
|
17266
|
+
};
|
|
17267
|
+
}
|
|
17268
|
+
function matchesFilters(skill, opts) {
|
|
17269
|
+
if (opts.target && !skill.targets.includes(opts.target)) return false;
|
|
17270
|
+
if (opts.category && (skill.category ?? null) !== opts.category) return false;
|
|
17271
|
+
if (opts.packs && (skill.packSkills?.length ?? 0) === 0) return false;
|
|
17272
|
+
return true;
|
|
17273
|
+
}
|
|
17274
|
+
var AI_ERRORS = {
|
|
17275
|
+
429: "AI search is rate limited (20 requests a minute per address); try again in a moment",
|
|
17276
|
+
503: "AI search is not configured on this API",
|
|
17277
|
+
502: "AI search is temporarily unavailable; try again shortly"
|
|
17278
|
+
};
|
|
17279
|
+
function aiErrorMessage(outcome) {
|
|
17280
|
+
if (outcome.status !== void 0 && AI_ERRORS[outcome.status]) return AI_ERRORS[outcome.status];
|
|
17281
|
+
if (outcome.status === 400 && outcome.apiError) return outcome.apiError;
|
|
17282
|
+
return outcome.message;
|
|
17283
|
+
}
|
|
17284
|
+
async function runAiSearch(opts, fetchImpl, apiUrl) {
|
|
17285
|
+
const query = (opts.query ?? "").trim();
|
|
17286
|
+
if (query === "") {
|
|
17287
|
+
return { lines: ["error: search --ai needs a query (describe what you need)"], exitCode: 2 };
|
|
17288
|
+
}
|
|
17289
|
+
const fetched = await getParsed(
|
|
17290
|
+
fetchImpl,
|
|
17291
|
+
`${apiUrl}/skills/search?q=${encodeURIComponent(query)}`,
|
|
17292
|
+
publicSkillListSchema
|
|
17293
|
+
);
|
|
17294
|
+
if (!fetched.ok) {
|
|
17295
|
+
return { lines: [`error: ${aiErrorMessage(fetched)}`], exitCode: 2 };
|
|
17296
|
+
}
|
|
17297
|
+
const matches = fetched.data.filter((skill) => matchesFilters(skill, opts));
|
|
17298
|
+
if (opts.json) {
|
|
17299
|
+
return { lines: [JSON.stringify(matches, null, 2)], exitCode: 0 };
|
|
17300
|
+
}
|
|
17301
|
+
if (matches.length === 0) {
|
|
17302
|
+
return { lines: ["No skills match by meaning - try other words"], exitCode: 0 };
|
|
17303
|
+
}
|
|
17304
|
+
const st = opts.style ?? PLAIN;
|
|
17305
|
+
return {
|
|
17306
|
+
lines: [
|
|
17307
|
+
...renderRows(matches, opts.width, st),
|
|
17308
|
+
"",
|
|
17309
|
+
st.dim(`${matches.length} skills by meaning - skillpass report <slug> shows the passport`)
|
|
17257
17310
|
],
|
|
17258
17311
|
exitCode: 0
|
|
17259
17312
|
};
|
|
@@ -17266,7 +17319,7 @@ async function locate(slug, opts, fetchImpl, apiUrl) {
|
|
|
17266
17319
|
const cwd = opts.cwd ?? process.cwd();
|
|
17267
17320
|
let areas = knownAreas(cwd, opts.home);
|
|
17268
17321
|
if (opts.target) {
|
|
17269
|
-
areas = areas.filter((a) => a.
|
|
17322
|
+
areas = areas.filter((a) => a.tools.includes(opts.target) && (opts.global ? a.global : a.project));
|
|
17270
17323
|
}
|
|
17271
17324
|
const hits = [];
|
|
17272
17325
|
for (const area of areas) {
|
|
@@ -17337,9 +17390,7 @@ async function runUpdate(ref, opts = {}) {
|
|
|
17337
17390
|
const hits = await locate(slug, opts, fetchImpl, apiUrl);
|
|
17338
17391
|
const memberHit = hits.find((h) => h.packSlug !== void 0 && h.packSlug !== slug);
|
|
17339
17392
|
if (memberHit?.packSlug !== void 0) {
|
|
17340
|
-
push(
|
|
17341
|
-
`error: ${slug} is part of the ${memberHit.packSlug} pack; run skillpass update ${memberHit.packSlug}`
|
|
17342
|
-
);
|
|
17393
|
+
push(`error: ${slug} is part of the ${memberHit.packSlug} pack; run skillpass update ${memberHit.packSlug}`);
|
|
17343
17394
|
return done(2);
|
|
17344
17395
|
}
|
|
17345
17396
|
if (hits.length === 0) {
|
|
@@ -17411,9 +17462,7 @@ async function changesVsInstalled(fetchImpl, apiUrl, slug, installed, target, st
|
|
|
17411
17462
|
publicPreflightSchema
|
|
17412
17463
|
);
|
|
17413
17464
|
if (!before.ok) {
|
|
17414
|
-
return [
|
|
17415
|
-
`Changes vs installed v${installed.installedVersion}: unavailable (that version is no longer published)`
|
|
17416
|
-
];
|
|
17465
|
+
return [`Changes vs installed v${installed.installedVersion}: unavailable (that version is no longer published)`];
|
|
17417
17466
|
}
|
|
17418
17467
|
const { added, removed } = permissionChanges(before.data.permissions, target.permissions);
|
|
17419
17468
|
if (added.length === 0 && removed.length === 0) {
|
|
@@ -17448,9 +17497,10 @@ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl
|
|
|
17448
17497
|
);
|
|
17449
17498
|
if (!confirmed) return done(2);
|
|
17450
17499
|
const areaDir = installed.area.dir;
|
|
17451
|
-
const
|
|
17500
|
+
const tool = opts.target ?? installed.area.tools[0];
|
|
17501
|
+
const { installs, skipped } = resolvePackMembers(detail.packMembers ?? [], installed.area.layout);
|
|
17452
17502
|
if (installs.length === 0) {
|
|
17453
|
-
push("", `error: none of the new version's skills support ${
|
|
17503
|
+
push("", `error: none of the new version's skills support ${tool}`);
|
|
17454
17504
|
return done(2);
|
|
17455
17505
|
}
|
|
17456
17506
|
const receipts = readReceipts(areaDir);
|
|
@@ -17479,7 +17529,7 @@ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl
|
|
|
17479
17529
|
return done(2);
|
|
17480
17530
|
}
|
|
17481
17531
|
for (const name of skipped) {
|
|
17482
|
-
push("", `note: ${name} does not support ${
|
|
17532
|
+
push("", `note: ${name} does not support ${tool}; skipped`);
|
|
17483
17533
|
}
|
|
17484
17534
|
const done1 = [];
|
|
17485
17535
|
try {
|
|
@@ -17560,11 +17610,11 @@ var USAGE = [
|
|
|
17560
17610
|
"skillpass - validate AI agent skills locally and inspect hosted passports",
|
|
17561
17611
|
"",
|
|
17562
17612
|
"Usage:",
|
|
17563
|
-
" skillpass search [query] [--target <tool>] [--category <slug>] [--packs] [--json]",
|
|
17613
|
+
" skillpass search [query] [--ai] [--target <tool>] [--category <slug>] [--packs] [--json]",
|
|
17564
17614
|
" find skills in the directory",
|
|
17565
17615
|
" skillpass scan <path> [--json] run the validator on a local skill package",
|
|
17566
17616
|
" skillpass report <slug>[@version] [--json] fetch the hosted passport pre-flight",
|
|
17567
|
-
" skillpass add <slug>[@version] [--target <tool
|
|
17617
|
+
" skillpass add <slug>[@version] [--target <tool>... [--global] | --dir <path>] [--yes]",
|
|
17568
17618
|
" install a skill through the pre-flight gate",
|
|
17569
17619
|
" skillpass remove <slug> [--target <tool> [--global] | --dir <path>]",
|
|
17570
17620
|
" remove an installed skill",
|
|
@@ -17575,11 +17625,15 @@ var USAGE = [
|
|
|
17575
17625
|
"",
|
|
17576
17626
|
"Flags:",
|
|
17577
17627
|
" --json print machine-readable JSON instead of the readable report",
|
|
17578
|
-
" --target
|
|
17579
|
-
"
|
|
17580
|
-
"
|
|
17628
|
+
" --target a tool's skills folder: claude-code, cline, or agents (the shared",
|
|
17629
|
+
" .agents/skills read by codex, cursor, windsurf, github-copilot,",
|
|
17630
|
+
" gemini-cli, and opencode; those names work too). add takes it more",
|
|
17631
|
+
" than once to install into several tools",
|
|
17632
|
+
" --global with --target, use the tool's user-level folder (~/.claude/skills,",
|
|
17633
|
+
" ~/.agents/skills, ~/.cline/skills)",
|
|
17581
17634
|
" --dir install target directory (default ./<slug>)",
|
|
17582
17635
|
" --yes skip the confirmation prompt for medium+ risk skills",
|
|
17636
|
+
" --ai search by meaning: describe what you need (needs a query)",
|
|
17583
17637
|
" --category search filter: a directory category slug",
|
|
17584
17638
|
" --packs search filter: multi-skill packs only",
|
|
17585
17639
|
" --version print the CLI version",
|
|
@@ -17597,7 +17651,7 @@ var COMMAND_FLAGS = {
|
|
|
17597
17651
|
list: [],
|
|
17598
17652
|
outdated: [],
|
|
17599
17653
|
update: ["--yes", "--target", "--global"],
|
|
17600
|
-
search: ["--target", "--category", "--packs", "--json"]
|
|
17654
|
+
search: ["--ai", "--target", "--category", "--packs", "--json"]
|
|
17601
17655
|
};
|
|
17602
17656
|
function parseCliArgs(argv) {
|
|
17603
17657
|
const parsed = {
|
|
@@ -17608,6 +17662,7 @@ function parseCliArgs(argv) {
|
|
|
17608
17662
|
yes: false,
|
|
17609
17663
|
global: false,
|
|
17610
17664
|
packs: false,
|
|
17665
|
+
ai: false,
|
|
17611
17666
|
seen: []
|
|
17612
17667
|
};
|
|
17613
17668
|
for (let i2 = 0; i2 < argv.length; i2++) {
|
|
@@ -17624,6 +17679,9 @@ function parseCliArgs(argv) {
|
|
|
17624
17679
|
} else if (arg === "--packs") {
|
|
17625
17680
|
parsed.packs = true;
|
|
17626
17681
|
parsed.seen.push(arg);
|
|
17682
|
+
} else if (arg === "--ai") {
|
|
17683
|
+
parsed.ai = true;
|
|
17684
|
+
parsed.seen.push(arg);
|
|
17627
17685
|
} else if (arg === "--dir" || arg === "--target" || arg === "--category") {
|
|
17628
17686
|
parsed.seen.push(arg);
|
|
17629
17687
|
const value = argv[i2 + 1];
|
|
@@ -17634,7 +17692,7 @@ function parseCliArgs(argv) {
|
|
|
17634
17692
|
if (arg === "--dir") {
|
|
17635
17693
|
parsed.dir = value;
|
|
17636
17694
|
} else if (arg === "--target") {
|
|
17637
|
-
parsed.
|
|
17695
|
+
parsed.targets = [.../* @__PURE__ */ new Set([...parsed.targets ?? [], value])];
|
|
17638
17696
|
} else {
|
|
17639
17697
|
parsed.category = value;
|
|
17640
17698
|
}
|
|
@@ -17684,12 +17742,17 @@ async function run(argv) {
|
|
|
17684
17742
|
if (disallowed) {
|
|
17685
17743
|
return { lines: [`error: ${args.command} does not take ${disallowed}`, "", USAGE], exitCode: 2 };
|
|
17686
17744
|
}
|
|
17745
|
+
const target = args.targets?.[0];
|
|
17746
|
+
if (args.command !== "add" && (args.targets?.length ?? 0) > 1) {
|
|
17747
|
+
return { lines: [`error: ${args.command} takes one --target`, "", USAGE], exitCode: 2 };
|
|
17748
|
+
}
|
|
17687
17749
|
if (args.command === "search") {
|
|
17688
17750
|
return runSearch({
|
|
17689
17751
|
query: args.positional.join(" "),
|
|
17690
|
-
target
|
|
17752
|
+
target,
|
|
17691
17753
|
category: args.category,
|
|
17692
17754
|
packs: args.packs,
|
|
17755
|
+
ai: args.ai,
|
|
17693
17756
|
json: args.json,
|
|
17694
17757
|
width: process.stdout.isTTY ? process.stdout.columns : void 0,
|
|
17695
17758
|
style: styler(Boolean(process.stdout.isTTY))
|
|
@@ -17707,7 +17770,7 @@ async function run(argv) {
|
|
|
17707
17770
|
if (!slug) {
|
|
17708
17771
|
return { lines: ["error: remove needs a skill slug", "", USAGE], exitCode: 2 };
|
|
17709
17772
|
}
|
|
17710
|
-
return runRemove(slug, { target
|
|
17773
|
+
return runRemove(slug, { target, dir: args.dir, global: args.global });
|
|
17711
17774
|
}
|
|
17712
17775
|
if (args.command === "list") {
|
|
17713
17776
|
if (args.positional.length > 0) {
|
|
@@ -17723,7 +17786,7 @@ async function run(argv) {
|
|
|
17723
17786
|
const tty = Boolean(process.stdin.isTTY);
|
|
17724
17787
|
return runUpdate(ref, {
|
|
17725
17788
|
yes: args.yes,
|
|
17726
|
-
target
|
|
17789
|
+
target,
|
|
17727
17790
|
global: args.global,
|
|
17728
17791
|
style: styler(Boolean(process.stdout.isTTY)),
|
|
17729
17792
|
confirmImpl: tty ? confirmViaTty : void 0,
|
|
@@ -17752,7 +17815,7 @@ async function run(argv) {
|
|
|
17752
17815
|
return runAdd(ref, {
|
|
17753
17816
|
yes: args.yes,
|
|
17754
17817
|
dir: args.dir,
|
|
17755
|
-
target: args.
|
|
17818
|
+
target: args.targets,
|
|
17756
17819
|
global: args.global,
|
|
17757
17820
|
style: styler(Boolean(process.stdout.isTTY)),
|
|
17758
17821
|
confirmImpl: tty ? confirmViaTty : void 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillpass",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Validation-first installer for AI agent skills: search the SkillPass directory, inspect a skill's passport, and install hash-verified skills and packs for Claude Code, Codex, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|