@zebralabs/context-cli 0.1.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.
Files changed (2) hide show
  1. package/package.json +13 -0
  2. package/src/context.js +517 -0
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@zebralabs/context-cli",
3
+ "version": "0.1.0",
4
+ "description": "Context-as-Code CLI (help/list/compile/validate)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "ctx": "src/context.js"
9
+ },
10
+ "dependencies": {
11
+ "yaml": "^2.5.0"
12
+ }
13
+ }
package/src/context.js ADDED
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env node
2
+ import os from "node:os";
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import YAML from "yaml";
8
+
9
+ function die(msg, code = 1) {
10
+ console.error(`ERROR: ${msg}`);
11
+ process.exit(code);
12
+ }
13
+
14
+ function showHelp() {
15
+ console.log(`
16
+ Context-as-Code CLI
17
+
18
+ Usage:
19
+ context help
20
+ context list
21
+ context compile
22
+ context validate
23
+
24
+ Commands:
25
+ list Show installed packs and the documentation roots they contribute.
26
+ compile Build a single AI-ready context bundle at practices-and-standards/.compiled/.
27
+ Output: system-prompt.md (paste/upload to AI), sources.md (what was included), report.md.
28
+ validate Validate that context.yaml and pack manifests are consistent and files exist.
29
+ `.trim() + "\n");
30
+ }
31
+
32
+ function findRepoContextRoot(startDir) {
33
+ let dir = startDir;
34
+ while (true) {
35
+ const candidate = path.join(dir, "practices-and-standards", "context.yaml");
36
+ if (fs.existsSync(candidate)) return dir;
37
+ const parent = path.dirname(dir);
38
+ if (parent === dir) return null;
39
+ dir = parent;
40
+ }
41
+ }
42
+
43
+ function readYamlFile(filePath) {
44
+ const raw = fs.readFileSync(filePath, "utf8");
45
+ return YAML.parse(raw);
46
+ }
47
+
48
+ function listMarkdownFiles(dir) {
49
+ const out = [];
50
+ const stack = [dir];
51
+ while (stack.length) {
52
+ const cur = stack.pop();
53
+ const entries = fs.readdirSync(cur, { withFileTypes: true });
54
+ for (const e of entries) {
55
+ const full = path.join(cur, e.name);
56
+ if (e.isDirectory()) stack.push(full);
57
+ else if (e.isFile() && e.name.toLowerCase().endsWith(".md")) out.push(full);
58
+ }
59
+ }
60
+ out.sort((a, b) => a.localeCompare(b));
61
+ return out;
62
+ }
63
+
64
+ function stripFrontmatter(text) {
65
+ // Remove YAML frontmatter if the file starts with ---
66
+ if (!text.startsWith("---\n") && !text.startsWith("---\r\n")) return text;
67
+ const lines = text.split(/\r?\n/);
68
+ let fenceCount = 0;
69
+ let endIndex = -1;
70
+ for (let i = 0; i < lines.length; i++) {
71
+ if (lines[i].trim() === "---") {
72
+ fenceCount++;
73
+ if (fenceCount === 2) {
74
+ endIndex = i;
75
+ break;
76
+ }
77
+ }
78
+ }
79
+ if (endIndex === -1) return text; // malformed; keep
80
+ return lines.slice(endIndex + 1).join("\n");
81
+ }
82
+
83
+ function orderInstalledPacks(installed, precedence) {
84
+ if (!Array.isArray(precedence) || precedence.length === 0) return installed;
85
+
86
+ const byId = new Map(installed.map(p => [p.id, p]));
87
+ const ordered = [];
88
+
89
+ for (const id of precedence) {
90
+ if (byId.has(id)) {
91
+ ordered.push(byId.get(id));
92
+ byId.delete(id);
93
+ }
94
+ }
95
+ // stable remainder in original order
96
+ for (const p of installed) {
97
+ if (byId.has(p.id)) {
98
+ ordered.push(p);
99
+ byId.delete(p.id);
100
+ }
101
+ }
102
+ return ordered;
103
+ }
104
+
105
+ function resolvePackManifestPath(repoRoot, manifestRel) {
106
+ // manifestRel in your schema is "practices-and-standards/packs/..."
107
+ return path.join(repoRoot, manifestRel);
108
+ }
109
+
110
+ function cmdList(repoRoot, ctx) {
111
+ const installed = orderInstalledPacks(ctx.installed_packs ?? [], ctx.precedence ?? []);
112
+
113
+ console.log("\nInstalled Context Packs");
114
+ console.log("-----------------------");
115
+ for (const p of installed) {
116
+ const manifestPath = resolvePackManifestPath(repoRoot, p.manifest);
117
+ if (!fs.existsSync(manifestPath)) die(`Missing pack manifest: ${manifestPath}`);
118
+
119
+ const pack = readYamlFile(manifestPath);
120
+ const roots = pack?.contributes?.roots ?? [];
121
+
122
+ console.log(`\n- ${p.id} @ ${p.version}`);
123
+ console.log(" Contributes:");
124
+ for (const r of roots) console.log(` - ${r}`);
125
+ }
126
+ console.log("");
127
+ }
128
+
129
+ function cmdValidate(repoRoot, ctx) {
130
+ const errors = [];
131
+ const warnings = [];
132
+
133
+ const installed = ctx.installed_packs ?? [];
134
+ const precedence = ctx.precedence ?? [];
135
+
136
+ if (!Array.isArray(installed) || installed.length === 0) {
137
+ warnings.push("No installed packs found in context.yaml");
138
+ }
139
+
140
+ // duplicate installed ids
141
+ const seen = new Set();
142
+ for (const p of installed) {
143
+ if (!p?.id) errors.push("Installed pack missing id");
144
+ else {
145
+ if (seen.has(p.id)) errors.push(`Duplicate installed pack id: ${p.id}`);
146
+ seen.add(p.id);
147
+ }
148
+ if (!p?.manifest) errors.push(`Pack '${p?.id ?? "unknown"}' missing manifest path`);
149
+ }
150
+
151
+ // precedence unknown ids
152
+ const installedIds = new Set(installed.map(p => p.id).filter(Boolean));
153
+ for (const pid of precedence) {
154
+ if (!installedIds.has(pid)) warnings.push(`precedence references unknown pack id: ${pid}`);
155
+ }
156
+
157
+ // manifest checks
158
+ for (const p of installed) {
159
+ if (!p?.id || !p?.manifest) continue;
160
+
161
+ const manifestPath = resolvePackManifestPath(repoRoot, p.manifest);
162
+ if (!fs.existsSync(manifestPath)) {
163
+ errors.push(`Pack '${p.id}' manifest not found: ${manifestPath}`);
164
+ continue;
165
+ }
166
+
167
+ const pack = readYamlFile(manifestPath);
168
+ const manifestId = pack?.id;
169
+ if (manifestId && manifestId !== p.id) {
170
+ warnings.push(`Pack '${p.id}' manifest id mismatch: manifest says '${manifestId}'`);
171
+ }
172
+
173
+ const parts = p.manifest.split(/[\\/]/);
174
+ const packsIdx = parts.indexOf("packs");
175
+ const folderId = (packsIdx >= 0 && parts[packsIdx + 1]) ? parts[packsIdx + 1] : null;
176
+ if (folderId && folderId !== p.id) {
177
+ warnings.push(`Pack '${p.id}' manifest folder mismatch: path folder is '${folderId}'`);
178
+ }
179
+
180
+ const roots = pack?.contributes?.roots ?? [];
181
+ if (!Array.isArray(roots) || roots.length === 0) {
182
+ warnings.push(`Pack '${p.id}' has no contributes.roots`);
183
+ } else {
184
+ for (const r of roots) {
185
+ const rootPath = path.join(repoRoot, r);
186
+ if (!fs.existsSync(rootPath)) errors.push(`Pack '${p.id}' contributes root missing on disk: ${r}`);
187
+ }
188
+ }
189
+ }
190
+
191
+ console.log("\nValidation Report");
192
+ console.log("-----------------");
193
+
194
+ if (errors.length === 0) console.log("✅ OK: No blocking errors found.");
195
+ else {
196
+ console.log("❌ Errors:");
197
+ for (const e of errors) console.log(` - ${e}`);
198
+ }
199
+
200
+ if (warnings.length > 0) {
201
+ console.log("\n⚠️ Warnings:");
202
+ for (const w of warnings) console.log(` - ${w}`);
203
+ }
204
+
205
+ console.log("");
206
+ process.exit(errors.length > 0 ? 1 : 0);
207
+ }
208
+
209
+ function cmdCompile(repoRoot, ctx) {
210
+ const installed = orderInstalledPacks(ctx.installed_packs ?? [], ctx.precedence ?? []);
211
+ const outDir = path.join(repoRoot, "practices-and-standards", ".compiled");
212
+ fs.mkdirSync(outDir, { recursive: true });
213
+
214
+ const promptPath = path.join(outDir, "system-prompt.md");
215
+ const sourcesPath = path.join(outDir, "sources.md");
216
+ const reportPath = path.join(outDir, "report.md");
217
+
218
+ const compiledPacks = [];
219
+ const sources = [];
220
+ const docFiles = [];
221
+
222
+ for (const p of installed) {
223
+ const manifestPath = resolvePackManifestPath(repoRoot, p.manifest);
224
+ if (!fs.existsSync(manifestPath)) die(`Missing pack manifest: ${manifestPath}`);
225
+
226
+ const pack = readYamlFile(manifestPath);
227
+ compiledPacks.push(`- ${p.id} @ ${p.version}`);
228
+
229
+ const roots = pack?.contributes?.roots ?? [];
230
+ for (const r of roots) {
231
+ const rootPath = path.join(repoRoot, r);
232
+ if (!fs.existsSync(rootPath)) continue;
233
+ const files = listMarkdownFiles(rootPath);
234
+ for (const f of files) {
235
+ docFiles.push(f);
236
+ sources.push(path.relative(repoRoot, f).replaceAll("\\", "/"));
237
+ }
238
+ }
239
+ }
240
+
241
+ docFiles.sort((a, b) => a.localeCompare(b));
242
+
243
+ const lines = [];
244
+ lines.push("# AI Context: Engineering & Documentation Standards", "");
245
+ lines.push("You must follow the standards and methodologies below.", "");
246
+
247
+ for (const f of docFiles) {
248
+ let content = fs.readFileSync(f, "utf8");
249
+ content = stripFrontmatter(content);
250
+
251
+ lines.push("");
252
+ lines.push(`## Source: ${path.basename(f)}`);
253
+ lines.push("");
254
+ lines.push(content.trimEnd());
255
+ }
256
+
257
+ fs.writeFileSync(promptPath, lines.join("\n") + "\n", "utf8");
258
+
259
+ const uniqSources = [...new Set(sources)].sort();
260
+ fs.writeFileSync(
261
+ sourcesPath,
262
+ ["# Context Sources", "", ...uniqSources.map(s => `- ${s}`)].join("\n") + "\n",
263
+ "utf8"
264
+ );
265
+
266
+ const promptSizeBytes = fs.statSync(promptPath).size;
267
+ const report = [
268
+ "# Context Compile Report",
269
+ "",
270
+ `- Date: ${new Date().toISOString()}`,
271
+ `- Documents included: ${docFiles.length}`,
272
+ `- Unique sources: ${uniqSources.length}`,
273
+ `- system-prompt.md size: ${promptSizeBytes} bytes`,
274
+ "",
275
+ "## Packs Compiled",
276
+ ...compiledPacks,
277
+ "",
278
+ "## Outputs",
279
+ `- ${path.relative(repoRoot, promptPath).replaceAll("\\", "/")}`,
280
+ `- ${path.relative(repoRoot, sourcesPath).replaceAll("\\", "/")}`,
281
+ `- ${path.relative(repoRoot, reportPath).replaceAll("\\", "/")}`
282
+ ].join("\n") + "\n";
283
+
284
+ fs.writeFileSync(reportPath, report, "utf8");
285
+
286
+ console.log("\nContext compiled successfully");
287
+ console.log("Output:");
288
+ console.log(` ${promptPath}`);
289
+ console.log(` ${sourcesPath}`);
290
+ console.log(` ${reportPath}`);
291
+ console.log("");
292
+ }
293
+
294
+ function ensureDir(p) {
295
+ fs.mkdirSync(p, { recursive: true });
296
+ }
297
+
298
+ function writeYamlFile(filePath, obj) {
299
+ fs.writeFileSync(filePath, YAML.stringify(obj), "utf8");
300
+ }
301
+
302
+ function readYamlIfExists(filePath) {
303
+ if (!fs.existsSync(filePath)) return null;
304
+ return readYamlFile(filePath);
305
+ }
306
+
307
+ function isWindows() {
308
+ return process.platform === "win32";
309
+ }
310
+
311
+ function findPwsh() {
312
+ // Prefer pwsh (PowerShell 7). Fallback to Windows PowerShell on Windows.
313
+ // We'll just try spawning; if it fails, we fallback.
314
+ return "pwsh";
315
+ }
316
+
317
+ function runPwsh(command, { cwd } = {}) {
318
+ let exe = findPwsh();
319
+ let r = spawnSync(exe, ["-NoProfile", "-Command", command], { stdio: "inherit", cwd });
320
+ if (r.error && isWindows()) {
321
+ // fallback to Windows PowerShell
322
+ exe = "powershell";
323
+ r = spawnSync(exe, ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command], { stdio: "inherit", cwd });
324
+ }
325
+ if (r.status !== 0) {
326
+ die(`PowerShell command failed (exit ${r.status}): ${command}`);
327
+ }
328
+ }
329
+
330
+ async function downloadToFile(url, token, outPath) {
331
+ const res = await fetch(url, {
332
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined
333
+ });
334
+ if (!res.ok) {
335
+ const body = await res.text().catch(() => "");
336
+ die(`Download failed (${res.status} ${res.statusText}): ${url}\n${body}`);
337
+ }
338
+ const buf = Buffer.from(await res.arrayBuffer());
339
+ fs.writeFileSync(outPath, buf);
340
+ }
341
+
342
+ async function fetchJson(url, token) {
343
+ const res = await fetch(url, {
344
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined
345
+ });
346
+ if (!res.ok) {
347
+ const body = await res.text().catch(() => "");
348
+ die(`Request failed (${res.status} ${res.statusText}): ${url}\n${body}`);
349
+ }
350
+ return await res.json();
351
+ }
352
+
353
+ function ensureContextInitialized(repoRoot, registryUrlMaybe) {
354
+ const psRoot = path.join(repoRoot, "practices-and-standards");
355
+ ensureDir(psRoot);
356
+
357
+ const ctxPath = path.join(psRoot, "context.yaml");
358
+ let ctx = readYamlIfExists(ctxPath);
359
+
360
+ if (!ctx) {
361
+ ctx = {
362
+ schema: "context-install/v1",
363
+ ...(registryUrlMaybe ? { registry: registryUrlMaybe } : {}),
364
+ installed_packs: [],
365
+ precedence: []
366
+ };
367
+ writeYamlFile(ctxPath, ctx);
368
+ } else {
369
+ // Ensure schema exists
370
+ if (!ctx.schema) ctx.schema = "context-install/v1";
371
+ if (registryUrlMaybe && !ctx.registry) ctx.registry = registryUrlMaybe;
372
+ if (!Array.isArray(ctx.installed_packs)) ctx.installed_packs = [];
373
+ if (!Array.isArray(ctx.precedence)) ctx.precedence = [];
374
+ writeYamlFile(ctxPath, ctx);
375
+ }
376
+
377
+ return { ctxPath, ctx, psRoot };
378
+ }
379
+
380
+ function upsertInstalledPack(ctxObj, packId, version) {
381
+ if (!Array.isArray(ctxObj.installed_packs)) ctxObj.installed_packs = [];
382
+
383
+ const manifest = `practices-and-standards/packs/${packId}/pack.yaml`;
384
+ const existing = ctxObj.installed_packs.find(p => p.id === packId);
385
+
386
+ if (existing) {
387
+ existing.version = version;
388
+ existing.manifest = manifest;
389
+ } else {
390
+ ctxObj.installed_packs.push({ id: packId, version, manifest });
391
+ }
392
+
393
+ // Keep precedence additive unless user already configured it
394
+ if (!Array.isArray(ctxObj.precedence)) ctxObj.precedence = [];
395
+ if (!ctxObj.precedence.includes(packId)) ctxObj.precedence.push(packId);
396
+ }
397
+
398
+ async function cmdPackInstall(repoRoot, packId, opts) {
399
+ const token = opts.token;
400
+ if (!token) die("Missing required --token");
401
+
402
+ // init or load context
403
+ const { ctxPath, ctx } = ensureContextInitialized(repoRoot, opts.registry);
404
+
405
+ // Determine registry
406
+ const registry = opts.registry || ctx.registry;
407
+ if (!registry) die("No registry configured. Provide --registry <url> or set registry: in practices-and-standards/context.yaml");
408
+
409
+ // Determine version
410
+ let version = opts.version;
411
+ if (!version) {
412
+ const latestUrl = `${registry.replace(/\/$/, "")}/packs/${encodeURIComponent(packId)}/latest`;
413
+ const latest = await fetchJson(latestUrl, token);
414
+ version = latest?.version;
415
+ if (!version) die(`Registry did not return a version from ${latestUrl}`);
416
+ }
417
+
418
+ // Download zip
419
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "context-pack-"));
420
+ const zipPath = path.join(tmpBase, `${packId}-${version}.zip`);
421
+ const extractDir = path.join(tmpBase, "extract");
422
+ ensureDir(extractDir);
423
+
424
+ const dlUrl = `${registry.replace(/\/$/, "")}/packs/${encodeURIComponent(packId)}/${encodeURIComponent(version)}/download`;
425
+ console.log(`\nDownloading ${packId}@${version}...`);
426
+ await downloadToFile(dlUrl, token, zipPath);
427
+
428
+ // Expand zip using PowerShell (no Node unzip deps)
429
+ console.log("Extracting...");
430
+ runPwsh(`Expand-Archive -Path "${zipPath}" -DestinationPath "${extractDir}" -Force`);
431
+
432
+ // Find installer inside extracted zip
433
+ // Your zips contain: practices-and-standards/install.ps1 at the root of the zip payload
434
+ const extractedPsRoot = path.join(extractDir, "practices-and-standards");
435
+ const installer = path.join(extractedPsRoot, "install.ps1");
436
+ if (!fs.existsSync(installer)) {
437
+ die(`Downloaded pack does not contain practices-and-standards/install.ps1 (looked at: ${installer})`);
438
+ }
439
+
440
+ // Run installer: it will merge into repoRoot/practices-and-standards
441
+ console.log("Installing (merge)...");
442
+ const cmd = `& "${installer}" -TargetRoot "${repoRoot}" -PackId "${packId}" -Mode "SkipExisting"`;
443
+ runPwsh(cmd, { cwd: extractDir });
444
+
445
+ // Update context.yaml
446
+ const ctx2 = readYamlFile(ctxPath);
447
+ upsertInstalledPack(ctx2, packId, version);
448
+ if (opts.registry && !ctx2.registry) ctx2.registry = opts.registry;
449
+ writeYamlFile(ctxPath, ctx2);
450
+
451
+ console.log("\n✅ Pack installed.");
452
+ console.log(`- Pack: ${packId} @ ${version}`);
453
+ console.log(`- Updated: ${path.relative(repoRoot, ctxPath).replaceAll("\\", "/")}`);
454
+ console.log("");
455
+ }
456
+
457
+
458
+ async function main() {
459
+ const cmd = (process.argv[2] ?? "help").toLowerCase();
460
+
461
+ if (cmd === "help") { showHelp(); return; }
462
+
463
+ // pack subcommands
464
+ if (cmd === "pack") {
465
+ const sub = (process.argv[3] ?? "").toLowerCase();
466
+ if (sub !== "install") {
467
+ console.log(`Unknown pack subcommand: ${sub}\n`);
468
+ showHelp();
469
+ process.exit(1);
470
+ }
471
+
472
+ const packId = process.argv[4];
473
+ if (!packId) die("Usage: context pack install <packId> --token <token> [--version <v>] [--registry <url>]");
474
+
475
+ // parse flags
476
+ const args = process.argv.slice(5);
477
+ const opts = {};
478
+ for (let i = 0; i < args.length; i++) {
479
+ const a = args[i];
480
+ if (a === "--token") opts.token = args[++i];
481
+ else if (a === "--version") opts.version = args[++i];
482
+ else if (a === "--registry") opts.registry = args[++i];
483
+ else die(`Unknown option: ${a}`);
484
+ }
485
+
486
+ // Repo root: prefer existing context.yaml search, else assume cwd is repo root
487
+ const found = findRepoContextRoot(process.cwd());
488
+ const repoRoot = found ?? process.cwd();
489
+
490
+ await cmdPackInstall(repoRoot, packId, opts);
491
+ return;
492
+ }
493
+
494
+ // Existing behavior: these require an existing context.yaml
495
+ const repoRoot = findRepoContextRoot(process.cwd());
496
+ if (!repoRoot) die("Could not find practices-and-standards/context.yaml in this directory or any parent.");
497
+
498
+ const contextPath = path.join(repoRoot, "practices-and-standards", "context.yaml");
499
+ const ctx = readYamlFile(contextPath);
500
+
501
+ if (!ctx?.schema || ctx.schema !== "context-install/v1") {
502
+ die(`Unsupported or missing schema in context.yaml (expected 'context-install/v1')`);
503
+ }
504
+
505
+ switch (cmd) {
506
+ case "list": return cmdList(repoRoot, ctx);
507
+ case "compile": return cmdCompile(repoRoot, ctx);
508
+ case "validate": return cmdValidate(repoRoot, ctx);
509
+ default:
510
+ console.log(`Unknown command: ${cmd}\n`);
511
+ showHelp();
512
+ process.exit(1);
513
+ }
514
+ }
515
+
516
+ main().catch(e => die(e?.stack || String(e)));
517
+