skillpass 0.2.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.js';
@@ -493,13 +493,13 @@ function unzipSync(data, opts) {
493
493
 
494
494
  // src/add.ts
495
495
  import {
496
- existsSync,
496
+ existsSync as existsSync2,
497
497
  mkdirSync as mkdirSync2,
498
- readdirSync as readdirSync2,
498
+ readdirSync as readdirSync3,
499
499
  renameSync,
500
500
  rmdirSync,
501
501
  rmSync,
502
- statSync as statSync2,
502
+ statSync as statSync3,
503
503
  writeFileSync as writeFileSync2
504
504
  } from "node:fs";
505
505
  import { dirname, isAbsolute, join as join4, resolve as resolve2 } from "node:path";
@@ -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
+ });
16054
16080
  }
16055
16081
  });
16056
16082
  }
16057
16083
  return findings;
16058
16084
  };
16059
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"
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 }
16118
+ });
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
+ }
16140
+ return findings;
16141
+ };
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",
@@ -16160,6 +16264,52 @@ async function fetchPreflight(fetchImpl, apiUrl, ref) {
16160
16264
  return { ok: true, slug, detail: detail.data, preflight: preflight.data };
16161
16265
  }
16162
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
+
16163
16313
  // src/pack.ts
16164
16314
  import { posix } from "node:path";
