@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.
- package/package.json +13 -0
- 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
|
+
|