mdkg 0.1.0 → 0.1.2
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 +93 -0
- package/README.md +108 -15
- package/dist/cli.js +566 -15
- package/dist/commands/archive.js +474 -0
- package/dist/commands/bundle.js +743 -0
- package/dist/commands/bundle_import.js +243 -0
- package/dist/commands/capability.js +162 -0
- package/dist/commands/doctor.js +233 -2
- package/dist/commands/format.js +38 -9
- package/dist/commands/index.js +11 -0
- package/dist/commands/init.js +188 -63
- package/dist/commands/init_manifest.js +19 -6
- package/dist/commands/list.js +5 -2
- package/dist/commands/new.js +6 -0
- package/dist/commands/next.js +7 -0
- package/dist/commands/node_card.js +4 -1
- package/dist/commands/pack.js +62 -2
- package/dist/commands/query_output.js +1 -0
- package/dist/commands/search.js +5 -2
- package/dist/commands/show.js +7 -14
- package/dist/commands/skill_mirror.js +22 -0
- package/dist/commands/task.js +3 -0
- package/dist/commands/upgrade.js +151 -13
- package/dist/commands/validate.js +19 -2
- package/dist/commands/work.js +365 -0
- package/dist/commands/workspace.js +12 -2
- package/dist/core/config.js +100 -1
- package/dist/graph/agent_file_types.js +78 -5
- package/dist/graph/archive_file.js +125 -0
- package/dist/graph/archive_integrity.js +66 -0
- package/dist/graph/bundle_imports.js +418 -0
- package/dist/graph/capabilities_index_cache.js +103 -0
- package/dist/graph/capabilities_indexer.js +231 -0
- package/dist/graph/frontmatter.js +19 -0
- package/dist/graph/index_cache.js +21 -4
- package/dist/graph/indexer.js +4 -1
- package/dist/graph/node.js +23 -4
- package/dist/graph/node_body.js +37 -0
- package/dist/graph/skills_indexer.js +8 -3
- package/dist/graph/template_schema.js +33 -5
- package/dist/graph/validate_graph.js +83 -7
- package/dist/graph/visibility.js +214 -0
- package/dist/graph/workspace_files.js +22 -0
- package/dist/init/AGENT_START.md +21 -0
- package/dist/init/CLI_COMMAND_MATRIX.md +58 -3
- package/dist/init/README.md +60 -3
- package/dist/init/config.json +13 -1
- package/dist/init/core/guide.md +6 -2
- package/dist/init/core/rule-3-cli-contract.md +71 -4
- package/dist/init/core/rule-4-repo-safety-and-ignores.md +20 -0
- package/dist/init/core/rule-6-templates-and-schemas.md +10 -1
- package/dist/init/init-manifest.json +19 -14
- package/dist/init/skills/default/build-pack-and-execute-task/SKILL.md +2 -1
- package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +26 -0
- package/dist/init/templates/default/archive.md +33 -0
- package/dist/init/templates/default/receipt.md +15 -1
- package/dist/init/templates/default/work.md +6 -1
- package/dist/init/templates/default/work_order.md +15 -1
- package/dist/pack/export_md.js +3 -0
- package/dist/pack/export_xml.js +3 -0
- package/dist/pack/order.js +1 -0
- package/dist/pack/pack.js +3 -13
- package/dist/templates/builtin.js +38 -0
- package/dist/templates/loader.js +9 -16
- package/dist/util/argparse.js +30 -0
- package/dist/util/refs.js +40 -0
- package/dist/util/zip.js +153 -0
- package/package.json +8 -2
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runBundleCreateCommand = runBundleCreateCommand;
|
|
7
|
+
exports.runBundleVerifyCommand = runBundleVerifyCommand;
|
|
8
|
+
exports.runBundleShowCommand = runBundleShowCommand;
|
|
9
|
+
exports.runBundleListCommand = runBundleListCommand;
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const config_1 = require("../core/config");
|
|
15
|
+
const capabilities_indexer_1 = require("../graph/capabilities_indexer");
|
|
16
|
+
const bundle_imports_1 = require("../graph/bundle_imports");
|
|
17
|
+
const indexer_1 = require("../graph/indexer");
|
|
18
|
+
const skills_indexer_1 = require("../graph/skills_indexer");
|
|
19
|
+
const zip_1 = require("../util/zip");
|
|
20
|
+
const errors_1 = require("../util/errors");
|
|
21
|
+
const refs_1 = require("../util/refs");
|
|
22
|
+
const visibility_1 = require("../graph/visibility");
|
|
23
|
+
const MANIFEST_ENTRY = "manifest.json";
|
|
24
|
+
const GENERATED_AT = "1970-01-01T00:00:00.000Z";
|
|
25
|
+
const INDEX_ENTRY_PATHS = {
|
|
26
|
+
global: ".mdkg/index/global.json",
|
|
27
|
+
skills: ".mdkg/index/skills.json",
|
|
28
|
+
capabilities: ".mdkg/index/capabilities.json",
|
|
29
|
+
};
|
|
30
|
+
function toPosixPath(value) {
|
|
31
|
+
return value.split(path_1.default.sep).join("/");
|
|
32
|
+
}
|
|
33
|
+
function sha256Buffer(buffer) {
|
|
34
|
+
return `sha256:${crypto_1.default.createHash("sha256").update(buffer).digest("hex")}`;
|
|
35
|
+
}
|
|
36
|
+
function sha256Hex(buffer) {
|
|
37
|
+
return crypto_1.default.createHash("sha256").update(buffer).digest("hex");
|
|
38
|
+
}
|
|
39
|
+
function stableJson(value) {
|
|
40
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
41
|
+
}
|
|
42
|
+
function readPackageVersion() {
|
|
43
|
+
const packagePath = path_1.default.resolve(__dirname, "..", "..", "package.json");
|
|
44
|
+
try {
|
|
45
|
+
const raw = JSON.parse(fs_1.default.readFileSync(packagePath, "utf8"));
|
|
46
|
+
return typeof raw.version === "string" ? raw.version : "unknown";
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function normalizeProfile(value, fallback = "private") {
|
|
53
|
+
const normalized = (value ?? fallback).toLowerCase();
|
|
54
|
+
if (normalized === "private" || normalized === "public") {
|
|
55
|
+
return normalized;
|
|
56
|
+
}
|
|
57
|
+
throw new errors_1.UsageError("--profile must be one of private, public");
|
|
58
|
+
}
|
|
59
|
+
function gitOutput(root, args) {
|
|
60
|
+
const result = (0, child_process_1.spawnSync)("git", args, { cwd: root, encoding: "utf8", stdio: "pipe" });
|
|
61
|
+
if (result.status !== 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const output = result.stdout.trim();
|
|
65
|
+
return output.length > 0 ? output : null;
|
|
66
|
+
}
|
|
67
|
+
function sourceInfo(root) {
|
|
68
|
+
const remote = gitOutput(root, ["config", "--get", "remote.origin.url"]);
|
|
69
|
+
const head = gitOutput(root, ["rev-parse", "HEAD"]);
|
|
70
|
+
const status = gitOutput(root, ["status", "--porcelain"]);
|
|
71
|
+
const dirtyPaths = status
|
|
72
|
+
? status
|
|
73
|
+
.split(/\r?\n/)
|
|
74
|
+
.map((line) => line.slice(3).replace(/^"|"$/g, ""))
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.filter((filePath) => {
|
|
77
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
78
|
+
return (normalized !== ".mdkg/bundles" &&
|
|
79
|
+
!normalized.startsWith(".mdkg/bundles/") &&
|
|
80
|
+
!normalized.includes("/.mdkg/bundles/"));
|
|
81
|
+
})
|
|
82
|
+
: [];
|
|
83
|
+
return {
|
|
84
|
+
repo: remote ?? path_1.default.basename(root),
|
|
85
|
+
git_head: head,
|
|
86
|
+
dirty: status === null ? false : dirtyPaths.length > 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function workspaceMdkgRoot(root, entry) {
|
|
90
|
+
return path_1.default.resolve(root, entry.path, entry.mdkg_dir);
|
|
91
|
+
}
|
|
92
|
+
function workspacePrefix(entry) {
|
|
93
|
+
const wsPath = entry.path === "." ? "" : `${toPosixPath(entry.path).replace(/\/+$/, "")}/`;
|
|
94
|
+
return `${wsPath}${toPosixPath(entry.mdkg_dir).replace(/^\/+|\/+$/g, "")}/`;
|
|
95
|
+
}
|
|
96
|
+
function listFilesRecursive(dir) {
|
|
97
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
101
|
+
const files = [];
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
104
|
+
if (entry.isDirectory()) {
|
|
105
|
+
files.push(...listFilesRecursive(fullPath));
|
|
106
|
+
}
|
|
107
|
+
else if (entry.isFile()) {
|
|
108
|
+
files.push(fullPath);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return files;
|
|
112
|
+
}
|
|
113
|
+
function relativePath(root, absolutePath) {
|
|
114
|
+
return toPosixPath(path_1.default.relative(root, absolutePath));
|
|
115
|
+
}
|
|
116
|
+
function isExcludedRelativePath(relative) {
|
|
117
|
+
const normalized = relative.replace(/\\/g, "/");
|
|
118
|
+
return (normalized.includes("/.mdkg/pack/") ||
|
|
119
|
+
normalized.startsWith(".mdkg/pack/") ||
|
|
120
|
+
normalized.includes("/.mdkg/bundles/") ||
|
|
121
|
+
normalized.startsWith(".mdkg/bundles/") ||
|
|
122
|
+
normalized.includes("/.mdkg/index/") ||
|
|
123
|
+
normalized.startsWith(".mdkg/index/") ||
|
|
124
|
+
((normalized.includes("/.mdkg/archive/") || normalized.startsWith(".mdkg/archive/")) &&
|
|
125
|
+
normalized.includes("/source/")));
|
|
126
|
+
}
|
|
127
|
+
function archiveVisibilityByPath(index) {
|
|
128
|
+
const visibility = new Map();
|
|
129
|
+
for (const node of Object.values(index.nodes)) {
|
|
130
|
+
if (node.type !== "archive") {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
visibility.set(node.path, String(node.attributes.visibility ?? "private"));
|
|
134
|
+
visibility.set(`${toPosixPath(path_1.default.dirname(node.path))}/`, String(node.attributes.visibility ?? "private"));
|
|
135
|
+
}
|
|
136
|
+
return visibility;
|
|
137
|
+
}
|
|
138
|
+
function archivePathVisibility(visibilityByPath, relative) {
|
|
139
|
+
const exact = visibilityByPath.get(relative);
|
|
140
|
+
if (exact) {
|
|
141
|
+
return exact;
|
|
142
|
+
}
|
|
143
|
+
const normalized = relative.replace(/\\/g, "/");
|
|
144
|
+
for (const [key, value] of visibilityByPath.entries()) {
|
|
145
|
+
if (key.endsWith("/") && normalized.startsWith(key)) {
|
|
146
|
+
return value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
function entryForFile(root, filePath, kind, workspace, visibility) {
|
|
152
|
+
const data = fs_1.default.readFileSync(filePath);
|
|
153
|
+
const rel = relativePath(root, filePath);
|
|
154
|
+
return {
|
|
155
|
+
entry: { name: rel, data },
|
|
156
|
+
manifestFile: {
|
|
157
|
+
path: rel,
|
|
158
|
+
kind,
|
|
159
|
+
workspace,
|
|
160
|
+
visibility,
|
|
161
|
+
size: data.length,
|
|
162
|
+
sha256: sha256Buffer(data),
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function selectedWorkspaceAliases(config, ws) {
|
|
167
|
+
const requested = (ws ?? "all").toLowerCase();
|
|
168
|
+
if (requested === "all") {
|
|
169
|
+
return Object.keys(config.workspaces)
|
|
170
|
+
.filter((alias) => config.workspaces[alias].enabled)
|
|
171
|
+
.sort();
|
|
172
|
+
}
|
|
173
|
+
const workspace = config.workspaces[requested];
|
|
174
|
+
if (!workspace || !workspace.enabled) {
|
|
175
|
+
throw new errors_1.NotFoundError(`workspace not found or disabled: ${requested}`);
|
|
176
|
+
}
|
|
177
|
+
return [requested];
|
|
178
|
+
}
|
|
179
|
+
function filterAliasesForProfile(config, aliases, profile) {
|
|
180
|
+
if (profile === "private") {
|
|
181
|
+
return aliases;
|
|
182
|
+
}
|
|
183
|
+
return aliases.filter((alias) => config.workspaces[alias].visibility === "public");
|
|
184
|
+
}
|
|
185
|
+
function normalizeIndexForBundle(index) {
|
|
186
|
+
const normalized = JSON.parse(JSON.stringify(index));
|
|
187
|
+
normalized.meta.generated_at = GENERATED_AT;
|
|
188
|
+
normalized.meta.root = ".";
|
|
189
|
+
return normalized;
|
|
190
|
+
}
|
|
191
|
+
function filterIndex(index, config, selectedAliases, profile) {
|
|
192
|
+
const nodes = {};
|
|
193
|
+
for (const [qid, node] of Object.entries(index.nodes)) {
|
|
194
|
+
if (!selectedAliases.has(node.ws)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (profile === "public" && !(0, visibility_1.isVisibleAt)((0, visibility_1.effectiveNodeVisibility)(node, config), "public")) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
{
|
|
201
|
+
nodes[qid] = node;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const reverse_edges = {};
|
|
205
|
+
for (const [edgeKey, targets] of Object.entries(index.reverse_edges)) {
|
|
206
|
+
for (const [target, sources] of Object.entries(targets)) {
|
|
207
|
+
const keptSources = sources.filter((source) => nodes[source]);
|
|
208
|
+
if (keptSources.length === 0) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
reverse_edges[edgeKey] = reverse_edges[edgeKey] ?? {};
|
|
212
|
+
reverse_edges[edgeKey][target] = keptSources;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const workspaces = {};
|
|
216
|
+
for (const [alias, workspace] of Object.entries(index.workspaces)) {
|
|
217
|
+
if (selectedAliases.has(alias)) {
|
|
218
|
+
workspaces[alias] = workspace;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const filtered = {
|
|
222
|
+
meta: {
|
|
223
|
+
...index.meta,
|
|
224
|
+
generated_at: GENERATED_AT,
|
|
225
|
+
root: ".",
|
|
226
|
+
workspaces: Array.from(selectedAliases).sort(),
|
|
227
|
+
},
|
|
228
|
+
workspaces,
|
|
229
|
+
nodes,
|
|
230
|
+
reverse_edges,
|
|
231
|
+
};
|
|
232
|
+
if (index.meta.latest_checkpoint_qid) {
|
|
233
|
+
const latest = {};
|
|
234
|
+
for (const [alias, qid] of Object.entries(index.meta.latest_checkpoint_qid)) {
|
|
235
|
+
if (selectedAliases.has(alias) && nodes[qid]) {
|
|
236
|
+
latest[alias] = qid;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (Object.keys(latest).length > 0) {
|
|
240
|
+
filtered.meta.latest_checkpoint_qid = latest;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return filtered;
|
|
244
|
+
}
|
|
245
|
+
function buildBundleSkillsIndex(root, config, selectedAliases) {
|
|
246
|
+
const skills = {};
|
|
247
|
+
for (const alias of Array.from(selectedAliases).sort()) {
|
|
248
|
+
const workspace = config.workspaces[alias];
|
|
249
|
+
const skillsRoot = path_1.default.join(workspaceMdkgRoot(root, workspace), "skills");
|
|
250
|
+
for (const file of (0, skills_indexer_1.listSkillMarkdownFiles)(skillsRoot)) {
|
|
251
|
+
const entry = (0, skills_indexer_1.buildSkillIndexEntryForWorkspace)(root, alias, file.slug, file.filePath);
|
|
252
|
+
const key = alias === "root" ? file.slug : `${alias}:${file.slug}`;
|
|
253
|
+
skills[key] = entry;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
meta: {
|
|
258
|
+
tool: config.tool,
|
|
259
|
+
schema_version: config.schema_version,
|
|
260
|
+
generated_at: GENERATED_AT,
|
|
261
|
+
root: ".",
|
|
262
|
+
skills_root: ".mdkg/skills",
|
|
263
|
+
skill_count: Object.keys(skills).length,
|
|
264
|
+
},
|
|
265
|
+
skills,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function normalizeCapabilitiesForBundle(root, config, index, selectedAliases) {
|
|
269
|
+
const capabilities = normalizeIndexForBundle((0, capabilities_indexer_1.buildCapabilitiesIndex)(root, config, index));
|
|
270
|
+
capabilities.records = capabilities.records
|
|
271
|
+
.filter((record) => selectedAliases.has(record.workspace))
|
|
272
|
+
.map((record) => ({ ...record, indexed_at: GENERATED_AT }));
|
|
273
|
+
capabilities.meta.generated_at = GENERATED_AT;
|
|
274
|
+
capabilities.meta.root = ".";
|
|
275
|
+
capabilities.meta.workspaces = Array.from(selectedAliases).sort();
|
|
276
|
+
capabilities.meta.record_count = capabilities.records.length;
|
|
277
|
+
return capabilities;
|
|
278
|
+
}
|
|
279
|
+
function addGeneratedIndex(entries, files, indexHashes, key, value) {
|
|
280
|
+
const data = Buffer.from(stableJson(value), "utf8");
|
|
281
|
+
const entryPath = INDEX_ENTRY_PATHS[key];
|
|
282
|
+
entries.push({ name: entryPath, data });
|
|
283
|
+
const hash = sha256Buffer(data);
|
|
284
|
+
indexHashes[entryPath] = hash;
|
|
285
|
+
files.push({
|
|
286
|
+
path: entryPath,
|
|
287
|
+
kind: "generated_index",
|
|
288
|
+
size: data.length,
|
|
289
|
+
sha256: hash,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function hashManifestFiles(files) {
|
|
293
|
+
return sha256Buffer(Buffer.from(stableJson(files.map((file) => ({
|
|
294
|
+
path: file.path,
|
|
295
|
+
kind: file.kind,
|
|
296
|
+
workspace: file.workspace,
|
|
297
|
+
visibility: file.visibility,
|
|
298
|
+
size: file.size,
|
|
299
|
+
sha256: file.sha256,
|
|
300
|
+
}))), "utf8"));
|
|
301
|
+
}
|
|
302
|
+
function collectArchiveUris(value) {
|
|
303
|
+
const found = [];
|
|
304
|
+
const visit = (item) => {
|
|
305
|
+
if (typeof item === "string") {
|
|
306
|
+
if (item.startsWith("archive://")) {
|
|
307
|
+
found.push(item);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (Array.isArray(item)) {
|
|
312
|
+
for (const child of item) {
|
|
313
|
+
visit(child);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (typeof item === "object" && item !== null) {
|
|
318
|
+
for (const child of Object.values(item)) {
|
|
319
|
+
visit(child);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
visit(value);
|
|
324
|
+
return found;
|
|
325
|
+
}
|
|
326
|
+
function publicFilteringErrors(index, includedQids) {
|
|
327
|
+
const errors = [];
|
|
328
|
+
const archiveNodesById = new Map();
|
|
329
|
+
for (const node of Object.values(index.nodes)) {
|
|
330
|
+
if (node.type === "archive") {
|
|
331
|
+
archiveNodesById.set(node.id, node);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const node of Object.values(index.nodes)) {
|
|
335
|
+
if (!includedQids.has(node.qid)) {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
for (const targets of [
|
|
339
|
+
[node.edges.epic].filter(Boolean),
|
|
340
|
+
[node.edges.parent].filter(Boolean),
|
|
341
|
+
[node.edges.prev].filter(Boolean),
|
|
342
|
+
[node.edges.next].filter(Boolean),
|
|
343
|
+
node.edges.relates,
|
|
344
|
+
node.edges.blocked_by,
|
|
345
|
+
node.edges.blocks,
|
|
346
|
+
]) {
|
|
347
|
+
for (const target of targets) {
|
|
348
|
+
if (index.nodes[target] && !includedQids.has(target)) {
|
|
349
|
+
errors.push(`${node.qid} references non-public graph node ${target}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const archiveRefs = [
|
|
354
|
+
...collectArchiveUris(node.attributes),
|
|
355
|
+
...node.artifacts.filter((value) => value.startsWith("archive://")),
|
|
356
|
+
...node.refs.filter((value) => value.startsWith("archive://")),
|
|
357
|
+
...node.links.filter((value) => value.startsWith("archive://")),
|
|
358
|
+
];
|
|
359
|
+
for (const archiveUri of archiveRefs) {
|
|
360
|
+
const archiveId = (0, refs_1.archiveIdFromUri)(archiveUri);
|
|
361
|
+
const archiveNode = archiveId ? archiveNodesById.get(archiveId) : undefined;
|
|
362
|
+
if (!archiveNode) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (!includedQids.has(archiveNode.qid) || archiveNode.attributes.visibility !== "public") {
|
|
366
|
+
errors.push(`${node.qid} references non-public archive ${archiveUri}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return errors;
|
|
371
|
+
}
|
|
372
|
+
function collectStringValues(value, out) {
|
|
373
|
+
if (typeof value === "string") {
|
|
374
|
+
out.push(value);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (Array.isArray(value)) {
|
|
378
|
+
for (const child of value) {
|
|
379
|
+
collectStringValues(child, out);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (typeof value === "object" && value !== null) {
|
|
384
|
+
for (const child of Object.values(value)) {
|
|
385
|
+
collectStringValues(child, out);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function publicImportReferenceErrors(config, index, includedQids) {
|
|
390
|
+
const privateImportAliases = new Set(Object.entries(config.bundle_imports)
|
|
391
|
+
.filter(([, entry]) => entry.enabled && entry.visibility !== "public")
|
|
392
|
+
.map(([alias]) => alias));
|
|
393
|
+
if (privateImportAliases.size === 0) {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
const errors = [];
|
|
397
|
+
for (const node of Object.values(index.nodes)) {
|
|
398
|
+
if (!includedQids.has(node.qid)) {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const values = [];
|
|
402
|
+
collectStringValues(node.edges, values);
|
|
403
|
+
collectStringValues(node.attributes, values);
|
|
404
|
+
values.push(...node.links, ...node.artifacts, ...node.refs, ...node.aliases);
|
|
405
|
+
for (const value of values) {
|
|
406
|
+
const [alias] = value.split(":");
|
|
407
|
+
if (alias && privateImportAliases.has(alias)) {
|
|
408
|
+
errors.push(`${node.qid} references private bundle import ${value}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return errors;
|
|
413
|
+
}
|
|
414
|
+
function defaultOutputPath(root, config, profile, ws) {
|
|
415
|
+
const outputDir = config.bundles.output_dir;
|
|
416
|
+
const workspaceName = (ws ?? "all").toLowerCase();
|
|
417
|
+
return path_1.default.resolve(root, outputDir, profile, `${workspaceName}.mdkg.zip`);
|
|
418
|
+
}
|
|
419
|
+
function resolveBundlePath(root, value) {
|
|
420
|
+
return path_1.default.isAbsolute(value) ? value : path_1.default.resolve(root, value);
|
|
421
|
+
}
|
|
422
|
+
function buildBundle(options) {
|
|
423
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
424
|
+
const profile = normalizeProfile(options.profile, config.bundles.default_profile);
|
|
425
|
+
const requestedAliases = selectedWorkspaceAliases(config, options.ws);
|
|
426
|
+
const selectedAliases = filterAliasesForProfile(config, requestedAliases, profile);
|
|
427
|
+
if (selectedAliases.length === 0) {
|
|
428
|
+
const hint = profile === "public"
|
|
429
|
+
? "mark a selected workspace visibility public or use --profile private"
|
|
430
|
+
: "enable at least one selected workspace";
|
|
431
|
+
throw new errors_1.UsageError(`no workspaces selected for ${profile} bundle; ${hint}`);
|
|
432
|
+
}
|
|
433
|
+
const selectedSet = new Set(selectedAliases);
|
|
434
|
+
const index = (0, indexer_1.buildIndex)(options.root, config);
|
|
435
|
+
const archiveVisibility = archiveVisibilityByPath(index);
|
|
436
|
+
const entries = [];
|
|
437
|
+
const files = [];
|
|
438
|
+
for (const alias of selectedAliases) {
|
|
439
|
+
const workspace = config.workspaces[alias];
|
|
440
|
+
const wsRoot = workspaceMdkgRoot(options.root, workspace);
|
|
441
|
+
const wsPrefix = workspacePrefix(workspace);
|
|
442
|
+
for (const filePath of listFilesRecursive(wsRoot)) {
|
|
443
|
+
const rel = relativePath(options.root, filePath);
|
|
444
|
+
if (isExcludedRelativePath(rel)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (profile === "public" && rel.endsWith(".mdkg/config.json")) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const visibility = archivePathVisibility(archiveVisibility, rel) ?? workspace.visibility;
|
|
451
|
+
if (profile === "public") {
|
|
452
|
+
if (!rel.startsWith(wsPrefix)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (rel.includes("/archive/") && visibility !== "public") {
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const kind = rel.includes("/archive/") && rel.endsWith(".zip") ? "archive_cache" : "authored";
|
|
460
|
+
const { entry, manifestFile } = entryForFile(options.root, filePath, kind, alias, visibility);
|
|
461
|
+
entries.push(entry);
|
|
462
|
+
files.push(manifestFile);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const filteredIndex = filterIndex(index, config, selectedSet, profile);
|
|
466
|
+
if (profile === "public") {
|
|
467
|
+
const includedQids = new Set(Object.keys(filteredIndex.nodes));
|
|
468
|
+
const mergedIndex = (0, bundle_imports_1.mergeBundleImportsIntoIndex)(index, (0, bundle_imports_1.buildBundleImportsIndex)(options.root, config));
|
|
469
|
+
const errors = (0, visibility_1.visibilityViolationMessages)((0, visibility_1.collectVisibilityViolations)(mergedIndex, config, {
|
|
470
|
+
includedQids,
|
|
471
|
+
scope: "public",
|
|
472
|
+
}));
|
|
473
|
+
if (errors.length > 0) {
|
|
474
|
+
throw new errors_1.ValidationError(`public bundle contains private references:\n${errors.join("\n")}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
const indexHashes = {};
|
|
478
|
+
addGeneratedIndex(entries, files, indexHashes, "global", normalizeIndexForBundle(filteredIndex));
|
|
479
|
+
addGeneratedIndex(entries, files, indexHashes, "skills", buildBundleSkillsIndex(options.root, config, selectedSet));
|
|
480
|
+
addGeneratedIndex(entries, files, indexHashes, "capabilities", normalizeCapabilitiesForBundle(options.root, config, filteredIndex, selectedSet));
|
|
481
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
482
|
+
const sourceFiles = files.filter((file) => file.kind !== "generated_index");
|
|
483
|
+
const sourceTreeHash = hashManifestFiles(sourceFiles);
|
|
484
|
+
const bundleHash = hashManifestFiles(files);
|
|
485
|
+
const manifest = {
|
|
486
|
+
manifest_version: 1,
|
|
487
|
+
tool: "mdkg",
|
|
488
|
+
mdkg_version: readPackageVersion(),
|
|
489
|
+
profile,
|
|
490
|
+
selected_workspaces: selectedAliases,
|
|
491
|
+
source: sourceInfo(options.root),
|
|
492
|
+
source_tree_hash: sourceTreeHash,
|
|
493
|
+
bundle_hash: bundleHash,
|
|
494
|
+
file_count: files.length,
|
|
495
|
+
index_hashes: indexHashes,
|
|
496
|
+
files,
|
|
497
|
+
};
|
|
498
|
+
const manifestData = Buffer.from(stableJson(manifest), "utf8");
|
|
499
|
+
const zip = (0, zip_1.createDeterministicZipFromEntries)([
|
|
500
|
+
...entries,
|
|
501
|
+
{ name: MANIFEST_ENTRY, data: manifestData },
|
|
502
|
+
]);
|
|
503
|
+
const outputPath = options.output
|
|
504
|
+
? resolveBundlePath(options.root, options.output)
|
|
505
|
+
: defaultOutputPath(options.root, config, profile, options.ws);
|
|
506
|
+
return {
|
|
507
|
+
manifest,
|
|
508
|
+
zip,
|
|
509
|
+
outputPath,
|
|
510
|
+
zipSha256: sha256Buffer(zip),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function parseBundle(bundlePath) {
|
|
514
|
+
const zip = fs_1.default.readFileSync(bundlePath);
|
|
515
|
+
const entries = new Map();
|
|
516
|
+
for (const entry of (0, zip_1.readZipEntries)(zip)) {
|
|
517
|
+
entries.set(entry.name, entry.data);
|
|
518
|
+
}
|
|
519
|
+
const manifestData = entries.get(MANIFEST_ENTRY);
|
|
520
|
+
if (!manifestData) {
|
|
521
|
+
throw new errors_1.ValidationError("bundle manifest missing");
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
return {
|
|
525
|
+
entries,
|
|
526
|
+
manifest: JSON.parse(manifestData.toString("utf8")),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
catch (err) {
|
|
530
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
531
|
+
throw new errors_1.ValidationError(`bundle manifest is invalid JSON: ${message}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function verifyBundle(root, bundlePath) {
|
|
535
|
+
const errors = [];
|
|
536
|
+
const stalePaths = [];
|
|
537
|
+
let manifest;
|
|
538
|
+
let entries = new Map();
|
|
539
|
+
try {
|
|
540
|
+
const parsed = parseBundle(bundlePath);
|
|
541
|
+
manifest = parsed.manifest;
|
|
542
|
+
entries = parsed.entries;
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
546
|
+
return {
|
|
547
|
+
action: "verified",
|
|
548
|
+
ok: false,
|
|
549
|
+
path: bundlePath,
|
|
550
|
+
selected_workspaces: [],
|
|
551
|
+
file_count: 0,
|
|
552
|
+
stale: false,
|
|
553
|
+
errors: [message],
|
|
554
|
+
stale_paths: [],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
const manifestPaths = new Set(manifest.files.map((file) => file.path));
|
|
558
|
+
for (const entryPath of entries.keys()) {
|
|
559
|
+
if (entryPath !== MANIFEST_ENTRY && !manifestPaths.has(entryPath)) {
|
|
560
|
+
errors.push(`unexpected bundle entry: ${entryPath}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
for (const file of manifest.files) {
|
|
564
|
+
const data = entries.get(file.path);
|
|
565
|
+
if (!data) {
|
|
566
|
+
errors.push(`missing bundle entry: ${file.path}`);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const hash = sha256Buffer(data);
|
|
570
|
+
if (hash !== file.sha256) {
|
|
571
|
+
errors.push(`hash mismatch for ${file.path}`);
|
|
572
|
+
}
|
|
573
|
+
if (data.length !== file.size) {
|
|
574
|
+
errors.push(`size mismatch for ${file.path}`);
|
|
575
|
+
}
|
|
576
|
+
if (file.kind !== "generated_index") {
|
|
577
|
+
const sourcePath = path_1.default.resolve(root, file.path);
|
|
578
|
+
if (!fs_1.default.existsSync(sourcePath)) {
|
|
579
|
+
stalePaths.push(file.path);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (sha256Buffer(fs_1.default.readFileSync(sourcePath)) !== file.sha256) {
|
|
583
|
+
stalePaths.push(file.path);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
for (const [indexPath, expectedHash] of Object.entries(manifest.index_hashes)) {
|
|
588
|
+
const data = entries.get(indexPath);
|
|
589
|
+
if (!data) {
|
|
590
|
+
errors.push(`missing generated index: ${indexPath}`);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (sha256Buffer(data) !== expectedHash) {
|
|
594
|
+
errors.push(`generated index hash mismatch: ${indexPath}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const computedSourceHash = hashManifestFiles(manifest.files.filter((file) => file.kind !== "generated_index"));
|
|
598
|
+
if (computedSourceHash !== manifest.source_tree_hash) {
|
|
599
|
+
errors.push("source_tree_hash mismatch");
|
|
600
|
+
}
|
|
601
|
+
const computedBundleHash = hashManifestFiles(manifest.files);
|
|
602
|
+
if (computedBundleHash !== manifest.bundle_hash) {
|
|
603
|
+
errors.push("bundle_hash mismatch");
|
|
604
|
+
}
|
|
605
|
+
const currentHead = gitOutput(root, ["rev-parse", "HEAD"]);
|
|
606
|
+
if (manifest.source.git_head && currentHead && manifest.source.git_head !== currentHead) {
|
|
607
|
+
stalePaths.push("git:HEAD");
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
action: "verified",
|
|
611
|
+
ok: errors.length === 0 && stalePaths.length === 0,
|
|
612
|
+
path: bundlePath,
|
|
613
|
+
profile: manifest.profile,
|
|
614
|
+
selected_workspaces: manifest.selected_workspaces,
|
|
615
|
+
file_count: manifest.file_count,
|
|
616
|
+
stale: stalePaths.length > 0,
|
|
617
|
+
errors,
|
|
618
|
+
stale_paths: Array.from(new Set(stalePaths)).sort(),
|
|
619
|
+
bundle_hash: manifest.bundle_hash,
|
|
620
|
+
zip_sha256: sha256Buffer(fs_1.default.readFileSync(bundlePath)),
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
function writeJson(value) {
|
|
624
|
+
console.log(JSON.stringify(value, null, 2));
|
|
625
|
+
}
|
|
626
|
+
function bundleSummary(manifest, bundlePath, zipSha256) {
|
|
627
|
+
return {
|
|
628
|
+
path: bundlePath,
|
|
629
|
+
profile: manifest.profile,
|
|
630
|
+
selected_workspaces: manifest.selected_workspaces,
|
|
631
|
+
file_count: manifest.file_count,
|
|
632
|
+
source_tree_hash: manifest.source_tree_hash,
|
|
633
|
+
bundle_hash: manifest.bundle_hash,
|
|
634
|
+
zip_sha256: zipSha256,
|
|
635
|
+
source: manifest.source,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function runBundleCreateCommand(options) {
|
|
639
|
+
const result = buildBundle(options);
|
|
640
|
+
fs_1.default.mkdirSync(path_1.default.dirname(result.outputPath), { recursive: true });
|
|
641
|
+
fs_1.default.writeFileSync(result.outputPath, result.zip);
|
|
642
|
+
const receipt = {
|
|
643
|
+
action: "created",
|
|
644
|
+
...bundleSummary(result.manifest, path_1.default.relative(options.root, result.outputPath), result.zipSha256),
|
|
645
|
+
};
|
|
646
|
+
if (options.json) {
|
|
647
|
+
writeJson(receipt);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
console.log(`bundle written: ${receipt.path}`);
|
|
651
|
+
console.log(`profile: ${receipt.profile}`);
|
|
652
|
+
console.log(`workspaces: ${receipt.selected_workspaces.join(", ") || "(none)"}`);
|
|
653
|
+
console.log(`files: ${receipt.file_count}`);
|
|
654
|
+
console.log(`bundle_hash: ${receipt.bundle_hash}`);
|
|
655
|
+
}
|
|
656
|
+
function runBundleVerifyCommand(options) {
|
|
657
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
658
|
+
const bundlePath = options.bundlePath
|
|
659
|
+
? resolveBundlePath(options.root, options.bundlePath)
|
|
660
|
+
: defaultOutputPath(options.root, config, config.bundles.default_profile, "all");
|
|
661
|
+
const result = verifyBundle(options.root, bundlePath);
|
|
662
|
+
const output = { ...result, path: path_1.default.relative(options.root, bundlePath) || bundlePath };
|
|
663
|
+
if (options.json) {
|
|
664
|
+
writeJson(output);
|
|
665
|
+
}
|
|
666
|
+
else if (result.ok) {
|
|
667
|
+
console.log(`bundle verified: ${output.path}`);
|
|
668
|
+
console.log(`profile: ${result.profile}`);
|
|
669
|
+
console.log(`files: ${result.file_count}`);
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
console.log(`bundle verify failed: ${output.path}`);
|
|
673
|
+
for (const error of result.errors) {
|
|
674
|
+
console.log(`error: ${error}`);
|
|
675
|
+
}
|
|
676
|
+
for (const stalePath of result.stale_paths) {
|
|
677
|
+
console.log(`stale: ${stalePath}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!result.ok) {
|
|
681
|
+
throw new errors_1.ValidationError("bundle verify failed");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function runBundleShowCommand(options) {
|
|
685
|
+
const bundlePath = resolveBundlePath(options.root, options.bundlePath);
|
|
686
|
+
if (!fs_1.default.existsSync(bundlePath)) {
|
|
687
|
+
throw new errors_1.NotFoundError(`bundle not found: ${options.bundlePath}`);
|
|
688
|
+
}
|
|
689
|
+
const { manifest } = parseBundle(bundlePath);
|
|
690
|
+
const summary = bundleSummary(manifest, path_1.default.relative(options.root, bundlePath) || bundlePath, sha256Buffer(fs_1.default.readFileSync(bundlePath)));
|
|
691
|
+
if (options.json) {
|
|
692
|
+
writeJson({ action: "show", bundle: summary, manifest });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
console.log(`${summary.path} | ${summary.profile} | ${summary.file_count} file(s)`);
|
|
696
|
+
console.log(`workspaces: ${summary.selected_workspaces.join(", ") || "(none)"}`);
|
|
697
|
+
console.log(`source: ${summary.source.git_head ?? "unknown"}${summary.source.dirty ? " dirty" : ""}`);
|
|
698
|
+
console.log(`bundle_hash: ${summary.bundle_hash}`);
|
|
699
|
+
}
|
|
700
|
+
function runBundleListCommand(options) {
|
|
701
|
+
const config = (0, config_1.loadConfig)(options.root);
|
|
702
|
+
const rootDir = path_1.default.resolve(options.root, config.bundles.output_dir);
|
|
703
|
+
const files = listFilesRecursive(rootDir)
|
|
704
|
+
.filter((filePath) => filePath.endsWith(".mdkg.zip"))
|
|
705
|
+
.sort();
|
|
706
|
+
const items = files.map((filePath) => {
|
|
707
|
+
try {
|
|
708
|
+
const { manifest } = parseBundle(filePath);
|
|
709
|
+
return {
|
|
710
|
+
path: relativePath(options.root, filePath),
|
|
711
|
+
ok: true,
|
|
712
|
+
profile: manifest.profile,
|
|
713
|
+
selected_workspaces: manifest.selected_workspaces,
|
|
714
|
+
file_count: manifest.file_count,
|
|
715
|
+
bundle_hash: manifest.bundle_hash,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
catch (err) {
|
|
719
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
720
|
+
return {
|
|
721
|
+
path: relativePath(options.root, filePath),
|
|
722
|
+
ok: false,
|
|
723
|
+
error: message,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
if (options.json) {
|
|
728
|
+
writeJson({ action: "list", count: items.length, items });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (items.length === 0) {
|
|
732
|
+
console.log("no bundles found");
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
for (const item of items) {
|
|
736
|
+
if (item.ok) {
|
|
737
|
+
console.log(`${item.path} | ${item.profile} | ${item.file_count} file(s)`);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
console.log(`${item.path} | invalid | ${item.error}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|