agent-skillboard 0.2.18 → 0.3.1
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 +85 -0
- package/README.md +161 -255
- package/bin/postinstall.mjs +2 -1
- package/docs/adapters.md +37 -113
- package/docs/ai-skill-routing-goal.md +41 -108
- package/docs/capabilities.md +17 -104
- package/docs/install.md +70 -473
- package/docs/policy-model.md +50 -280
- package/docs/positioning.md +19 -86
- package/docs/profiles.md +21 -153
- package/docs/reference.md +133 -362
- package/docs/rollout-runbook.md +23 -25
- package/docs/routing.md +23 -90
- package/docs/user-flow.md +68 -279
- package/docs/value-proof.md +23 -181
- package/docs/variant-lifecycle.md +47 -67
- package/docs/versioning.md +49 -269
- 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 +96 -13
- package/src/agent-integration-content.mjs +21 -11
- package/src/agent-integration-files.mjs +1 -1
- package/src/agent-integration-home.mjs +14 -1
- package/src/agent-inventory-platforms.mjs +21 -8
- package/src/agent-inventory.mjs +44 -16
- package/src/agent-root-registry.mjs +127 -0
- package/src/agent-skill-import.mjs +2 -2
- package/src/agent-skill-roots.mjs +70 -13
- 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 +521 -235
- 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 +71 -5
- 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/install-health.mjs +177 -0
- package/src/inventory-install-units.mjs +63 -0
- package/src/inventory-json.mjs +279 -0
- package/src/inventory-refresh.mjs +168 -19
- 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-reconcile.mjs +97 -0
- package/src/shared-skill.mjs +325 -0
- package/src/source-digest.mjs +42 -0
- package/src/source-profiles.mjs +171 -144
- 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 +161 -0
- package/src/workspace.mjs +119 -79
package/src/source-profiles.mjs
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
-
import YAML from "yaml";
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import YAML from "yaml";
|
|
4
4
|
import { readString, requireRecord } from "./config-helpers.mjs";
|
|
5
5
|
import { textChangePlan } from "./change-plan.mjs";
|
|
6
6
|
import { loadSourceProfile } from "./source-profile-loader.mjs";
|
|
7
|
-
|
|
8
|
-
export { loadSourceProfile };
|
|
9
|
-
|
|
10
|
-
export async function importSource(options) {
|
|
11
|
-
const sourceRoot = resolve(options.sourceRoot);
|
|
12
|
-
const profile = options.profile;
|
|
13
|
-
const files = await findSkillFiles(sourceRoot);
|
|
14
|
-
const matchedFiles = files
|
|
15
|
-
.map((file) => ({
|
|
16
|
-
file,
|
|
7
|
+
|
|
8
|
+
export { loadSourceProfile };
|
|
9
|
+
|
|
10
|
+
export async function importSource(options) {
|
|
11
|
+
const sourceRoot = resolve(options.sourceRoot);
|
|
12
|
+
const profile = options.profile;
|
|
13
|
+
const files = await findSkillFiles(sourceRoot);
|
|
14
|
+
const matchedFiles = files
|
|
15
|
+
.map((file) => ({
|
|
16
|
+
file,
|
|
17
17
|
relativeFile: relative(sourceRoot, file).replace(/\\/g, "/")
|
|
18
|
-
}))
|
|
19
|
-
.filter((entry) => matchesAnyProfilePattern(entry.relativeFile, profile.skillPaths))
|
|
20
|
-
.sort((left, right) => left.relativeFile.localeCompare(right.relativeFile));
|
|
21
|
-
const skills = [];
|
|
22
|
-
const warnings = [];
|
|
23
|
-
|
|
18
|
+
}))
|
|
19
|
+
.filter((entry) => matchesAnyProfilePattern(entry.relativeFile, profile.skillPaths))
|
|
20
|
+
.sort((left, right) => left.relativeFile.localeCompare(right.relativeFile));
|
|
21
|
+
const skills = [];
|
|
22
|
+
const warnings = [];
|
|
23
|
+
|
|
24
24
|
for (const entry of matchedFiles) {
|
|
25
25
|
const frontmatter = parseSkillFrontmatter(await readFile(entry.file, "utf8"));
|
|
26
26
|
const pathRule = matchingPathRule(entry.relativeFile, profile);
|
|
@@ -30,6 +30,7 @@ export async function importSource(options) {
|
|
|
30
30
|
skills.push({
|
|
31
31
|
id,
|
|
32
32
|
path,
|
|
33
|
+
skillFile: entry.file,
|
|
33
34
|
status: pathRule?.status ?? profile.defaultStatus,
|
|
34
35
|
invocation: safeDefaultInvocation(pathRule?.invocation ?? profile.defaultInvocation),
|
|
35
36
|
exposure: pathRule?.exposure ?? profile.defaultExposure,
|
|
@@ -37,17 +38,17 @@ export async function importSource(options) {
|
|
|
37
38
|
ownerInstallUnit: profile.id,
|
|
38
39
|
description: frontmatter.description
|
|
39
40
|
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (skills.length === 0) {
|
|
43
|
-
warnings.push(`No SKILL.md files matched profile ${profile.id} under ${sourceRoot}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
profile,
|
|
48
|
-
sourceRoot,
|
|
49
|
-
skills,
|
|
50
|
-
installUnit: {
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (skills.length === 0) {
|
|
44
|
+
warnings.push(`No SKILL.md files matched profile ${profile.id} under ${sourceRoot}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
profile,
|
|
49
|
+
sourceRoot,
|
|
50
|
+
skills,
|
|
51
|
+
installUnit: {
|
|
51
52
|
id: profile.id,
|
|
52
53
|
kind: profile.kind,
|
|
53
54
|
sourceClass: profile.sourceClass,
|
|
@@ -58,37 +59,40 @@ export async function importSource(options) {
|
|
|
58
59
|
publicKey: profile.publicKey,
|
|
59
60
|
verifiedAt: profile.verifiedAt,
|
|
60
61
|
source: profile.source || sourceRoot,
|
|
61
|
-
scope: profile.scope,
|
|
62
|
-
manifestPath: profile.manifestPath,
|
|
63
|
-
cachePath: profile.cachePath,
|
|
64
|
-
providedComponents: profile.providedComponents,
|
|
65
|
-
components: {
|
|
66
|
-
skills: skills.map((skill) => skill.id),
|
|
67
|
-
commands: profile.components.commands,
|
|
68
|
-
hooks: profile.components.hooks,
|
|
69
|
-
mcpServers: profile.components.mcpServers
|
|
70
|
-
},
|
|
71
|
-
modifiedConfigFiles: profile.modifiedConfigFiles,
|
|
72
|
-
autoUpdate: profile.autoUpdate,
|
|
73
|
-
enabled: profile.enabled,
|
|
74
|
-
workflowDependencies: profile.workflowDependencies,
|
|
75
|
-
permissionRisk: profile.permissionRisk,
|
|
76
|
-
rollback: profile.rollback
|
|
77
|
-
},
|
|
78
|
-
warnings
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function renderImportFragment(imported) {
|
|
83
|
-
return YAML.stringify(importFragment(imported));
|
|
84
|
-
}
|
|
85
|
-
|
|
62
|
+
scope: profile.scope,
|
|
63
|
+
manifestPath: profile.manifestPath,
|
|
64
|
+
cachePath: profile.cachePath,
|
|
65
|
+
providedComponents: profile.providedComponents,
|
|
66
|
+
components: {
|
|
67
|
+
skills: skills.map((skill) => skill.id),
|
|
68
|
+
commands: profile.components.commands,
|
|
69
|
+
hooks: profile.components.hooks,
|
|
70
|
+
mcpServers: profile.components.mcpServers
|
|
71
|
+
},
|
|
72
|
+
modifiedConfigFiles: profile.modifiedConfigFiles,
|
|
73
|
+
autoUpdate: profile.autoUpdate,
|
|
74
|
+
enabled: profile.enabled,
|
|
75
|
+
workflowDependencies: profile.workflowDependencies,
|
|
76
|
+
permissionRisk: profile.permissionRisk,
|
|
77
|
+
rollback: profile.rollback
|
|
78
|
+
},
|
|
79
|
+
warnings
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function renderImportFragment(imported) {
|
|
84
|
+
return YAML.stringify(importFragment(imported));
|
|
85
|
+
}
|
|
86
|
+
|
|
86
87
|
export function mergeImportFragment(configText, imported, options = {}) {
|
|
87
88
|
const document = YAML.parseDocument(configText);
|
|
88
89
|
if (document.errors.length > 0) {
|
|
89
90
|
throw new Error(`Invalid YAML config: ${document.errors.map((error) => error.message).join("; ")}`);
|
|
90
91
|
}
|
|
91
92
|
requireYamlMap(document.contents, "config root");
|
|
93
|
+
if (document.get("version") === 2) {
|
|
94
|
+
return mergeV2ImportFragment(document, configText, imported);
|
|
95
|
+
}
|
|
92
96
|
const fragment = importFragment(imported);
|
|
93
97
|
const replace = options.replace === true;
|
|
94
98
|
const skillIds = Object.keys(fragment.skills);
|
|
@@ -117,7 +121,30 @@ export function mergeImportFragment(configText, imported, options = {}) {
|
|
|
117
121
|
addedSkills: skillIds,
|
|
118
122
|
addedInstallUnits: unitIds,
|
|
119
123
|
replacedSkills: duplicateSkills,
|
|
120
|
-
replacedInstallUnits: duplicateUnits
|
|
124
|
+
replacedInstallUnits: duplicateUnits
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function mergeV2ImportFragment(document, configText, imported) {
|
|
129
|
+
const existingSkills = ensureMap(document, "skills");
|
|
130
|
+
const addedSkills = [];
|
|
131
|
+
for (const skill of imported.skills) {
|
|
132
|
+
if (existingSkills.get(skill.id, true) !== undefined) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
existingSkills.set(skill.id, document.createNode({ enabled: true, shared: false }));
|
|
136
|
+
addedSkills.push(skill.id);
|
|
137
|
+
}
|
|
138
|
+
const text = preserveLineEndings(String(document), configText);
|
|
139
|
+
const plan = textChangePlan(configText, text);
|
|
140
|
+
return {
|
|
141
|
+
text,
|
|
142
|
+
changed: plan.changed,
|
|
143
|
+
plan,
|
|
144
|
+
addedSkills,
|
|
145
|
+
addedInstallUnits: [],
|
|
146
|
+
replacedSkills: [],
|
|
147
|
+
replacedInstallUnits: []
|
|
121
148
|
};
|
|
122
149
|
}
|
|
123
150
|
|
|
@@ -141,20 +168,20 @@ function requireYamlMap(value, label) {
|
|
|
141
168
|
function preserveLineEndings(text, reference) {
|
|
142
169
|
return reference.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
|
|
143
170
|
}
|
|
144
|
-
|
|
171
|
+
|
|
145
172
|
function importFragment(imported) {
|
|
146
|
-
const skills = {};
|
|
147
|
-
for (const skill of imported.skills) {
|
|
148
|
-
skills[skill.id] = {
|
|
149
|
-
path: skill.path,
|
|
150
|
-
status: skill.status,
|
|
151
|
-
invocation: skill.invocation,
|
|
152
|
-
exposure: skill.exposure,
|
|
153
|
-
category: skill.category,
|
|
154
|
-
owner_install_unit: skill.ownerInstallUnit
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
const unit = imported.installUnit;
|
|
173
|
+
const skills = {};
|
|
174
|
+
for (const skill of imported.skills) {
|
|
175
|
+
skills[skill.id] = {
|
|
176
|
+
path: skill.path,
|
|
177
|
+
status: skill.status,
|
|
178
|
+
invocation: skill.invocation,
|
|
179
|
+
exposure: skill.exposure,
|
|
180
|
+
category: skill.category,
|
|
181
|
+
owner_install_unit: skill.ownerInstallUnit
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const unit = imported.installUnit;
|
|
158
185
|
return stripUndefined({
|
|
159
186
|
skills,
|
|
160
187
|
install_units: {
|
|
@@ -168,64 +195,64 @@ function importFragment(imported) {
|
|
|
168
195
|
public_key: unit.publicKey,
|
|
169
196
|
verified_at: unit.verifiedAt,
|
|
170
197
|
source: unit.source,
|
|
171
|
-
scope: unit.scope,
|
|
172
|
-
manifest_path: unit.manifestPath,
|
|
173
|
-
cache_path: unit.cachePath,
|
|
174
|
-
provided_components: unit.providedComponents,
|
|
175
|
-
components: {
|
|
176
|
-
skills: unit.components.skills,
|
|
177
|
-
commands: unit.components.commands,
|
|
178
|
-
hooks: unit.components.hooks,
|
|
179
|
-
mcp_servers: unit.components.mcpServers
|
|
180
|
-
},
|
|
181
|
-
modified_config_files: unit.modifiedConfigFiles,
|
|
182
|
-
auto_update: unit.autoUpdate,
|
|
183
|
-
enabled: unit.enabled,
|
|
184
|
-
workflow_dependencies: unit.workflowDependencies,
|
|
185
|
-
permission_risk: unit.permissionRisk,
|
|
198
|
+
scope: unit.scope,
|
|
199
|
+
manifest_path: unit.manifestPath,
|
|
200
|
+
cache_path: unit.cachePath,
|
|
201
|
+
provided_components: unit.providedComponents,
|
|
202
|
+
components: {
|
|
203
|
+
skills: unit.components.skills,
|
|
204
|
+
commands: unit.components.commands,
|
|
205
|
+
hooks: unit.components.hooks,
|
|
206
|
+
mcp_servers: unit.components.mcpServers
|
|
207
|
+
},
|
|
208
|
+
modified_config_files: unit.modifiedConfigFiles,
|
|
209
|
+
auto_update: unit.autoUpdate,
|
|
210
|
+
enabled: unit.enabled,
|
|
211
|
+
workflow_dependencies: unit.workflowDependencies,
|
|
212
|
+
permission_risk: unit.permissionRisk,
|
|
186
213
|
rollback: unit.rollback
|
|
187
214
|
}
|
|
188
215
|
}
|
|
189
216
|
});
|
|
190
217
|
}
|
|
191
|
-
|
|
192
|
-
function duplicateMessage(skillIds, unitIds) {
|
|
193
|
-
const parts = [];
|
|
194
|
-
if (skillIds.length > 0) {
|
|
195
|
-
parts.push(`skills already exist: ${skillIds.join(", ")}`);
|
|
196
|
-
}
|
|
197
|
-
if (unitIds.length > 0) {
|
|
198
|
-
parts.push(`install units already exist: ${unitIds.join(", ")}`);
|
|
199
|
-
}
|
|
200
|
-
return `${parts.join("; ")}. Re-run with --replace to overwrite.`;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async function findSkillFiles(root) {
|
|
204
|
-
const files = [];
|
|
205
|
-
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
206
|
-
for (const entry of entries) {
|
|
207
|
-
const path = join(root, entry.name);
|
|
208
|
-
if (entry.isDirectory()) {
|
|
209
|
-
files.push(...(await findSkillFiles(path)));
|
|
210
|
-
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
211
|
-
files.push(path);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return files;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function parseSkillFrontmatter(text) {
|
|
218
|
-
const match = /^---[ \t]*\r?\n(?<body>[\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
|
|
219
|
-
if (match?.groups === undefined) {
|
|
220
|
-
throw new Error("SKILL.md is missing YAML frontmatter");
|
|
221
|
-
}
|
|
222
|
-
const raw = requireRecord(YAML.parse(match.groups.body), "SKILL.md frontmatter");
|
|
223
|
-
return {
|
|
224
|
-
name: readString(raw, "name", ""),
|
|
225
|
-
description: readString(raw, "description", "")
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
|
|
218
|
+
|
|
219
|
+
function duplicateMessage(skillIds, unitIds) {
|
|
220
|
+
const parts = [];
|
|
221
|
+
if (skillIds.length > 0) {
|
|
222
|
+
parts.push(`skills already exist: ${skillIds.join(", ")}`);
|
|
223
|
+
}
|
|
224
|
+
if (unitIds.length > 0) {
|
|
225
|
+
parts.push(`install units already exist: ${unitIds.join(", ")}`);
|
|
226
|
+
}
|
|
227
|
+
return `${parts.join("; ")}. Re-run with --replace to overwrite.`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function findSkillFiles(root) {
|
|
231
|
+
const files = [];
|
|
232
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
233
|
+
for (const entry of entries) {
|
|
234
|
+
const path = join(root, entry.name);
|
|
235
|
+
if (entry.isDirectory()) {
|
|
236
|
+
files.push(...(await findSkillFiles(path)));
|
|
237
|
+
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
238
|
+
files.push(path);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return files;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function parseSkillFrontmatter(text) {
|
|
245
|
+
const match = /^---[ \t]*\r?\n(?<body>[\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
|
|
246
|
+
if (match?.groups === undefined) {
|
|
247
|
+
throw new Error("SKILL.md is missing YAML frontmatter");
|
|
248
|
+
}
|
|
249
|
+
const raw = requireRecord(YAML.parse(match.groups.body), "SKILL.md frontmatter");
|
|
250
|
+
return {
|
|
251
|
+
name: readString(raw, "name", ""),
|
|
252
|
+
description: readString(raw, "description", "")
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
229
256
|
function matchesAnyProfilePattern(file, patterns) {
|
|
230
257
|
const activePatterns = patterns.length === 0 ? ["**/SKILL.md"] : patterns;
|
|
231
258
|
return activePatterns.some((pattern) => patternToRegExp(pattern).test(file));
|
|
@@ -234,16 +261,16 @@ function matchesAnyProfilePattern(file, patterns) {
|
|
|
234
261
|
function matchingPathRule(file, profile) {
|
|
235
262
|
return (profile.pathRules ?? []).find((rule) => patternToRegExp(rule.pattern).test(file));
|
|
236
263
|
}
|
|
237
|
-
|
|
238
|
-
function patternToRegExp(pattern) {
|
|
239
|
-
const escaped = pattern
|
|
240
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
264
|
+
|
|
265
|
+
function patternToRegExp(pattern) {
|
|
266
|
+
const escaped = pattern
|
|
267
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
241
268
|
.split("**").join("\0")
|
|
242
269
|
.split("*").join("[^/]*")
|
|
243
270
|
.split("\0").join(".*");
|
|
244
|
-
return new RegExp(`^${escaped}$`);
|
|
245
|
-
}
|
|
246
|
-
|
|
271
|
+
return new RegExp(`^${escaped}$`);
|
|
272
|
+
}
|
|
273
|
+
|
|
247
274
|
function skillSlug(relativeFile, frontmatter, profile) {
|
|
248
275
|
if (profile.idStrategy === "frontmatter-name" && frontmatter.name.length > 0) {
|
|
249
276
|
const segments = frontmatter.name.split(".");
|
|
@@ -251,14 +278,14 @@ function skillSlug(relativeFile, frontmatter, profile) {
|
|
|
251
278
|
}
|
|
252
279
|
return normalizeSlug(basename(dirname(relativeFile)));
|
|
253
280
|
}
|
|
254
|
-
|
|
255
|
-
function skillId(slug, frontmatter, profile) {
|
|
256
|
-
if (profile.idStrategy === "frontmatter-name" && frontmatter.name.includes(".")) {
|
|
257
|
-
return frontmatter.name;
|
|
258
|
-
}
|
|
259
|
-
return profile.namespace.length === 0 ? slug : `${profile.namespace}.${slug}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
281
|
+
|
|
282
|
+
function skillId(slug, frontmatter, profile) {
|
|
283
|
+
if (profile.idStrategy === "frontmatter-name" && frontmatter.name.includes(".")) {
|
|
284
|
+
return frontmatter.name;
|
|
285
|
+
}
|
|
286
|
+
return profile.namespace.length === 0 ? slug : `${profile.namespace}.${slug}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
262
289
|
function skillTargetPath(slug, profile) {
|
|
263
290
|
return profile.targetPathPrefix.length === 0 ? slug : `${profile.targetPathPrefix}/${slug}`;
|
|
264
291
|
}
|
|
@@ -276,13 +303,13 @@ function categoryFromPath(relativeFile, profile) {
|
|
|
276
303
|
}
|
|
277
304
|
|
|
278
305
|
function normalizeSlug(value) {
|
|
279
|
-
return value
|
|
280
|
-
.trim()
|
|
281
|
-
.toLowerCase()
|
|
282
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
283
|
-
.replace(/^-+|-+$/g, "");
|
|
284
|
-
}
|
|
285
|
-
|
|
306
|
+
return value
|
|
307
|
+
.trim()
|
|
308
|
+
.toLowerCase()
|
|
309
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
310
|
+
.replace(/^-+|-+$/g, "");
|
|
311
|
+
}
|
|
312
|
+
|
|
286
313
|
function safeDefaultInvocation(invocation) {
|
|
287
314
|
return invocation === "global-auto" ? "blocked" : invocation;
|
|
288
315
|
}
|
|
@@ -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
|
+
}
|