16165
16315
  function resolvePackMembers(members, target) {
@@ -16464,7 +16614,7 @@ function writeTree(files, target) {
16464
16614
  writeFileSync2(filePath, file2.content);
16465
16615
  }
16466
16616
  mkdirSync2(dirname(target), { recursive: true });
16467
- if (existsSync(target)) {
16617
+ if (existsSync2(target)) {
16468
16618
  rmdirSync(target);
16469
16619
  }
16470
16620
  renameSync(tempDir, target);
@@ -16473,13 +16623,6 @@ function writeTree(files, target) {
16473
16623
  throw err2;
16474
16624
  }
16475
16625
  }
16476
- function memberFiles(files, sourceDir) {
16477
- if (sourceDir === ".") {
16478
- return files;
16479
- }
16480
- const prefix = `${sourceDir}/`;
16481
- return files.filter((f) => f.path.startsWith(prefix)).map((f) => ({ path: f.path.slice(prefix.length), content: f.content }));
16482
- }
16483
16626
  function installChoices(declared, slug) {
16484
16627
  const choices = [];
16485
16628
  const ordered = [
@@ -16531,22 +16674,146 @@ function packChoices(declared, slug, count) {
16531
16674
  choices.push({ label: `current directory (./${slug}, raw pack source)` });
16532
16675
  return choices;
16533
16676
  }
16534
- async function runAdd(ref, opts = {}) {
16535
- const lines = [];
16536
- const streamed = opts.emit !== void 0;
16537
- const push = (...next) => {
16538
- lines.push(...next);
16539
- if (next.length > 0) {
16540
- opts.emit?.(next.join("\n"));
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}`);
16721
+ return done(2);
16722
+ }
16723
+ const { installs, skipped } = resolvePackMembers(members, tool);
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);
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);
16737
+ }
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);
16742
+ }
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);
16748
+ }
16749
+ for (const name of skipped) {
16750
+ push("", `note: ${name} does not support ${tool}; skipped`);
16751
+ }
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));
16541
16758
  }
16542
- };
16543
- const done = (exitCode) => ({ lines, exitCode, streamed });
16759
+ } catch {
16760
+ push(
16761
+ "",
16762
+ `error: could not write ${plans[written.length].name}; installed before the failure: ${written.join(", ") || "none"}`
16763
+ );
16764
+ return done(2);
16765
+ }
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()) {
16779
+ push("", `error: target ${target} already exists and is not a directory`);
16780
+ return done(2);
16781
+ }
16782
+ if (readdirSync3(target).length > 0) {
16783
+ push("", `error: target directory ${target} already exists and is not empty`);
16784
+ return done(2);
16785
+ }
16786
+ }
16787
+ const download = await downloadVerified(ctx.fetchImpl, ctx.apiUrl, slug, preflight.version, preflight.sourceHash);
16788
+ if (!download.ok) {
16789
+ push("", `error: ${download.message}`);
16790
+ return done(2);
16791
+ }
16792
+ try {
16793
+ writeTree(download.files, target);
16794
+ } catch {
16795
+ push("", "error: could not write the install; nothing was installed");
16796
+ return done(2);
16797
+ }
16798
+ const area = knownAreas(ctx.cwd).find((a) => a.dir === dirname(target));
16799
+ if (area) {
16800
+ recordReceipt(area.dir, slug, receiptFor(preflight));
16801
+ }
16802
+ push(
16803
+ "",
16804
+ `Installed ${download.files.length} file(s) to ${target}`,
16805
+ "Source hash verified against the Skill Passport."
16806
+ );
16807
+ return done(0);
16808
+ }
16809
+ async function runAdd(ref, opts = {}) {
16810
+ const { push, done } = createOutput(opts.emit);
16544
16811
  if (opts.target && opts.dir) {
16545
- push("error: pass --target or --dir, not both");
16812
+ push(TARGET_OR_DIR);
16546
16813
  return done(2);
16547
16814
  }
16548
16815
  if (opts.global && !opts.target) {
16549
- push("error: --global needs --target (e.g. --target claude-code)");
16816
+ push(GLOBAL_NEEDS_TARGET);
16550
16817
  return done(2);
16551
16818
  }
16552
16819
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
@@ -16564,8 +16831,7 @@ async function runAdd(ref, opts = {}) {
16564
16831
  const members = detail.packMembers ?? [];
16565
16832
  const isPack = members.length > 0;
16566
16833
  let targetDir = opts.dir;
16567
- let packTool;
16568
- let packGlobal = false;
16834
+ let pack;
16569
16835
  if (opts.target) {
16570
16836
  const resolved = isPack ? resolveTargetArea(opts.target, opts.global) : resolveTargetDir(opts.target, slug, opts.global);
16571
16837
  if (!resolved.ok) {
@@ -16573,8 +16839,7 @@ async function runAdd(ref, opts = {}) {
16573
16839
  return done(2);
16574
16840
  }
16575
16841
  if (isPack) {
16576
- packTool = opts.target;
16577
- packGlobal = opts.global ?? false;
16842
+ pack = { tool: opts.target, global: opts.global ?? false };
16578
16843
  } else {
16579
16844
  targetDir = resolved.dir;
16580
16845
  }
@@ -16582,127 +16847,30 @@ async function runAdd(ref, opts = {}) {
16582
16847
  push("", `warning: this skill does not declare ${opts.target} as a target`);
16583
16848
  }
16584
16849
  }
16585
- if (preflight.riskLevel !== "low" && !opts.yes) {
16586
- if (!opts.confirmImpl) {
16587
- push("", `error: a ${preflight.riskLevel}-risk skill needs confirmation; rerun with --yes`);
16588
- return done(2);
16589
- }
16590
- const confirmed = await opts.confirmImpl(
16591
- `Install ${slug}@${preflight.version} (${preflight.riskLevel} risk)? [y/N] `
16592
- );
16593
- if (!confirmed) {
16594
- push("", "Install aborted.");
16595
- return done(2);
16596
- }
16597
- }
16598
- const prompt = opts.promptImpl;
16599
- const promptIndex = async (ask, count) => {
16600
- while (true) {
16601
- const answer = (await ask(`Where should it go? [1-${count}, default 1] `)).trim();
16602
- if (answer === "") {
16603
- return 0;
16604
- }
16605
- const index = /^\d+$/.test(answer) ? Number.parseInt(answer, 10) - 1 : -1;
16606
- if (index >= 0 && index < count) {
16607
- return index;
16608
- }
16609
- push(` answer 1-${count}, or press enter for the default`);
16610
- }
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
16611
16866
  };
16612
- if (isPack && packTool === void 0 && targetDir === void 0) {
16613
- if (prompt) {
16614
- const choices = packChoices(detail.targets, slug, members.length);
16615
- push("", "Install location:");
16616
- choices.forEach((choice, i2) => push(` ${i2 + 1}) ${choice.label}`));
16617
- const chosen = choices[await promptIndex(prompt, choices.length)];
16618
- if (chosen.tool) {
16619
- packTool = chosen.tool;
16620
- packGlobal = chosen.global ?? false;
16621
- } else {
16622
- targetDir = slug;
16623
- }
16624
- } else {
16625
- targetDir = slug;
16626
- const mappable = mappableDeclaredTargets(detail.targets);
16627
- if (mappable.length > 0) {
16628
- push(
16629
- "",
16630
- `tip: --target ${mappable[0]} installs the ${members.length} skills into the tool's skills folder`
16631
- );
16632
- }
16633
- }
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;
16634
16871
  }
