agent-skillboard 0.2.17 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +128 -260
- package/bin/postinstall.mjs +2 -2
- package/docs/adapters.md +37 -113
- package/docs/ai-skill-routing-goal.md +35 -109
- package/docs/capabilities.md +17 -104
- package/docs/install.md +39 -493
- package/docs/policy-model.md +50 -280
- package/docs/positioning.md +19 -86
- package/docs/profiles.md +21 -153
- package/docs/reference.md +117 -356
- package/docs/rollout-runbook.md +23 -25
- package/docs/routing.md +23 -90
- package/docs/user-flow.md +60 -292
- package/docs/value-proof.md +23 -181
- package/docs/variant-lifecycle.md +47 -67
- package/docs/versioning.md +31 -264
- package/examples/v2-multi-source.config.yaml +35 -0
- package/examples/v2-policy-error.config.yaml +6 -0
- package/package.json +1 -1
- package/src/advisor/actions.mjs +102 -6
- package/src/advisor/application-commands.mjs +10 -9
- package/src/advisor/apply-action.mjs +74 -1
- package/src/advisor/guidance.mjs +24 -16
- package/src/advisor/schema.mjs +17 -6
- package/src/advisor/skills.mjs +18 -5
- package/src/advisor.mjs +27 -9
- package/src/agent-integration-cli.mjs +13 -4
- package/src/agent-integration-content.mjs +22 -12
- package/src/agent-integration-files.mjs +1 -1
- package/src/agent-inventory-platforms.mjs +10 -0
- package/src/agent-inventory.mjs +23 -1
- package/src/agent-skill-import.mjs +2 -2
- package/src/audit-paths.mjs +42 -0
- package/src/brief-cli.mjs +3 -2
- package/src/brief-renderer.mjs +1 -0
- package/src/cli.mjs +398 -127
- package/src/compatibility.mjs +24 -0
- package/src/control/can-use-guard.mjs +21 -1
- package/src/control/config-write.mjs +32 -2
- package/src/control/skill-crud.mjs +5 -0
- package/src/control/v2-guard.mjs +175 -0
- package/src/control/v2-skill-crud.mjs +32 -0
- package/src/control/v2-skill-forget.mjs +38 -0
- package/src/control/variant-status.mjs +47 -1
- package/src/control.mjs +55 -0
- package/src/doctor.mjs +65 -6
- package/src/domain/v2-policy.mjs +111 -0
- package/src/hook-plan.mjs +33 -3
- package/src/impact.mjs +52 -29
- package/src/index.mjs +25 -1
- package/src/init.mjs +50 -34
- package/src/inventory-install-units.mjs +63 -0
- package/src/inventory-json.mjs +279 -0
- package/src/inventory-refresh.mjs +163 -18
- package/src/lifecycle-cli.mjs +40 -12
- package/src/lifecycle-content.mjs +52 -67
- package/src/migration/v1-to-v2.mjs +212 -0
- package/src/migration/v2-files.mjs +211 -0
- package/src/migration/v2-journal.mjs +169 -0
- package/src/migration/v2-projection.mjs +108 -0
- package/src/migration/v2-transaction.mjs +205 -0
- package/src/policy.mjs +3 -0
- package/src/reconcile.mjs +139 -111
- package/src/report.mjs +168 -148
- package/src/review.mjs +2 -0
- package/src/route-advisory.mjs +47 -2
- package/src/route-selection.mjs +38 -2
- package/src/route.mjs +62 -2
- package/src/shared-skill.mjs +301 -0
- package/src/source-digest.mjs +42 -0
- package/src/source-profiles.mjs +27 -0
- package/src/source-verification.mjs +32 -48
- package/src/uninstall.mjs +22 -0
- package/src/user-state-paths.mjs +19 -0
- package/src/user-uninstall.mjs +146 -0
- package/src/workspace.mjs +41 -1
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { lstat, readFile,
|
|
2
|
-
import {
|
|
1
|
+
import { lstat, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { verify as verifySignature } from "node:crypto";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
5
|
import YAML from "yaml";
|
|
6
|
+
import { auditPath, expandPortablePath, portableObservation, redactPathError } from "./audit-paths.mjs";
|
|
6
7
|
import { installUnitSourceClass, isLocalSourceReference } from "./domain/source-classes.mjs";
|
|
8
|
+
import { V1_MUTATION_ERROR } from "./compatibility.mjs";
|
|
9
|
+
import { skillContentDigest, sourceDigest } from "./source-digest.mjs";
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
export { skillContentDigest, sourceDigest };
|
|
9
12
|
|
|
10
13
|
export async function verifySources(workspace, options = {}) {
|
|
11
14
|
const configDir = options.configPath === undefined ? process.cwd() : dirname(resolve(options.configPath));
|
|
@@ -24,6 +27,7 @@ export async function verifySources(workspace, options = {}) {
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export async function writeLockfile(workspace, options) {
|
|
30
|
+
if (workspace.version === 1) throw new Error(V1_MUTATION_ERROR);
|
|
27
31
|
const verified = await verifySources(workspace, { configPath: options.configPath, rootDir: options.rootDir });
|
|
28
32
|
if (!verified.ok && options.allowUnverified !== true) {
|
|
29
33
|
throw new Error(`Cannot write lockfile because source verification failed:\n${verified.errors.join("\n")}`);
|
|
@@ -34,6 +38,7 @@ export async function writeLockfile(workspace, options) {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
export async function renderLockfile(workspace, options = {}) {
|
|
41
|
+
if (workspace.version === 2) return renderV2Lockfile(workspace, options);
|
|
37
42
|
const verified = options.verified ?? await verifySources(workspace, { configPath: options.configPath, rootDir: options.rootDir });
|
|
38
43
|
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
39
44
|
const skillDigests = await skillContentDigests(workspace, options.skillsRoot);
|
|
@@ -76,6 +81,20 @@ export async function renderLockfile(workspace, options = {}) {
|
|
|
76
81
|
});
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
async function renderV2Lockfile(workspace, options) {
|
|
85
|
+
const generatedAt = options.generatedAt ?? new Date().toISOString();
|
|
86
|
+
return YAML.stringify({
|
|
87
|
+
version: 2,
|
|
88
|
+
policy_projection_version: 2,
|
|
89
|
+
generated_at: generatedAt,
|
|
90
|
+
audit_authoritative_for_availability: false,
|
|
91
|
+
skills: Object.fromEntries(workspace.skills.map((skill) => [skill.id, {
|
|
92
|
+
enabled: skill.enabled,
|
|
93
|
+
shared: skill.shared
|
|
94
|
+
}]))
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
79
98
|
async function verifyInstallUnit(unit, options) {
|
|
80
99
|
const target = await verificationTarget(unit, options);
|
|
81
100
|
const localPath = target.path;
|
|
@@ -87,7 +106,7 @@ async function verifyInstallUnit(unit, options) {
|
|
|
87
106
|
|
|
88
107
|
if (localPath !== null && options.restrictToRoot === true && !isPathInside(options.rootDir, localPath)) {
|
|
89
108
|
status = "unverified";
|
|
90
|
-
findings.push({ severity: "error", message: `${target.field} is outside the allowed root: ${localPath}` });
|
|
109
|
+
findings.push({ severity: "error", message: `${target.field} is outside the allowed root: ${auditPath(localPath, options)}` });
|
|
91
110
|
} else if (localPath === null) {
|
|
92
111
|
if (unit.sourceDigest === undefined) {
|
|
93
112
|
findings.push({ severity: "warning", message: "remote or command source has no source_digest pin" });
|
|
@@ -117,16 +136,16 @@ async function verifyInstallUnit(unit, options) {
|
|
|
117
136
|
}
|
|
118
137
|
} catch (error) {
|
|
119
138
|
status = "unverified";
|
|
120
|
-
findings.push({ severity: "error", message: `cannot verify local source: ${error
|
|
139
|
+
findings.push({ severity: "error", message: `cannot verify local source: ${redactPathError(error, options)}` });
|
|
121
140
|
}
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
return {
|
|
125
144
|
id: unit.id,
|
|
126
145
|
kind: unit.kind,
|
|
127
|
-
source: unit.source,
|
|
128
|
-
cachePath: unit.cachePath.length === 0 ? null : unit.cachePath,
|
|
129
|
-
verifiedPath: localPath,
|
|
146
|
+
source: portableObservation(unit.source, options),
|
|
147
|
+
cachePath: unit.cachePath.length === 0 ? null : portableObservation(unit.cachePath, options),
|
|
148
|
+
verifiedPath: localPath === null ? null : auditPath(localPath, options),
|
|
130
149
|
verifiedField: target.field,
|
|
131
150
|
sourceClass: installUnitSourceClass(unit),
|
|
132
151
|
trustLevel: unit.trustLevel,
|
|
@@ -156,39 +175,6 @@ async function verificationTarget(unit, options) {
|
|
|
156
175
|
return { path: await localSourcePath(unit.source, options, { allowBareRelative: false }), field: "source" };
|
|
157
176
|
}
|
|
158
177
|
|
|
159
|
-
export async function sourceDigest(path) {
|
|
160
|
-
const hash = createHash("sha256");
|
|
161
|
-
hash.update("skillboard-source-digest-v1\n");
|
|
162
|
-
await addPathDigest(hash, path, path);
|
|
163
|
-
return `${DIGEST_PREFIX}${hash.digest("hex")}`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function addPathDigest(hash, root, path) {
|
|
167
|
-
const stats = await lstat(path);
|
|
168
|
-
const rel = relative(root, path).replace(/\\/g, "/") || ".";
|
|
169
|
-
if (stats.isSymbolicLink()) {
|
|
170
|
-
hash.update(`symlink\0${rel}\0${await readlink(path)}\n`);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (stats.isDirectory()) {
|
|
174
|
-
hash.update(`dir\0${rel}\n`);
|
|
175
|
-
const entries = (await readdir(path, { withFileTypes: true }))
|
|
176
|
-
.filter((entry) => entry.name !== ".git" && entry.name !== "node_modules")
|
|
177
|
-
.sort((left, right) => left.name.localeCompare(right.name));
|
|
178
|
-
for (const entry of entries) {
|
|
179
|
-
await addPathDigest(hash, root, join(path, entry.name));
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (!stats.isFile()) {
|
|
184
|
-
hash.update(`other\0${rel}\n`);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
hash.update(`file\0${rel}\0${stats.size}\0`);
|
|
188
|
-
hash.update(await readFile(path));
|
|
189
|
-
hash.update("\n");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
178
|
function verifyDigestSignature(digest, signature, publicKey) {
|
|
193
179
|
try {
|
|
194
180
|
return verifySignature("sha256", Buffer.from(digest), publicKey, Buffer.from(signature, "base64"));
|
|
@@ -198,7 +184,10 @@ function verifyDigestSignature(digest, signature, publicKey) {
|
|
|
198
184
|
}
|
|
199
185
|
|
|
200
186
|
async function localSourcePath(source, paths, options) {
|
|
201
|
-
const value = source.trim();
|
|
187
|
+
const value = expandPortablePath(source.trim(), paths);
|
|
188
|
+
if (value === null) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
202
191
|
if (!isLocalSourceReference(value, options)) {
|
|
203
192
|
return null;
|
|
204
193
|
}
|
|
@@ -215,6 +204,7 @@ async function localSourcePath(source, paths, options) {
|
|
|
215
204
|
return await firstExistingPath(candidates) ?? candidates[0] ?? null;
|
|
216
205
|
}
|
|
217
206
|
|
|
207
|
+
|
|
218
208
|
async function firstExistingPath(paths) {
|
|
219
209
|
for (const path of paths) {
|
|
220
210
|
try {
|
|
@@ -233,12 +223,6 @@ function uniquePaths(paths) {
|
|
|
233
223
|
return [...new Set(paths)];
|
|
234
224
|
}
|
|
235
225
|
|
|
236
|
-
export async function skillContentDigest(skillFilePath) {
|
|
237
|
-
const hash = createHash("sha256");
|
|
238
|
-
hash.update(await readFile(skillFilePath));
|
|
239
|
-
return `${DIGEST_PREFIX}${hash.digest("hex")}`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
226
|
async function skillContentDigests(workspace, skillsRoot) {
|
|
243
227
|
const digests = new Map();
|
|
244
228
|
if (skillsRoot === undefined) {
|
package/src/uninstall.mjs
CHANGED
|
@@ -54,6 +54,12 @@ export async function uninstallProject(options) {
|
|
|
54
54
|
}
|
|
55
55
|
recordFileResult(entry.label, result, { removed, updated, preserved });
|
|
56
56
|
}
|
|
57
|
+
if (!keepBridge) {
|
|
58
|
+
const inventoryPath = join(root, ".skillboard", "inventory.json");
|
|
59
|
+
const inventoryResult = await removeGeneratedInventory(inventoryPath, dryRun, root);
|
|
60
|
+
if (inventoryResult === "removed") plannedRemovedPaths.add(inventoryPath);
|
|
61
|
+
recordFileResult(".skillboard/inventory.json", inventoryResult, { removed, updated, preserved });
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
if (options.removeReports === true && options.removeProjectState !== true) {
|
|
@@ -200,6 +206,22 @@ async function removeGeneratedFile(path, expected, dryRun, root = null) {
|
|
|
200
206
|
return "removed";
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
async function removeGeneratedInventory(path, dryRun, root) {
|
|
210
|
+
if (await hasUnsafeAncestor(path, root)) return "preserved";
|
|
211
|
+
const stats = await pathStats(path);
|
|
212
|
+
if (stats === null) return "absent";
|
|
213
|
+
if (stats.isSymbolicLink() || !stats.isFile()) return "preserved";
|
|
214
|
+
let inventory;
|
|
215
|
+
try {
|
|
216
|
+
inventory = JSON.parse(await readFile(path, "utf8"));
|
|
217
|
+
} catch {
|
|
218
|
+
return "preserved";
|
|
219
|
+
}
|
|
220
|
+
if (inventory?.generated !== true || inventory?.authoritative_for_availability !== false) return "preserved";
|
|
221
|
+
if (!dryRun) await rm(path);
|
|
222
|
+
return "removed";
|
|
223
|
+
}
|
|
224
|
+
|
|
203
225
|
async function removeConfigFile(path, dryRun) {
|
|
204
226
|
const stats = await pathStats(path);
|
|
205
227
|
if (stats === null) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function resolveUserStatePaths(options = {}) {
|
|
5
|
+
const home = resolve(options.home ?? options.env?.HOME ?? options.env?.USERPROFILE ?? homedir());
|
|
6
|
+
const configPath = options.configPath === undefined
|
|
7
|
+
? join(home, "skillboard.config.yaml")
|
|
8
|
+
: resolveFrom(options.configPath, options.cwd ?? process.cwd());
|
|
9
|
+
const inventoryPath = options.inventoryPath === undefined
|
|
10
|
+
? options.configPath === undefined
|
|
11
|
+
? join(home, ".skillboard", "inventory.json")
|
|
12
|
+
: join(dirname(configPath), ".skillboard", "inventory.json")
|
|
13
|
+
: resolveFrom(options.inventoryPath, options.cwd ?? process.cwd());
|
|
14
|
+
return { home, configPath, inventoryPath, stateRoot: dirname(inventoryPath) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveFrom(path, cwd) {
|
|
18
|
+
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
19
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { uninstallAgentIntegration } from "./agent-integration-files.mjs";
|
|
4
|
+
import { resolveSetupHome } from "./agent-integration-home.mjs";
|
|
5
|
+
import {
|
|
6
|
+
agentSkillRootCandidates,
|
|
7
|
+
setupAgentSkillTargets,
|
|
8
|
+
supportedAgentNames
|
|
9
|
+
} from "./agent-skill-roots.mjs";
|
|
10
|
+
|
|
11
|
+
const SHARE_MARKER = ".skillboard-share.json";
|
|
12
|
+
|
|
13
|
+
export async function uninstallUser(options = {}) {
|
|
14
|
+
const env = options.env ?? process.env;
|
|
15
|
+
const home = await resolveSetupHome(env, options.runtime ?? {});
|
|
16
|
+
const planned = await planUserUninstall(home, env);
|
|
17
|
+
if (options.dryRun) {
|
|
18
|
+
const guidance = await uninstallAgentIntegration(planned.guidanceTargets, true);
|
|
19
|
+
return result(home, true, planned.managedCopies, guidance, planned.statePaths, planned.preserved);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const removedCopies = [];
|
|
23
|
+
const preserved = [...planned.preserved];
|
|
24
|
+
for (const copy of planned.managedCopies) {
|
|
25
|
+
if (await removeManagedCopy(copy)) {
|
|
26
|
+
removedCopies.push(copy);
|
|
27
|
+
} else {
|
|
28
|
+
preserved.push(copy.path);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const guidance = await uninstallAgentIntegration(planned.guidanceTargets, false);
|
|
32
|
+
const removedState = [];
|
|
33
|
+
for (const state of planned.statePaths) {
|
|
34
|
+
if (await removeStatePath(state)) removedState.push(state);
|
|
35
|
+
else preserved.push(state);
|
|
36
|
+
}
|
|
37
|
+
return result(home, false, removedCopies, guidance, removedState, preserved);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function planUserUninstall(home, env) {
|
|
41
|
+
const preserved = [];
|
|
42
|
+
const roots = [{ root: join(home, ".agents", "shared-skills"), agent: null }];
|
|
43
|
+
for (const agent of supportedAgentNames()) {
|
|
44
|
+
for (const candidate of await agentSkillRootCandidates(agent, home, env)) {
|
|
45
|
+
roots.push({ root: candidate.skillRoot, agent });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const managedCopies = [];
|
|
49
|
+
const seenRoots = new Set();
|
|
50
|
+
for (const entry of roots) {
|
|
51
|
+
const root = resolve(entry.root);
|
|
52
|
+
if (seenRoots.has(root)) continue;
|
|
53
|
+
seenRoots.add(root);
|
|
54
|
+
managedCopies.push(...await managedCopiesInRoot(root, entry.agent, preserved));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const guidanceTargets = [];
|
|
58
|
+
for (const agent of supportedAgentNames()) {
|
|
59
|
+
guidanceTargets.push(...await setupAgentSkillTargets(agent, home, env));
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
managedCopies: managedCopies.sort((left, right) => left.path.localeCompare(right.path)),
|
|
63
|
+
guidanceTargets,
|
|
64
|
+
statePaths: await existingStatePaths(home),
|
|
65
|
+
preserved: [...new Set(preserved)].sort()
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function managedCopiesInRoot(root, agent, preserved) {
|
|
70
|
+
const stats = await pathStats(root);
|
|
71
|
+
if (stats === null) return [];
|
|
72
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
73
|
+
preserved.push(root);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const copies = [];
|
|
77
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
80
|
+
const path = join(root, entry.name);
|
|
81
|
+
const marker = await readManagedMarker(path, entry.name, agent);
|
|
82
|
+
if (marker !== null) copies.push({ path, skill: entry.name, agent, mode: marker.mode });
|
|
83
|
+
}
|
|
84
|
+
return copies;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readManagedMarker(path, skill, agent) {
|
|
88
|
+
const markerPath = join(path, SHARE_MARKER);
|
|
89
|
+
const stats = await pathStats(markerPath);
|
|
90
|
+
if (stats === null || stats.isSymbolicLink() || !stats.isFile()) return null;
|
|
91
|
+
const value = await readFile(markerPath, "utf8").then(JSON.parse, () => null);
|
|
92
|
+
if (value?.version !== 1 || value.managed_by !== "skillboard" || value.skill !== skill) return null;
|
|
93
|
+
if (agent === null && value.mode === "shared-source") return value;
|
|
94
|
+
if (agent !== null && value.mode === "agent-copy" && value.target_agent === agent) return value;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function removeManagedCopy(copy) {
|
|
99
|
+
const stats = await pathStats(copy.path);
|
|
100
|
+
if (stats === null) return true;
|
|
101
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) return false;
|
|
102
|
+
const marker = await readManagedMarker(copy.path, copy.skill, copy.agent);
|
|
103
|
+
if (marker === null || marker.mode !== copy.mode) return false;
|
|
104
|
+
await rm(copy.path, { recursive: true, force: true });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function existingStatePaths(home) {
|
|
109
|
+
const result = [];
|
|
110
|
+
for (const path of [join(home, "skillboard.config.yaml"), join(home, ".skillboard")]) {
|
|
111
|
+
if (await pathStats(path) !== null) result.push(path);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function removeStatePath(path) {
|
|
117
|
+
const stats = await pathStats(path);
|
|
118
|
+
if (stats === null) return true;
|
|
119
|
+
const name = basename(path);
|
|
120
|
+
const supported = name === "skillboard.config.yaml"
|
|
121
|
+
? stats.isFile() || stats.isSymbolicLink()
|
|
122
|
+
: name === ".skillboard" && (stats.isDirectory() || stats.isSymbolicLink());
|
|
123
|
+
if (!supported) return false;
|
|
124
|
+
await rm(path, { recursive: stats.isDirectory(), force: true });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function result(home, dryRun, managedCopies, guidance, statePaths, preserved) {
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
mode: "user",
|
|
132
|
+
dry_run: dryRun,
|
|
133
|
+
home,
|
|
134
|
+
managed_copies: managedCopies,
|
|
135
|
+
guidance,
|
|
136
|
+
state_paths: statePaths,
|
|
137
|
+
preserved: [...new Set([...preserved, ...guidance.preserved])].sort()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function pathStats(path) {
|
|
142
|
+
return await lstat(path).catch((error) => {
|
|
143
|
+
if (error?.code === "ENOENT") return null;
|
|
144
|
+
throw error;
|
|
145
|
+
});
|
|
146
|
+
}
|
package/src/workspace.mjs
CHANGED
|
@@ -19,16 +19,26 @@ import {
|
|
|
19
19
|
} from "./domain/constants.mjs";
|
|
20
20
|
import { parseInstallUnits } from "./install-units.mjs";
|
|
21
21
|
import { normalizeSkillPath } from "./skill-paths.mjs";
|
|
22
|
+
import { parseV2Policy, serializeV2Policy } from "./domain/v2-policy.mjs";
|
|
23
|
+
import { loadV2InventoryIndex } from "./control/v2-guard.mjs";
|
|
24
|
+
import { compatibilityForVersion } from "./compatibility.mjs";
|
|
22
25
|
|
|
26
|
+
export { serializeV2Policy };
|
|
27
|
+
|
|
28
|
+
/** @returns {Promise<any>} */
|
|
23
29
|
export async function loadWorkspace(options) {
|
|
24
30
|
const configText = await readFile(options.configPath, "utf8");
|
|
25
31
|
const parsed = YAML.parse(configText);
|
|
26
32
|
const config = requireRecord(parsed, "config root");
|
|
27
33
|
const version = parseVersion(config.version);
|
|
34
|
+
if (version === 2) {
|
|
35
|
+
return loadV2Workspace(config, options);
|
|
36
|
+
}
|
|
28
37
|
const skills = parseSkills(config.skills);
|
|
29
38
|
const installUnits = parseInstallUnits(config.install_units);
|
|
30
39
|
return {
|
|
31
40
|
version,
|
|
41
|
+
compatibility: compatibilityForVersion(version),
|
|
32
42
|
defaults: parseDefaults(config.defaults),
|
|
33
43
|
installedSkills: await discoverInstalledSkills(options.skillsRoot, skills, {
|
|
34
44
|
configPath: options.configPath,
|
|
@@ -44,6 +54,29 @@ export async function loadWorkspace(options) {
|
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
async function loadV2Workspace(config, options) {
|
|
58
|
+
const { workflows, skills } = parseV2Policy(config);
|
|
59
|
+
const inventoryPath = options.inventoryPath ?? join(dirname(options.configPath), ".skillboard", "inventory.json");
|
|
60
|
+
const inventory = await loadV2InventoryIndex(inventoryPath);
|
|
61
|
+
return {
|
|
62
|
+
version: 2,
|
|
63
|
+
compatibility: null,
|
|
64
|
+
defaults: {},
|
|
65
|
+
installedSkills: await discoverInstalledSkills(options.skillsRoot, skills, {
|
|
66
|
+
configPath: options.configPath,
|
|
67
|
+
env: options.env ?? process.env,
|
|
68
|
+
home: options.home,
|
|
69
|
+
installUnits: []
|
|
70
|
+
}),
|
|
71
|
+
skills,
|
|
72
|
+
capabilities: [],
|
|
73
|
+
harnesses: [],
|
|
74
|
+
installUnits: inventory.installUnits ?? [],
|
|
75
|
+
workflows,
|
|
76
|
+
inventory
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
47
80
|
async function discoverInstalledSkills(skillsRoot, declaredSkills, options = {}) {
|
|
48
81
|
const installed = [];
|
|
49
82
|
const installedKeys = new Set();
|
|
@@ -175,7 +208,7 @@ function parseVersion(value) {
|
|
|
175
208
|
if (value === undefined) {
|
|
176
209
|
return 1;
|
|
177
210
|
}
|
|
178
|
-
if (value !== 1) {
|
|
211
|
+
if (value !== 1 && value !== 2) {
|
|
179
212
|
throw new Error(`Unsupported config version: ${value}`);
|
|
180
213
|
}
|
|
181
214
|
return value;
|
|
@@ -194,6 +227,13 @@ function parseSkills(value) {
|
|
|
194
227
|
const raw = requireRecord(value ?? {}, "skills");
|
|
195
228
|
return Object.entries(raw).map(([id, entry]) => {
|
|
196
229
|
const skill = requireRecord(entry, `skills.${id}`);
|
|
230
|
+
const v2Keys = ["enabled", "shared", "preference"].filter((key) => Object.prototype.hasOwnProperty.call(skill, key));
|
|
231
|
+
if (v2Keys.length > 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`skills.${id} uses version 2 key ${v2Keys.join(", ")} in a version 1 config. ` +
|
|
234
|
+
"Run `skillboard migrate v2` to convert the complete policy."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
197
237
|
const status = readString(skill, "status", "vendor");
|
|
198
238
|
const invocation = readString(skill, "invocation", "manual-only");
|
|
199
239
|
const exposure = readString(skill, "exposure", "exported");
|