@delorenj/skillex 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12018,10 +12018,94 @@ async function migrate(input = {}) {
12018
12018
 
12019
12019
  // src/core/profile-sync.ts
12020
12020
  import { randomUUID as randomUUID9 } from "node:crypto";
12021
- import { constants as constants10 } from "node:fs";
12022
- import { mkdir as mkdir10, open as open13, readdir as readdir21, realpath as realpath22, rename as rename8, symlink as symlink6, unlink as unlink9 } from "node:fs/promises";
12023
- import { join as join31, resolve as resolve17 } from "node:path";
12021
+ import { constants as constants11 } from "node:fs";
12022
+ import { mkdir as mkdir10, open as open14, readdir as readdir21, realpath as realpath22, rename as rename8, symlink as symlink6, unlink as unlink9 } from "node:fs/promises";
12023
+ import { join as join32, resolve as resolve17 } from "node:path";
12024
12024
  import { isDeepStrictEqual as isDeepStrictEqual6 } from "node:util";
12025
+
12026
+ // src/core/profile-policy.ts
12027
+ import { constants as constants10 } from "node:fs";
12028
+ import { open as open13, readFile as readFile12 } from "node:fs/promises";
12029
+ import { join as join31 } from "node:path";
12030
+ import { parseDocument as parseDocument3 } from "yaml";
12031
+ var metadataFiles = /* @__PURE__ */ new Set([
12032
+ ".usage.json",
12033
+ ".usage.json.lock",
12034
+ ".curator_state",
12035
+ ".curator_suppressed",
12036
+ ".sync_state"
12037
+ ]);
12038
+ var metadataDirectories = /* @__PURE__ */ new Set([".curator_backups"]);
12039
+ function hermesMetadata(item2) {
12040
+ if (item2.kind === "file") return metadataFiles.has(item2.name);
12041
+ if (item2.kind === "directory") return metadataDirectories.has(item2.name);
12042
+ return false;
12043
+ }
12044
+ function refuse3(path, message) {
12045
+ return fail(
12046
+ "E_PROFILE_SKILLEX_ONLY",
12047
+ message,
12048
+ {
12049
+ path,
12050
+ fix: "Preserve foreign content outside discovery roots; clear skills.external_dirs through the locked config renderer, then preview skillex profile sync NAME --project PATH --skillex-only. Never delete or adopt local payloads implicitly."
12051
+ },
12052
+ ExitCode.REFUSED
12053
+ );
12054
+ }
12055
+ async function strictProfile(profile, options) {
12056
+ if (options.skillexOnly !== void 0 && typeof options.skillexOnly !== "boolean")
12057
+ refuse3(profile.root, "skillexOnly must be boolean.");
12058
+ const marker = await entry2(join31(profile.root, ".skillex-only"));
12059
+ if (marker && !marker.info.isFile())
12060
+ refuse3(marker.path, "The strict policy marker must be a regular file.");
12061
+ return options.skillexOnly === true || marker !== void 0;
12062
+ }
12063
+ async function assertStrictProfile(profile, options, local) {
12064
+ if (!await strictProfile(profile, options)) return false;
12065
+ for (const item2 of local)
12066
+ if (!hermesMetadata(item2))
12067
+ refuse3(item2.path, `Skillex-only profile contains unowned entry ${item2.name}.`);
12068
+ const path = join31(profile.root, "config.yaml");
12069
+ const config = await entry2(path);
12070
+ if (config) {
12071
+ if (!config.info.isFile())
12072
+ refuse3(path, "Strict profile config must be a regular generated file.");
12073
+ const document = parseDocument3(await readFile12(path, "utf8"), { uniqueKeys: true });
12074
+ if (document.errors.length) refuse3(path, "Cannot validate strict skills configuration.");
12075
+ const dirs = document.toJS()?.skills?.external_dirs;
12076
+ if (dirs !== void 0 && (!Array.isArray(dirs) || dirs.length))
12077
+ refuse3(path, "Skillex-only profiles require skills.external_dirs: [].");
12078
+ }
12079
+ const optOut = await entry2(join31(profile.root, ".no-bundled-skills"));
12080
+ if (optOut && !optOut.info.isFile())
12081
+ refuse3(optOut.path, "Bundled opt-out marker must be a regular file.");
12082
+ return true;
12083
+ }
12084
+ async function publishStrictPolicy(profile) {
12085
+ for (const name of [".no-bundled-skills", ".skillex-only"]) {
12086
+ const path = join31(profile.root, name);
12087
+ const existing = await entry2(path);
12088
+ if (existing) {
12089
+ if (!existing.info.isFile()) refuse3(path, "Policy markers must be regular files.");
12090
+ continue;
12091
+ }
12092
+ const handle = await open13(
12093
+ path,
12094
+ constants10.O_WRONLY | constants10.O_CREAT | constants10.O_EXCL | constants10.O_NOFOLLOW,
12095
+ 384
12096
+ );
12097
+ try {
12098
+ await handle.writeFile(
12099
+ "Skillex owns this PM skill projection. Use skillex profile sync; no local installs.\n"
12100
+ );
12101
+ await handle.sync();
12102
+ } finally {
12103
+ await handle.close();
12104
+ }
12105
+ }
12106
+ }
12107
+
12108
+ // src/core/profile-sync.ts
12025
12109
  function refused3(code, message, path, fix) {
12026
12110
  fail(code, message, { path, fix }, ExitCode.REFUSED);
12027
12111
  }
@@ -12040,14 +12124,14 @@ function receiptOptions(profile, options, scopes = []) {
12040
12124
  const { home, cwd } = normalized;
12041
12125
  const expanded = (value) => resolve17(
12042
12126
  cwd,
12043
- value === "~" ? home : value.startsWith("~/") ? join31(home, value.slice(2)) : value
12127
+ value === "~" ? home : value.startsWith("~/") ? join32(home, value.slice(2)) : value
12044
12128
  );
12045
12129
  return {
12046
12130
  ...normalized,
12047
12131
  forbiddenRoots: [
12048
12132
  .../* @__PURE__ */ new Set([
12049
12133
  profile.root,
12050
- join31(home, ".agents"),
12134
+ join32(home, ".agents"),
12051
12135
  ...options.project ? [expanded(options.project)] : [],
12052
12136
  ...options.registryRoot ? [expanded(options.registryRoot)] : [],
12053
12137
  ...scopes.flatMap((scope) => [
@@ -12109,7 +12193,7 @@ async function resolveProjection(options) {
12109
12193
  }
12110
12194
  const locations = /* @__PURE__ */ new Map();
12111
12195
  for (const scope of scopes) {
12112
- locations.set(join31(scope.registry.root, "all-skills"), "catalog");
12196
+ locations.set(join32(scope.registry.root, "all-skills"), "catalog");
12113
12197
  if (scope.pack) locations.set(scope.pack.path, "pack");
12114
12198
  }
12115
12199
  const sources = [];
@@ -12136,7 +12220,7 @@ async function observations(profile, state) {
12136
12220
  );
12137
12221
  if (root)
12138
12222
  for (const name of (await readdir21(profile.skillsRoot)).sort()) {
12139
- const path = join31(profile.skillsRoot, name);
12223
+ const path = join32(profile.skillsRoot, name);
12140
12224
  const actual = await entry2(path);
12141
12225
  if (!actual) continue;
12142
12226
  entries.set(name, actual);
@@ -12188,7 +12272,7 @@ async function buildWork(profile, state, options) {
12188
12272
  for (const scope of projection.scopes) {
12189
12273
  for (const protectedRoot of [
12190
12274
  scope.registry.root,
12191
- ...scope.scope === "project" ? [scope.root] : [join31(scope.root, ".agents")]
12275
+ ...scope.scope === "project" ? [scope.root] : [join32(scope.root, ".agents")]
12192
12276
  ])
12193
12277
  if (within2(profile.skillsRoot, protectedRoot) || within2(protectedRoot, profile.skillsRoot))
12194
12278
  refused3(
@@ -12208,18 +12292,19 @@ async function buildWork(profile, state, options) {
12208
12292
  );
12209
12293
  state = await readProfileState(profile, receiptOptions(profile, options, projection.scopes));
12210
12294
  const observed = await observations(profile, state);
12295
+ await assertStrictProfile(profile, options, observed.local);
12211
12296
  const links = { ...state.data.links };
12212
12297
  const changes = [];
12213
12298
  if (!observed.root) changes.push({ action: "mkdir", path: profile.skillsRoot });
12214
12299
  for (const [name, claim2] of Object.entries(links))
12215
12300
  if (!owned2(observed.entries.get(name), claim2)) {
12216
12301
  delete links[name];
12217
- changes.push({ action: "release", name, path: join31(profile.skillsRoot, name) });
12302
+ changes.push({ action: "release", name, path: join32(profile.skillsRoot, name) });
12218
12303
  }
12219
12304
  const candidates = [];
12220
12305
  const desiredNames = new Set(projection.desired.map((desired) => desired.name));
12221
12306
  for (const desired of projection.desired) {
12222
- const path = join31(profile.skillsRoot, desired.name);
12307
+ const path = join32(profile.skillsRoot, desired.name);
12223
12308
  const actual = observed.entries.get(desired.name);
12224
12309
  const recorded = claim(links, desired.name);
12225
12310
  const state2 = actual ? recorded ? lexicalTarget(path, actual.raw ?? "") === desired.target ? "unchanged" : "update" : "shadowed" : "create";
@@ -12236,7 +12321,7 @@ async function buildWork(profile, state, options) {
12236
12321
  }
12237
12322
  for (const name of Object.keys(links).sort())
12238
12323
  if (!desiredNames.has(name))
12239
- changes.push({ action: "prune", name, path: join31(profile.skillsRoot, name) });
12324
+ changes.push({ action: "prune", name, path: join32(profile.skillsRoot, name) });
12240
12325
  const next = {
12241
12326
  version: 1,
12242
12327
  root: observed.root ? identity(observed.root) : null,
@@ -12274,6 +12359,7 @@ async function inspectProfile(profile, options = {}) {
12274
12359
  }
12275
12360
  try {
12276
12361
  preserved = (await observations(profile, state)).local;
12362
+ await assertStrictProfile(profile, options, preserved);
12277
12363
  } catch (error) {
12278
12364
  const failure6 = asFailure(error);
12279
12365
  findings.push(...failure6.findings);
@@ -12325,9 +12411,9 @@ async function inspectProfile(profile, options = {}) {
12325
12411
  }
12326
12412
  async function flush3(path, expected) {
12327
12413
  await requireIdentity(path, expected);
12328
- const handle = await open13(
12414
+ const handle = await open14(
12329
12415
  path,
12330
- constants10.O_RDONLY | constants10.O_DIRECTORY | constants10.O_NOFOLLOW
12416
+ constants11.O_RDONLY | constants11.O_DIRECTORY | constants11.O_NOFOLLOW
12331
12417
  );
12332
12418
  try {
12333
12419
  if (!matches2({ path, info: await handle.stat({ bigint: true }) }, expected))
@@ -12372,7 +12458,7 @@ async function recoverProfile(profile, state, options, mark, findings) {
12372
12458
  if (!pending || !root) return state;
12373
12459
  checkProfileSignal(options);
12374
12460
  await rootGuard(profile, root);
12375
- const path = join31(profile.skillsRoot, pending.name);
12461
+ const path = join32(profile.skillsRoot, pending.name);
12376
12462
  let current = await entry2(path);
12377
12463
  const parked = await entry2(pending.parked);
12378
12464
  const staged = await entry2(pending.stage);
@@ -12498,8 +12584,8 @@ async function apply2(work, mark, begin) {
12498
12584
  }
12499
12585
  }
12500
12586
  const id = randomUUID9();
12501
- const stage2 = join31(profile.skillsRoot, `.skillex-tmp-profile-${id}-new`);
12502
- const parked = join31(profile.skillsRoot, `.skillex-tmp-profile-${id}-old`);
12587
+ const stage2 = join32(profile.skillsRoot, `.skillex-tmp-profile-${id}-new`);
12588
+ const parked = join32(profile.skillsRoot, `.skillex-tmp-profile-${id}-old`);
12503
12589
  await absent(stage2);
12504
12590
  await absent(parked);
12505
12591
  let next;
@@ -12644,7 +12730,7 @@ async function runProfileSync(name, options) {
12644
12730
  { findings, exit: readExit(findings, state.data.pending !== void 0) }
12645
12731
  );
12646
12732
  }
12647
- if (work && !work.result.changes.length)
12733
+ if (work && !work.result.changes.length && !await strictProfile(profile, options))
12648
12734
  return makeResult(
12649
12735
  "profile sync",
12650
12736
  { ...work.result, project: work.projection.project, dryRun: false, applied },
@@ -12672,6 +12758,8 @@ async function runProfileSync(name, options) {
12672
12758
  });
12673
12759
  }
12674
12760
  await assertProfileIdentity(selected);
12761
+ if (await assertStrictProfile(selected, options, (await observations(selected, state)).local))
12762
+ await publishStrictPolicy(selected);
12675
12763
  const currentProfile = await discoverProfile(name, options);
12676
12764
  if (currentProfile.root !== selected.root || currentProfile.skillsRoot !== selected.skillsRoot)
12677
12765
  refused3(
@@ -12709,21 +12797,21 @@ async function runProfileSync(name, options) {
12709
12797
  const latest = await readProfileState(profile, receiptOptions(profile, options));
12710
12798
  const pending = latest.data.pending;
12711
12799
  if (pending) {
12712
- const current = await entry2(join31(profile.skillsRoot, pending.name));
12800
+ const current = await entry2(join32(profile.skillsRoot, pending.name));
12713
12801
  if (pending.next && matches2(current, pending.next) && !applied.some((change2) => change2.path === current?.path))
12714
12802
  applied.push({
12715
12803
  action: pending.previous ? "update" : "create",
12716
12804
  name: pending.name,
12717
- path: join31(profile.skillsRoot, pending.name),
12805
+ path: join32(profile.skillsRoot, pending.name),
12718
12806
  target: pending.next.raw
12719
12807
  });
12720
12808
  if (!pending.next && !current && pending.previous && matches2(await entry2(pending.parked), pending.previous) && !applied.some(
12721
- (change2) => change2.path === join31(profile.skillsRoot, pending.name)
12809
+ (change2) => change2.path === join32(profile.skillsRoot, pending.name)
12722
12810
  ))
12723
12811
  applied.push({
12724
12812
  action: "prune",
12725
12813
  name: pending.name,
12726
- path: join31(profile.skillsRoot, pending.name)
12814
+ path: join32(profile.skillsRoot, pending.name)
12727
12815
  });
12728
12816
  }
12729
12817
  state = latest;
@@ -12865,13 +12953,13 @@ async function syncProfile(name, options) {
12865
12953
 
12866
12954
  // src/core/selection-commands.ts
12867
12955
  import { realpath as realpath24, stat as stat5 } from "node:fs/promises";
12868
- import { dirname as dirname21, join as join33, parse as parse4, resolve as resolve19 } from "node:path";
12956
+ import { dirname as dirname21, join as join34, parse as parse4, resolve as resolve19 } from "node:path";
12869
12957
 
12870
12958
  // src/core/selection-manifest.ts
12871
12959
  import { randomUUID as randomUUID10 } from "node:crypto";
12872
- import { constants as constants11 } from "node:fs";
12873
- import { link as link7, lstat as lstat19, mkdir as mkdir11, open as open14, realpath as realpath23, rename as rename9, rmdir as rmdir6, unlink as unlink10 } from "node:fs/promises";
12874
- import { dirname as dirname20, join as join32, resolve as resolve18 } from "node:path";
12960
+ import { constants as constants12 } from "node:fs";
12961
+ import { link as link7, lstat as lstat19, mkdir as mkdir11, open as open15, realpath as realpath23, rename as rename9, rmdir as rmdir6, unlink as unlink10 } from "node:fs/promises";
12962
+ import { dirname as dirname20, join as join33, resolve as resolve18 } from "node:path";
12875
12963
  import { isDeepStrictEqual as isDeepStrictEqual7 } from "node:util";
12876
12964
  var SelectionManifestWriteError = class extends SkillexError {
12877
12965
  constructor(error, published) {
@@ -12963,13 +13051,13 @@ async function contextFor3(root) {
12963
13051
  unsafe2(path, "A scope ancestor is missing or is not a real directory.");
12964
13052
  parents2.set(path, info2);
12965
13053
  }
12966
- const agents = join32(canonical2, ".agents");
13054
+ const agents = join33(canonical2, ".agents");
12967
13055
  const info = await inspect5(agents);
12968
13056
  if (info) {
12969
13057
  if (!info.isDirectory()) unsafe2(agents, "The scope .agents path must be a real directory.");
12970
13058
  parents2.set(agents, info);
12971
13059
  }
12972
- return { root: canonical2, agents, path: join32(agents, "skills.json"), parents: parents2 };
13060
+ return { root: canonical2, agents, path: join33(agents, "skills.json"), parents: parents2 };
12973
13061
  }
12974
13062
  async function assertParents4(context2) {
12975
13063
  for (const [path, expected] of context2.parents) {
@@ -12986,7 +13074,7 @@ async function readStored2(path) {
12986
13074
  const first = await inspect5(path);
12987
13075
  if (!first) return void 0;
12988
13076
  if (!first.isFile()) unsafe2(path, "The selection declaration must be a real regular file.");
12989
- const handle = await open14(path, constants11.O_RDONLY | constants11.O_NOFOLLOW);
13077
+ const handle = await open15(path, constants12.O_RDONLY | constants12.O_NOFOLLOW);
12990
13078
  try {
12991
13079
  const before = await handle.stat({ bigint: true });
12992
13080
  if (!before.isFile() || !sameObject2(first, before))
@@ -13058,9 +13146,9 @@ async function assertPrevious2(context2, previous) {
13058
13146
  return current;
13059
13147
  }
13060
13148
  async function flushDirectory2(path, identity4) {
13061
- const handle = await open14(
13149
+ const handle = await open15(
13062
13150
  path,
13063
- constants11.O_RDONLY | constants11.O_DIRECTORY | constants11.O_NOFOLLOW
13151
+ constants12.O_RDONLY | constants12.O_DIRECTORY | constants12.O_NOFOLLOW
13064
13152
  );
13065
13153
  try {
13066
13154
  if (!sameObject2(identity4, await handle.stat({ bigint: true })))
@@ -13128,11 +13216,11 @@ async function writeSelectionManifest(previous, raw) {
13128
13216
  await flushDirectory2(context2.root, rootIdentity);
13129
13217
  }
13130
13218
  await assertPrevious2(context2, prior);
13131
- const path = join32(context2.agents, `.skillex-tmp-${randomUUID10()}`);
13219
+ const path = join33(context2.agents, `.skillex-tmp-${randomUUID10()}`);
13132
13220
  const mode = prior.file ? Number(prior.file.identity.mode & 0o7777n) : 420;
13133
- const file = await open14(
13221
+ const file = await open15(
13134
13222
  path,
13135
- constants11.O_WRONLY | constants11.O_CREAT | constants11.O_EXCL | constants11.O_NOFOLLOW,
13223
+ constants12.O_WRONLY | constants12.O_CREAT | constants12.O_EXCL | constants12.O_NOFOLLOW,
13136
13224
  mode
13137
13225
  );
13138
13226
  try {
@@ -13244,7 +13332,7 @@ async function directory(value, cwd, home) {
13244
13332
  fail("E_PROJECT_ROOT", "A project or working directory must be a nonempty path.", {
13245
13333
  fix: "Pass an existing directory as --project or cwd."
13246
13334
  });
13247
- const expanded = value === "~" ? home : value.startsWith("~/") ? join33(home, value.slice(2)) : value;
13335
+ const expanded = value === "~" ? home : value.startsWith("~/") ? join34(home, value.slice(2)) : value;
13248
13336
  const path = resolve19(cwd, expanded);
13249
13337
  const info = await inspectPath(path);
13250
13338
  if (!info || !(await stat5(path)).isDirectory())
@@ -13257,7 +13345,7 @@ async function directory(value, cwd, home) {
13257
13345
  async function nearestGitRoot(cwd, home) {
13258
13346
  let current = cwd;
13259
13347
  while (current !== home && current !== parse4(current).root) {
13260
- if (await inspectPath(join33(current, ".git"))) return current;
13348
+ if (await inspectPath(join34(current, ".git"))) return current;
13261
13349
  current = dirname21(current);
13262
13350
  }
13263
13351
  return void 0;
@@ -13424,7 +13512,7 @@ async function prepare3(options, edit) {
13424
13512
  ...options,
13425
13513
  home: target2.home,
13426
13514
  forbiddenRoots: [
13427
- join33(target2.root, ".agents"),
13515
+ join34(target2.root, ".agents"),
13428
13516
  ...target2.scope === "project" ? [target2.root] : []
13429
13517
  ]
13430
13518
  });
@@ -13579,7 +13667,7 @@ async function setInheritance(enabled, options = {}) {
13579
13667
 
13580
13668
  // src/core/sets.ts
13581
13669
  import { readdir as readdir22 } from "node:fs/promises";
13582
- import { join as join34 } from "node:path";
13670
+ import { join as join35 } from "node:path";
13583
13671
  function validateName(name) {
13584
13672
  parseManifest({ sets: [{ name }] }, "set name");
13585
13673
  }
@@ -13596,7 +13684,7 @@ async function listSets(options = {}) {
13596
13684
  const command = "set list";
13597
13685
  try {
13598
13686
  const registry = await discoverRegistry(options);
13599
- const path = join34(registry.root, "sets");
13687
+ const path = join35(registry.root, "sets");
13600
13688
  const sets = [];
13601
13689
  const failures = [];
13602
13690
  if (await inspectPath(path)) {
@@ -13609,7 +13697,7 @@ async function listSets(options = {}) {
13609
13697
  try {
13610
13698
  sets.push(await readSet(registry.root, entry3.name));
13611
13699
  } catch (error) {
13612
- failures.push(collectedError(error, join34(path, entry3.name)));
13700
+ failures.push(collectedError(error, join35(path, entry3.name)));
13613
13701
  }
13614
13702
  }
13615
13703
  }
@@ -13631,7 +13719,7 @@ async function showSet(name, options = {}) {
13631
13719
  async function prepareSet(name, names2, operation, options) {
13632
13720
  validateName(name);
13633
13721
  const registry = await discoverRegistry(options);
13634
- const path = join34(registry.root, "sets", name);
13722
+ const path = join35(registry.root, "sets", name);
13635
13723
  const exists = await inspectPath(path);
13636
13724
  const previous = operation === "create" && !exists ? { kind: "set", name, path, skills: [] } : await readSet(registry.root, name);
13637
13725
  const requested = await requestedSkills(registry.root, names2, path);
@@ -13647,10 +13735,10 @@ async function prepareSet(name, names2, operation, options) {
13647
13735
  const plan2 = await newPlan(registry, composition2, options.dryRun === true);
13648
13736
  await planDirectory(plan2, path);
13649
13737
  for (const skill of previous.skills)
13650
- await inspectCanonicalLink(plan2, join34(path, skill.name), skill.path);
13738
+ await inspectCanonicalLink(plan2, join35(path, skill.name), skill.path);
13651
13739
  for (const [member, target2] of requested) {
13652
- if (operation === "add") await planLink(plan2, join34(path, member), target2);
13653
- else if (operation === "remove") await planUnlink(plan2, join34(path, member), target2);
13740
+ if (operation === "add") await planLink(plan2, join35(path, member), target2);
13741
+ else if (operation === "remove") await planUnlink(plan2, join35(path, member), target2);
13654
13742
  }
13655
13743
  return plan2;
13656
13744
  }
@@ -13666,7 +13754,7 @@ async function removeSetSkills(name, names2, options = {}) {
13666
13754
 
13667
13755
  // src/core/vendor-inspect.ts
13668
13756
  import { readdir as readdir23 } from "node:fs/promises";
13669
- import { join as join35 } from "node:path";
13757
+ import { join as join36 } from "node:path";
13670
13758
 
13671
13759
  // src/core/vendor-git.ts
13672
13760
  import { execFile as execFile4 } from "node:child_process";
@@ -14028,19 +14116,19 @@ var Observation = class {
14028
14116
  async function recordedInventory(manifest, observation, options) {
14029
14117
  const recorded = /* @__PURE__ */ new Map();
14030
14118
  const failed2 = /* @__PURE__ */ new Set();
14031
- const catalog = join35(manifest.registry.root, "all-skills");
14119
+ const catalog = join36(manifest.registry.root, "all-skills");
14032
14120
  for (const entry3 of (await readdir23(catalog, { withFileTypes: true })).sort(
14033
14121
  (a, b) => a.name.localeCompare(b.name)
14034
14122
  )) {
14035
14123
  checkVendorSignal(options);
14036
14124
  if (!isSkillName(entry3.name) || !entry3.isDirectory()) continue;
14037
- const path = join35(catalog, entry3.name);
14125
+ const path = join36(catalog, entry3.name);
14038
14126
  try {
14039
14127
  const receipt2 = await readVendorProvenance(path);
14040
14128
  if (receipt2) recorded.set(entry3.name, receipt2);
14041
14129
  } catch (error) {
14042
14130
  failed2.add(entry3.name);
14043
- observation.capture(error, join35(path, ".source.yaml"));
14131
+ observation.capture(error, join36(path, ".source.yaml"));
14044
14132
  }
14045
14133
  }
14046
14134
  return { recorded, failed: failed2 };
@@ -14057,7 +14145,7 @@ function emptySkill(source, manifest, name, upstreamCommit) {
14057
14145
  return {
14058
14146
  name,
14059
14147
  source: source.name,
14060
- path: join35(manifest.registry.root, "all-skills", name),
14148
+ path: join36(manifest.registry.root, "all-skills", name),
14061
14149
  state: "unknown",
14062
14150
  recordedCommit: null,
14063
14151
  upstreamCommit,
@@ -14438,9 +14526,9 @@ async function inspectVendorStatus(options = {}) {
14438
14526
 
14439
14527
  // src/core/vendor-sync.ts
14440
14528
  import { randomUUID as randomUUID11 } from "node:crypto";
14441
- import { chmod as chmod3, lstat as lstat20, mkdir as mkdir12, open as open15, readdir as readdir24, rename as rename10 } from "node:fs/promises";
14442
- import { dirname as dirname22, join as join36 } from "node:path";
14443
- function refuse3(code, message, path, fix) {
14529
+ import { chmod as chmod3, lstat as lstat20, mkdir as mkdir12, open as open16, readdir as readdir24, rename as rename10 } from "node:fs/promises";
14530
+ import { dirname as dirname22, join as join37 } from "node:path";
14531
+ function refuse4(code, message, path, fix) {
14444
14532
  fail(code, message, { path, fix }, ExitCode.REFUSED);
14445
14533
  }
14446
14534
  function attributed(provenance, source) {
@@ -14450,21 +14538,21 @@ function baseline(provenance) {
14450
14538
  return provenance.digest !== null && (provenance.digestFormat === null || ["skillex-tree-v1", "skillex-tree-v1+symlinks"].includes(provenance.digestFormat));
14451
14539
  }
14452
14540
  async function planSkill(source, skill, commit, extractedAt, catalog, options) {
14453
- const path = join36(catalog, skill.name);
14541
+ const path = join37(catalog, skill.name);
14454
14542
  const info = await vendorLstat(path);
14455
14543
  let before = null;
14456
14544
  let provenance = null;
14457
14545
  let action = "create";
14458
14546
  if (info) {
14459
14547
  if (info.isSymbolicLink())
14460
- refuse3(
14548
+ refuse4(
14461
14549
  "E_VENDOR_DESTINATION_LINK",
14462
14550
  "Vendor sync will not replace a linked canonical destination.",
14463
14551
  path,
14464
14552
  "Use the explicit migrate workflow to account for this link and its consumers before vendoring."
14465
14553
  );
14466
14554
  if (!info.isDirectory())
14467
- refuse3(
14555
+ refuse4(
14468
14556
  "E_VENDOR_DESTINATION_TYPE",
14469
14557
  "The canonical destination is not a real skill directory.",
14470
14558
  path,
@@ -14474,21 +14562,21 @@ async function planSkill(source, skill, commit, extractedAt, catalog, options) {
14474
14562
  const digest = await digestVendorTree(path);
14475
14563
  if (provenance?.type === "vendored") {
14476
14564
  if (!attributed(provenance, source))
14477
- refuse3(
14565
+ refuse4(
14478
14566
  "E_VENDOR_FOREIGN_SOURCE",
14479
14567
  "The canonical skill is attributed to a different source.",
14480
14568
  path,
14481
14569
  "Correct the declaration or migrate the existing provenance explicitly; adopt does not transfer another vendor's ownership."
14482
14570
  );
14483
14571
  if (!baseline(provenance) && !options.discardLocalEdits)
14484
- refuse3(
14572
+ refuse4(
14485
14573
  "E_VENDOR_BASELINE_MISSING",
14486
14574
  "The managed skill has no supported baseline digest proving unchanged content.",
14487
14575
  path,
14488
14576
  "Preserve or commit local content, then explicitly use --discard-local-edits to replace this attributed skill; it cannot be implicitly pruned."
14489
14577
  );
14490
14578
  if (baseline(provenance) && (digest !== provenance.digest || provenance.modifiedLocally) && !options.discardLocalEdits)
14491
- refuse3(
14579
+ refuse4(
14492
14580
  "E_VENDOR_LOCAL_EDITS",
14493
14581
  "The vendored skill has local content or executable-mode edits.",
14494
14582
  path,
@@ -14497,14 +14585,14 @@ async function planSkill(source, skill, commit, extractedAt, catalog, options) {
14497
14585
  action = digest === skill.digest && !provenance.modifiedLocally && provenance.digest === skill.digest && provenance.upstreamCommit === commit && provenance.upstreamTree === skill.tree && provenance.upstreamPath === skill.upstreamPath && provenance.upstreamVersion === source.version && provenance.extractedAt !== null && baseline(provenance) ? "unchanged" : "update";
14498
14586
  } else {
14499
14587
  if (!options.adopt)
14500
- refuse3(
14588
+ refuse4(
14501
14589
  "E_VENDOR_UNMANAGED",
14502
14590
  "The canonical skill is not managed by this vendor source.",
14503
14591
  path,
14504
14592
  "Use --adopt to record ownership; if bytes or modes differ from upstream, also use --discard-local-edits after preserving them."
14505
14593
  );
14506
14594
  if (digest !== skill.digest && !options.discardLocalEdits)
14507
- refuse3(
14595
+ refuse4(
14508
14596
  "E_VENDOR_LOCAL_EDITS",
14509
14597
  "Adopting this skill would replace local bytes or executable modes.",
14510
14598
  path,
@@ -14514,14 +14602,14 @@ async function planSkill(source, skill, commit, extractedAt, catalog, options) {
14514
14602
  }
14515
14603
  before = await captureVendorTree(path);
14516
14604
  if (before.root.dev !== String((await lstat20(catalog, { bigint: true })).dev))
14517
- refuse3(
14605
+ refuse4(
14518
14606
  "E_VENDOR_CROSS_DEVICE",
14519
14607
  "The existing skill is mounted on a different filesystem from catalog staging.",
14520
14608
  path,
14521
14609
  "Move the skill into the catalog filesystem before updating it; vendor replacement requires same-filesystem renames."
14522
14610
  );
14523
14611
  if (before.root.dev !== String(info.dev) || before.root.ino !== String(info.ino))
14524
- refuse3(
14612
+ refuse4(
14525
14613
  "E_VENDOR_CHANGED",
14526
14614
  "The destination changed during preflight.",
14527
14615
  path,
@@ -14560,7 +14648,7 @@ async function planSkill(source, skill, commit, extractedAt, catalog, options) {
14560
14648
  async function prepare4(options, extractedAt) {
14561
14649
  vendorInterrupted(options);
14562
14650
  const manifest = await readVendorSources(options);
14563
- const catalog = join36(manifest.registry.root, "all-skills");
14651
+ const catalog = join37(manifest.registry.root, "all-skills");
14564
14652
  const journal = await readVendorJournal(manifest.registry, options);
14565
14653
  const selected = selectVendorSources(manifest, options.sources);
14566
14654
  const sources = [];
@@ -14572,7 +14660,7 @@ async function prepare4(options, extractedAt) {
14572
14660
  const checkout = await resolveVendorCheckout(source, options);
14573
14661
  if (!checkout.root) {
14574
14662
  if (!source.optional)
14575
- refuse3(
14663
+ refuse4(
14576
14664
  "E_SOURCE_CHECKOUT_MISSING",
14577
14665
  `Local checkout ${source.checkout} is unavailable.`,
14578
14666
  manifest.path,
@@ -14625,7 +14713,7 @@ async function prepare4(options, extractedAt) {
14625
14713
  });
14626
14714
  for (const skill of upstream.skills) {
14627
14715
  if (names2.has(skill.name))
14628
- refuse3(
14716
+ refuse4(
14629
14717
  "E_VENDOR_DUPLICATE_SKILL",
14630
14718
  `Sources ${names2.get(skill.name)} and ${source.name} select the same canonical skill.`,
14631
14719
  manifest.path,
@@ -14638,7 +14726,7 @@ async function prepare4(options, extractedAt) {
14638
14726
  if (options.prune) {
14639
14727
  for (const name of (await readdir24(catalog)).filter(isSkillName).sort()) {
14640
14728
  if (names2.has(name)) continue;
14641
- const path = join36(catalog, name);
14729
+ const path = join37(catalog, name);
14642
14730
  if (!(await vendorLstat(path))?.isDirectory()) continue;
14643
14731
  let provenance;
14644
14732
  try {
@@ -14658,14 +14746,14 @@ async function prepare4(options, extractedAt) {
14658
14746
  const source = provenance?.source ? enumerated.get(provenance.source) : void 0;
14659
14747
  if (!provenance || !source || !attributed(provenance, source)) continue;
14660
14748
  if (!baseline(provenance))
14661
- refuse3(
14749
+ refuse4(
14662
14750
  "E_VENDOR_BASELINE_MISSING",
14663
14751
  "An orphaned managed skill has no supported baseline proving it is safe to prune.",
14664
14752
  path,
14665
14753
  "Restore its verified provenance or remove it explicitly after inspection; --discard-local-edits never authorizes pruning unknown content."
14666
14754
  );
14667
14755
  if (provenance.modifiedLocally || await digestVendorTree(path) !== provenance.digest)
14668
- refuse3(
14756
+ refuse4(
14669
14757
  "E_VENDOR_LOCAL_EDITS",
14670
14758
  "An orphaned managed skill has local edits and cannot be pruned.",
14671
14759
  path,
@@ -14699,7 +14787,7 @@ async function prepare4(options, extractedAt) {
14699
14787
  }
14700
14788
  async function absent2(path) {
14701
14789
  if (await vendorLstat(path))
14702
- refuse3(
14790
+ refuse4(
14703
14791
  "E_VENDOR_CHANGED",
14704
14792
  "A destination or recovery path appeared after preflight.",
14705
14793
  path,
@@ -14711,7 +14799,7 @@ async function assertCurrentIntent(prepared, options) {
14711
14799
  const selected = selectVendorSources(current, options.sources);
14712
14800
  const expected = selectVendorSources(prepared.manifest, options.sources);
14713
14801
  if (current.registry.root !== prepared.manifest.registry.root || JSON.stringify(selected) !== JSON.stringify(expected))
14714
- refuse3(
14802
+ refuse4(
14715
14803
  "E_VENDOR_INTENT_CHANGED",
14716
14804
  "The selected vendor declarations changed during staging.",
14717
14805
  prepared.manifest.path,
@@ -14721,7 +14809,7 @@ async function assertCurrentIntent(prepared, options) {
14721
14809
  const checkout = await resolveVendorCheckout(source, options);
14722
14810
  const previous = prepared.result.sources.find((entry3) => entry3.name === source.name);
14723
14811
  if (!previous || checkout.root !== previous.checkout)
14724
- refuse3(
14812
+ refuse4(
14725
14813
  "E_VENDOR_INTENT_CHANGED",
14726
14814
  "A selected local checkout mapping changed during staging.",
14727
14815
  prepared.manifest.path,
@@ -14730,7 +14818,7 @@ async function assertCurrentIntent(prepared, options) {
14730
14818
  }
14731
14819
  }
14732
14820
  async function syncDirectory2(path) {
14733
- const handle = await open15(path, "r");
14821
+ const handle = await open16(path, "r");
14734
14822
  try {
14735
14823
  await handle.sync();
14736
14824
  } finally {
@@ -14745,12 +14833,12 @@ async function stage(plan2, path, catalog, options) {
14745
14833
  for (const entry3 of plan2.entries) {
14746
14834
  vendorInterrupted(options);
14747
14835
  await assertVendorIdentity(path, root);
14748
- const target2 = join36(path, entry3.path);
14836
+ const target2 = join37(path, entry3.path);
14749
14837
  let parent = dirname22(entry3.path);
14750
14838
  while (parent !== ".") {
14751
14839
  const identity4 = parents2.get(parent);
14752
14840
  if (!identity4) throw new Error(`Unprepared vendor content parent: ${parent}`);
14753
- await assertVendorIdentity(join36(path, parent), identity4);
14841
+ await assertVendorIdentity(join37(path, parent), identity4);
14754
14842
  parent = dirname22(parent);
14755
14843
  }
14756
14844
  if (entry3.kind === "directory") {
@@ -14758,7 +14846,7 @@ async function stage(plan2, path, catalog, options) {
14758
14846
  await chmod3(target2, 493);
14759
14847
  parents2.set(entry3.path, vendorIdentity(await lstat20(target2, { bigint: true })));
14760
14848
  } else if (entry3.kind === "file") {
14761
- const handle = await open15(target2, "wx", entry3.mode);
14849
+ const handle = await open16(target2, "wx", entry3.mode);
14762
14850
  try {
14763
14851
  await handle.writeFile(entry3.bytes);
14764
14852
  await handle.chmod(entry3.mode);
@@ -14807,7 +14895,7 @@ async function recover2(snapshot3, findings, options, onWrite) {
14807
14895
  if (published) {
14808
14896
  await assertVendorTree(operation.change.path, operation.after);
14809
14897
  if (stageInfo)
14810
- refuse3(
14898
+ refuse4(
14811
14899
  "E_VENDOR_RECOVERY_CONFLICT",
14812
14900
  "Both the staged and published paths exist for one prepared skill.",
14813
14901
  operation.change.path,
@@ -14815,7 +14903,7 @@ async function recover2(snapshot3, findings, options, onWrite) {
14815
14903
  );
14816
14904
  } else if (destination) {
14817
14905
  if (!operation.before)
14818
- refuse3(
14906
+ refuse4(
14819
14907
  "E_VENDOR_RECOVERY_CONFLICT",
14820
14908
  "A foreign destination blocks vendor recovery.",
14821
14909
  operation.change.path,
@@ -14823,7 +14911,7 @@ async function recover2(snapshot3, findings, options, onWrite) {
14823
14911
  );
14824
14912
  await assertVendorTree(operation.change.path, operation.before);
14825
14913
  if (parked)
14826
- refuse3(
14914
+ refuse4(
14827
14915
  "E_VENDOR_RECOVERY_CONFLICT",
14828
14916
  "Both the old destination and its parked path exist.",
14829
14917
  operation.change.path,
@@ -14831,7 +14919,7 @@ async function recover2(snapshot3, findings, options, onWrite) {
14831
14919
  );
14832
14920
  } else if (!parked && operation.before) {
14833
14921
  if (operation.change.action !== "prune")
14834
- refuse3(
14922
+ refuse4(
14835
14923
  "E_VENDOR_RECOVERY_CONFLICT",
14836
14924
  "The old and new skill directories are both missing.",
14837
14925
  operation.change.path,
@@ -14904,7 +14992,7 @@ async function syncVendorSources(options = {}) {
14904
14992
  initiallyPending = initialJournal.document !== null;
14905
14993
  fallback = {
14906
14994
  registry,
14907
- manifest: join36(registry.root, "all-skills", "sources.toml"),
14995
+ manifest: join37(registry.root, "all-skills", "sources.toml"),
14908
14996
  dryRun: options.dryRun === true,
14909
14997
  sources: [],
14910
14998
  changes: [],
@@ -14951,7 +15039,7 @@ async function syncVendorSources(options = {}) {
14951
15039
  }
14952
15040
  prepared = await prepare4(options, extractedAt);
14953
15041
  if (prepared.manifest.registry.root !== initialRoot)
14954
- refuse3(
15042
+ refuse4(
14955
15043
  "E_CATALOG_CHANGED",
14956
15044
  "Registry discovery changed while waiting for its catalog lock.",
14957
15045
  initialRoot,
@@ -14973,8 +15061,8 @@ async function syncVendorSources(options = {}) {
14973
15061
  change: plan2.change,
14974
15062
  before: plan2.before,
14975
15063
  after: null,
14976
- stage: plan2.change.action === "prune" ? null : join36(journal.catalog, `.skillex-tmp-vendor-${id}-new`),
14977
- parked: plan2.before ? join36(journal.catalog, `.skillex-tmp-vendor-${id}-old`) : null
15064
+ stage: plan2.change.action === "prune" ? null : join37(journal.catalog, `.skillex-tmp-vendor-${id}-new`),
15065
+ parked: plan2.before ? join37(journal.catalog, `.skillex-tmp-vendor-${id}-old`) : null
14978
15066
  };
14979
15067
  });
14980
15068
  vendorInterrupted(options);