16635
- if (packTool !== void 0) {
16636
- const area2 = resolveTargetArea(packTool, packGlobal);
16637
- if (!area2.ok) {
16638
- push("", `error: ${area2.message}`);
16639
- return done(2);
16640
- }
16641
- const { installs, skipped } = resolvePackMembers(members, packTool);
16642
- if (installs.length === 0) {
16643
- push("", `error: none of this pack's skills support ${packTool}`);
16644
- return done(2);
16645
- }
16646
- const areaAbs = resolve2(opts.cwd ?? process.cwd(), area2.dir);
16647
- const conflicts = installs.map((m) => join4(areaAbs, m.name)).filter(
16648
- (dest) => existsSync(dest) && (!statSync2(dest).isDirectory() || readdirSync2(dest).length > 0)
16649
- );
16650
- if (conflicts.length > 0) {
16651
- push(
16652
- "",
16653
- "error: these destinations already exist and are not empty; nothing was installed:",
16654
- ...conflicts.map((c) => ` ${c}`)
16655
- );
16656
- return done(2);
16657
- }
16658
- const download2 = await downloadVerified(
16659
- fetchImpl,
16660
- apiUrl,
16661
- slug,
16662
- preflight.version,
16663
- preflight.sourceHash
16664
- );
16665
- if (!download2.ok) {
16666
- push("", `error: ${download2.message}`);
16667
- return done(2);
16668
- }
16669
- const plans = installs.map((m) => ({ ...m, files: memberFiles(download2.files, m.sourceDir) }));
16670
- const missing = plans.find((p) => p.files.length === 0);
16671
- if (missing) {
16672
- push("", `error: the snapshot has no files for "${missing.name}"; nothing was installed`);
16673
- return done(2);
16674
- }
16675
- for (const name of skipped) {
16676
- push("", `note: ${name} does not support ${packTool}; skipped`);
16677
- }
16678
- const written = [];
16679
- try {
16680
- for (const plan of plans) {
16681
- writeTree(plan.files, join4(areaAbs, plan.name));
16682
- written.push(plan.name);
16683
- }
16684
- } catch {
16685
- push(
16686
- "",
16687
- `error: could not write ${plans[written.length].name}; installed before the failure: ${written.join(", ") || "none"}`
16688
- );
16689
- return done(2);
16690
- }
16691
- for (const name of written) {
16692
- recordReceipt(areaAbs, name, {
16693
- version: preflight.version,
16694
- sourceHash: preflight.sourceHash,
16695
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
16696
- pack: { slug, version: preflight.version }
16697
- });
16698
- }
16699
- push(
16700
- "",
16701
- `Installed ${written.length} skills to ${areaAbs}`,
16702
- ` ${written.join(", ")}`,
16703
- "Source hash verified against the Skill Passport."
16704
- );
16705
- return done(0);
16872
+ if (pack !== void 0) {
16873
+ return installPack(ctx, members, pack.tool, pack.global);
16706
16874
  }
16707
16875
  if (isPack && opts.dir) {
16708
16876
  push(
@@ -16710,72 +16878,17 @@ async function runAdd(ref, opts = {}) {
16710
16878
  `note: --dir installs the raw pack source; --target <tool> installs the ${members.length} skills individually`
16711
16879
  );
16712
16880
  }
16713
- if (!targetDir) {
16714
- const choices = installChoices(detail.targets, slug);
16715
- if (prompt && choices.length > 1) {
16716
- push("", "Install location:");
16717
- choices.forEach((choice, i2) => push(` ${i2 + 1}) ${choice.label}`));
16718
- targetDir = choices[await promptIndex(prompt, choices.length)].dir;
16719
- } else {
16720
- targetDir = slug;
16721
- const mappable = mappableDeclaredTargets(detail.targets);
16722
- if (mappable.length > 0) {
16723
- push("", `tip: --target ${mappable[0]} installs into the tool's skills folder`);
16724
- }
16725
- }
16726
- }
16727
- const target = resolve2(opts.cwd ?? process.cwd(), targetDir);
16728
- if (existsSync(target)) {
16729
- if (!statSync2(target).isDirectory()) {
16730
- push("", `error: target ${target} already exists and is not a directory`);
16731
- return done(2);
16732
- }
16733
- if (readdirSync2(target).length > 0) {
16734
- push("", `error: target directory ${target} already exists and is not empty`);
16735
- return done(2);
16736
- }
16737
- }
16738
- const download = await downloadVerified(
16739
- fetchImpl,
16740
- apiUrl,
16741
- slug,
16742
- preflight.version,
16743
- preflight.sourceHash
16744
- );
16745
- if (!download.ok) {
16746
- push("", `error: ${download.message}`);
16747
- return done(2);
16748
- }
16749
- try {
16750
- writeTree(download.files, target);
16751
- } catch {
16752
- push("", "error: could not write the install; nothing was installed");
16753
- return done(2);
16754
- }
16755
- const area = knownAreas(opts.cwd ?? process.cwd()).find((a) => a.dir === dirname(target));
16756
- if (area) {
16757
- recordReceipt(area.dir, slug, {
16758
- version: preflight.version,
16759
- sourceHash: preflight.sourceHash,
16760
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
16761
- });
16762
- }
16763
- push(
16764
- "",
16765
- `Installed ${download.files.length} file(s) to ${target}`,
16766
- "Source hash verified against the Skill Passport."
16767
- );
16768
- return done(0);
16881
+ return installSingle(ctx, targetDir ?? await chooseSingleLocation(ctx, detail, opts.promptImpl));
16769
16882
  }
16770
16883
 
16771
16884
  // src/list.ts
16772
- import { existsSync as existsSync2, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
16885
+ import { existsSync as existsSync3, readdirSync as readdirSync4, statSync as statSync4 } from "node:fs";
16773
16886
  import { join as join5 } from "node:path";
16774
16887
  function installedIn(dir) {
16775
- if (!existsSync2(dir)) {
16888
+ if (!existsSync3(dir)) {
16776
16889
  return [];
16777
16890
  }
16778
- return readdirSync3(dir).sort().filter((name) => statSync3(join5(dir, name)).isDirectory());
16891
+ return readdirSync4(dir).sort().filter((name) => statSync4(join5(dir, name), { throwIfNoEntry: false })?.isDirectory() ?? false);
16779
16892
  }
16780
16893
  function runList(opts = {}) {
16781
16894
  const cwd = opts.cwd ?? process.cwd();
@@ -16927,33 +17040,21 @@ async function runOutdated(opts = {}) {
16927
17040
  }
16928
17041
 
16929
17042
  // src/remove.ts
16930
- import { existsSync as existsSync3, rmSync as rmSync2, statSync as statSync4 } from "node:fs";
17043
+ import { existsSync as existsSync4, rmSync as rmSync2, statSync as statSync5 } from "node:fs";
16931
17044
  import { dirname as dirname2, join as join7, resolve as resolve3 } from "node:path";
16932
17045
  function looksLikeInstalledSkill(dir) {
16933
- return existsSync3(join7(dir, "SKILL.md")) || existsSync3(join7(dir, "skill.json"));
17046
+ return existsSync4(join7(dir, "SKILL.md")) || existsSync4(join7(dir, "skill.json"));
16934
17047
  }
16935
17048
  function candidateDirs(slug, cwd, home) {
16936
- const dirs = [];
16937
- const byTarget = installAreas(home);
16938
- for (const tool of MAPPED_TARGETS) {
16939
- const area = byTarget[tool];
16940
- if (!area) {
16941
- continue;
16942
- }
16943
- dirs.push(resolve3(cwd, join7(area.project, slug)));
16944
- if (area.global) {
16945
- dirs.push(join7(area.global, slug));
16946
- }
16947
- }
16948
- dirs.push(resolve3(cwd, slug));
16949
- return [...new Set(dirs)];
17049
+ const inAreas = knownAreas(cwd, home).map((area) => join7(area.dir, slug));
17050
+ return [.../* @__PURE__ */ new Set([...inAreas, resolve3(cwd, slug)])];
16950
17051
  }
16951
17052
  function runRemove(slug, opts = {}) {
16952
17053
  if (opts.target && opts.dir) {
16953
- return { lines: ["error: pass --target or --dir, not both"], exitCode: 2 };
17054
+ return { lines: [TARGET_OR_DIR], exitCode: 2 };
16954
17055
  }
16955
17056
  if (opts.global && !opts.target) {
16956
- return { lines: ["error: --global needs --target (e.g. --target claude-code)"], exitCode: 2 };
17057
+ return { lines: [GLOBAL_NEEDS_TARGET], exitCode: 2 };
16957
17058
  }
16958
17059
  const cwd = opts.cwd ?? process.cwd();
16959
17060
  if (!opts.dir) {
@@ -17000,7 +17101,7 @@ function runRemove(slug, opts = {}) {
17000
17101
  dir = resolve3(cwd, opts.dir);
17001
17102
  } else {
17002
17103
  const found = candidateDirs(slug, cwd, opts.home).filter(
17003
- (candidate) => existsSync3(candidate)
17104
+ (candidate) => existsSync4(candidate)
17004
17105
  );
17005
17106
  if (found.length === 0) {
17006
17107
  return { lines: [`error: ${slug} is not installed in any known location`], exitCode: 2 };
@@ -17016,10 +17117,10 @@ function runRemove(slug, opts = {}) {
17016
17117
  }
17017
17118
  dir = found[0];
17018
17119
  }
17019
- if (!existsSync3(dir)) {
17120
+ if (!existsSync4(dir)) {
17020
17121
  return { lines: [`error: nothing installed at ${dir}`], exitCode: 2 };
17021
17122
  }
17022
- if (!statSync4(dir).isDirectory()) {
17123
+ if (!statSync5(dir).isDirectory()) {
17023
17124
  return { lines: [`error: ${dir} is not a directory`], exitCode: 2 };
17024
17125
  }
17025
17126
  if (!looksLikeInstalledSkill(dir)) {
@@ -17159,7 +17260,7 @@ async function runSearch(opts = {}) {
17159
17260
  }
17160
17261
 
17161
17262
  // src/update.ts
17162
- import { existsSync as existsSync4, readdirSync as readdirSync4, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
17263
+ import { existsSync as existsSync5, renameSync as renameSync2, rmSync as rmSync3 } from "node:fs";
17163
17264
  import { join as join8 } from "node:path";
17164
17265
  async function locate(slug, opts, fetchImpl, apiUrl) {
17165
17266
  const cwd = opts.cwd ?? process.cwd();
@@ -17186,7 +17287,7 @@ async function locate(slug, opts, fetchImpl, apiUrl) {
17186
17287
  hits.push({ area, dir: area.dir, installedVersion: packMember.pack.version, packSlug: slug });
17187
17288
  continue;
17188
17289
  }
17189
- if (existsSync4(dir)) {
17290
+ if (existsSync5(dir)) {
17190
17291
  const identified = await identifyByHash(fetchImpl, apiUrl, slug, dir);
17191
17292
  if (identified !== void 0) {
17192
17293
  hits.push({ area, dir, installedVersion: identified });
@@ -17207,30 +17308,23 @@ function permissionChanges(installed, target) {
17207
17308
  function swapTree(files, dir) {
17208
17309
  const fresh = `${dir}.new-${process.pid}`;
17209
17310
  const aside = `${dir}.old-${process.pid}`;
17311
+ rmSync3(fresh, { recursive: true, force: true });
17210
17312
  writeTree(files, fresh);
17211
- renameSync2(dir, aside);
17212
17313
  try {
17314
+ if (existsSync5(dir)) renameSync2(dir, aside);
17213
17315
  renameSync2(fresh, dir);
17214
17316
  } catch (err2) {
17215
- renameSync2(aside, dir);
17317
+ if (existsSync5(aside) && !existsSync5(dir)) renameSync2(aside, dir);
17216
17318
  rmSync3(fresh, { recursive: true, force: true });
17217
17319
  throw err2;
17218
17320
  }
17219
17321
  rmSync3(aside, { recursive: true, force: true });
17220
17322
  }
17221
17323
  async function runUpdate(ref, opts = {}) {
17222
- const lines = [];
17223
- const streamed = opts.emit !== void 0;
17224
- const push = (...next) => {
17225
- lines.push(...next);
17226
- if (next.length > 0) {
17227
- opts.emit?.(next.join("\n"));
17228
- }
17229
- };
17230
- const done = (exitCode) => ({ lines, exitCode, streamed });
17324
+ const { push, done } = createOutput(opts.emit);
17231
17325
  const st = opts.style ?? PLAIN;
17232
17326
  if (opts.global && !opts.target) {
17233
- push("error: --global needs --target (e.g. --target claude-code)");
17327
+ push(GLOBAL_NEEDS_TARGET);
17234
17328
  return done(2);
17235
17329
  }
17236
17330
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
@@ -17280,19 +17374,14 @@ async function runUpdate(ref, opts = {}) {
17280
17374
  if (preflight.blocked) {
17281
17375
  return done(1);
17282
17376
  }
17283
- if (preflight.riskLevel !== "low" && !opts.yes) {
17284
- if (!opts.confirmImpl) {
17285
- push("", `error: a ${preflight.riskLevel}-risk skill needs confirmation; rerun with --yes`);
17286
- return done(2);
17287
- }
17288
- const confirmed = await opts.confirmImpl(
17289
- `Update ${slug} to ${preflight.version} (${preflight.riskLevel} risk)? [y/N] `
17290
- );
17291
- if (!confirmed) {
17292
- push("", "Update aborted; the installed version was left in place.");
17293
- return done(2);
17294
- }
17295
- }
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);
17296
17385
  const download = await downloadVerified(fetchImpl, apiUrl, slug, preflight.version, preflight.sourceHash);
17297
17386
  if (!download.ok) {
17298
17387
  push("", `error: ${download.message}`);
@@ -17304,11 +17393,7 @@ async function runUpdate(ref, opts = {}) {
17304
17393
  push("", "error: could not replace the install; the installed version was left in place");
17305
17394
  return done(2);
17306
17395
  }
17307
- recordReceipt(installed.area.dir, slug, {
17308
- version: preflight.version,
17309
- sourceHash: preflight.sourceHash,
17310
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
17311
- });
17396
+ recordReceipt(installed.area.dir, slug, receiptFor(preflight));
17312
17397
  push(
17313
17398
  "",
17314
17399
  `Updated ${slug} ${installed.installedVersion ?? "?"} -> ${preflight.version} in ${installed.dir}`,
@@ -17354,19 +17439,14 @@ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl
17354
17439
  if (preflight.blocked) {
17355
17440
  return done(1);
17356
17441
  }
17357
- if (preflight.riskLevel !== "low" && !opts.yes) {
17358
- if (!opts.confirmImpl) {
17359
- push("", `error: a ${preflight.riskLevel}-risk skill needs confirmation; rerun with --yes`);
17360
- return done(2);
17361
- }
17362
- const confirmed = await opts.confirmImpl(
17363
- `Update ${slug} to ${preflight.version} (${preflight.riskLevel} risk)? [y/N] `
17364
- );
17365
- if (!confirmed) {
17366
- push("", "Update aborted; the installed version was left in place.");
17367
- return done(2);
17368
- }
17369
- }
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);
17370
17450
  const areaDir = installed.area.dir;
17371
17451
  const { installs, skipped } = resolvePackMembers(detail.packMembers ?? [], installed.area.tool);
17372
17452
  if (installs.length === 0) {
@@ -17378,9 +17458,7 @@ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl
17378
17458
  const newNames = new Set(installs.map((m) => m.name));
17379
17459
  const toRemove = currentMembers.filter((name) => !newNames.has(name));
17380
17460
  const additions = installs.filter((m) => !currentMembers.includes(m.name));
17381
- const conflicts = additions.map((m) => join8(areaDir, m.name)).filter(
17382
- (dest) => existsSync4(dest) && (!statSync5(dest).isDirectory() || readdirSync4(dest).length > 0)
17383
- );
17461
+ const conflicts = additions.map((m) => join8(areaDir, m.name)).filter(isOccupied);
17384
17462
  if (conflicts.length > 0) {
17385
17463
  push(
17386
17464
  "",
@@ -17394,7 +17472,7 @@ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl
17394
17472
  push("", `error: ${download.message}`);
17395
17473
  return done(2);
17396
17474
  }
17397
- const plans = installs.map((m) => ({ ...m, files: memberFiles(download.files, m.sourceDir) }));
17475
+ const plans = planMembers(installs, download.files);
17398
17476
  const missing = plans.find((p) => p.files.length === 0);
17399
17477
  if (missing) {
17400
17478
  push("", `error: the snapshot has no files for "${missing.name}"; nothing was changed`);
@@ -17407,18 +17485,13 @@ async function runPackUpdate(slug, installed, detail, preflight, opts, fetchImpl
17407
17485
  try {
17408
17486
  for (const plan of plans) {
17409
17487
  const dest = join8(areaDir, plan.name);
17410
- if (existsSync4(dest)) {
17488
+ if (existsSync5(dest)) {
17411
17489
  swapTree(plan.files, dest);
17412
17490
  } else {
17413
17491
  writeTree(plan.files, dest);
17414
17492
  }
17415
17493
  done1.push(plan.name);
17416
- recordReceipt(areaDir, plan.name, {
17417
- version: preflight.version,
17418
- sourceHash: preflight.sourceHash,
17419
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
17420
- pack: { slug, version: preflight.version }
17421
- });
17494
+ recordReceipt(areaDir, plan.name, receiptFor(preflight, slug));
17422
17495
  }
17423
17496
  } catch {
17424
17497
  push(
@@ -17689,10 +17762,17 @@ async function run(argv) {
17689
17762
  }
17690
17763
  return { lines: [`error: unknown command "${args.command}"`, "", USAGE], exitCode: 2 };
17691
17764
  }
17692
- async function main(argv) {
17693
- 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
+ }
17694
17774
  if (!result.streamed) {
17695
- console.log(result.lines.join("\n"));
17775
+ (result.exitCode === 2 ? console.error : console.log)(result.lines.join("\n"));
17696
17776
  }
17697
17777
  process.exitCode = result.exitCode;
17698
17778
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillpass",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",
@@ -21,7 +21,7 @@
21
21
  "author": "Brad Traversy",
22
22
  "type": "module",
23
23
  "bin": {
24
- "skillpass": "./bin/skillpass.mjs"
24
+ "skillpass": "./bin/skillpass.js"
25
25
  },
26
26
  "files": [
27
27
  "bin",
@@ -36,11 +36,11 @@
36
36
  "esbuild": "^0.28.1",
37
37
  "fflate": "^0.8.3",
38
38
  "typescript": "^6.0.3",
39
- "validator": "0.3.0",
40
- "skill-schema": "0.1.0"
39
+ "skill-schema": "0.1.0",
40
+ "validator": "0.3.0"
41
41
  },
42
42
  "scripts": {
43
- "build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.mjs --log-level=warning",
43
+ "build": "esbuild src/cli.ts --bundle --platform=node --format=esm --outfile=dist/cli.js --log-level=warning",
44
44
  "typecheck": "tsc --noEmit"
45
45
  }
46
46
  }
package/bin/skillpass.mjs DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import '../dist/cli.mjs';