@snarkipus/opencode-beads 0.8.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/LICENSE +9 -0
- package/README.md +150 -0
- package/dist/init/artifacts/beads/SKILL.md +16 -0
- package/dist/init/artifacts/beads/references/DEPENDENCIES.md +5 -0
- package/dist/init/artifacts/beads/references/ISSUE_CREATION.md +5 -0
- package/dist/init/artifacts/beads/references/RESUMABILITY.md +5 -0
- package/dist/init/manifest.json +54 -0
- package/package.json +82 -0
- package/src/init-cli.ts +172 -0
- package/src/init.ts +700 -0
- package/src/plugin-core.ts +290 -0
- package/src/plugin.ts +150 -0
- package/src/prime.ts +88 -0
- package/src/skill-collision-policy.ts +133 -0
- package/src/vendor-adaptations.ts +146 -0
- package/src/vendor-provenance.ts +252 -0
- package/src/vendor.ts +297 -0
- package/vendor/agents/task-agent.md +61 -0
- package/vendor/commands/audit.md +28 -0
- package/vendor/commands/blocked.md +15 -0
- package/vendor/commands/close.md +18 -0
- package/vendor/commands/comments.md +30 -0
- package/vendor/commands/compact.md +31 -0
- package/vendor/commands/create.md +19 -0
- package/vendor/commands/decision.md +111 -0
- package/vendor/commands/delete.md +32 -0
- package/vendor/commands/dep.md +101 -0
- package/vendor/commands/epic.md +27 -0
- package/vendor/commands/export.md +25 -0
- package/vendor/commands/import.md +18 -0
- package/vendor/commands/init.md +18 -0
- package/vendor/commands/label.md +33 -0
- package/vendor/commands/list.md +64 -0
- package/vendor/commands/prime.md +10 -0
- package/vendor/commands/quickstart.md +13 -0
- package/vendor/commands/ready.md +15 -0
- package/vendor/commands/rename-prefix.md +24 -0
- package/vendor/commands/reopen.md +22 -0
- package/vendor/commands/restore.md +27 -0
- package/vendor/commands/search.md +103 -0
- package/vendor/commands/show.md +17 -0
- package/vendor/commands/stats.md +17 -0
- package/vendor/commands/sync.md +17 -0
- package/vendor/commands/template.md +337 -0
- package/vendor/commands/update.md +24 -0
- package/vendor/commands/version.md +26 -0
- package/vendor/commands/workflow.md +58 -0
- package/vendor/manifest.json +168 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
interface Replacement {
|
|
2
|
+
from: string;
|
|
3
|
+
to: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const replacements: Readonly<Record<string, readonly Replacement[]>> = {
|
|
7
|
+
"agents/task-agent.md": [
|
|
8
|
+
{ from: "Use the `ready` MCP tool to get unblocked tasks", to: "Run `bd ready --json` via bash to get unblocked tasks" },
|
|
9
|
+
{ from: "Use the `show` tool to get full task details", to: "Run `bd show <id> --json` via bash to get full task details" },
|
|
10
|
+
{ from: "Use the `claim` tool for atomic start-work semantics", to: "Run `bd update <id> --claim --json` via bash for atomic start-work semantics" },
|
|
11
|
+
{ from: "Use `create` tool to file new issues", to: "Run `bd create \"<title>\" ... --json` via bash to file new issues" },
|
|
12
|
+
{ from: "Use `dep` tool with `discovered-from` to link them", to: "Run `bd dep add <new-id> <current-id> --type discovered-from --json` via bash to link them" },
|
|
13
|
+
{ from: "Use `close` tool with a clear completion message", to: "Run `bd close <id> --reason \"<message>\" --json` via bash" },
|
|
14
|
+
{ from: "Always claim before working (MCP: `claim`; CLI: `--claim`) and close when done", to: "Always claim with `bd update <id> --claim --json` before working and close when done" },
|
|
15
|
+
{ from: "If blocked, use `update` to set status to `blocked` and explain why", to: "If blocked, run `bd update <id> --status blocked --json` and explain why" },
|
|
16
|
+
{
|
|
17
|
+
from: `# Available Tools
|
|
18
|
+
|
|
19
|
+
Via beads MCP server:
|
|
20
|
+
- \`ready\` - Find unblocked tasks
|
|
21
|
+
- \`show\` - Get task details
|
|
22
|
+
- \`claim\` - Atomically claim task for work
|
|
23
|
+
- \`update\` - Update task status/fields
|
|
24
|
+
- \`create\` - Create new issues
|
|
25
|
+
- \`dep\` - Manage dependencies
|
|
26
|
+
- \`close\` - Complete tasks
|
|
27
|
+
- \`blocked\` - Check blocked issues
|
|
28
|
+
- \`stats\` - View project stats`,
|
|
29
|
+
to: `# Available Commands
|
|
30
|
+
|
|
31
|
+
Run the \`bd\` CLI via bash. Use \`--json\` when structured output improves reliability:
|
|
32
|
+
- \`bd ready --json\` - Find unblocked tasks
|
|
33
|
+
- \`bd show <id> --json\` - Get task details
|
|
34
|
+
- \`bd update <id> --claim --json\` - Atomically claim task for work
|
|
35
|
+
- \`bd create ... --json\` - Create new issues
|
|
36
|
+
- \`bd dep ... --json\` - Manage dependencies
|
|
37
|
+
- \`bd close <id> --reason \"...\" --json\` - Complete tasks
|
|
38
|
+
- \`bd blocked --json\` - Check blocked issues
|
|
39
|
+
- \`bd stats --json\` - View project stats`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
"commands/close.md": [
|
|
43
|
+
{ from: "Use the beads MCP `close` tool to close the issue.", to: "Run `bd close <id> --reason \"<reason>\" --json` via bash to close the issue." },
|
|
44
|
+
{ from: "(use `ready` tool)", to: "(run `bd ready --json`)" },
|
|
45
|
+
{ from: "(use `create` tool with `discovered-from` link)", to: "(run `bd create ... --deps discovered-from:<id> --json`)" },
|
|
46
|
+
],
|
|
47
|
+
"commands/create.md": [
|
|
48
|
+
{ from: "Use the beads MCP `create` tool to create the issue.", to: "Run `bd create \"<title>\" --type <type> --priority <priority> --description \"<description>\" --json` via bash." },
|
|
49
|
+
],
|
|
50
|
+
"commands/init.md": [
|
|
51
|
+
{ from: "Use the beads MCP `init` tool with the prefix parameter (if provided) to set up a new beads database.", to: "Run `bd init [--prefix <prefix>]` via bash to set up a new beads database." },
|
|
52
|
+
{ from: "show project stats using the `stats` tool", to: "show project stats by running `bd stats --json`" },
|
|
53
|
+
],
|
|
54
|
+
"commands/ready.md": [
|
|
55
|
+
{ from: "Use the beads MCP server to find tasks that are ready to work on (no blocking dependencies).", to: "Run `bd ready --json` via bash to find tasks that are ready to work on (no blocking dependencies)." },
|
|
56
|
+
{ from: "Call the `ready` tool to get a list of unblocked issues.", to: "Use the command output to get a list of unblocked issues." },
|
|
57
|
+
{ from: "use the `claim` tool to start work atomically", to: "run `bd update <id> --claim --json` via bash to start work atomically" },
|
|
58
|
+
{ from: "checking `blocked` issues or creating a new issue with the `create` tool", to: "running `bd blocked --json` or creating a new issue with `bd create ... --json`" },
|
|
59
|
+
],
|
|
60
|
+
"commands/search.md": [
|
|
61
|
+
{ from: "when accessed via MCP", to: "when using structured CLI output" },
|
|
62
|
+
{ from: "You're using an LLM/MCP and want to minimize context usage", to: "You're using an LLM and want to minimize context usage" },
|
|
63
|
+
],
|
|
64
|
+
"commands/show.md": [
|
|
65
|
+
{ from: "Use the beads MCP `show` tool to retrieve issue details", to: "Run `bd show <id> --json` via bash to retrieve issue details" },
|
|
66
|
+
],
|
|
67
|
+
"commands/stats.md": [
|
|
68
|
+
{ from: "Use the beads MCP `stats` tool to retrieve project metrics", to: "Run `bd stats --json` via bash to retrieve project metrics" },
|
|
69
|
+
],
|
|
70
|
+
"commands/update.md": [
|
|
71
|
+
{ from: "Use the beads MCP `update` tool to apply the changes.", to: "Run the appropriate `bd update <id> ... --json` command via bash." },
|
|
72
|
+
],
|
|
73
|
+
"commands/version.md": [
|
|
74
|
+
{
|
|
75
|
+
from: `Check the installed versions of beads components and verify compatibility.
|
|
76
|
+
|
|
77
|
+
**Note:** The MCP server automatically checks bd CLI version >= 0.9.0 on startup. This command provides detailed version info and update instructions.
|
|
78
|
+
|
|
79
|
+
Use the beads MCP tools to:
|
|
80
|
+
1. Run \`bd version\` via bash to get the CLI version
|
|
81
|
+
2. Check the plugin version (0.9.2)
|
|
82
|
+
3. Compare versions and report any mismatches
|
|
83
|
+
|
|
84
|
+
Display:
|
|
85
|
+
- bd CLI version (from \`bd version\`)
|
|
86
|
+
- Plugin version (0.9.2)
|
|
87
|
+
- MCP server version (0.9.2)
|
|
88
|
+
- MCP server status (from \`stats\` tool or connection test)
|
|
89
|
+
- Compatibility status (✓ compatible or ⚠️ update needed)
|
|
90
|
+
|
|
91
|
+
If versions are mismatched, provide instructions:
|
|
92
|
+
- Update bd CLI: \`curl -fsSL https://raw.githubusercontent.com/gastownhall/beads/main/scripts/install.sh | bash\`
|
|
93
|
+
- Update plugin: \`/plugin update beads\`
|
|
94
|
+
- Restart Claude Code after updating
|
|
95
|
+
|
|
96
|
+
Suggest checking for updates if the user is on an older version.`,
|
|
97
|
+
to: `Check the installed Beads and OpenCode versions against this plugin's documented compatibility range.
|
|
98
|
+
|
|
99
|
+
Run \`bd version\` and \`opencode --version\` via bash. Report both versions and point to the opencode-beads README compatibility table. Do not infer compatibility from unrelated integration or upstream plugin versions.
|
|
100
|
+
|
|
101
|
+
If \`bd\` is outdated, link to the current Beads installation guide. If opencode-beads is pinned in the OpenCode configuration, explain that updating requires changing the pinned package version and restarting OpenCode.`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
"commands/workflow.md": [
|
|
105
|
+
{ from: "Use `/beads:ready` or the `ready` MCP tool", to: "Use `/beads:ready` or run `bd ready --json`" },
|
|
106
|
+
{ from: "- Via MCP tool: `claim` with `issue_id: \"<id>\"`", to: "- Via CLI: `bd update <id> --claim --json`" },
|
|
107
|
+
{ from: "- Create issues: `/beads:create` or `create` MCP tool", to: "- Create issues: `/beads:create` or `bd create ... --json`" },
|
|
108
|
+
{ from: "- Link them: Use `dep` MCP tool with `type: \"discovered-from\"`", to: "- Link them: `bd dep add <new-id> <current-id> --type discovered-from --json`" },
|
|
109
|
+
{ from: "- Via MCP tool: `close` with reason", to: "- Via CLI: `bd close <id> --reason \"<summary>\" --json`" },
|
|
110
|
+
{
|
|
111
|
+
from: `## MCP Tools Available
|
|
112
|
+
Use these via the beads MCP server:
|
|
113
|
+
- \`ready\`, \`list\`, \`show\`, \`create\`, \`claim\`, \`update\`, \`close\`
|
|
114
|
+
- \`dep\` (manage dependencies), \`blocked\`, \`stats\`
|
|
115
|
+
- \`init\` (initialize bd in a project)`,
|
|
116
|
+
to: `## CLI Access
|
|
117
|
+
Run the \`bd\` CLI via bash for operations not covered by a slash command. Use \`bd <command> --help\` for current syntax and \`--json\` when structured output improves reliability.`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const unsupportedPattern = /\bmcp\b|claude code|\/plugin update beads/i;
|
|
123
|
+
|
|
124
|
+
/** Paths with explicit OpenCode adaptations, used as the expected inventory. */
|
|
125
|
+
export const adaptedVendorPaths = Object.freeze(Object.keys(replacements).sort());
|
|
126
|
+
|
|
127
|
+
/** Apply strict, reviewable OpenCode adaptations to one upstream prompt body. */
|
|
128
|
+
export function adaptVendorPrompt(relativePath: string, content: string): string {
|
|
129
|
+
let adapted = content;
|
|
130
|
+
|
|
131
|
+
for (const replacement of replacements[relativePath] ?? []) {
|
|
132
|
+
const first = adapted.indexOf(replacement.from);
|
|
133
|
+
const last = adapted.lastIndexOf(replacement.from);
|
|
134
|
+
if (first === -1 || first !== last) {
|
|
135
|
+
throw new Error(`Expected exactly one adaptation source in ${relativePath}: ${replacement.from}`);
|
|
136
|
+
}
|
|
137
|
+
adapted = adapted.replace(replacement.from, replacement.to);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const unsupported = adapted.match(unsupportedPattern)?.[0];
|
|
141
|
+
if (unsupported) {
|
|
142
|
+
throw new Error(`Unsupported ${unsupported} instruction remains in ${relativePath}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return adapted;
|
|
146
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const VENDOR_MANIFEST = "manifest.json";
|
|
6
|
+
export const VENDOR_SOURCES = Object.freeze([
|
|
7
|
+
{
|
|
8
|
+
source: "plugins/beads/agents/task-agent.md",
|
|
9
|
+
target: "agents/task-agent.md",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
source: "plugins/beads/skills/beads/commands",
|
|
13
|
+
target: "commands",
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
interface Semver {
|
|
18
|
+
major: bigint;
|
|
19
|
+
minor: bigint;
|
|
20
|
+
patch: bigint;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VendorFileRecord {
|
|
24
|
+
path: string;
|
|
25
|
+
bytes: number;
|
|
26
|
+
sha256: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface VendorManifest {
|
|
30
|
+
schemaVersion: 1;
|
|
31
|
+
repository: string;
|
|
32
|
+
tag: string;
|
|
33
|
+
commit: string;
|
|
34
|
+
sources: typeof VENDOR_SOURCES;
|
|
35
|
+
files: VendorFileRecord[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ExpectedVendorProvenance {
|
|
39
|
+
repository: string;
|
|
40
|
+
tag: string;
|
|
41
|
+
commit: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface VendorRelease {
|
|
45
|
+
tag: string;
|
|
46
|
+
commit: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseStableTag(tag: string): Semver | undefined {
|
|
50
|
+
const match = tag.match(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/);
|
|
51
|
+
if (!match) return undefined;
|
|
52
|
+
|
|
53
|
+
const [, major, minor, patch] = match;
|
|
54
|
+
if (major === undefined || minor === undefined || patch === undefined) return undefined;
|
|
55
|
+
return { major: BigInt(major), minor: BigInt(minor), patch: BigInt(patch) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compareSemver(left: Semver, right: Semver): number {
|
|
59
|
+
for (const key of ["major", "minor", "patch"] as const) {
|
|
60
|
+
if (left[key] < right[key]) return -1;
|
|
61
|
+
if (left[key] > right[key]) return 1;
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Select the newest stable semver release from git ls-remote output. */
|
|
67
|
+
export function selectStableRelease(lsRemoteOutput: string): VendorRelease {
|
|
68
|
+
const releases = new Map<
|
|
69
|
+
string,
|
|
70
|
+
{ tagCommit?: string; peeledCommit?: string; version: Semver }
|
|
71
|
+
>();
|
|
72
|
+
|
|
73
|
+
for (const line of lsRemoteOutput.split(/\r?\n/)) {
|
|
74
|
+
if (!line.trim()) continue;
|
|
75
|
+
const match = line.match(/^([0-9a-fA-F]{40})\s+refs\/tags\/([^\s^]+)(\^\{\})?$/);
|
|
76
|
+
const commit = match?.[1]?.toLowerCase();
|
|
77
|
+
const tag = match?.[2];
|
|
78
|
+
const version = tag ? parseStableTag(tag) : undefined;
|
|
79
|
+
if (!commit || !tag || !version) continue;
|
|
80
|
+
|
|
81
|
+
const release = releases.get(tag) ?? { version };
|
|
82
|
+
const existingCommit = match[3] ? release.peeledCommit : release.tagCommit;
|
|
83
|
+
if (releases.has(tag) && existingCommit && existingCommit !== commit) {
|
|
84
|
+
throw new Error(`Conflicting commits for vendor tag: ${tag}`);
|
|
85
|
+
}
|
|
86
|
+
if (match[3]) release.peeledCommit = commit;
|
|
87
|
+
else release.tagCommit = commit;
|
|
88
|
+
releases.set(tag, release);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const candidates = [...releases].flatMap(([tag, release]) => {
|
|
92
|
+
const commit = release.peeledCommit ?? release.tagCommit;
|
|
93
|
+
return commit ? [{ tag, commit, version: release.version }] : [];
|
|
94
|
+
});
|
|
95
|
+
candidates.sort(
|
|
96
|
+
(left, right) =>
|
|
97
|
+
compareSemver(right.version, left.version) ||
|
|
98
|
+
(left.tag < right.tag ? -1 : left.tag > right.tag ? 1 : 0)
|
|
99
|
+
);
|
|
100
|
+
const selected = candidates[0];
|
|
101
|
+
if (!selected) throw new Error("No stable semver Beads tags found");
|
|
102
|
+
return { tag: selected.tag, commit: selected.commit };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Select the newest stable semver tag from git ls-remote output. */
|
|
106
|
+
export function selectStableTag(lsRemoteOutput: string): string {
|
|
107
|
+
return selectStableRelease(lsRemoteOutput).tag;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function listVendorFiles(vendorDir: string): Promise<string[]> {
|
|
111
|
+
const files: string[] = [];
|
|
112
|
+
|
|
113
|
+
async function visit(relativeDir: string): Promise<void> {
|
|
114
|
+
const entries = await fs.readdir(path.join(vendorDir, relativeDir), { withFileTypes: true });
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const relativePath = path.posix.join(relativeDir, entry.name);
|
|
117
|
+
if (entry.isDirectory()) await visit(relativePath);
|
|
118
|
+
else if (entry.isFile()) files.push(relativePath);
|
|
119
|
+
else throw new Error(`Unsupported vendor entry: ${relativePath}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await visit("agents");
|
|
124
|
+
await visit("commands");
|
|
125
|
+
return files.sort();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function fileRecord(vendorDir: string, relativePath: string): Promise<VendorFileRecord> {
|
|
129
|
+
const content = await fs.readFile(path.join(vendorDir, relativePath));
|
|
130
|
+
return {
|
|
131
|
+
path: relativePath,
|
|
132
|
+
bytes: content.byteLength,
|
|
133
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Build deterministic provenance for a validated vendor candidate. */
|
|
138
|
+
export async function createVendorManifest(
|
|
139
|
+
vendorDir: string,
|
|
140
|
+
repository: string,
|
|
141
|
+
tag: string,
|
|
142
|
+
commit: string
|
|
143
|
+
): Promise<VendorManifest> {
|
|
144
|
+
if (!repository) throw new Error("Vendor repository is required");
|
|
145
|
+
if (!parseStableTag(tag)) throw new Error(`Vendor tag is not stable semver: ${tag}`);
|
|
146
|
+
if (!/^[0-9a-f]{40}$/.test(commit)) throw new Error(`Invalid vendor commit SHA: ${commit}`);
|
|
147
|
+
|
|
148
|
+
const files = await listVendorFiles(vendorDir);
|
|
149
|
+
if (files.length === 0) throw new Error("Vendor inventory is empty");
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
schemaVersion: 1,
|
|
153
|
+
repository,
|
|
154
|
+
tag,
|
|
155
|
+
commit,
|
|
156
|
+
sources: VENDOR_SOURCES,
|
|
157
|
+
files: await Promise.all(files.map((relativePath) => fileRecord(vendorDir, relativePath))),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function serializeVendorManifest(manifest: VendorManifest): string {
|
|
162
|
+
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
166
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function hasExactKeys(value: Record<string, unknown>, expectedKeys: string[]): boolean {
|
|
170
|
+
const actualKeys = Object.keys(value);
|
|
171
|
+
return (
|
|
172
|
+
actualKeys.length === expectedKeys.length &&
|
|
173
|
+
expectedKeys.every((key) => Object.hasOwn(value, key))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseManifest(content: string): VendorManifest {
|
|
178
|
+
const parsed: unknown = JSON.parse(content);
|
|
179
|
+
if (!isRecord(parsed)) {
|
|
180
|
+
throw new Error("Vendor manifest must be an object");
|
|
181
|
+
}
|
|
182
|
+
const values = parsed;
|
|
183
|
+
if (!hasExactKeys(values, ["schemaVersion", "repository", "tag", "commit", "sources", "files"])) {
|
|
184
|
+
throw new Error("Vendor manifest has unexpected or missing fields");
|
|
185
|
+
}
|
|
186
|
+
if (values.schemaVersion !== 1) throw new Error("Unsupported vendor manifest schema");
|
|
187
|
+
if (typeof values.repository !== "string" || !values.repository) throw new Error("Missing vendor repository");
|
|
188
|
+
if (typeof values.tag !== "string" || !parseStableTag(values.tag)) throw new Error("Invalid vendor tag");
|
|
189
|
+
if (typeof values.commit !== "string" || !/^[0-9a-f]{40}$/.test(values.commit)) throw new Error("Invalid vendor commit");
|
|
190
|
+
if (JSON.stringify(values.sources) !== JSON.stringify(VENDOR_SOURCES)) throw new Error("Vendor source paths differ from the expected inventory");
|
|
191
|
+
if (!Array.isArray(values.files) || values.files.length === 0) {
|
|
192
|
+
throw new Error("Missing vendor file inventory");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const files = values.files.map((value): VendorFileRecord => {
|
|
196
|
+
if (!isRecord(value)) {
|
|
197
|
+
throw new Error("Invalid vendor file record");
|
|
198
|
+
}
|
|
199
|
+
const record = value;
|
|
200
|
+
if (!hasExactKeys(record, ["path", "bytes", "sha256"])) {
|
|
201
|
+
throw new Error("Vendor file record has unexpected or missing fields");
|
|
202
|
+
}
|
|
203
|
+
if (typeof record.path !== "string" || !record.path) throw new Error("Invalid vendor file path");
|
|
204
|
+
if (typeof record.bytes !== "number" || !Number.isSafeInteger(record.bytes) || record.bytes < 0) throw new Error("Invalid vendor file size");
|
|
205
|
+
if (typeof record.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(record.sha256)) throw new Error("Invalid vendor file checksum");
|
|
206
|
+
return { path: record.path, bytes: record.bytes, sha256: record.sha256 };
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
repository: values.repository,
|
|
212
|
+
tag: values.tag,
|
|
213
|
+
commit: values.commit,
|
|
214
|
+
sources: VENDOR_SOURCES,
|
|
215
|
+
files,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Validate manifest inventory and checksums against files on disk. */
|
|
220
|
+
export async function validateVendorManifest(
|
|
221
|
+
vendorDir: string,
|
|
222
|
+
expected?: ExpectedVendorProvenance
|
|
223
|
+
): Promise<VendorManifest> {
|
|
224
|
+
const manifest = parseManifest(
|
|
225
|
+
await fs.readFile(path.join(vendorDir, VENDOR_MANIFEST), "utf-8")
|
|
226
|
+
);
|
|
227
|
+
if (
|
|
228
|
+
expected &&
|
|
229
|
+
(manifest.repository !== expected.repository ||
|
|
230
|
+
manifest.tag !== expected.tag ||
|
|
231
|
+
manifest.commit !== expected.commit)
|
|
232
|
+
) {
|
|
233
|
+
throw new Error("Vendor release provenance differs from the resolved upstream release");
|
|
234
|
+
}
|
|
235
|
+
const actualPaths = await listVendorFiles(vendorDir);
|
|
236
|
+
const manifestPaths = manifest.files.map((file) => file.path);
|
|
237
|
+
if (JSON.stringify(manifestPaths) !== JSON.stringify([...manifestPaths].sort())) {
|
|
238
|
+
throw new Error("Vendor manifest inventory is not sorted");
|
|
239
|
+
}
|
|
240
|
+
if (JSON.stringify(manifestPaths) !== JSON.stringify(actualPaths)) {
|
|
241
|
+
throw new Error("Vendor file inventory differs from the manifest");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const expected of manifest.files) {
|
|
245
|
+
const actual = await fileRecord(vendorDir, expected.path);
|
|
246
|
+
if (actual.bytes !== expected.bytes || actual.sha256 !== expected.sha256) {
|
|
247
|
+
throw new Error(`Vendor checksum mismatch: ${expected.path}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return manifest;
|
|
252
|
+
}
|