skillpass 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/index.ts
8
- import { readFileSync as readFileSync2 } from "node:fs";
8
+ import { readFileSync as readFileSync3 } from "node:fs";
9
9
  import { createInterface } from "node:readline/promises";
10
10
 
11
11
  // ../../node_modules/.pnpm/fflate@0.8.3/node_modules/fflate/esm/index.mjs
@@ -493,16 +493,16 @@ function unzipSync(data, opts) {
493
493
 
494
494
  // src/add.ts
495
495
  import {
496
- existsSync,
497
- mkdirSync,
498
- readdirSync as readdirSync2,
496
+ existsSync as existsSync2,
497
+ mkdirSync as mkdirSync2,
498
+ readdirSync as readdirSync3,
499
499
  renameSync,
500
500
  rmdirSync,
501
501
  rmSync,
502
- statSync as statSync2,
503
- writeFileSync
502
+ statSync as statSync3,
503
+ writeFileSync as writeFileSync2
504
504
  } from "node:fs";
505
- import { dirname, isAbsolute, join as join3, resolve } from "node:path";
505
+ import { dirname, isAbsolute, join as join4, resolve as resolve2 } from "node:path";
506
506
 
507
507
  // ../validator/src/load.ts
508
508
  import { createHash } from "node:crypto";
@@ -15281,10 +15281,10 @@ function parseManifest(input) {
15281
15281
  }
15282
15282
 
15283
15283
  // ../skill-schema/src/report.ts
15284
- var reportFindingSchema = external_exports.strictObject({
15284
+ var reportFindingSchema = external_exports.object({
15285
15285
  code: external_exports.string().min(1),
15286
15286
  message: external_exports.string().min(1),
15287
- location: external_exports.strictObject({
15287
+ location: external_exports.object({
15288
15288
  path: external_exports.string().min(1),
15289
15289
  line: external_exports.number().int().positive().optional(),
15290
15290
  snippet: external_exports.string().optional()
@@ -15313,11 +15313,11 @@ var validationReportSchema = external_exports.strictObject({
15313
15313
  });
15314
15314
 
15315
15315
  // ../skill-schema/src/passport.ts
15316
- var skillPassportSchema = external_exports.strictObject({
15316
+ var skillPassportSchema = external_exports.object({
15317
15317
  schemaVersion: external_exports.literal("0.1"),
15318
15318
  validationStatus: validationStatusSchema,
15319
15319
  riskLevel: riskLevelSchema,
15320
- permissionsSummary: external_exports.strictObject({
15320
+ permissionsSummary: external_exports.object({
15321
15321
  declared: external_exports.array(permissionKeySchema),
15322
15322
  detected: external_exports.array(permissionKeySchema)
15323
15323
  }),
@@ -15338,7 +15338,7 @@ var skillPassportSchema = external_exports.strictObject({
15338
15338
 
15339
15339
  // ../skill-schema/src/ai-review.ts
15340
15340
  var aiReviewVerdictSchema = external_exports.enum(["clear", "caution", "concern"]);
15341
- var aiReviewSchema = external_exports.strictObject({
15341
+ var aiReviewSchema = external_exports.object({
15342
15342
  summary: external_exports.string().min(1).max(600),
15343
15343
  verdict: aiReviewVerdictSchema,
15344
15344
  reasoning: external_exports.string().min(1).max(800),
@@ -15390,9 +15390,14 @@ var publishResultSchema = external_exports.object({
15390
15390
  slug: external_exports.string().min(1),
15391
15391
  version: external_exports.string().min(1)
15392
15392
  });
15393
+ var MAX_SLUG_LENGTH = 60;
15394
+ function slugForSkill(name) {
15395
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_SLUG_LENGTH).replace(/-+$/, "");
15396
+ return slug || "skill";
15397
+ }
15393
15398
 
15394
15399
  // ../skill-schema/src/public.ts
15395
- var publicSkillSummarySchema = external_exports.strictObject({
15400
+ var publicSkillSummarySchema = external_exports.object({
15396
15401
  slug: external_exports.string().min(1),
15397
15402
  name: external_exports.string().min(1),
15398
15403
  summary: external_exports.string().min(1),
@@ -15422,7 +15427,7 @@ var publicSkillSummarySchema = external_exports.strictObject({
15422
15427
  publishedAt: external_exports.iso.datetime()
15423
15428
  });
15424
15429
  var publicSkillListSchema = external_exports.array(publicSkillSummarySchema);
15425
- var publicSkillVersionSchema = external_exports.strictObject({
15430
+ var publicSkillVersionSchema = external_exports.object({
15426
15431
  version: external_exports.string().min(1),
15427
15432
  validationStatus: validationStatusSchema,
15428
15433
  riskLevel: riskLevelSchema,
@@ -15431,10 +15436,10 @@ var publicSkillVersionSchema = external_exports.strictObject({
15431
15436
  var publicSkillDetailSchema = publicSkillSummarySchema.extend({
15432
15437
  // Full member entries (incl. per-target variants) for a pack; the CLI's
15433
15438
  // per-target install resolution reads exactly this. Null for single skills.
15434
- packMembers: external_exports.array(skillEntrySchema).nullable().optional(),
15439
+ packMembers: external_exports.array(external_exports.object(skillEntrySchema.shape)).nullable().optional(),
15435
15440
  githubRepoUrl: external_exports.string().min(1).nullable(),
15436
15441
  passport: skillPassportSchema,
15437
- maintainerInfo: external_exports.strictObject({
15442
+ maintainerInfo: external_exports.object({
15438
15443
  username: external_exports.string().min(1),
15439
15444
  displayName: external_exports.string().min(1),
15440
15445
  avatarUrl: external_exports.string().min(1)
@@ -15443,18 +15448,18 @@ var publicSkillDetailSchema = publicSkillSummarySchema.extend({
15443
15448
  // The cached AI review for this version's source hash; null until generated.
15444
15449
  aiReview: aiReviewSchema.nullable()
15445
15450
  });
15446
- var publicSkillSourceSchema = external_exports.strictObject({
15451
+ var publicSkillSourceSchema = external_exports.object({
15447
15452
  version: external_exports.string().min(1),
15448
15453
  sourceHash: external_exports.string().min(1),
15449
- files: external_exports.array(external_exports.strictObject({ path: external_exports.string().min(1), content: external_exports.string() }))
15454
+ files: external_exports.array(external_exports.object({ path: external_exports.string().min(1), content: external_exports.string() }))
15450
15455
  });
15451
15456
 
15452
15457
  // ../skill-schema/src/preflight.ts
15453
- var permissionSetDiffSchema = external_exports.strictObject({
15458
+ var permissionSetDiffSchema = external_exports.object({
15454
15459
  added: external_exports.array(permissionKeySchema),
15455
15460
  removed: external_exports.array(permissionKeySchema)
15456
15461
  });
15457
- var publicPreflightSchema = external_exports.strictObject({
15462
+ var publicPreflightSchema = external_exports.object({
15458
15463
  version: external_exports.string().min(1),
15459
15464
  validationStatus: validationStatusSchema,
15460
15465
  riskLevel: riskLevelSchema,
@@ -15462,11 +15467,11 @@ var publicPreflightSchema = external_exports.strictObject({
15462
15467
  sourceVerified: external_exports.boolean(),
15463
15468
  resolvedCommitSha: external_exports.string().min(1).nullable(),
15464
15469
  generatedAt: external_exports.iso.datetime(),
15465
- permissions: external_exports.strictObject({
15470
+ permissions: external_exports.object({
15466
15471
  declared: external_exports.array(permissionKeySchema),
15467
15472
  detected: external_exports.array(permissionKeySchema)
15468
15473
  }),
15469
- diff: external_exports.strictObject({
15474
+ diff: external_exports.object({
15470
15475
  previousVersion: external_exports.string().min(1),
15471
15476
  declared: permissionSetDiffSchema,
15472
15477
  detected: permissionSetDiffSchema
@@ -15546,6 +15551,39 @@ var resolveReportInputSchema = external_exports.strictObject({
15546
15551
  status: external_exports.enum(["reviewed", "actioned"])
15547
15552
  });
15548
15553
 
15554
+ // ../skill-schema/src/maintainer.ts
15555
+ var SKILL_STATUSES = ["published", "draft", "private", "flagged"];
15556
+ var skillStatusSchema = external_exports.enum(SKILL_STATUSES);
15557
+ var maintainerSkillSchema = external_exports.strictObject({
15558
+ slug: external_exports.string().min(1),
15559
+ name: external_exports.string().min(1),
15560
+ status: skillStatusSchema,
15561
+ version: external_exports.string().nullable(),
15562
+ validationStatus: validationStatusSchema.nullable(),
15563
+ riskLevel: riskLevelSchema.nullable(),
15564
+ updatedAt: external_exports.iso.datetime()
15565
+ });
15566
+ var maintainerReportSchema = external_exports.strictObject({
15567
+ id: external_exports.number().int().positive(),
15568
+ skill: external_exports.strictObject({ slug: external_exports.string(), name: external_exports.string() }),
15569
+ reason: external_exports.string(),
15570
+ status: abuseReportStatusSchema,
15571
+ createdAt: external_exports.iso.datetime()
15572
+ });
15573
+
15574
+ // ../skill-schema/src/user.ts
15575
+ var USER_ROLES = ["user", "maintainer", "admin"];
15576
+ var userRoleSchema = external_exports.enum(USER_ROLES);
15577
+ var publicUserSchema = external_exports.object({
15578
+ id: external_exports.number().int().positive(),
15579
+ username: external_exports.string().min(1),
15580
+ displayName: external_exports.string().min(1),
15581
+ avatarUrl: external_exports.string().min(1),
15582
+ role: userRoleSchema,
15583
+ reputation: external_exports.number().int(),
15584
+ createdAt: external_exports.iso.datetime()
15585
+ });
15586
+
15549
15587
  // ../validator/src/rules/permissions.ts
15550
15588
  var PERMISSION_SIGNALS = [
15551
15589
  {
@@ -15600,24 +15638,30 @@ var PERMISSION_SIGNALS = [
15600
15638
  }
15601
15639
  ];
15602
15640
  function detectPermissions(files) {
15603
- const detected = /* @__PURE__ */ new Map();
15641
+ const detected = /* @__PURE__ */ new Set();
15604
15642
  for (const file2 of files) {
15605
- file2.content.split("\n").forEach((line, i2) => {
15643
+ for (const line of file2.content.split("\n")) {
15606
15644
  for (const signal of PERMISSION_SIGNALS) {
15607
15645
  if (!detected.has(signal.permission) && signal.pattern.test(line)) {
15608
- detected.set(signal.permission, {
15609
- permission: signal.permission,
15610
- description: signal.description,
15611
- location: { path: file2.path, line: i2 + 1, snippet: line.trim() }
15612
- });
15646
+ detected.add(signal.permission);
15613
15647
  }
15614
15648
  }
15615
- });
15649
+ }
15616
15650
  }
15617
- return [...detected.values()];
15651
+ return [...detected];
15618
15652
  }
15619
15653
 
15620
15654
  // ../validator/src/load.ts
15655
+ var NUL_SCAN_BYTES = 8e3;
15656
+ function isBinary(bytes) {
15657
+ if (bytes.subarray(0, NUL_SCAN_BYTES).includes(0)) return true;
15658
+ try {
15659
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes);
15660
+ return false;
15661
+ } catch {
15662
+ return true;
15663
+ }
15664
+ }
15621
15665
  var PackageReadError = class extends Error {
15622
15666
  constructor(dir, cause) {
15623
15667
  super(`cannot read skill package at "${dir}"`, { cause });
@@ -15626,19 +15670,23 @@ var PackageReadError = class extends Error {
15626
15670
  };
15627
15671
  var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
15628
15672
  function walk(root, rel = "") {
15629
- const files = [];
15673
+ const tree = { files: [], binaries: [] };
15630
15674
  for (const name of readdirSync(join(root, rel)).sort()) {
15631
15675
  const relPath = rel === "" ? name : `${rel}/${name}`;
15632
15676
  const stats = statSync(join(root, relPath));
15633
15677
  if (stats.isDirectory()) {
15634
15678
  if (!SKIP_DIRS.has(name)) {
15635
- files.push(...walk(root, relPath));
15679
+ const sub = walk(root, relPath);
15680
+ tree.files.push(...sub.files);
15681
+ tree.binaries.push(...sub.binaries);
15636
15682
  }
15637
15683
  } else {
15638
- files.push({ path: relPath, content: readFileSync(join(root, relPath), "utf8") });
15684
+ const bytes = readFileSync(join(root, relPath));
15685
+ if (isBinary(bytes)) tree.binaries.push(relPath);
15686
+ else tree.files.push({ path: relPath, content: bytes.toString("utf8") });
15639
15687
  }
15640
15688
  }
15641
- return files;
15689
+ return tree;
15642
15690
  }
15643
15691
  function hashFiles(files) {
15644
15692
  const hash2 = createHash("sha256");
@@ -15665,7 +15713,7 @@ function readManifest(files) {
15665
15713
  if (!result.success) {
15666
15714
  return { state: "invalid", error: result.error };
15667
15715
  }
15668
- return { state: "ok", data: result.data, raw: file2.content };
15716
+ return { state: "ok", data: result.data };
15669
15717
  }
15670
15718
  var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
15671
15719
  function readSkillMeta(content) {
@@ -15690,19 +15738,43 @@ function readSkillMeta(content) {
15690
15738
  const text = block[1] === ">" ? gathered.join(" ").replace(/\s+/g, " ").trim() : gathered.join("\n").trim();
15691
15739
  if (text) out[key] = text;
15692
15740
  } else {
15693
- out[key] = kv[2].replace(/^["']|["']$/g, "");
15741
+ const parts = [kv[2]];
15742
+ for (let j = i2 + 1; j < lines.length && /^\s+\S/.test(lines[j]); j++) parts.push(lines[j].trim());
15743
+ out[key] = parts.join(" ").replace(/^["']|["']$/g, "");
15694
15744
  }
15695
15745
  }
15696
15746
  }
15697
15747
  if (!out.description) {
15698
- const prose = body.split("\n").map((line) => line.trim()).find((line) => line && !line.startsWith("#"));
15699
- if (prose) out.description = prose.slice(0, 200);
15748
+ const prose = firstParagraph(body);
15749
+ if (prose) out.description = prose;
15700
15750
  }
15701
15751
  return out;
15702
15752
  }
15703
- function slugify2(value) {
15704
- const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15705
- return slug || "skill";
15753
+ var HTML_TAG_RE = /<\/?[a-zA-Z][^>]*>/g;
15754
+ var MD_LINK_RE = /\[([^\]]*)\]\([^)]*\)/g;
15755
+ var STRUCTURAL_LINE_RE = /^(#|!\[|\[!\[|---$|\*\*\*$|___$)|^<h[1-6][\s>]/i;
15756
+ var DESCRIPTION_MAX = 200;
15757
+ var cleanInline = (line) => line.replace(HTML_TAG_RE, " ").replace(MD_LINK_RE, "$1").replace(/^>\s?/, "").replace(/\s+/g, " ").trim();
15758
+ function clip(text, max2) {
15759
+ if (text.length <= max2) return text;
15760
+ const head = text.slice(0, max2);
15761
+ const sentenceEnd = Math.max(head.lastIndexOf(". "), head.lastIndexOf("! "), head.lastIndexOf("? "));
15762
+ if (sentenceEnd >= 60) return head.slice(0, sentenceEnd + 1);
15763
+ const wordEnd = head.lastIndexOf(" ");
15764
+ return (wordEnd > 0 ? head.slice(0, wordEnd) : head).replace(/[,;:]+$/, "").trim();
15765
+ }
15766
+ function firstParagraph(text) {
15767
+ const kept = [];
15768
+ for (const raw of text.split("\n")) {
15769
+ const source = raw.trim();
15770
+ const line = STRUCTURAL_LINE_RE.test(source) ? "" : cleanInline(source);
15771
+ if (!line) {
15772
+ if (kept.length > 0) break;
15773
+ continue;
15774
+ }
15775
+ kept.push(line);
15776
+ }
15777
+ return kept.length > 0 ? clip(kept.join(" "), DESCRIPTION_MAX) : void 0;
15706
15778
  }
15707
15779
  function inferTargets(files) {
15708
15780
  const targets = /* @__PURE__ */ new Set();
@@ -15715,8 +15787,8 @@ function inferTargets(files) {
15715
15787
  function inferManifest(files, fallbackName) {
15716
15788
  const skillFile = files.find((f) => f.path === "SKILL.md");
15717
15789
  const meta3 = skillFile ? readSkillMeta(skillFile.content) : {};
15718
- const name = slugify2(meta3.name ?? fallbackName);
15719
- const permissions = [...new Set(detectPermissions(files).map((d) => d.permission))];
15790
+ const name = slugForSkill(meta3.name ?? fallbackName);
15791
+ const permissions = detectPermissions(files);
15720
15792
  return {
15721
15793
  schemaVersion: "0.1",
15722
15794
  name,
@@ -15751,11 +15823,7 @@ function collectMemberFiles(files) {
15751
15823
  }
15752
15824
  function readmeProse(files) {
15753
15825
  const readme = files.find((f) => f.path === "README.md");
15754
- if (!readme) return void 0;
15755
- const prose = readme.content.split("\n").map((line) => line.trim()).find(
15756
- (line) => line && !line.startsWith("#") && !line.startsWith("![") && !line.startsWith("[![") && !line.startsWith("<")
15757
- );
15758
- return prose?.slice(0, 200);
15826
+ return readme ? firstParagraph(readme.content) : void 0;
15759
15827
  }
15760
15828
  function inferNestedManifest(files, fallbackName) {
15761
15829
  const members = collectMemberFiles(files).sort(
@@ -15770,8 +15838,8 @@ function inferNestedManifest(files, fallbackName) {
15770
15838
  const entryPath = member.claudePath ?? member.agentsPath ?? member.plainPath;
15771
15839
  const file2 = files.find((f) => f.path === entryPath);
15772
15840
  const meta3 = readSkillMeta(file2.content);
15773
- let entryName = slugify2(meta3.name ?? member.folder);
15774
- if (used.has(entryName)) entryName = slugify2(member.folder);
15841
+ let entryName = slugForSkill(meta3.name ?? member.folder);
15842
+ if (used.has(entryName)) entryName = slugForSkill(member.folder);
15775
15843
  used.add(entryName);
15776
15844
  const targets = member.plainPath ? inferTargets(files) : [
15777
15845
  ...member.claudePath ? ["claude-code"] : [],
@@ -15787,10 +15855,10 @@ function inferNestedManifest(files, fallbackName) {
15787
15855
  };
15788
15856
  });
15789
15857
  const single = entries.length === 1 ? entries[0] : null;
15790
- const permissions = [...new Set(detectPermissions(files).map((d) => d.permission))];
15858
+ const permissions = detectPermissions(files);
15791
15859
  return {
15792
15860
  schemaVersion: "0.1",
15793
- name: single ? single.name : slugify2(fallbackName),
15861
+ name: single ? single.name : slugForSkill(fallbackName),
15794
15862
  description: single ? single.description ?? single.name : readmeProse(files) ?? `A pack of ${entries.length} skills`,
15795
15863
  targets: [...packTargets],
15796
15864
  permissions,
@@ -15820,82 +15888,33 @@ function resolveEntries(dir, manifest, files) {
15820
15888
  ]);
15821
15889
  }
15822
15890
  var byPath = (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
15823
- function loadPackageFromFiles(files, name = "package") {
15891
+ function loadPackageFromFiles(files, name = "package", binaries = []) {
15824
15892
  const sorted = [...files].sort(byPath);
15825
15893
  let manifest = readManifest(sorted);
15826
15894
  if (manifest.state === "missing") {
15827
15895
  const data = sorted.some((f) => f.path === "SKILL.md") ? inferManifest(sorted, name) : inferNestedManifest(sorted, name);
15828
15896
  if (data) {
15829
- manifest = { state: "ok", inferred: true, data, raw: JSON.stringify(data, null, 2) };
15897
+ manifest = { state: "ok", inferred: true, data };
15830
15898
  }
15831
15899
  }
15832
15900
  return {
15833
- dir: name,
15834
15901
  files: sorted,
15835
15902
  manifest,
15836
15903
  entries: resolveEntries(name, manifest, sorted),
15837
- sourceHash: hashFiles(sorted)
15904
+ sourceHash: hashFiles(sorted),
15905
+ binaries
15838
15906
  };
15839
15907
  }
15840
15908
  function loadPackage(dir) {
15841
- let files;
15909
+ let tree;
15842
15910
  try {
15843
- files = walk(dir);
15911
+ tree = walk(dir);
15844
15912
  } catch (err2) {
15845
15913
  throw new PackageReadError(dir, err2);
15846
15914
  }
15847
- return loadPackageFromFiles(files, dir);
15915
+ return loadPackageFromFiles(tree.files, dir, tree.binaries);
15848
15916
  }
15849
15917
 
15850
- // ../validator/src/rules/structure.ts
15851
- var structureRule = (pkg) => {
15852
- if (pkg.files.length === 0) {
15853
- return [
15854
- {
15855
- severity: "failure",
15856
- code: "empty-package",
15857
- message: "the package contains no files"
15858
- }
15859
- ];
15860
- }
15861
- const findings = [];
15862
- if (pkg.manifest.state === "missing") {
15863
- findings.push({
15864
- severity: "warning",
15865
- code: "missing-manifest",
15866
- message: "no skill.json manifest; permissions and targets are undeclared"
15867
- });
15868
- } else if (pkg.manifest.state === "invalid") {
15869
- findings.push({
15870
- severity: "failure",
15871
- code: "invalid-manifest",
15872
- message: `skill.json is invalid: ${pkg.manifest.error}`,
15873
- location: { path: "skill.json" }
15874
- });
15875
- }
15876
- for (const entry of pkg.entries) {
15877
- if (!entry.exists) {
15878
- findings.push({
15879
- severity: "failure",
15880
- code: "missing-skill-file",
15881
- message: `skill "${entry.skillName}" references "${entry.path}", which does not exist`,
15882
- location: { path: entry.path }
15883
- });
15884
- } else {
15885
- const file2 = pkg.files.find((f) => f.path === entry.path);
15886
- if (file2 !== void 0 && file2.content.trim() === "") {
15887
- findings.push({
15888
- severity: "failure",
15889
- code: "missing-skill-file",
15890
- message: `skill "${entry.skillName}" entry "${entry.path}" is empty`,
15891
- location: { path: entry.path }
15892
- });
15893
- }
15894
- }
15895
- }
15896
- return findings;
15897
- };
15898
-
15899
15918
  // ../validator/src/rules/content.ts
15900
15919
  var SECRET_PATTERNS = [
15901
15920
  {
@@ -16037,28 +16056,113 @@ var ALL_ROWS = [
16037
16056
  ...advisory(MALWARE_PATTERNS),
16038
16057
  ...advisory(CREDENTIAL_PATTERNS)
16039
16058
  ];
16059
+ var DOCUMENTED_PLACEHOLDERS = ["AKIAIOSFODNN7EXAMPLE", "AKIAI44QH8DHBEXAMPLE"];
16060
+ var withoutPlaceholders = (line) => DOCUMENTED_PLACEHOLDERS.reduce((l, p) => l.replaceAll(p, " "), line);
16061
+ var REDACTIONS = ALL_ROWS.filter((row2) => row2.redact).map(
16062
+ (row2) => new RegExp(row2.pattern.source, `${row2.pattern.flags}g`)
16063
+ );
16064
+ var redactLine = (line) => REDACTIONS.reduce((l, re) => l.replace(re, "[redacted]"), line);
16040
16065
  var contentRule = (pkg) => {
16041
16066
  const findings = [];
16042
16067
  for (const file2 of pkg.files) {
16043
- file2.content.split("\n").forEach((line, i2) => {
16044
- for (const row2 of ALL_ROWS) {
16045
- if (row2.pattern.test(line)) {
16046
- const snippet = row2.redact ? line.trim().replace(row2.pattern, "[redacted]") : line.trim();
16047
- findings.push({
16048
- severity: row2.severity ?? "failure",
16049
- code: row2.code,
16050
- message: row2.message,
16051
- location: { path: file2.path, line: i2 + 1, snippet }
16052
- });
16053
- }
16068
+ file2.content.split("\n").forEach((rawLine, i2) => {
16069
+ const line = withoutPlaceholders(rawLine);
16070
+ const hits = ALL_ROWS.filter((row2) => row2.pattern.test(line));
16071
+ if (hits.length === 0) return;
16072
+ const snippet = redactLine(line).trim();
16073
+ for (const row2 of hits) {
16074
+ findings.push({
16075
+ severity: row2.severity ?? "failure",
16076
+ code: row2.code,
16077
+ message: row2.message,
16078
+ location: { path: file2.path, line: i2 + 1, snippet }
16079
+ });
16080
+ }
16081
+ });
16082
+ }
16083
+ return findings;
16084
+ };
16085
+
16086
+ // ../validator/src/rules/structure.ts
16087
+ var structureRule = (pkg) => {
16088
+ if (pkg.files.length === 0) {
16089
+ return [
16090
+ {
16091
+ severity: "failure",
16092
+ code: "empty-package",
16093
+ message: "the package contains no files"
16054
16094
  }
16095
+ ];
16096
+ }
16097
+ const findings = [];
16098
+ if (pkg.manifest.state === "missing") {
16099
+ findings.push({
16100
+ severity: "warning",
16101
+ code: "missing-manifest",
16102
+ message: "no skill.json manifest; permissions and targets are undeclared"
16103
+ });
16104
+ } else if (pkg.manifest.state === "invalid") {
16105
+ findings.push({
16106
+ severity: "failure",
16107
+ code: "invalid-manifest",
16108
+ message: `skill.json is invalid: ${pkg.manifest.error}`,
16109
+ location: { path: "skill.json" }
16110
+ });
16111
+ }
16112
+ for (const path of pkg.binaries) {
16113
+ findings.push({
16114
+ severity: "warning",
16115
+ code: "binary-dropped",
16116
+ message: `"${path}" is not a text file and was left out of the snapshot; installs will not include it`,
16117
+ location: { path }
16055
16118
  });
16056
16119
  }
16120
+ for (const entry of pkg.entries) {
16121
+ if (!entry.exists) {
16122
+ findings.push({
16123
+ severity: "failure",
16124
+ code: "missing-skill-file",
16125
+ message: `skill "${entry.skillName}" references "${entry.path}", which does not exist`,
16126
+ location: { path: entry.path }
16127
+ });
16128
+ } else {
16129
+ const file2 = pkg.files.find((f) => f.path === entry.path);
16130
+ if (file2 !== void 0 && file2.content.trim() === "") {
16131
+ findings.push({
16132
+ severity: "failure",
16133
+ code: "missing-skill-file",
16134
+ message: `skill "${entry.skillName}" entry "${entry.path}" is empty`,
16135
+ location: { path: entry.path }
16136
+ });
16137
+ }
16138
+ }
16139
+ }
16057
16140
  return findings;
16058
16141
  };
16059
16142
 
16143
+ // ../validator/package.json
16144
+ var package_default = {
16145
+ name: "validator",
16146
+ version: "0.3.0",
16147
+ private: true,
16148
+ type: "module",
16149
+ exports: {
16150
+ ".": "./src/index.ts"
16151
+ },
16152
+ scripts: {
16153
+ typecheck: "tsc --noEmit"
16154
+ },
16155
+ dependencies: {
16156
+ "skill-schema": "workspace:*"
16157
+ },
16158
+ devDependencies: {
16159
+ "@types/node": "^22.0.0",
16160
+ typescript: "^6.0.3"
16161
+ }
16162
+ };
16163
+
16060
16164
  // ../validator/src/validate.ts
16061
- var ENGINE_VERSION = "0.3.0";
16165
+ var ENGINE_VERSION = package_default.version;
16062
16166
  var RULES = [
16063
16167
  { key: "structure", label: "Check package structure", run: structureRule },
16064
16168
  { key: "content", label: "Scan content for risky patterns", run: contentRule }
@@ -16070,7 +16174,7 @@ function toReportFindings(findings) {
16070
16174
  }
16071
16175
  function buildReport(pkg, findings, opts = {}) {
16072
16176
  const declared = pkg.manifest.state === "ok" ? pkg.manifest.data.permissions : [];
16073
- const detected = detectPermissions(pkg.files).map((d) => d.permission).sort();
16177
+ const detected = [...detectPermissions(pkg.files)].sort();
16074
16178
  const status = findings.some((f) => f.severity === "failure") ? "failed" : findings.some((f) => f.severity === "warning") ? "warning" : "passed";
16075
16179
  const report = {
16076
16180
  schemaVersion: "0.1",
@@ -16103,7 +16207,8 @@ async function validatePackage(dir, opts = {}) {
16103
16207
  // src/api.ts
16104
16208
  var DEFAULT_API_URL = "https://api.skillpass.dev";
16105
16209
  function resolveApiUrl(override) {
16106
- return (override ?? process.env.SKILLPASS_API ?? DEFAULT_API_URL).replace(/\/$/, "");
16210
+ const url2 = override ?? process.env.SKILLPASS_API;
16211
+ return (url2 || DEFAULT_API_URL).replace(/\/$/, "");
16107
16212
  }
16108
16213
  function parseSkillRef(ref) {
16109
16214
  const at = ref.indexOf("@");
@@ -16159,6 +16264,52 @@ async function fetchPreflight(fetchImpl, apiUrl, ref) {
16159
16264
  return { ok: true, slug, detail: detail.data, preflight: preflight.data };
16160
16265
  }
16161
16266
 
16267
+ // src/install.ts
16268
+ import { existsSync, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
16269
+ var GLOBAL_NEEDS_TARGET = "error: --global needs --target (e.g. --target claude-code)";
16270
+ var TARGET_OR_DIR = "error: pass --target or --dir, not both";
16271
+ function createOutput(emit) {
16272
+ const lines = [];
16273
+ const streamed = emit !== void 0;
16274
+ const push = (...next) => {
16275
+ lines.push(...next);
16276
+ if (next.length > 0) emit?.(next.join("\n"));
16277
+ };
16278
+ const done = (exitCode) => ({ lines, exitCode, streamed });
16279
+ return { push, done };
16280
+ }
16281
+ function isOccupied(path) {
16282
+ return existsSync(path) && (!statSync2(path).isDirectory() || readdirSync2(path).length > 0);
16283
+ }
16284
+ function receiptFor(preflight, packSlug) {
16285
+ return {
16286
+ version: preflight.version,
16287
+ sourceHash: preflight.sourceHash,
16288
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
16289
+ ...packSlug ? { pack: { slug: packSlug, version: preflight.version } } : {}
16290
+ };
16291
+ }
16292
+ function memberFiles(files, sourceDir) {
16293
+ if (sourceDir === ".") {
16294
+ return files;
16295
+ }
16296
+ const prefix = `${sourceDir}/`;
16297
+ return files.filter((f) => f.path.startsWith(prefix)).map((f) => ({ path: f.path.slice(prefix.length), content: f.content }));
16298
+ }
16299
+ function planMembers(installs, files) {
16300
+ return installs.map((m) => ({ ...m, files: memberFiles(files, m.sourceDir) }));
16301
+ }
16302
+ async function confirmRisk(preflight, opts, question, abortLine, push) {
16303
+ if (preflight.riskLevel === "low" || opts.yes) return true;
16304
+ if (!opts.confirmImpl) {
16305
+ push("", `error: a ${preflight.riskLevel}-risk skill needs confirmation; rerun with --yes`);
16306
+ return false;
16307
+ }
16308
+ if (await opts.confirmImpl(question)) return true;
16309
+ push("", abortLine);
16310
+ return false;
16311
+ }
16312
+
16162
16313
  // src/pack.ts
16163
16314
  import { posix } from "node:path";
16164
16315
  function resolvePackMembers(members, target) {
@@ -16177,6 +16328,53 @@ function resolvePackMembers(members, target) {
16177
16328
  return { installs, skipped };
16178
16329
  }
16179
16330
 
16331
+ // src/receipts.ts
16332
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
16333
+ import { join as join2 } from "node:path";
16334
+ var RECEIPT_FILE = ".skillpass.json";
16335
+ function looksLikeReceipt(value) {
16336
+ return typeof value === "object" && value !== null && typeof value.version === "string" && typeof value.sourceHash === "string";
16337
+ }
16338
+ function readReceipts(areaDir) {
16339
+ let parsed;
16340
+ try {
16341
+ parsed = JSON.parse(readFileSync2(join2(areaDir, RECEIPT_FILE), "utf8"));
16342
+ } catch {
16343
+ return {};
16344
+ }
16345
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
16346
+ return {};
16347
+ }
16348
+ const index = {};
16349
+ for (const [slug, receipt] of Object.entries(parsed)) {
16350
+ if (looksLikeReceipt(receipt)) {
16351
+ index[slug] = receipt;
16352
+ }
16353
+ }
16354
+ return index;
16355
+ }
16356
+ function writeReceipts(areaDir, index) {
16357
+ try {
16358
+ mkdirSync(areaDir, { recursive: true });
16359
+ writeFileSync(join2(areaDir, RECEIPT_FILE), `${JSON.stringify(index, null, " ")}
16360
+ `);
16361
+ } catch {
16362
+ }
16363
+ }
16364
+ function recordReceipt(areaDir, slug, receipt) {
16365
+ const index = readReceipts(areaDir);
16366
+ index[slug] = receipt;
16367
+ writeReceipts(areaDir, index);
16368
+ }
16369
+ function removeReceipt(areaDir, slug) {
16370
+ const index = readReceipts(areaDir);
16371
+ if (!(slug in index)) {
16372
+ return;
16373
+ }
16374
+ delete index[slug];
16375
+ writeReceipts(areaDir, index);
16376
+ }
16377
+
16180
16378
  // src/style.ts
16181
16379
  var wrap = (open, close) => (text) => `\x1B[${open}m${text}\x1B[${close}m`;
16182
16380
  var ANSI = {
@@ -16285,11 +16483,11 @@ function renderDiff(diff) {
16285
16483
 
16286
16484
  // src/targets.ts
16287
16485
  import { homedir } from "node:os";
16288
- import { join as join2 } from "node:path";
16486
+ import { join as join3, resolve } from "node:path";
16289
16487
  function installAreas(home = homedir()) {
16290
16488
  return {
16291
- "claude-code": { project: join2(".claude", "skills"), global: join2(home, ".claude", "skills") },
16292
- codex: { project: join2(".agents", "skills") }
16489
+ "claude-code": { project: join3(".claude", "skills"), global: join3(home, ".claude", "skills") },
16490
+ codex: { project: join3(".agents", "skills") }
16293
16491
  };
16294
16492
  }
16295
16493
  var MAPPED_TARGETS = Object.keys(installAreas());
@@ -16320,11 +16518,31 @@ function resolveTargetArea(target, global = false, home) {
16320
16518
  }
16321
16519
  function resolveTargetDir(target, slug, global = false, home) {
16322
16520
  const area = resolveTargetArea(target, global, home);
16323
- return area.ok ? { ok: true, dir: join2(area.dir, slug) } : area;
16521
+ return area.ok ? { ok: true, dir: join3(area.dir, slug) } : area;
16324
16522
  }
16325
16523
  function mappableDeclaredTargets(declared) {
16326
16524
  return declared.filter((t) => MAPPED_TARGETS.includes(t));
16327
16525
  }
16526
+ function knownAreas(cwd, home) {
16527
+ const areas = [];
16528
+ const byTarget = installAreas(home);
16529
+ for (const tool of MAPPED_TARGETS) {
16530
+ const area = byTarget[tool];
16531
+ if (!area) {
16532
+ continue;
16533
+ }
16534
+ areas.push({
16535
+ tool,
16536
+ global: false,
16537
+ label: `${tool} project (${area.project})`,
16538
+ dir: resolve(cwd, area.project)
16539
+ });
16540
+ if (area.global) {
16541
+ areas.push({ tool, global: true, label: `${tool} user (${area.global})`, dir: area.global });
16542
+ }
16543
+ }
16544
+ return areas;
16545
+ }
16328
16546
 
16329
16547
  // src/add.ts
16330
16548
  function unsafeEntryPath(path) {
@@ -16391,12 +16609,12 @@ function writeTree(files, target) {
16391
16609
  const tempDir = `${target}.tmp-${process.pid}`;
16392
16610
  try {
16393
16611
  for (const file2 of files) {
16394
- const filePath = join3(tempDir, file2.path);
16395
- mkdirSync(dirname(filePath), { recursive: true });
16396
- writeFileSync(filePath, file2.content);
16612
+ const filePath = join4(tempDir, file2.path);
16613
+ mkdirSync2(dirname(filePath), { recursive: true });
16614
+ writeFileSync2(filePath, file2.content);
16397
16615
  }
16398
- mkdirSync(dirname(target), { recursive: true });
16399
- if (existsSync(target)) {
16616
+ mkdirSync2(dirname(target), { recursive: true });
16617
+ if (existsSync2(target)) {
16400
16618
  rmdirSync(target);
16401
16619
  }
16402
16620
  renameSync(tempDir, target);
@@ -16405,13 +16623,6 @@ function writeTree(files, target) {
16405
16623
  throw err2;
16406
16624
  }
16407
16625
  }
16408
- function memberFiles(files, sourceDir) {
16409
- if (sourceDir === ".") {
16410
- return files;
16411
- }
16412
- const prefix = `${sourceDir}/`;
16413
- return files.filter((f) => f.path.startsWith(prefix)).map((f) => ({ path: f.path.slice(prefix.length), content: f.content }));
16414
- }
16415
16626
  function installChoices(declared, slug) {
16416
16627
  const choices = [];
16417
16628
  const ordered = [
@@ -16463,209 +16674,117 @@ function packChoices(declared, slug, count) {
16463
16674
  choices.push({ label: `current directory (./${slug}, raw pack source)` });
16464
16675
  return choices;
16465
16676
  }
16466
- async function runAdd(ref, opts = {}) {
16467
- const lines = [];
16468
- const streamed = opts.emit !== void 0;
16469
- const push = (...next) => {
16470
- lines.push(...next);
16471
- if (next.length > 0) {
16472
- opts.emit?.(next.join("\n"));
16473
- }
16474
- };
16475
- const done = (exitCode) => ({ lines, exitCode, streamed });
16476
- if (opts.target && opts.dir) {
16477
- push("error: pass --target or --dir, not both");
16677
+ async function promptIndex(ask, count, push) {
16678
+ while (true) {
16679
+ const answer = (await ask(`Where should it go? [1-${count}, default 1] `)).trim();
16680
+ if (answer === "") return 0;
16681
+ const index = /^\d+$/.test(answer) ? Number.parseInt(answer, 10) - 1 : -1;
16682
+ if (index >= 0 && index < count) return index;
16683
+ push(` answer 1-${count}, or press enter for the default`);
16684
+ }
16685
+ }
16686
+ async function choosePackLocation(ctx, detail, memberCount, prompt) {
16687
+ if (prompt) {
16688
+ const choices = packChoices(detail.targets, ctx.slug, memberCount);
16689
+ ctx.push("", "Install location:");
16690
+ choices.forEach((choice, i2) => ctx.push(` ${i2 + 1}) ${choice.label}`));
16691
+ const chosen = choices[await promptIndex(prompt, choices.length, ctx.push)];
16692
+ return chosen.tool ? { tool: chosen.tool, global: chosen.global ?? false } : { dir: ctx.slug };
16693
+ }
16694
+ const mappable = mappableDeclaredTargets(detail.targets);
16695
+ if (mappable.length > 0) {
16696
+ ctx.push(
16697
+ "",
16698
+ `tip: --target ${mappable[0]} installs the ${memberCount} skills into the tool's skills folder`
16699
+ );
16700
+ }
16701
+ return { dir: ctx.slug };
16702
+ }
16703
+ async function chooseSingleLocation(ctx, detail, prompt) {
16704
+ const choices = installChoices(detail.targets, ctx.slug);
16705
+ if (prompt && choices.length > 1) {
16706
+ ctx.push("", "Install location:");
16707
+ choices.forEach((choice, i2) => ctx.push(` ${i2 + 1}) ${choice.label}`));
16708
+ return choices[await promptIndex(prompt, choices.length, ctx.push)].dir;
16709
+ }
16710
+ const mappable = mappableDeclaredTargets(detail.targets);
16711
+ if (mappable.length > 0) {
16712
+ ctx.push("", `tip: --target ${mappable[0]} installs into the tool's skills folder`);
16713
+ }
16714
+ return ctx.slug;
16715
+ }
16716
+ async function installPack(ctx, members, tool, global) {
16717
+ const { push, done, slug, preflight } = ctx;
16718
+ const area = resolveTargetArea(tool, global);
16719
+ if (!area.ok) {
16720
+ push("", `error: ${area.message}`);
16478
16721
  return done(2);
16479
16722
  }
16480
- if (opts.global && !opts.target) {
16481
- push("error: --global needs --target (e.g. --target claude-code)");
16723
+ const { installs, skipped } = resolvePackMembers(members, tool);
16724
+ if (installs.length === 0) {
16725
+ push("", `error: none of this pack's skills support ${tool}`);
16482
16726
  return done(2);
16483
16727
  }
16484
- const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
16485
- const apiUrl = resolveApiUrl(opts.apiUrl);
16486
- const fetched = await fetchPreflight(fetchImpl, apiUrl, ref);
16487
- if (!fetched.ok) {
16488
- push(...fetched.result.lines);
16489
- return done(fetched.result.exitCode);
16728
+ const areaAbs = resolve2(ctx.cwd, area.dir);
16729
+ const conflicts = installs.map((m) => join4(areaAbs, m.name)).filter(isOccupied);
16730
+ if (conflicts.length > 0) {
16731
+ push(
16732
+ "",
16733
+ "error: these destinations already exist and are not empty; nothing was installed:",
16734
+ ...conflicts.map((c) => ` ${c}`)
16735
+ );
16736
+ return done(2);
16490
16737
  }
16491
- const { slug, detail, preflight } = fetched;
16492
- push(...renderPreflightReport(detail, preflight, opts.style));
16493
- if (preflight.blocked) {
16494
- return done(1);
16738
+ const download = await downloadVerified(ctx.fetchImpl, ctx.apiUrl, slug, preflight.version, preflight.sourceHash);
16739
+ if (!download.ok) {
16740
+ push("", `error: ${download.message}`);
16741
+ return done(2);
16495
16742
  }
16496
- const members = detail.packMembers ?? [];
16497
- const isPack = members.length > 0;
16498
- let targetDir = opts.dir;
16499
- let packTool;
16500
- let packGlobal = false;
16501
- if (opts.target) {
16502
- const resolved = isPack ? resolveTargetArea(opts.target, opts.global) : resolveTargetDir(opts.target, slug, opts.global);
16503
- if (!resolved.ok) {
16504
- push("", `error: ${resolved.message}`);
16505
- return done(2);
16506
- }
16507
- if (isPack) {
16508
- packTool = opts.target;
16509
- packGlobal = opts.global ?? false;
16510
- } else {
16511
- targetDir = resolved.dir;
16512
- }
16513
- if (!detail.targets.includes(opts.target)) {
16514
- push("", `warning: this skill does not declare ${opts.target} as a target`);
16515
- }
16516
- }
16517
- if (preflight.riskLevel !== "low" && !opts.yes) {
16518
- if (!opts.confirmImpl) {
16519
- push("", `error: a ${preflight.riskLevel}-risk skill needs confirmation; rerun with --yes`);
16520
- return done(2);
16521
- }
16522
- const confirmed = await opts.confirmImpl(
16523
- `Install ${slug}@${preflight.version} (${preflight.riskLevel} risk)? [y/N] `
16524
- );
16525
- if (!confirmed) {
16526
- push("", "Install aborted.");
16527
- return done(2);
16528
- }
16743
+ const plans = planMembers(installs, download.files);
16744
+ const missing = plans.find((p) => p.files.length === 0);
16745
+ if (missing) {
16746
+ push("", `error: the snapshot has no files for "${missing.name}"; nothing was installed`);
16747
+ return done(2);
16529
16748
  }
16530
- const prompt = opts.promptImpl;
16531
- const promptIndex = async (ask, count) => {
16532
- while (true) {
16533
- const answer = (await ask(`Where should it go? [1-${count}, default 1] `)).trim();
16534
- if (answer === "") {
16535
- return 0;
16536
- }
16537
- const index = /^\d+$/.test(answer) ? Number.parseInt(answer, 10) - 1 : -1;
16538
- if (index >= 0 && index < count) {
16539
- return index;
16540
- }
16541
- push(` answer 1-${count}, or press enter for the default`);
16542
- }
16543
- };
16544
- if (isPack && packTool === void 0 && targetDir === void 0) {
16545
- if (prompt) {
16546
- const choices = packChoices(detail.targets, slug, members.length);
16547
- push("", "Install location:");
16548
- choices.forEach((choice, i2) => push(` ${i2 + 1}) ${choice.label}`));
16549
- const chosen = choices[await promptIndex(prompt, choices.length)];
16550
- if (chosen.tool) {
16551
- packTool = chosen.tool;
16552
- packGlobal = chosen.global ?? false;
16553
- } else {
16554
- targetDir = slug;
16555
- }
16556
- } else {
16557
- targetDir = slug;
16558
- const mappable = mappableDeclaredTargets(detail.targets);
16559
- if (mappable.length > 0) {
16560
- push(
16561
- "",
16562
- `tip: --target ${mappable[0]} installs the ${members.length} skills into the tool's skills folder`
16563
- );
16564
- }
16565
- }
16749
+ for (const name of skipped) {
16750
+ push("", `note: ${name} does not support ${tool}; skipped`);
16566
16751
  }
16567
- if (packTool !== void 0) {
16568
- const area = resolveTargetArea(packTool, packGlobal);
16569
- if (!area.ok) {
16570
- push("", `error: ${area.message}`);
16571
- return done(2);
16572
- }
16573
- const { installs, skipped } = resolvePackMembers(members, packTool);
16574
- if (installs.length === 0) {
16575
- push("", `error: none of this pack's skills support ${packTool}`);
16576
- return done(2);
16577
- }
16578
- const areaAbs = resolve(opts.cwd ?? process.cwd(), area.dir);
16579
- const conflicts = installs.map((m) => join3(areaAbs, m.name)).filter(
16580
- (dest) => existsSync(dest) && (!statSync2(dest).isDirectory() || readdirSync2(dest).length > 0)
16581
- );
16582
- if (conflicts.length > 0) {
16583
- push(
16584
- "",
16585
- "error: these destinations already exist and are not empty; nothing was installed:",
16586
- ...conflicts.map((c) => ` ${c}`)
16587
- );
16588
- return done(2);
16589
- }
16590
- const download2 = await downloadVerified(
16591
- fetchImpl,
16592
- apiUrl,
16593
- slug,
16594
- preflight.version,
16595
- preflight.sourceHash
16596
- );
16597
- if (!download2.ok) {
16598
- push("", `error: ${download2.message}`);
16599
- return done(2);
16600
- }
16601
- const plans = installs.map((m) => ({ ...m, files: memberFiles(download2.files, m.sourceDir) }));
16602
- const missing = plans.find((p) => p.files.length === 0);
16603
- if (missing) {
16604
- push("", `error: the snapshot has no files for "${missing.name}"; nothing was installed`);
16605
- return done(2);
16606
- }
16607
- for (const name of skipped) {
16608
- push("", `note: ${name} does not support ${packTool}; skipped`);
16609
- }
16610
- const written = [];
16611
- try {
16612
- for (const plan of plans) {
16613
- writeTree(plan.files, join3(areaAbs, plan.name));
16614
- written.push(plan.name);
16615
- }
16616
- } catch {
16617
- push(
16618
- "",
16619
- `error: could not write ${plans[written.length].name}; installed before the failure: ${written.join(", ") || "none"}`
16620
- );
16621
- return done(2);
16752
+ const written = [];
16753
+ try {
16754
+ for (const plan of plans) {
16755
+ writeTree(plan.files, join4(areaAbs, plan.name));
16756
+ written.push(plan.name);
16757
+ recordReceipt(areaAbs, plan.name, receiptFor(preflight, slug));
16622
16758
  }
16759
+ } catch {
16623
16760
  push(
16624
16761
  "",
16625
- `Installed ${written.length} skills to ${areaAbs}`,
16626
- ` ${written.join(", ")}`,
16627
- "Source hash verified against the Skill Passport."
16628
- );
16629
- return done(0);
16630
- }
16631
- if (isPack && opts.dir) {
16632
- push(
16633
- "",
16634
- `note: --dir installs the raw pack source; --target <tool> installs the ${members.length} skills individually`
16762
+ `error: could not write ${plans[written.length].name}; installed before the failure: ${written.join(", ") || "none"}`
16635
16763
  );
16764
+ return done(2);
16636
16765
  }
16637
- if (!targetDir) {
16638
- const choices = installChoices(detail.targets, slug);
16639
- if (prompt && choices.length > 1) {
16640
- push("", "Install location:");
16641
- choices.forEach((choice, i2) => push(` ${i2 + 1}) ${choice.label}`));
16642
- targetDir = choices[await promptIndex(prompt, choices.length)].dir;
16643
- } else {
16644
- targetDir = slug;
16645
- const mappable = mappableDeclaredTargets(detail.targets);
16646
- if (mappable.length > 0) {
16647
- push("", `tip: --target ${mappable[0]} installs into the tool's skills folder`);
16648
- }
16649
- }
16650
- }
16651
- const target = resolve(opts.cwd ?? process.cwd(), targetDir);
16652
- if (existsSync(target)) {
16653
- if (!statSync2(target).isDirectory()) {
16766
+ push(
16767
+ "",
16768
+ `Installed ${written.length} skills to ${areaAbs}`,
16769
+ ` ${written.join(", ")}`,
16770
+ "Source hash verified against the Skill Passport."
16771
+ );
16772
+ return done(0);
16773
+ }
16774
+ async function installSingle(ctx, targetDir) {
16775
+ const { push, done, slug, preflight } = ctx;
16776
+ const target = resolve2(ctx.cwd, targetDir);
16777
+ if (existsSync2(target)) {
16778
+ if (!statSync3(target).isDirectory()) {
16654
16779
  push("", `error: target ${target} already exists and is not a directory`);
16655
16780
  return done(2);
16656
16781
  }
16657
- if (readdirSync2(target).length > 0) {
16782
+ if (readdirSync3(target).length > 0) {
16658
16783
  push("", `error: target directory ${target} already exists and is not empty`);
16659
16784
  return done(2);
16660
16785
  }
16661
16786
  }
16662
- const download = await downloadVerified(
16663
- fetchImpl,
16664
- apiUrl,
16665
- slug,
16666
- preflight.version,
16667
- preflight.sourceHash
16668
- );
16787
+ const download = await downloadVerified(ctx.fetchImpl, ctx.apiUrl, slug, preflight.version, preflight.sourceHash);
16669
16788
  if (!download.ok) {
16670
16789
  push("", `error: ${download.message}`);
16671
16790
  return done(2);
@@ -16676,6 +16795,10 @@ async function runAdd(ref, opts = {}) {
16676
16795
  push("", "error: could not write the install; nothing was installed");
16677
16796
  return done(2);
16678
16797
  }
16798
+ const area = knownAreas(ctx.cwd).find((a) => a.dir === dirname(target));
16799
+ if (area) {
16800
+ recordReceipt(area.dir, slug, receiptFor(preflight));
16801
+ }
16679
16802
  push(
16680
16803
  "",
16681
16804
  `Installed ${download.files.length} file(s) to ${target}`,
@@ -16683,30 +16806,89 @@ async function runAdd(ref, opts = {}) {
16683
16806
  );
16684
16807
  return done(0);
16685
16808
  }
16686
-
16687
- // src/list.ts
16688
- import { existsSync as existsSync2, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
16689
- import { join as join4, resolve as resolve2 } from "node:path";
16690
- function knownAreas(cwd, home) {
16691
- const areas = [];
16692
- const byTarget = installAreas(home);
16693
- for (const tool of MAPPED_TARGETS) {
16694
- const area = byTarget[tool];
16695
- if (!area) {
16696
- continue;
16809
+ async function runAdd(ref, opts = {}) {
16810
+ const { push, done } = createOutput(opts.emit);
16811
+ if (opts.target && opts.dir) {
16812
+ push(TARGET_OR_DIR);
16813
+ return done(2);
16814
+ }
16815
+ if (opts.global && !opts.target) {
16816
+ push(GLOBAL_NEEDS_TARGET);
16817
+ return done(2);
16818
+ }
16819
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
16820
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16821
+ const fetched = await fetchPreflight(fetchImpl, apiUrl, ref);
16822
+ if (!fetched.ok) {
16823
+ push(...fetched.result.lines);
16824
+ return done(fetched.result.exitCode);
16825
+ }
16826
+ const { slug, detail, preflight } = fetched;
16827
+ push(...renderPreflightReport(detail, preflight, opts.style));
16828
+ if (preflight.blocked) {
16829
+ return done(1);
16830
+ }
16831
+ const members = detail.packMembers ?? [];
16832
+ const isPack = members.length > 0;
16833
+ let targetDir = opts.dir;
16834
+ let pack;
16835
+ if (opts.target) {
16836
+ const resolved = isPack ? resolveTargetArea(opts.target, opts.global) : resolveTargetDir(opts.target, slug, opts.global);
16837
+ if (!resolved.ok) {
16838
+ push("", `error: ${resolved.message}`);
16839
+ return done(2);
16697
16840
  }
16698
- areas.push({ label: `${tool} project (${area.project})`, dir: resolve2(cwd, area.project) });
16699
- if (area.global) {
16700
- areas.push({ label: `${tool} user (${area.global})`, dir: area.global });
16841
+ if (isPack) {
16842
+ pack = { tool: opts.target, global: opts.global ?? false };
16843
+ } else {
16844
+ targetDir = resolved.dir;
16845
+ }
16846
+ if (!detail.targets.includes(opts.target)) {
16847
+ push("", `warning: this skill does not declare ${opts.target} as a target`);
16701
16848
  }
16702
16849
  }
16703
- return areas;
16850
+ const confirmed = await confirmRisk(
16851
+ preflight,
16852
+ opts,
16853
+ `Install ${slug}@${preflight.version} (${preflight.riskLevel} risk)? [y/N] `,
16854
+ "Install aborted.",
16855
+ push
16856
+ );
16857
+ if (!confirmed) return done(2);
16858
+ const ctx = {
16859
+ push,
16860
+ done,
16861
+ fetchImpl,
16862
+ apiUrl,
16863
+ cwd: opts.cwd ?? process.cwd(),
16864
+ slug,
16865
+ preflight
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) {
16876
+ push(
16877
+ "",
16878
+ `note: --dir installs the raw pack source; --target <tool> installs the ${members.length} skills individually`
16879
+ );
16880
+ }
16881
+ return installSingle(ctx, targetDir ?? await chooseSingleLocation(ctx, detail, opts.promptImpl));
16704
16882
  }
16883
+
16884
+ // src/list.ts
16885
+ import { existsSync as existsSync3, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
16886
+ import { join as join5 } from "node:path";
16705
16887
  function installedIn(dir) {
16706
- if (!existsSync2(dir)) {
16888
+ if (!existsSync3(dir)) {
16707
16889
  return [];
16708
16890
  }
16709
- return readdirSync3(dir).sort().filter((name) => statSync3(join4(dir, name)).isDirectory());
16891
+ return readdirSync4(dir).sort().filter((name) => statSync4(join5(dir, name), { throwIfNoEntry: false })?.isDirectory() ?? false);
16710
16892
  }
16711
16893
  function runList(opts = {}) {
16712
16894
  const cwd = opts.cwd ?? process.cwd();
@@ -16720,8 +16902,10 @@ function runList(opts = {}) {
16720
16902
  lines.push("");
16721
16903
  }
16722
16904
  lines.push(area.label);
16905
+ const receipts = readReceipts(area.dir);
16723
16906
  for (const name of names) {
16724
- lines.push(` ${name}`);
16907
+ const version2 = receipts[name]?.version;
16908
+ lines.push(version2 ? ` ${name} ${version2}` : ` ${name}`);
16725
16909
  }
16726
16910
  }
16727
16911
  if (lines.length === 0) {
@@ -16730,36 +16914,182 @@ function runList(opts = {}) {
16730
16914
  return { lines, exitCode: 0 };
16731
16915
  }
16732
16916
 
16733
- // src/remove.ts
16734
- import { existsSync as existsSync3, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
16735
- import { join as join5, resolve as resolve3 } from "node:path";
16736
- function looksLikeInstalledSkill(dir) {
16737
- return existsSync3(join5(dir, "SKILL.md")) || existsSync3(join5(dir, "skill.json"));
16917
+ // src/outdated.ts
16918
+ import { join as join6 } from "node:path";
16919
+ async function identifyByHash(fetchImpl, apiUrl, slug, dir) {
16920
+ let hash2;
16921
+ try {
16922
+ hash2 = loadPackage(dir).sourceHash;
16923
+ } catch {
16924
+ return void 0;
16925
+ }
16926
+ const detail = await getParsed(
16927
+ fetchImpl,
16928
+ `${apiUrl}/skills/${encodeURIComponent(slug)}`,
16929
+ publicSkillDetailSchema
16930
+ );
16931
+ if (!detail.ok) {
16932
+ return void 0;
16933
+ }
16934
+ for (const version2 of detail.data.versions) {
16935
+ const preflight = await getParsed(
16936
+ fetchImpl,
16937
+ `${apiUrl}/skills/${encodeURIComponent(slug)}/${encodeURIComponent(version2.version)}/preflight`,
16938
+ publicPreflightSchema
16939
+ );
16940
+ if (preflight.ok && preflight.data.sourceHash === hash2) {
16941
+ return version2.version;
16942
+ }
16943
+ }
16944
+ return void 0;
16738
16945
  }
16739
- function candidateDirs(slug, cwd, home) {
16740
- const dirs = [];
16741
- const byTarget = installAreas(home);
16742
- for (const tool of MAPPED_TARGETS) {
16743
- const area = byTarget[tool];
16744
- if (!area) {
16946
+ async function runOutdated(opts = {}) {
16947
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
16948
+ const apiUrl = resolveApiUrl(opts.apiUrl);
16949
+ const cwd = opts.cwd ?? process.cwd();
16950
+ const fetched = await getParsed(fetchImpl, `${apiUrl}/skills`, publicSkillListSchema);
16951
+ if (!fetched.ok) {
16952
+ return { lines: [`error: ${fetched.message}`], exitCode: 2 };
16953
+ }
16954
+ const latestBySlug = new Map(fetched.data.map((s) => [s.slug, s]));
16955
+ const lines = [];
16956
+ let outdatedCount = 0;
16957
+ let tracked = 0;
16958
+ for (const area of knownAreas(cwd, opts.home)) {
16959
+ const names = installedIn(area.dir);
16960
+ if (names.length === 0) {
16745
16961
  continue;
16746
16962
  }
16747
- dirs.push(resolve3(cwd, join5(area.project, slug)));
16748
- if (area.global) {
16749
- dirs.push(join5(area.global, slug));
16963
+ const receipts = readReceipts(area.dir);
16964
+ const rows = [];
16965
+ const seenPacks = /* @__PURE__ */ new Set();
16966
+ let unrelated = 0;
16967
+ for (const name of names) {
16968
+ const receipt = receipts[name];
16969
+ if (receipt?.pack) {
16970
+ if (seenPacks.has(receipt.pack.slug)) {
16971
+ continue;
16972
+ }
16973
+ seenPacks.add(receipt.pack.slug);
16974
+ tracked += 1;
16975
+ const members = Object.values(receipts).filter(
16976
+ (r) => r.pack?.slug === receipt.pack?.slug
16977
+ ).length;
16978
+ const latest = latestBySlug.get(receipt.pack.slug)?.version;
16979
+ if (latest === void 0) {
16980
+ rows.push(` ${receipt.pack.slug} ${receipt.pack.version} not in the directory`);
16981
+ } else if (latest === receipt.pack.version) {
16982
+ rows.push(` ${receipt.pack.slug} ${receipt.pack.version} current (pack, ${members} skills)`);
16983
+ } else {
16984
+ outdatedCount += 1;
16985
+ rows.push(
16986
+ ` ${receipt.pack.slug} ${receipt.pack.version} -> ${latest} (pack, ${members} skills)`
16987
+ );
16988
+ }
16989
+ continue;
16990
+ }
16991
+ if (receipt) {
16992
+ tracked += 1;
16993
+ const latest = latestBySlug.get(name)?.version;
16994
+ if (latest === void 0) {
16995
+ rows.push(` ${name} ${receipt.version} not in the directory`);
16996
+ } else if (latest === receipt.version) {
16997
+ rows.push(` ${name} ${receipt.version} current`);
16998
+ } else {
16999
+ outdatedCount += 1;
17000
+ rows.push(` ${name} ${receipt.version} -> ${latest}`);
17001
+ }
17002
+ continue;
17003
+ }
17004
+ if (!latestBySlug.has(name)) {
17005
+ unrelated += 1;
17006
+ continue;
17007
+ }
17008
+ const identified = await identifyByHash(fetchImpl, apiUrl, name, join6(area.dir, name));
17009
+ if (identified !== void 0) {
17010
+ tracked += 1;
17011
+ const latest = latestBySlug.get(name)?.version;
17012
+ if (latest === identified) {
17013
+ rows.push(` ${name} ${identified} current (identified by hash)`);
17014
+ } else {
17015
+ outdatedCount += 1;
17016
+ rows.push(` ${name} ${identified} -> ${latest} (identified by hash)`);
17017
+ }
17018
+ continue;
17019
+ }
17020
+ rows.push(` ${name} ? no receipt - reinstall to track updates`);
16750
17021
  }
17022
+ if (rows.length > 0) {
17023
+ if (lines.length > 0) {
17024
+ lines.push("");
17025
+ }
17026
+ lines.push(area.label, ...rows);
17027
+ if (unrelated > 0) {
17028
+ lines.push(` (${unrelated} folder(s) not from the directory)`);
17029
+ }
17030
+ }
17031
+ }
17032
+ if (tracked === 0 && lines.length === 0) {
17033
+ return { lines: ["No skills installed in the known install areas."], exitCode: 0 };
16751
17034
  }
16752
- dirs.push(resolve3(cwd, slug));
16753
- return [...new Set(dirs)];
17035
+ lines.push(
17036
+ "",
17037
+ outdatedCount === 0 ? "Everything is current." : `${outdatedCount} update(s) available - skillpass update <slug>`
17038
+ );
17039
+ return { lines, exitCode: outdatedCount === 0 ? 0 : 1 };
17040
+ }
17041
+
17042
+ // src/remove.ts
17043
+ import { existsSync as existsSync4, rmSync as rmSync2, statSync as statSync5 } from "node:fs";
17044
+ import { dirname as dirname2, join as join7, resolve as resolve3 } from "node:path";
17045
+ function looksLikeInstalledSkill(dir) {
17046
+ return existsSync4(join7(dir, "SKILL.md")) || existsSync4(join7(dir, "skill.json"));
17047
+ }
17048
+ function candidateDirs(slug, cwd, home) {
17049
+ const inAreas = knownAreas(cwd, home).map((area) => join7(area.dir, slug));
17050
+ return [.../* @__PURE__ */ new Set([...inAreas, resolve3(cwd, slug)])];
16754
17051
  }
16755
17052
  function runRemove(slug, opts = {}) {
16756
17053
  if (opts.target && opts.dir) {
16757
- return { lines: ["error: pass --target or --dir, not both"], exitCode: 2 };
17054
+ return { lines: [TARGET_OR_DIR], exitCode: 2 };
16758
17055
  }
16759
17056
  if (opts.global && !opts.target) {
16760
- return { lines: ["error: --global needs --target (e.g. --target claude-code)"], exitCode: 2 };
17057
+ return { lines: [GLOBAL_NEEDS_TARGET], exitCode: 2 };
16761
17058
  }
16762
17059
  const cwd = opts.cwd ?? process.cwd();
17060
+ if (!opts.dir) {
17061
+ let areas = knownAreas(cwd, opts.home);
17062
+ if (opts.target) {
17063
+ areas = areas.filter((a) => a.tool === opts.target && a.global === (opts.global ?? false));
17064
+ }
17065
+ const packHits = areas.map((area) => ({
17066
+ area,
17067
+ members: Object.entries(readReceipts(area.dir)).filter(([, r]) => r.pack?.slug === slug).map(([name]) => name)
17068
+ })).filter((hit) => hit.members.length > 0);
17069
+ if (packHits.length > 1) {
17070
+ return {
17071
+ lines: [
17072
+ `error: the ${slug} pack is installed in more than one place; pick one with --target:`,
17073
+ ...packHits.map((hit) => ` ${hit.area.dir}`)
17074
+ ],
17075
+ exitCode: 2
17076
+ };
17077
+ }
17078
+ if (packHits.length === 1) {
17079
+ const { area, members } = packHits[0];
17080
+ for (const name of members) {
17081
+ rmSync2(join7(area.dir, name), { recursive: true, force: true });
17082
+ removeReceipt(area.dir, name);
17083
+ }
17084
+ return {
17085
+ lines: [
17086
+ `Removed pack ${slug} (${members.length} skills) from ${area.dir}`,
17087
+ ` ${members.sort().join(", ")}`
17088
+ ],
17089
+ exitCode: 0
17090
+ };
17091
+ }
17092
+ }
16763
17093
  let dir;
16764
17094
  if (opts.target) {
16765
17095
  const resolved = resolveTargetDir(opts.target, slug, opts.global, opts.home);
@@ -16771,7 +17101,7 @@ function runRemove(slug, opts = {}) {
16771
17101
  dir = resolve3(cwd, opts.dir);
16772
17102
  } else {
16773
17103
  const found = candidateDirs(slug, cwd, opts.home).filter(
16774
- (candidate) => existsSync3(candidate)
17104
+ (candidate) => existsSync4(candidate)
16775
17105
  );
16776
17106
  if (found.length === 0) {
16777
17107
  return { lines: [`error: ${slug} is not installed in any known location`], exitCode: 2 };
@@ -16787,10 +17117,10 @@ function runRemove(slug, opts = {}) {
16787
17117
  }
16788
17118
  dir = found[0];
16789
17119
  }
16790
- if (!existsSync3(dir)) {
17120
+ if (!existsSync4(dir)) {
16791
17121
  return { lines: [`error: nothing installed at ${dir}`], exitCode: 2 };
16792
17122
  }
16793
- if (!statSync4(dir).isDirectory()) {
17123
+ if (!statSync5(dir).isDirectory()) {
16794
17124
  return { lines: [`error: ${dir} is not a directory`], exitCode: 2 };
16795
17125
  }
16796
17126
  if (!looksLikeInstalledSkill(dir)) {
@@ -16800,6 +17130,7 @@ function runRemove(slug, opts = {}) {
16800
17130
  };
16801
17131
  }
16802
17132
  rmSync2(dir, { recursive: true });
17133
+ removeReceipt(dirname2(dir), slug);
16803
17134
  return { lines: [`Removed ${slug} from ${dir}`], exitCode: 0 };
16804
17135
  }
16805
17136
 
@@ -16928,6 +17259,260 @@ async function runSearch(opts = {}) {
16928
17259
  };
16929
17260
  }
16930
17261
 
17262
+ // src/update.ts
17263
+ import { existsSync as existsSync5, renameSync as renameSync2, rmSync as rmSync3 } from "node:fs";
17264
+ import { join as join8 } from "node:path";
17265
+ async function locate(slug, opts, fetchImpl, apiUrl) {
17266
+ const cwd = opts.cwd ?? process.cwd();
17267
+ let areas = knownAreas(cwd, opts.home);
17268
+ if (opts.target) {
17269
+ areas = areas.filter((a) => a.tool === opts.target && a.global === (opts.global ?? false));
17270
+ }
17271
+ const hits = [];
17272
+ for (const area of areas) {
17273
+ const receipts = readReceipts(area.dir);
17274
+ const receipt = receipts[slug];
17275
+ const dir = join8(area.dir, slug);
17276
+ if (receipt) {
17277
+ hits.push({
17278
+ area,
17279
+ dir,
17280
+ installedVersion: receipt.version,
17281
+ packSlug: receipt.pack?.slug
17282
+ });
17283
+ continue;
17284
+ }
17285
+ const packMember = Object.values(receipts).find((r) => r.pack?.slug === slug);
17286
+ if (packMember?.pack) {
17287
+ hits.push({ area, dir: area.dir, installedVersion: packMember.pack.version, packSlug: slug });
17288
+ continue;
17289
+ }
17290
+ if (existsSync5(dir)) {
17291
+ const identified = await identifyByHash(fetchImpl, apiUrl, slug, dir);
17292
+ if (identified !== void 0) {
17293
+ hits.push({ area, dir, installedVersion: identified });
17294
+ }
17295
+ }
17296
+ }
17297
+ return hits;
17298
+ }
17299
+ function permissionChanges(installed, target) {
17300
+ const flat = (sets) => /* @__PURE__ */ new Set([...sets.declared, ...sets.detected]);
17301
+ const before = flat(installed);
17302
+ const after = flat(target);
17303
+ return {
17304
+ added: [...after].filter((key) => !before.has(key)).sort(),
17305
+ removed: [...before].filter((key) => !after.has(key)).sort()
17306
+ };
17307
+ }
17308
+ function swapTree(files, dir) {
17309
+ const fresh = `${dir}.new-${process.pid}`;
17310
+ const aside = `${dir}.old-${process.pid}`;
17311
+ rmSync3(fresh, { recursive: true, force: true });
17312
+ writeTree(files, fresh);
17313
+ try {
17314
+ if (existsSync5(dir)) renameSync2(dir, aside);
17315
+ renameSync2(fresh, dir);
17316
+ } catch (err2) {
17317
+ if (existsSync5(aside) && !existsSync5(dir)) renameSync2(aside, dir);
17318
+ rmSync3(fresh, { recursive: true, force: true });
17319
+ throw err2;
17320
+ }
17321
+ rmSync3(aside, { recursive: true, force: true });
17322
+ }
17323
+ async function runUpdate(ref, opts = {}) {
17324
+ const { push, done } = createOutput(opts.emit);
17325
+ const st = opts.style ?? PLAIN;
17326
+ if (opts.global && !opts.target) {
17327
+ push(GLOBAL_NEEDS_TARGET);
17328
+ return done(2);
17329
+ }
17330
+ const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
17331
+ const apiUrl = resolveApiUrl(opts.apiUrl);
17332
+ const { slug } = parseSkillRef(ref);
17333
+ if (!slug) {
17334
+ push(`error: invalid skill reference "${ref}"`);
17335
+ return done(2);
17336
+ }
17337
+ const hits = await locate(slug, opts, fetchImpl, apiUrl);
17338
+ const memberHit = hits.find((h) => h.packSlug !== void 0 && h.packSlug !== slug);
17339
+ if (memberHit?.packSlug !== void 0) {
17340
+ push(
17341
+ `error: ${slug} is part of the ${memberHit.packSlug} pack; run skillpass update ${memberHit.packSlug}`
17342
+ );
17343
+ return done(2);
17344
+ }
17345
+ if (hits.length === 0) {
17346
+ push(
17347
+ `error: ${slug} is not installed in a known skills area (only receipted or hash-identifiable installs can update)`
17348
+ );
17349
+ return done(2);
17350
+ }
17351
+ if (hits.length > 1) {
17352
+ push(
17353
+ `error: ${slug} is installed in more than one place; pick one with --target [--global]:`,
17354
+ ...hits.map((h) => ` ${h.dir}`)
17355
+ );
17356
+ return done(2);
17357
+ }
17358
+ const installed = hits[0];
17359
+ const fetched = await fetchPreflight(fetchImpl, apiUrl, ref);
17360
+ if (!fetched.ok) {
17361
+ push(...fetched.result.lines);
17362
+ return done(fetched.result.exitCode);
17363
+ }
17364
+ const { detail, preflight } = fetched;
17365
+ if ((detail.packMembers?.length ?? 0) > 0 || installed.packSlug === slug) {
17366
+ return runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl, apiUrl, st, push, done);
17367
+ }
17368
+ if (installed.installedVersion === preflight.version) {
17369
+ push(`${slug} is already at ${preflight.version}.`);
17370
+ return done(0);
17371
+ }
17372
+ push(...renderPreflightReport(detail, preflight, opts.style));
17373
+ push("", ...await changesVsInstalled(fetchImpl, apiUrl, slug, installed, preflight, st));
17374
+ if (preflight.blocked) {
17375
+ return done(1);
17376
+ }
17377
+ const confirmed = await confirmRisk(
17378
+ preflight,
17379
+ opts,
17380
+ `Update ${slug} to ${preflight.version} (${preflight.riskLevel} risk)? [y/N] `,
17381
+ "Update aborted; the installed version was left in place.",
17382
+ push
17383
+ );
17384
+ if (!confirmed) return done(2);
17385
+ const download = await downloadVerified(fetchImpl, apiUrl, slug, preflight.version, preflight.sourceHash);
17386
+ if (!download.ok) {
17387
+ push("", `error: ${download.message}`);
17388
+ return done(2);
17389
+ }
17390
+ try {
17391
+ swapTree(download.files, installed.dir);
17392
+ } catch {
17393
+ push("", "error: could not replace the install; the installed version was left in place");
17394
+ return done(2);
17395
+ }
17396
+ recordReceipt(installed.area.dir, slug, receiptFor(preflight));
17397
+ push(
17398
+ "",
17399
+ `Updated ${slug} ${installed.installedVersion ?? "?"} -> ${preflight.version} in ${installed.dir}`,
17400
+ "Source hash verified against the Skill Passport."
17401
+ );
17402
+ return done(0);
17403
+ }
17404
+ async function changesVsInstalled(fetchImpl, apiUrl, slug, installed, target, st) {
17405
+ if (installed.installedVersion === void 0) {
17406
+ return ["Changes vs installed: unknown (no version identified)"];
17407
+ }
17408
+ const before = await getParsed(
17409
+ fetchImpl,
17410
+ `${apiUrl}/skills/${encodeURIComponent(slug)}/${encodeURIComponent(installed.installedVersion)}/preflight`,
17411
+ publicPreflightSchema
17412
+ );
17413
+ if (!before.ok) {
17414
+ return [
17415
+ `Changes vs installed v${installed.installedVersion}: unavailable (that version is no longer published)`
17416
+ ];
17417
+ }
17418
+ const { added, removed } = permissionChanges(before.data.permissions, target.permissions);
17419
+ if (added.length === 0 && removed.length === 0) {
17420
+ return [`Changes vs installed v${installed.installedVersion}: no permission changes`];
17421
+ }
17422
+ return [
17423
+ `Changes vs installed v${installed.installedVersion}`,
17424
+ ...added.map((key) => st.red(` + ${key} (new)`)),
17425
+ ...removed.map((key) => ` - ${key} (no longer requested)`)
17426
+ ];
17427
+ }
17428
+ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl, apiUrl, st, push, done) {
17429
+ if (installed.packSlug !== slug) {
17430
+ push(`error: ${slug} is a pack but this install is not tracked as one; reinstall with skillpass add`);
17431
+ return done(2);
17432
+ }
17433
+ if (installed.installedVersion === preflight.version) {
17434
+ push(`${slug} is already at ${preflight.version}.`);
17435
+ return done(0);
17436
+ }
17437
+ push(...renderPreflightReport(detail, preflight, opts.style));
17438
+ push("", ...await changesVsInstalled(fetchImpl, apiUrl, slug, installed, preflight, st));
17439
+ if (preflight.blocked) {
17440
+ return done(1);
17441
+ }
17442
+ const confirmed = await confirmRisk(
17443
+ preflight,
17444
+ opts,
17445
+ `Update ${slug} to ${preflight.version} (${preflight.riskLevel} risk)? [y/N] `,
17446
+ "Update aborted; the installed version was left in place.",
17447
+ push
17448
+ );
17449
+ if (!confirmed) return done(2);
17450
+ const areaDir = installed.area.dir;
17451
+ const { installs, skipped } = resolvePackMembers(detail.packMembers ?? [], installed.area.tool);
17452
+ if (installs.length === 0) {
17453
+ push("", `error: none of the new version's skills support ${installed.area.tool}`);
17454
+ return done(2);
17455
+ }
17456
+ const receipts = readReceipts(areaDir);
17457
+ const currentMembers = Object.entries(receipts).filter(([, r]) => r.pack?.slug === slug).map(([name]) => name);
17458
+ const newNames = new Set(installs.map((m) => m.name));
17459
+ const toRemove = currentMembers.filter((name) => !newNames.has(name));
17460
+ const additions = installs.filter((m) => !currentMembers.includes(m.name));
17461
+ const conflicts = additions.map((m) => join8(areaDir, m.name)).filter(isOccupied);
17462
+ if (conflicts.length > 0) {
17463
+ push(
17464
+ "",
17465
+ "error: these destinations already exist and are not empty; nothing was changed:",
17466
+ ...conflicts.map((c) => ` ${c}`)
17467
+ );
17468
+ return done(2);
17469
+ }
17470
+ const download = await downloadVerified(fetchImpl, apiUrl, slug, preflight.version, preflight.sourceHash);
17471
+ if (!download.ok) {
17472
+ push("", `error: ${download.message}`);
17473
+ return done(2);
17474
+ }
17475
+ const plans = planMembers(installs, download.files);
17476
+ const missing = plans.find((p) => p.files.length === 0);
17477
+ if (missing) {
17478
+ push("", `error: the snapshot has no files for "${missing.name}"; nothing was changed`);
17479
+ return done(2);
17480
+ }
17481
+ for (const name of skipped) {
17482
+ push("", `note: ${name} does not support ${installed.area.tool}; skipped`);
17483
+ }
17484
+ const done1 = [];
17485
+ try {
17486
+ for (const plan of plans) {
17487
+ const dest = join8(areaDir, plan.name);
17488
+ if (existsSync5(dest)) {
17489
+ swapTree(plan.files, dest);
17490
+ } else {
17491
+ writeTree(plan.files, dest);
17492
+ }
17493
+ done1.push(plan.name);
17494
+ recordReceipt(areaDir, plan.name, receiptFor(preflight, slug));
17495
+ }
17496
+ } catch {
17497
+ push(
17498
+ "",
17499
+ `error: could not write ${plans[done1.length].name}; updated before the failure: ${done1.join(", ") || "none"}`
17500
+ );
17501
+ return done(2);
17502
+ }
17503
+ for (const name of toRemove) {
17504
+ rmSync3(join8(areaDir, name), { recursive: true, force: true });
17505
+ removeReceipt(areaDir, name);
17506
+ }
17507
+ push(
17508
+ "",
17509
+ `Updated ${slug} ${installed.installedVersion ?? "?"} -> ${preflight.version} in ${areaDir}`,
17510
+ ` ${done1.length} skill(s)${additions.length > 0 ? `, added: ${additions.map((m) => m.name).join(", ")}` : ""}${toRemove.length > 0 ? `, removed: ${toRemove.join(", ")}` : ""}`,
17511
+ "Source hash verified against the Skill Passport."
17512
+ );
17513
+ return done(0);
17514
+ }
17515
+
16931
17516
  // src/scan.ts
16932
17517
  async function runScan(path, opts = {}) {
16933
17518
  let report;
@@ -16970,7 +17555,7 @@ async function runScan(path, opts = {}) {
16970
17555
  }
16971
17556
 
16972
17557
  // src/index.ts
16973
- var VERSION = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8")).version;
17558
+ var VERSION = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8")).version;
16974
17559
  var USAGE = [
16975
17560
  "skillpass - validate AI agent skills locally and inspect hosted passports",
16976
17561
  "",
@@ -16984,6 +17569,9 @@ var USAGE = [
16984
17569
  " skillpass remove <slug> [--target <tool> [--global] | --dir <path>]",
16985
17570
  " remove an installed skill",
16986
17571
  " skillpass list show installed skills in the known areas",
17572
+ " skillpass outdated compare installed skills to the directory",
17573
+ " skillpass update <slug>[@version] [--target <tool> [--global]] [--yes]",
17574
+ " update an installed skill through the gate",
16987
17575
  "",
16988
17576
  "Flags:",
16989
17577
  " --json print machine-readable JSON instead of the readable report",
@@ -16998,7 +17586,8 @@ var USAGE = [
16998
17586
  " --help show this message",
16999
17587
  "",
17000
17588
  "The search, report, and add commands read the API base URL from SKILLPASS_API.",
17001
- "Exit codes: 0 ok/warning, 1 failed or blocked, 2 usage/load/network errors."
17589
+ "Exit codes: 0 ok/warning, 1 failed or blocked, 2 usage/load/network errors",
17590
+ "(outdated exits 1 when updates are available)."
17002
17591
  ].join("\n");
17003
17592
  var COMMAND_FLAGS = {
17004
17593
  scan: ["--json"],
@@ -17006,6 +17595,8 @@ var COMMAND_FLAGS = {
17006
17595
  add: ["--yes", "--target", "--dir", "--global"],
17007
17596
  remove: ["--target", "--dir", "--global"],
17008
17597
  list: [],
17598
+ outdated: [],
17599
+ update: ["--yes", "--target", "--global"],
17009
17600
  search: ["--target", "--category", "--packs", "--json"]
17010
17601
  };
17011
17602
  function parseCliArgs(argv) {
@@ -17124,6 +17715,27 @@ async function run(argv) {
17124
17715
  }
17125
17716
  return runList();
17126
17717
  }
17718
+ if (args.command === "update") {
17719
+ const [ref] = args.positional;
17720
+ if (!ref) {
17721
+ return { lines: ["error: update needs a skill slug", "", USAGE], exitCode: 2 };
17722
+ }
17723
+ const tty = Boolean(process.stdin.isTTY);
17724
+ return runUpdate(ref, {
17725
+ yes: args.yes,
17726
+ target: args.target,
17727
+ global: args.global,
17728
+ style: styler(Boolean(process.stdout.isTTY)),
17729
+ confirmImpl: tty ? confirmViaTty : void 0,
17730
+ emit: (text) => console.log(text)
17731
+ });
17732
+ }
17733
+ if (args.command === "outdated") {
17734
+ if (args.positional.length > 0) {
17735
+ return { lines: ["error: outdated takes no arguments", "", USAGE], exitCode: 2 };
17736
+ }
17737
+ return runOutdated();
17738
+ }
17127
17739
  if (args.command === "report") {
17128
17740
  const [ref] = args.positional;
17129
17741
  if (!ref) {
@@ -17150,10 +17762,17 @@ async function run(argv) {
17150
17762
  }
17151
17763
  return { lines: [`error: unknown command "${args.command}"`, "", USAGE], exitCode: 2 };
17152
17764
  }
17153
- async function main(argv) {
17154
- const result = await run(argv);
17765
+ async function main(argv, runImpl = run) {
17766
+ let result;
17767
+ try {
17768
+ result = await runImpl(argv);
17769
+ } catch (err2) {
17770
+ console.error(`error: ${err2 instanceof Error ? err2.message : String(err2)}`);
17771
+ process.exitCode = 2;
17772
+ return;
17773
+ }
17155
17774
  if (!result.streamed) {
17156
- console.log(result.lines.join("\n"));
17775
+ (result.exitCode === 2 ? console.error : console.log)(result.lines.join("\n"));
17157
17776
  }
17158
17777
  process.exitCode = result.exitCode;
17159
17778
  }