@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 +176 -84
- package/dist/core/profile-policy.d.ts +4 -0
- package/dist/core/profile-types.d.ts +1 -0
- package/dist/index.js +170 -82
- package/package.json +1 -1
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
|
|
12027
|
-
import { mkdir as mkdir10, open as
|
|
12028
|
-
import { join as
|
|
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("~/") ?
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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] : [
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
12419
|
+
const handle = await open14(
|
|
12334
12420
|
path,
|
|
12335
|
-
|
|
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 =
|
|
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 =
|
|
12507
|
-
const parked =
|
|
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(
|
|
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:
|
|
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 ===
|
|
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:
|
|
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
|
|
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
|
|
12878
|
-
import { link as link7, lstat as lstat19, mkdir as mkdir11, open as
|
|
12879
|
-
import { dirname as dirname20, join as
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
13154
|
+
const handle = await open15(
|
|
13067
13155
|
path,
|
|
13068
|
-
|
|
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 =
|
|
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
|
|
13226
|
+
const file = await open15(
|
|
13139
13227
|
path,
|
|
13140
|
-
|
|
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("~/") ?
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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,
|
|
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,
|
|
13658
|
-
else if (operation === "remove") await planUnlink(plan2,
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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:
|
|
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
|
|
14447
|
-
import { dirname as dirname22, join as
|
|
14448
|
-
function
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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 :
|
|
14982
|
-
parked: plan2.before ?
|
|
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
|
);
|