@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/cli.js CHANGED
@@ -12023,10 +12023,94 @@ async function migrate(input = {}) {
12023
12023
 
12024
12024
  // src/core/profile-sync.ts
12025
12025
  import { randomUUID as randomUUID9 } from "node:crypto";
12026
- import { constants as constants10 } from "node:fs";
12027
- 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";
12028
- import { join as join31, resolve as resolve17 } from "node:path";
12026
+ import { constants as constants11 } from "node:fs";
12027
+ 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";
12028
+ import { join as join32, resolve as resolve17 } from "node:path";
12029
12029
  import { isDeepStrictEqual as isDeepStrictEqual6 } from "node:util";
12030
+
12031
+ // src/core/profile-policy.ts
12032
+ import { constants as constants10 } from "node:fs";
12033
+ import { open as open13, readFile as readFile12 } from "node:fs/promises";
12034
+ import { join as join31 } from "node:path";
12035
+ import { parseDocument as parseDocument3 } from "yaml";
12036
+ var metadataFiles = /* @__PURE__ */ new Set([
12037
+ ".usage.json",
12038
+ ".usage.json.lock",
12039
+ ".curator_state",
12040
+ ".curator_suppressed",
12041
+ ".sync_state"
12042
+ ]);
12043
+ var metadataDirectories = /* @__PURE__ */ new Set([".curator_backups"]);
12044
+ function hermesMetadata(item2) {
12045
+ if (item2.kind === "file") return metadataFiles.has(item2.name);
12046
+ if (item2.kind === "directory") return metadataDirectories.has(item2.name);
12047
+ return false;
12048
+ }
12049
+ function refuse3(path, message) {
12050
+ return fail(
12051
+ "E_PROFILE_SKILLEX_ONLY",
12052
+ message,
12053
+ {
12054
+ path,
12055
+ 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."
12056
+ },
12057
+ ExitCode.REFUSED
12058
+ );
12059
+ }
12060
+ async function strictProfile(profile, options) {
12061
+ if (options.skillexOnly !== void 0 && typeof options.skillexOnly !== "boolean")
12062
+ refuse3(profile.root, "skillexOnly must be boolean.");
12063
+ const marker = await entry2(join31(profile.root, ".skillex-only"));
12064
+ if (marker && !marker.info.isFile())
12065
+ refuse3(marker.path, "The strict policy marker must be a regular file.");
12066
+ return options.skillexOnly === true || marker !== void 0;
12067
+ }
12068
+ async function assertStrictProfile(profile, options, local) {
12069
+ if (!await strictProfile(profile, options)) return false;
12070
+ for (const item2 of local)
12071
+ if (!hermesMetadata(item2))
12072
+ refuse3(item2.path, `Skillex-only profile contains unowned entry ${item2.name}.`);
12073
+ const path = join31(profile.root, "config.yaml");
12074
+ const config = await entry2(path);
12075
+ if (config) {
12076
+ if (!config.info.isFile())
12077
+ refuse3(path, "Strict profile config must be a regular generated file.");
12078
+ const document = parseDocument3(await readFile12(path, "utf8"), { uniqueKeys: true });
12079
+ if (document.errors.length) refuse3(path, "Cannot validate strict skills configuration.");
12080
+ const dirs = document.toJS()?.skills?.external_dirs;
12081
+ if (dirs !== void 0 && (!Array.isArray(dirs) || dirs.length))
12082
+ refuse3(path, "Skillex-only profiles require skills.external_dirs: [].");
12083
+ }
12084
+ const optOut = await entry2(join31(profile.root, ".no-bundled-skills"));
12085
+ if (optOut && !optOut.info.isFile())
12086
+ refuse3(optOut.path, "Bundled opt-out marker must be a regular file.");
12087
+ return true;
12088
+ }
12089
+ async function publishStrictPolicy(profile) {
12090
+ for (const name of [".no-bundled-skills", ".skillex-only"]) {
12091
+ const path = join31(profile.root, name);
12092
+ const existing = await entry2(path);
12093
+ if (existing) {
12094
+ if (!existing.info.isFile()) refuse3(path, "Policy markers must be regular files.");
12095
+ continue;
12096
+ }
12097
+ const handle = await open13(
12098
+ path,
12099
+ constants10.O_WRONLY | constants10.O_CREAT | constants10.O_EXCL | constants10.O_NOFOLLOW,
12100
+ 384
12101
+ );
12102
+ try {
12103
+ await handle.writeFile(
12104
+ "Skillex owns this PM skill projection. Use skillex profile sync; no local installs.\n"
12105
+ );
12106
+ await handle.sync();
12107
+ } finally {
12108
+ await handle.close();
12109
+ }
12110
+ }
12111
+ }
12112
+
12113
+ // src/core/profile-sync.ts
12030
12114
  function refused3(code, message, path, fix) {
12031
12115
  fail(code, message, { path, fix }, ExitCode.REFUSED);
12032
12116
  }
@@ -12045,14 +12129,14 @@ function receiptOptions(profile, options, scopes = []) {
12045
12129
  const { home, cwd } = normalized;
12046
12130
  const expanded = (value) => resolve17(
12047
12131
  cwd,
12048
- value === "~" ? home : value.startsWith("~/") ? join31(home, value.slice(2)) : value
12132
+ value === "~" ? home : value.startsWith("~/") ? join32(home, value.slice(2)) : value
12049
12133
  );
12050
12134
  return {
12051
12135
  ...normalized,
12052
12136
  forbiddenRoots: [
12053
12137
  .../* @__PURE__ */ new Set([
12054
12138
  profile.root,
12055
- join31(home, ".agents"),
12139
+ join32(home, ".agents"),
12056
12140
  ...options.project ? [expanded(options.project)] : [],
12057
12141
  ...options.registryRoot ? [expanded(options.registryRoot)] : [],
12058
12142
  ...scopes.flatMap((scope) => [
@@ -12114,7 +12198,7 @@ async function resolveProjection(options) {
12114
12198
  }
12115
12199
  const locations = /* @__PURE__ */ new Map();
12116
12200
  for (const scope of scopes) {
12117
- locations.set(join31(scope.registry.root, "all-skills"), "catalog");
12201
+ locations.set(join32(scope.registry.root, "all-skills"), "catalog");
12118
12202
  if (scope.pack) locations.set(scope.pack.path, "pack");
12119
12203
  }
12120
12204
  const sources = [];
@@ -12141,7 +12225,7 @@ async function observations(profile, state) {
12141
12225
  );
12142
12226
  if (root)
12143
12227
  for (const name of (await readdir21(profile.skillsRoot)).sort()) {
12144
- const path = join31(profile.skillsRoot, name);
12228
+ const path = join32(profile.skillsRoot, name);
12145
12229
  const actual = await entry2(path);
12146
12230
  if (!actual) continue;
12147
12231
  entries.set(name, actual);
@@ -12193,7 +12277,7 @@ async function buildWork(profile, state, options) {
12193
12277
  for (const scope of projection.scopes) {
12194
12278
  for (const protectedRoot of [
12195
12279
  scope.registry.root,
12196
- ...scope.scope === "project" ? [scope.root] : [join31(scope.root, ".agents")]
12280
+ ...scope.scope === "project" ? [scope.root] : [join32(scope.root, ".agents")]
12197
12281
  ])
12198
12282
  if (within2(profile.skillsRoot, protectedRoot) || within2(protectedRoot, profile.skillsRoot))
12199
12283
  refused3(
@@ -12213,18 +12297,19 @@ async function buildWork(profile, state, options) {
12213
12297
  );
12214
12298
  state = await readProfileState(profile, receiptOptions(profile, options, projection.scopes));
12215
12299
  const observed = await observations(profile, state);
12300
+ await assertStrictProfile(profile, options, observed.local);
12216
12301
  const links = { ...state.data.links };
12217
12302
  const changes = [];
12218
12303
  if (!observed.root) changes.push({ action: "mkdir", path: profile.skillsRoot });
12219
12304
  for (const [name, claim2] of Object.entries(links))
12220
12305
  if (!owned2(observed.entries.get(name), claim2)) {
12221
12306
  delete links[name];
12222
- changes.push({ action: "release", name, path: join31(profile.skillsRoot, name) });
12307
+ changes.push({ action: "release", name, path: join32(profile.skillsRoot, name) });
12223
12308
  }
12224
12309
  const candidates = [];
12225
12310
  const desiredNames = new Set(projection.desired.map((desired) => desired.name));
12226
12311
  for (const desired of projection.desired) {
12227
- const path = join31(profile.skillsRoot, desired.name);
12312
+ const path = join32(profile.skillsRoot, desired.name);
12228
12313
  const actual = observed.entries.get(desired.name);
12229
12314
  const recorded = claim(links, desired.name);
12230
12315
  const state2 = actual ? recorded ? lexicalTarget(path, actual.raw ?? "") === desired.target ? "unchanged" : "update" : "shadowed" : "create";
@@ -12241,7 +12326,7 @@ async function buildWork(profile, state, options) {
12241
12326
  }
12242
12327
  for (const name of Object.keys(links).sort())
12243
12328
  if (!desiredNames.has(name))
12244
- changes.push({ action: "prune", name, path: join31(profile.skillsRoot, name) });
12329
+ changes.push({ action: "prune", name, path: join32(profile.skillsRoot, name) });
12245
12330
  const next = {
12246
12331
  version: 1,
12247
12332
  root: observed.root ? identity(observed.root) : null,
@@ -12279,6 +12364,7 @@ async function inspectProfile(profile, options = {}) {
12279
12364
  }
12280
12365
  try {
12281
12366
  preserved = (await observations(profile, state)).local;
12367
+ await assertStrictProfile(profile, options, preserved);
12282
12368
  } catch (error) {
12283
12369
  const failure6 = asFailure(error);
12284
12370
  findings.push(...failure6.findings);
@@ -12330,9 +12416,9 @@ async function inspectProfile(profile, options = {}) {
12330
12416
  }
12331
12417
  async function flush3(path, expected) {
12332
12418
  await requireIdentity(path, expected);
12333
- const handle = await open13(
12419
+ const handle = await open14(
12334
12420
  path,
12335
- constants10.O_RDONLY | constants10.O_DIRECTORY | constants10.O_NOFOLLOW
12421
+ constants11.O_RDONLY | constants11.O_DIRECTORY | constants11.O_NOFOLLOW
12336
12422
  );
12337
12423
  try {
12338
12424
  if (!matches2({ path, info: await handle.stat({ bigint: true }) }, expected))
@@ -12377,7 +12463,7 @@ async function recoverProfile(profile, state, options, mark, findings) {
12377
12463
  if (!pending || !root) return state;
12378
12464
  checkProfileSignal(options);
12379
12465
  await rootGuard(profile, root);
12380
- const path = join31(profile.skillsRoot, pending.name);
12466
+ const path = join32(profile.skillsRoot, pending.name);
12381
12467
  let current = await entry2(path);
12382
12468
  const parked = await entry2(pending.parked);
12383
12469
  const staged = await entry2(pending.stage);
@@ -12503,8 +12589,8 @@ async function apply2(work, mark, begin) {
12503
12589
  }
12504
12590
  }
12505
12591
  const id = randomUUID9();
12506
- const stage2 = join31(profile.skillsRoot, `.skillex-tmp-profile-${id}-new`);
12507
- const parked = join31(profile.skillsRoot, `.skillex-tmp-profile-${id}-old`);
12592
+ const stage2 = join32(profile.skillsRoot, `.skillex-tmp-profile-${id}-new`);
12593
+ const parked = join32(profile.skillsRoot, `.skillex-tmp-profile-${id}-old`);
12508
12594
  await absent(stage2);
12509
12595
  await absent(parked);
12510
12596
  let next;
@@ -12649,7 +12735,7 @@ async function runProfileSync(name, options) {
12649
12735
  { findings, exit: readExit(findings, state.data.pending !== void 0) }
12650
12736
  );
12651
12737
  }
12652
- if (work && !work.result.changes.length)
12738
+ if (work && !work.result.changes.length && !await strictProfile(profile, options))
12653
12739
  return makeResult(
12654
12740
  "profile sync",
12655
12741
  { ...work.result, project: work.projection.project, dryRun: false, applied },
@@ -12677,6 +12763,8 @@ async function runProfileSync(name, options) {
12677
12763
  });
12678
12764
  }
12679
12765
  await assertProfileIdentity(selected);
12766
+ if (await assertStrictProfile(selected, options, (await observations(selected, state)).local))
12767
+ await publishStrictPolicy(selected);
12680
12768
  const currentProfile = await discoverProfile(name, options);
12681
12769
  if (currentProfile.root !== selected.root || currentProfile.skillsRoot !== selected.skillsRoot)
12682
12770
  refused3(
@@ -12714,21 +12802,21 @@ async function runProfileSync(name, options) {
12714
12802
  const latest = await readProfileState(profile, receiptOptions(profile, options));
12715
12803
  const pending = latest.data.pending;
12716
12804
  if (pending) {
12717
- const current = await entry2(join31(profile.skillsRoot, pending.name));
12805
+ const current = await entry2(join32(profile.skillsRoot, pending.name));
12718
12806
  if (pending.next && matches2(current, pending.next) && !applied.some((change2) => change2.path === current?.path))
12719
12807
  applied.push({
12720
12808
  action: pending.previous ? "update" : "create",
12721
12809
  name: pending.name,
12722
- path: join31(profile.skillsRoot, pending.name),
12810
+ path: join32(profile.skillsRoot, pending.name),
12723
12811
  target: pending.next.raw
12724
12812
  });
12725
12813
  if (!pending.next && !current && pending.previous && matches2(await entry2(pending.parked), pending.previous) && !applied.some(
12726
- (change2) => change2.path === join31(profile.skillsRoot, pending.name)
12814
+ (change2) => change2.path === join32(profile.skillsRoot, pending.name)
12727
12815
  ))
12728
12816
  applied.push({
12729
12817
  action: "prune",
12730
12818
  name: pending.name,
12731
- path: join31(profile.skillsRoot, pending.name)
12819
+ path: join32(profile.skillsRoot, pending.name)
12732
12820
  });
12733
12821
  }
12734
12822
  state = latest;
@@ -12870,13 +12958,13 @@ async function syncProfile(name, options) {
12870
12958
 
12871
12959
  // src/core/selection-commands.ts
12872
12960
  import { realpath as realpath24, stat as stat5 } from "node:fs/promises";
12873
- import { dirname as dirname21, join as join33, parse as parse4, resolve as resolve19 } from "node:path";
12961
+ import { dirname as dirname21, join as join34, parse as parse4, resolve as resolve19 } from "node:path";
12874
12962
 
12875
12963
  // src/core/selection-manifest.ts
12876
12964
  import { randomUUID as randomUUID10 } from "node:crypto";
12877
- import { constants as constants11 } from "node:fs";
12878
- 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";
12879
- import { dirname as dirname20, join as join32, resolve as resolve18 } from "node:path";
12965
+ import { constants as constants12 } from "node:fs";
12966
+ 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";
12967
+ import { dirname as dirname20, join as join33, resolve as resolve18 } from "node:path";
12880
12968
  import { isDeepStrictEqual as isDeepStrictEqual7 } from "node:util";
12881
12969
  var SelectionManifestWriteError = class extends SkillexError {
12882
12970
  constructor(error, published) {
@@ -12968,13 +13056,13 @@ async function contextFor3(root) {
12968
13056
  unsafe2(path, "A scope ancestor is missing or is not a real directory.");
12969
13057
  parents2.set(path, info2);
12970
13058
  }
12971
- const agents = join32(canonical2, ".agents");
13059
+ const agents = join33(canonical2, ".agents");
12972
13060
  const info = await inspect5(agents);
12973
13061
  if (info) {
12974
13062
  if (!info.isDirectory()) unsafe2(agents, "The scope .agents path must be a real directory.");
12975
13063
  parents2.set(agents, info);
12976
13064
  }
12977
- return { root: canonical2, agents, path: join32(agents, "skills.json"), parents: parents2 };
13065
+ return { root: canonical2, agents, path: join33(agents, "skills.json"), parents: parents2 };
12978
13066
  }
12979
13067
  async function assertParents4(context2) {
12980
13068
  for (const [path, expected] of context2.parents) {
@@ -12991,7 +13079,7 @@ async function readStored2(path) {
12991
13079
  const first = await inspect5(path);
12992
13080
  if (!first) return void 0;
12993
13081
  if (!first.isFile()) unsafe2(path, "The selection declaration must be a real regular file.");
12994
- const handle = await open14(path, constants11.O_RDONLY | constants11.O_NOFOLLOW);
13082
+ const handle = await open15(path, constants12.O_RDONLY | constants12.O_NOFOLLOW);
12995
13083
  try {
12996
13084
  const before = await handle.stat({ bigint: true });
12997
13085
  if (!before.isFile() || !sameObject2(first, before))
@@ -13063,9 +13151,9 @@ async function assertPrevious2(context2, previous) {
13063
13151
  return current;
13064
13152
  }
13065
13153
  async function flushDirectory2(path, identity5) {
13066
- const handle = await open14(
13154
+ const handle = await open15(
13067
13155
  path,
13068
- constants11.O_RDONLY | constants11.O_DIRECTORY | constants11.O_NOFOLLOW
13156
+ constants12.O_RDONLY | constants12.O_DIRECTORY | constants12.O_NOFOLLOW
13069
13157
  );
13070
13158
  try {
13071
13159
  if (!sameObject2(identity5, await handle.stat({ bigint: true })))
@@ -13133,11 +13221,11 @@ async function writeSelectionManifest(previous, raw) {
13133
13221
  await flushDirectory2(context2.root, rootIdentity);
13134
13222
  }
13135
13223
  await assertPrevious2(context2, prior);
13136
- const path = join32(context2.agents, `.skillex-tmp-${randomUUID10()}`);
13224
+ const path = join33(context2.agents, `.skillex-tmp-${randomUUID10()}`);
13137
13225
  const mode = prior.file ? Number(prior.file.identity.mode & 0o7777n) : 420;
13138
- const file = await open14(
13226
+ const file = await open15(
13139
13227
  path,
13140
- constants11.O_WRONLY | constants11.O_CREAT | constants11.O_EXCL | constants11.O_NOFOLLOW,
13228
+ constants12.O_WRONLY | constants12.O_CREAT | constants12.O_EXCL | constants12.O_NOFOLLOW,
13141
13229
  mode
13142
13230
  );
13143
13231
  try {
@@ -13249,7 +13337,7 @@ async function directory(value, cwd, home) {
13249
13337
  fail("E_PROJECT_ROOT", "A project or working directory must be a nonempty path.", {
13250
13338
  fix: "Pass an existing directory as --project or cwd."
13251
13339
  });
13252
- const expanded = value === "~" ? home : value.startsWith("~/") ? join33(home, value.slice(2)) : value;
13340
+ const expanded = value === "~" ? home : value.startsWith("~/") ? join34(home, value.slice(2)) : value;
13253
13341
  const path = resolve19(cwd, expanded);
13254
13342
  const info = await inspectPath(path);
13255
13343
  if (!info || !(await stat5(path)).isDirectory())
@@ -13262,7 +13350,7 @@ async function directory(value, cwd, home) {
13262
13350
  async function nearestGitRoot(cwd, home) {
13263
13351
  let current = cwd;
13264
13352
  while (current !== home && current !== parse4(current).root) {
13265
- if (await inspectPath(join33(current, ".git"))) return current;
13353
+ if (await inspectPath(join34(current, ".git"))) return current;
13266
13354
  current = dirname21(current);
13267
13355
  }
13268
13356
  return void 0;
@@ -13429,7 +13517,7 @@ async function prepare3(options, edit) {
13429
13517
  ...options,
13430
13518
  home: target2.home,
13431
13519
  forbiddenRoots: [
13432
- join33(target2.root, ".agents"),
13520
+ join34(target2.root, ".agents"),
13433
13521
  ...target2.scope === "project" ? [target2.root] : []
13434
13522
  ]
13435
13523
  });
@@ -13584,7 +13672,7 @@ async function setInheritance(enabled, options = {}) {
13584
13672
 
13585
13673
  // src/core/sets.ts
13586
13674
  import { readdir as readdir22 } from "node:fs/promises";
13587
- import { join as join34 } from "node:path";
13675
+ import { join as join35 } from "node:path";
13588
13676
  function validateName(name) {
13589
13677
  parseManifest({ sets: [{ name }] }, "set name");
13590
13678
  }
@@ -13601,7 +13689,7 @@ async function listSets(options = {}) {
13601
13689
  const command = "set list";
13602
13690
  try {
13603
13691
  const registry = await discoverRegistry(options);
13604
- const path = join34(registry.root, "sets");
13692
+ const path = join35(registry.root, "sets");
13605
13693
  const sets = [];
13606
13694
  const failures = [];
13607
13695
  if (await inspectPath(path)) {
@@ -13614,7 +13702,7 @@ async function listSets(options = {}) {
13614
13702
  try {
13615
13703
  sets.push(await readSet(registry.root, entry3.name));
13616
13704
  } catch (error) {
13617
- failures.push(collectedError(error, join34(path, entry3.name)));
13705
+ failures.push(collectedError(error, join35(path, entry3.name)));
13618
13706
  }
13619
13707
  }
13620
13708
  }
@@ -13636,7 +13724,7 @@ async function showSet(name, options = {}) {
13636
13724
  async function prepareSet(name, names2, operation, options) {
13637
13725
  validateName(name);
13638
13726
  const registry = await discoverRegistry(options);
13639
- const path = join34(registry.root, "sets", name);
13727
+ const path = join35(registry.root, "sets", name);
13640
13728
  const exists = await inspectPath(path);
13641
13729
  const previous = operation === "create" && !exists ? { kind: "set", name, path, skills: [] } : await readSet(registry.root, name);
13642
13730
  const requested = await requestedSkills(registry.root, names2, path);
@@ -13652,10 +13740,10 @@ async function prepareSet(name, names2, operation, options) {
13652
13740
  const plan2 = await newPlan(registry, composition2, options.dryRun === true);
13653
13741
  await planDirectory(plan2, path);
13654
13742
  for (const skill2 of previous.skills)
13655
- await inspectCanonicalLink(plan2, join34(path, skill2.name), skill2.path);
13743
+ await inspectCanonicalLink(plan2, join35(path, skill2.name), skill2.path);
13656
13744
  for (const [member, target2] of requested) {
13657
- if (operation === "add") await planLink(plan2, join34(path, member), target2);
13658
- else if (operation === "remove") await planUnlink(plan2, join34(path, member), target2);
13745
+ if (operation === "add") await planLink(plan2, join35(path, member), target2);
13746
+ else if (operation === "remove") await planUnlink(plan2, join35(path, member), target2);
13659
13747
  }
13660
13748
  return plan2;
13661
13749
  }
@@ -13671,7 +13759,7 @@ async function removeSetSkills(name, names2, options = {}) {
13671
13759
 
13672
13760
  // src/core/vendor-inspect.ts
13673
13761
  import { readdir as readdir23 } from "node:fs/promises";
13674
- import { join as join35 } from "node:path";
13762
+ import { join as join36 } from "node:path";
13675
13763
 
13676
13764
  // src/core/vendor-git.ts
13677
13765
  import { execFile as execFile4 } from "node:child_process";
@@ -14033,19 +14121,19 @@ var Observation = class {
14033
14121
  async function recordedInventory(manifest, observation, options) {
14034
14122
  const recorded = /* @__PURE__ */ new Map();
14035
14123
  const failed2 = /* @__PURE__ */ new Set();
14036
- const catalog = join35(manifest.registry.root, "all-skills");
14124
+ const catalog = join36(manifest.registry.root, "all-skills");
14037
14125
  for (const entry3 of (await readdir23(catalog, { withFileTypes: true })).sort(
14038
14126
  (a, b) => a.name.localeCompare(b.name)
14039
14127
  )) {
14040
14128
  checkVendorSignal(options);
14041
14129
  if (!isSkillName(entry3.name) || !entry3.isDirectory()) continue;
14042
- const path = join35(catalog, entry3.name);
14130
+ const path = join36(catalog, entry3.name);
14043
14131
  try {
14044
14132
  const receipt2 = await readVendorProvenance(path);
14045
14133
  if (receipt2) recorded.set(entry3.name, receipt2);
14046
14134
  } catch (error) {
14047
14135
  failed2.add(entry3.name);
14048
- observation.capture(error, join35(path, ".source.yaml"));
14136
+ observation.capture(error, join36(path, ".source.yaml"));
14049
14137
  }
14050
14138
  }
14051
14139
  return { recorded, failed: failed2 };
@@ -14062,7 +14150,7 @@ function emptySkill(source, manifest, name, upstreamCommit) {
14062
14150
  return {
14063
14151
  name,
14064
14152
  source: source.name,
14065
- path: join35(manifest.registry.root, "all-skills", name),
14153
+ path: join36(manifest.registry.root, "all-skills", name),
14066
14154
  state: "unknown",
14067
14155
  recordedCommit: null,
14068
14156
  upstreamCommit,
@@ -14443,9 +14531,9 @@ async function inspectVendorStatus(options = {}) {
14443
14531
 
14444
14532
  // src/core/vendor-sync.ts
14445
14533
  import { randomUUID as randomUUID11 } from "node:crypto";
14446
- import { chmod as chmod3, lstat as lstat20, mkdir as mkdir12, open as open15, readdir as readdir24, rename as rename10 } from "node:fs/promises";
14447
- import { dirname as dirname22, join as join36 } from "node:path";
14448
- function refuse3(code, message, path, fix) {
14534
+ import { chmod as chmod3, lstat as lstat20, mkdir as mkdir12, open as open16, readdir as readdir24, rename as rename10 } from "node:fs/promises";
14535
+ import { dirname as dirname22, join as join37 } from "node:path";
14536
+ function refuse4(code, message, path, fix) {
14449
14537
  fail(code, message, { path, fix }, ExitCode.REFUSED);
14450
14538
  }
14451
14539
  function attributed(provenance, source) {
@@ -14455,21 +14543,21 @@ function baseline(provenance) {
14455
14543
  return provenance.digest !== null && (provenance.digestFormat === null || ["skillex-tree-v1", "skillex-tree-v1+symlinks"].includes(provenance.digestFormat));
14456
14544
  }
14457
14545
  async function planSkill(source, skill2, commit, extractedAt, catalog, options) {
14458
- const path = join36(catalog, skill2.name);
14546
+ const path = join37(catalog, skill2.name);
14459
14547
  const info = await vendorLstat(path);
14460
14548
  let before = null;
14461
14549
  let provenance = null;
14462
14550
  let action = "create";
14463
14551
  if (info) {
14464
14552
  if (info.isSymbolicLink())
14465
- refuse3(
14553
+ refuse4(
14466
14554
  "E_VENDOR_DESTINATION_LINK",
14467
14555
  "Vendor sync will not replace a linked canonical destination.",
14468
14556
  path,
14469
14557
  "Use the explicit migrate workflow to account for this link and its consumers before vendoring."
14470
14558
  );
14471
14559
  if (!info.isDirectory())
14472
- refuse3(
14560
+ refuse4(
14473
14561
  "E_VENDOR_DESTINATION_TYPE",
14474
14562
  "The canonical destination is not a real skill directory.",
14475
14563
  path,
@@ -14479,21 +14567,21 @@ async function planSkill(source, skill2, commit, extractedAt, catalog, options)
14479
14567
  const digest = await digestVendorTree(path);
14480
14568
  if (provenance?.type === "vendored") {
14481
14569
  if (!attributed(provenance, source))
14482
- refuse3(
14570
+ refuse4(
14483
14571
  "E_VENDOR_FOREIGN_SOURCE",
14484
14572
  "The canonical skill is attributed to a different source.",
14485
14573
  path,
14486
14574
  "Correct the declaration or migrate the existing provenance explicitly; adopt does not transfer another vendor's ownership."
14487
14575
  );
14488
14576
  if (!baseline(provenance) && !options.discardLocalEdits)
14489
- refuse3(
14577
+ refuse4(
14490
14578
  "E_VENDOR_BASELINE_MISSING",
14491
14579
  "The managed skill has no supported baseline digest proving unchanged content.",
14492
14580
  path,
14493
14581
  "Preserve or commit local content, then explicitly use --discard-local-edits to replace this attributed skill; it cannot be implicitly pruned."
14494
14582
  );
14495
14583
  if (baseline(provenance) && (digest !== provenance.digest || provenance.modifiedLocally) && !options.discardLocalEdits)
14496
- refuse3(
14584
+ refuse4(
14497
14585
  "E_VENDOR_LOCAL_EDITS",
14498
14586
  "The vendored skill has local content or executable-mode edits.",
14499
14587
  path,
@@ -14502,14 +14590,14 @@ async function planSkill(source, skill2, commit, extractedAt, catalog, options)
14502
14590
  action = digest === skill2.digest && !provenance.modifiedLocally && provenance.digest === skill2.digest && provenance.upstreamCommit === commit && provenance.upstreamTree === skill2.tree && provenance.upstreamPath === skill2.upstreamPath && provenance.upstreamVersion === source.version && provenance.extractedAt !== null && baseline(provenance) ? "unchanged" : "update";
14503
14591
  } else {
14504
14592
  if (!options.adopt)
14505
- refuse3(
14593
+ refuse4(
14506
14594
  "E_VENDOR_UNMANAGED",
14507
14595
  "The canonical skill is not managed by this vendor source.",
14508
14596
  path,
14509
14597
  "Use --adopt to record ownership; if bytes or modes differ from upstream, also use --discard-local-edits after preserving them."
14510
14598
  );
14511
14599
  if (digest !== skill2.digest && !options.discardLocalEdits)
14512
- refuse3(
14600
+ refuse4(
14513
14601
  "E_VENDOR_LOCAL_EDITS",
14514
14602
  "Adopting this skill would replace local bytes or executable modes.",
14515
14603
  path,
@@ -14519,14 +14607,14 @@ async function planSkill(source, skill2, commit, extractedAt, catalog, options)
14519
14607
  }
14520
14608
  before = await captureVendorTree(path);
14521
14609
  if (before.root.dev !== String((await lstat20(catalog, { bigint: true })).dev))
14522
- refuse3(
14610
+ refuse4(
14523
14611
  "E_VENDOR_CROSS_DEVICE",
14524
14612
  "The existing skill is mounted on a different filesystem from catalog staging.",
14525
14613
  path,
14526
14614
  "Move the skill into the catalog filesystem before updating it; vendor replacement requires same-filesystem renames."
14527
14615
  );
14528
14616
  if (before.root.dev !== String(info.dev) || before.root.ino !== String(info.ino))
14529
- refuse3(
14617
+ refuse4(
14530
14618
  "E_VENDOR_CHANGED",
14531
14619
  "The destination changed during preflight.",
14532
14620
  path,
@@ -14565,7 +14653,7 @@ async function planSkill(source, skill2, commit, extractedAt, catalog, options)
14565
14653
  async function prepare4(options, extractedAt) {
14566
14654
  vendorInterrupted(options);
14567
14655
  const manifest = await readVendorSources(options);
14568
- const catalog = join36(manifest.registry.root, "all-skills");
14656
+ const catalog = join37(manifest.registry.root, "all-skills");
14569
14657
  const journal = await readVendorJournal(manifest.registry, options);
14570
14658
  const selected = selectVendorSources(manifest, options.sources);
14571
14659
  const sources = [];
@@ -14577,7 +14665,7 @@ async function prepare4(options, extractedAt) {
14577
14665
  const checkout = await resolveVendorCheckout(source, options);
14578
14666
  if (!checkout.root) {
14579
14667
  if (!source.optional)
14580
- refuse3(
14668
+ refuse4(
14581
14669
  "E_SOURCE_CHECKOUT_MISSING",
14582
14670
  `Local checkout ${source.checkout} is unavailable.`,
14583
14671
  manifest.path,
@@ -14630,7 +14718,7 @@ async function prepare4(options, extractedAt) {
14630
14718
  });
14631
14719
  for (const skill2 of upstream.skills) {
14632
14720
  if (names2.has(skill2.name))
14633
- refuse3(
14721
+ refuse4(
14634
14722
  "E_VENDOR_DUPLICATE_SKILL",
14635
14723
  `Sources ${names2.get(skill2.name)} and ${source.name} select the same canonical skill.`,
14636
14724
  manifest.path,
@@ -14643,7 +14731,7 @@ async function prepare4(options, extractedAt) {
14643
14731
  if (options.prune) {
14644
14732
  for (const name of (await readdir24(catalog)).filter(isSkillName).sort()) {
14645
14733
  if (names2.has(name)) continue;
14646
- const path = join36(catalog, name);
14734
+ const path = join37(catalog, name);
14647
14735
  if (!(await vendorLstat(path))?.isDirectory()) continue;
14648
14736
  let provenance;
14649
14737
  try {
@@ -14663,14 +14751,14 @@ async function prepare4(options, extractedAt) {
14663
14751
  const source = provenance?.source ? enumerated.get(provenance.source) : void 0;
14664
14752
  if (!provenance || !source || !attributed(provenance, source)) continue;
14665
14753
  if (!baseline(provenance))
14666
- refuse3(
14754
+ refuse4(
14667
14755
  "E_VENDOR_BASELINE_MISSING",
14668
14756
  "An orphaned managed skill has no supported baseline proving it is safe to prune.",
14669
14757
  path,
14670
14758
  "Restore its verified provenance or remove it explicitly after inspection; --discard-local-edits never authorizes pruning unknown content."
14671
14759
  );
14672
14760
  if (provenance.modifiedLocally || await digestVendorTree(path) !== provenance.digest)
14673
- refuse3(
14761
+ refuse4(
14674
14762
  "E_VENDOR_LOCAL_EDITS",
14675
14763
  "An orphaned managed skill has local edits and cannot be pruned.",
14676
14764
  path,
@@ -14704,7 +14792,7 @@ async function prepare4(options, extractedAt) {
14704
14792
  }
14705
14793
  async function absent2(path) {
14706
14794
  if (await vendorLstat(path))
14707
- refuse3(
14795
+ refuse4(
14708
14796
  "E_VENDOR_CHANGED",
14709
14797
  "A destination or recovery path appeared after preflight.",
14710
14798
  path,
@@ -14716,7 +14804,7 @@ async function assertCurrentIntent(prepared, options) {
14716
14804
  const selected = selectVendorSources(current, options.sources);
14717
14805
  const expected = selectVendorSources(prepared.manifest, options.sources);
14718
14806
  if (current.registry.root !== prepared.manifest.registry.root || JSON.stringify(selected) !== JSON.stringify(expected))
14719
- refuse3(
14807
+ refuse4(
14720
14808
  "E_VENDOR_INTENT_CHANGED",
14721
14809
  "The selected vendor declarations changed during staging.",
14722
14810
  prepared.manifest.path,
@@ -14726,7 +14814,7 @@ async function assertCurrentIntent(prepared, options) {
14726
14814
  const checkout = await resolveVendorCheckout(source, options);
14727
14815
  const previous = prepared.result.sources.find((entry3) => entry3.name === source.name);
14728
14816
  if (!previous || checkout.root !== previous.checkout)
14729
- refuse3(
14817
+ refuse4(
14730
14818
  "E_VENDOR_INTENT_CHANGED",
14731
14819
  "A selected local checkout mapping changed during staging.",
14732
14820
  prepared.manifest.path,
@@ -14735,7 +14823,7 @@ async function assertCurrentIntent(prepared, options) {
14735
14823
  }
14736
14824
  }
14737
14825
  async function syncDirectory2(path) {
14738
- const handle = await open15(path, "r");
14826
+ const handle = await open16(path, "r");
14739
14827
  try {
14740
14828
  await handle.sync();
14741
14829
  } finally {
@@ -14750,12 +14838,12 @@ async function stage(plan2, path, catalog, options) {
14750
14838
  for (const entry3 of plan2.entries) {
14751
14839
  vendorInterrupted(options);
14752
14840
  await assertVendorIdentity(path, root);
14753
- const target2 = join36(path, entry3.path);
14841
+ const target2 = join37(path, entry3.path);
14754
14842
  let parent = dirname22(entry3.path);
14755
14843
  while (parent !== ".") {
14756
14844
  const identity5 = parents2.get(parent);
14757
14845
  if (!identity5) throw new Error(`Unprepared vendor content parent: ${parent}`);
14758
- await assertVendorIdentity(join36(path, parent), identity5);
14846
+ await assertVendorIdentity(join37(path, parent), identity5);
14759
14847
  parent = dirname22(parent);
14760
14848
  }
14761
14849
  if (entry3.kind === "directory") {
@@ -14763,7 +14851,7 @@ async function stage(plan2, path, catalog, options) {
14763
14851
  await chmod3(target2, 493);
14764
14852
  parents2.set(entry3.path, vendorIdentity(await lstat20(target2, { bigint: true })));
14765
14853
  } else if (entry3.kind === "file") {
14766
- const handle = await open15(target2, "wx", entry3.mode);
14854
+ const handle = await open16(target2, "wx", entry3.mode);
14767
14855
  try {
14768
14856
  await handle.writeFile(entry3.bytes);
14769
14857
  await handle.chmod(entry3.mode);
@@ -14812,7 +14900,7 @@ async function recover2(snapshot3, findings, options, onWrite) {
14812
14900
  if (published) {
14813
14901
  await assertVendorTree(operation.change.path, operation.after);
14814
14902
  if (stageInfo)
14815
- refuse3(
14903
+ refuse4(
14816
14904
  "E_VENDOR_RECOVERY_CONFLICT",
14817
14905
  "Both the staged and published paths exist for one prepared skill.",
14818
14906
  operation.change.path,
@@ -14820,7 +14908,7 @@ async function recover2(snapshot3, findings, options, onWrite) {
14820
14908
  );
14821
14909
  } else if (destination) {
14822
14910
  if (!operation.before)
14823
- refuse3(
14911
+ refuse4(
14824
14912
  "E_VENDOR_RECOVERY_CONFLICT",
14825
14913
  "A foreign destination blocks vendor recovery.",
14826
14914
  operation.change.path,
@@ -14828,7 +14916,7 @@ async function recover2(snapshot3, findings, options, onWrite) {
14828
14916
  );
14829
14917
  await assertVendorTree(operation.change.path, operation.before);
14830
14918
  if (parked)
14831
- refuse3(
14919
+ refuse4(
14832
14920
  "E_VENDOR_RECOVERY_CONFLICT",
14833
14921
  "Both the old destination and its parked path exist.",
14834
14922
  operation.change.path,
@@ -14836,7 +14924,7 @@ async function recover2(snapshot3, findings, options, onWrite) {
14836
14924
  );
14837
14925
  } else if (!parked && operation.before) {
14838
14926
  if (operation.change.action !== "prune")
14839
- refuse3(
14927
+ refuse4(
14840
14928
  "E_VENDOR_RECOVERY_CONFLICT",
14841
14929
  "The old and new skill directories are both missing.",
14842
14930
  operation.change.path,
@@ -14909,7 +14997,7 @@ async function syncVendorSources(options = {}) {
14909
14997
  initiallyPending = initialJournal.document !== null;
14910
14998
  fallback = {
14911
14999
  registry,
14912
- manifest: join36(registry.root, "all-skills", "sources.toml"),
15000
+ manifest: join37(registry.root, "all-skills", "sources.toml"),
14913
15001
  dryRun: options.dryRun === true,
14914
15002
  sources: [],
14915
15003
  changes: [],
@@ -14956,7 +15044,7 @@ async function syncVendorSources(options = {}) {
14956
15044
  }
14957
15045
  prepared = await prepare4(options, extractedAt);
14958
15046
  if (prepared.manifest.registry.root !== initialRoot)
14959
- refuse3(
15047
+ refuse4(
14960
15048
  "E_CATALOG_CHANGED",
14961
15049
  "Registry discovery changed while waiting for its catalog lock.",
14962
15050
  initialRoot,
@@ -14978,8 +15066,8 @@ async function syncVendorSources(options = {}) {
14978
15066
  change: plan2.change,
14979
15067
  before: plan2.before,
14980
15068
  after: null,
14981
- stage: plan2.change.action === "prune" ? null : join36(journal.catalog, `.skillex-tmp-vendor-${id}-new`),
14982
- parked: plan2.before ? join36(journal.catalog, `.skillex-tmp-vendor-${id}-old`) : null
15069
+ stage: plan2.change.action === "prune" ? null : join37(journal.catalog, `.skillex-tmp-vendor-${id}-new`),
15070
+ parked: plan2.before ? join37(journal.catalog, `.skillex-tmp-vendor-${id}-old`) : null
14983
15071
  };
14984
15072
  });
14985
15073
  vendorInterrupted(options);
@@ -15511,14 +15599,18 @@ function registerProfileCommands(program2, output2) {
15511
15599
  ).option(
15512
15600
  "--dry-run",
15513
15601
  "Preview links, preserved entries, and receipts without writing or recovering."
15602
+ ).option(
15603
+ "--skillex-only",
15604
+ "Persist strict catalog-only ownership; refuse local skills and external discovery."
15514
15605
  ).action(async (name, _local, command) => {
15515
- const { project, dryRun } = command.opts();
15606
+ const { project, dryRun, skillexOnly } = command.opts();
15516
15607
  await run2(
15517
15608
  command,
15518
15609
  (options) => syncProfile(name, {
15519
15610
  ...options,
15520
15611
  project,
15521
- ...dryRun === void 0 ? {} : { dryRun }
15612
+ ...dryRun === void 0 ? {} : { dryRun },
15613
+ ...skillexOnly === void 0 ? {} : { skillexOnly }
15522
15614
  }),
15523
15615
  syncText
15524
15616
  );