@stndrds/cli 1.0.0-alpha.258 → 1.0.0-alpha.260
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/dist/assets/standards-drift.yml +242 -0
- package/dist/assets/standards-pull-skill.md +101 -0
- package/dist/bin.mjs +753 -213
- package/package.json +5 -2
- package/dist/assets/schema-builder-reference.md +0 -513
package/dist/bin.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/program.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk9 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/client.ts
|
|
@@ -60,6 +60,29 @@ function createClient(config) {
|
|
|
60
60
|
body: options?.body ? JSON.stringify(options.body) : void 0
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
+
async function getText(path, params) {
|
|
64
|
+
const url = buildUrl(apiUrl, path, params);
|
|
65
|
+
let response;
|
|
66
|
+
try {
|
|
67
|
+
response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(3e4) });
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error instanceof TypeError || error instanceof DOMException && error.name === "TimeoutError") {
|
|
70
|
+
throw new ApiClientError(0, `Could not connect to ${apiUrl}. Is the server running?`);
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
const text = await response.text();
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
let message = `Request failed with status ${response.status}`;
|
|
77
|
+
try {
|
|
78
|
+
message = JSON.parse(text).message ?? message;
|
|
79
|
+
} catch {
|
|
80
|
+
if (text) message = text;
|
|
81
|
+
}
|
|
82
|
+
throw new ApiClientError(response.status, message);
|
|
83
|
+
}
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
63
86
|
function postMultipart(path, form) {
|
|
64
87
|
const url = buildUrl(apiUrl, path);
|
|
65
88
|
const { "Content-Type": _ct, ...multipartHeaders } = headers;
|
|
@@ -81,6 +104,7 @@ function createClient(config) {
|
|
|
81
104
|
delete(path) {
|
|
82
105
|
return request("DELETE", path);
|
|
83
106
|
},
|
|
107
|
+
getText,
|
|
84
108
|
postMultipart
|
|
85
109
|
};
|
|
86
110
|
}
|
|
@@ -105,6 +129,9 @@ function getClientFromCommand(cmd) {
|
|
|
105
129
|
}
|
|
106
130
|
return createClient({ apiUrl: root.apiUrl, apiKey: root.apiKey, tenantId: root.tenant });
|
|
107
131
|
}
|
|
132
|
+
function messageOf(error) {
|
|
133
|
+
return error instanceof Error ? error.message : String(error);
|
|
134
|
+
}
|
|
108
135
|
|
|
109
136
|
// src/commands/auth.ts
|
|
110
137
|
function registerAuthCommand(program) {
|
|
@@ -278,6 +305,354 @@ function registerConnectorsCommand(program) {
|
|
|
278
305
|
});
|
|
279
306
|
}
|
|
280
307
|
|
|
308
|
+
// src/commands/diff.ts
|
|
309
|
+
import chalk4 from "chalk";
|
|
310
|
+
|
|
311
|
+
// src/drift/engine.ts
|
|
312
|
+
import { createHash } from "crypto";
|
|
313
|
+
import { ValidationError, canonicalStringify, viewSyncPayload } from "@stndrds/schema";
|
|
314
|
+
var SUPPORTED_HASH_VERSION = 1;
|
|
315
|
+
var CONFIG_SUBKEYS = ["tabs", "defaultFilters", "sidePanel"];
|
|
316
|
+
function viewSyncHashOf(view) {
|
|
317
|
+
return createHash("sha256").update(canonicalStringify(viewSyncPayload(view))).digest("hex");
|
|
318
|
+
}
|
|
319
|
+
function viewKey(object, name, type) {
|
|
320
|
+
return `${object}:${name}:${type}`;
|
|
321
|
+
}
|
|
322
|
+
function classifyViewDrift(input2) {
|
|
323
|
+
const { codeHash, dbHash, baselineHash, resolutions } = input2;
|
|
324
|
+
if (baselineHash === null) {
|
|
325
|
+
return codeHash === dbHash ? "in-sync" : "runtime-diverged";
|
|
326
|
+
}
|
|
327
|
+
const codeMoved = codeHash !== baselineHash;
|
|
328
|
+
const dbMoved = dbHash !== baselineHash;
|
|
329
|
+
if (!(codeMoved || dbMoved)) {
|
|
330
|
+
return "in-sync";
|
|
331
|
+
}
|
|
332
|
+
if (codeMoved && !dbMoved) {
|
|
333
|
+
return "code-changed";
|
|
334
|
+
}
|
|
335
|
+
if (!codeMoved && dbMoved) {
|
|
336
|
+
return "runtime-diverged";
|
|
337
|
+
}
|
|
338
|
+
const hasExactResolution = resolutions.some(
|
|
339
|
+
(resolution) => resolution.codeHash === codeHash && resolution.dbHash === dbHash
|
|
340
|
+
);
|
|
341
|
+
return hasExactResolution ? "conflict-resolved" : "conflict";
|
|
342
|
+
}
|
|
343
|
+
function computeChangedFields(localView, entry) {
|
|
344
|
+
const changedFields = [];
|
|
345
|
+
if (canonicalStringify(localView.label) !== canonicalStringify(entry.label)) {
|
|
346
|
+
changedFields.push("label");
|
|
347
|
+
}
|
|
348
|
+
if ((localView.description ?? null) !== entry.description) {
|
|
349
|
+
changedFields.push("description");
|
|
350
|
+
}
|
|
351
|
+
if ((localView.icon ?? null) !== entry.icon) {
|
|
352
|
+
changedFields.push("icon");
|
|
353
|
+
}
|
|
354
|
+
if ((localView.default ?? false) !== entry.default) {
|
|
355
|
+
changedFields.push("default");
|
|
356
|
+
}
|
|
357
|
+
if (canonicalStringify(localView.metadata ?? null) !== canonicalStringify(entry.metadata)) {
|
|
358
|
+
changedFields.push("metadata");
|
|
359
|
+
}
|
|
360
|
+
const localConfig = localView.config;
|
|
361
|
+
const dbConfig = entry.config;
|
|
362
|
+
for (const key of CONFIG_SUBKEYS) {
|
|
363
|
+
const localValue = localConfig[key] ?? null;
|
|
364
|
+
const dbValue = dbConfig[key] ?? null;
|
|
365
|
+
if (canonicalStringify(localValue) !== canonicalStringify(dbValue)) {
|
|
366
|
+
changedFields.push(`config.${key}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return changedFields;
|
|
370
|
+
}
|
|
371
|
+
function computeDriftReport(local, state) {
|
|
372
|
+
if (state.hashVersion !== SUPPORTED_HASH_VERSION) {
|
|
373
|
+
throw new ValidationError(
|
|
374
|
+
`Server hash version ${state.hashVersion} is not supported by this CLI (expected ${SUPPORTED_HASH_VERSION}). Upgrade the CLI or the server so both sides compute comparable hashes.`,
|
|
375
|
+
[]
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const stateByKey = /* @__PURE__ */ new Map();
|
|
379
|
+
for (const entry of state.views) {
|
|
380
|
+
stateByKey.set(viewKey(entry.object, entry.name, entry.type), entry);
|
|
381
|
+
}
|
|
382
|
+
const localKeys = /* @__PURE__ */ new Set();
|
|
383
|
+
const views = [];
|
|
384
|
+
const created = [];
|
|
385
|
+
for (const localView of local.views) {
|
|
386
|
+
const key = viewKey(localView.object, localView.name, localView.type);
|
|
387
|
+
localKeys.add(key);
|
|
388
|
+
const entry = stateByKey.get(key);
|
|
389
|
+
if (!entry) {
|
|
390
|
+
created.push({ key, object: localView.object, name: localView.name, type: localView.type });
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const codeHash = viewSyncHashOf(localView);
|
|
394
|
+
const driftState = classifyViewDrift({
|
|
395
|
+
codeHash,
|
|
396
|
+
dbHash: entry.dbHash,
|
|
397
|
+
baselineHash: entry.baselineHash,
|
|
398
|
+
resolutions: entry.resolutions
|
|
399
|
+
});
|
|
400
|
+
views.push({
|
|
401
|
+
key,
|
|
402
|
+
type: localView.type,
|
|
403
|
+
state: driftState,
|
|
404
|
+
viewId: entry.viewId,
|
|
405
|
+
codeHash,
|
|
406
|
+
dbHash: entry.dbHash,
|
|
407
|
+
changedFields: computeChangedFields(localView, entry)
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const orphans = state.views.filter((entry) => !localKeys.has(viewKey(entry.object, entry.name, entry.type))).map((entry) => ({
|
|
411
|
+
key: viewKey(entry.object, entry.name, entry.type),
|
|
412
|
+
viewId: entry.viewId,
|
|
413
|
+
object: entry.object,
|
|
414
|
+
name: entry.name,
|
|
415
|
+
type: entry.type
|
|
416
|
+
}));
|
|
417
|
+
const hasUnresolvedConflict = views.some((view) => view.state === "conflict");
|
|
418
|
+
const exitCode = hasUnresolvedConflict || state.drift.hasUnexpectedDrift ? 1 : 0;
|
|
419
|
+
return { views, created, orphans, schema: state.drift, exitCode };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/schema-loader.ts
|
|
423
|
+
import { dirname, resolve } from "path";
|
|
424
|
+
import { ValidationError as ValidationError2 } from "@stndrds/schema";
|
|
425
|
+
import * as esbuild from "esbuild";
|
|
426
|
+
async function loadLocalRegistry(entryPath) {
|
|
427
|
+
const absoluteEntryPath = resolve(entryPath);
|
|
428
|
+
const virtualEntry = [
|
|
429
|
+
`import ${JSON.stringify(absoluteEntryPath)};`,
|
|
430
|
+
`export { registry, viewRegistry } from "@stndrds/schema";`
|
|
431
|
+
].join("\n");
|
|
432
|
+
let bundledCode;
|
|
433
|
+
try {
|
|
434
|
+
const result = await esbuild.build({
|
|
435
|
+
stdin: {
|
|
436
|
+
contents: virtualEntry,
|
|
437
|
+
resolveDir: dirname(absoluteEntryPath),
|
|
438
|
+
loader: "ts",
|
|
439
|
+
sourcefile: "standards-diff-virtual-entry.ts"
|
|
440
|
+
},
|
|
441
|
+
bundle: true,
|
|
442
|
+
platform: "node",
|
|
443
|
+
format: "esm",
|
|
444
|
+
write: false,
|
|
445
|
+
external: ["node:*"],
|
|
446
|
+
logLevel: "silent"
|
|
447
|
+
});
|
|
448
|
+
const outputFile = result.outputFiles[0];
|
|
449
|
+
if (!outputFile) {
|
|
450
|
+
throw entryLoadError(absoluteEntryPath, new Error("esbuild produced no output"));
|
|
451
|
+
}
|
|
452
|
+
bundledCode = outputFile.text;
|
|
453
|
+
} catch (error) {
|
|
454
|
+
throw entryLoadError(absoluteEntryPath, error);
|
|
455
|
+
}
|
|
456
|
+
let loadedModule;
|
|
457
|
+
try {
|
|
458
|
+
const dataUrl = `data:text/javascript;base64,${Buffer.from(bundledCode, "utf-8").toString("base64")}`;
|
|
459
|
+
loadedModule = await import(dataUrl);
|
|
460
|
+
} catch (error) {
|
|
461
|
+
throw entryLoadError(absoluteEntryPath, error);
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
objects: loadedModule.registry.getAll(),
|
|
465
|
+
views: loadedModule.viewRegistry.getAll()
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function entryLoadError(entryPath, cause) {
|
|
469
|
+
if (cause instanceof ValidationError2) {
|
|
470
|
+
return cause;
|
|
471
|
+
}
|
|
472
|
+
const causeMessage = cause instanceof Error ? cause.message : String(cause);
|
|
473
|
+
return new ValidationError2(
|
|
474
|
+
`Schema entry failed to load in isolation: ${entryPath}
|
|
475
|
+
${causeMessage}`,
|
|
476
|
+
[]
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/standards-config.ts
|
|
481
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
482
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
483
|
+
import { ValidationError as ValidationError3 } from "@stndrds/schema";
|
|
484
|
+
|
|
485
|
+
// src/standards-dir.ts
|
|
486
|
+
import { existsSync } from "fs";
|
|
487
|
+
import { join } from "path";
|
|
488
|
+
function resolveStandardsDir(startDir = process.cwd()) {
|
|
489
|
+
let dir = startDir;
|
|
490
|
+
while (true) {
|
|
491
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
492
|
+
return join(dir, ".standards");
|
|
493
|
+
}
|
|
494
|
+
const parent = join(dir, "..");
|
|
495
|
+
if (parent === dir) break;
|
|
496
|
+
dir = parent;
|
|
497
|
+
}
|
|
498
|
+
return join(startDir, ".standards");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/standards-config.ts
|
|
502
|
+
function readStandardsProjectConfig(startDir = process.cwd()) {
|
|
503
|
+
const standardsDir = resolveStandardsDir(startDir);
|
|
504
|
+
const projectRoot = dirname2(standardsDir);
|
|
505
|
+
const configPath = join2(standardsDir, "config.json");
|
|
506
|
+
if (!existsSync2(configPath)) {
|
|
507
|
+
throw new ValidationError3(
|
|
508
|
+
`No Standards project config found at ${configPath}. Run "standards init" to create one.`,
|
|
509
|
+
[]
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
513
|
+
let parsed;
|
|
514
|
+
try {
|
|
515
|
+
parsed = JSON.parse(raw);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
const causeMessage = error instanceof Error ? error.message : String(error);
|
|
518
|
+
throw new ValidationError3(`${configPath} is not valid JSON: ${causeMessage}`, []);
|
|
519
|
+
}
|
|
520
|
+
if (typeof parsed.schemaEntry !== "string" || parsed.schemaEntry.length === 0) {
|
|
521
|
+
throw new ValidationError3(
|
|
522
|
+
`${configPath} is missing required "schemaEntry" field. Run "standards init" to (re)create it.`,
|
|
523
|
+
[]
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
schemaEntry: resolve2(projectRoot, parsed.schemaEntry)
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// src/commands/diff.ts
|
|
532
|
+
async function runDiffCommand(deps) {
|
|
533
|
+
const loadRegistry = deps.loadRegistry ?? loadLocalRegistry;
|
|
534
|
+
let local;
|
|
535
|
+
let state;
|
|
536
|
+
try {
|
|
537
|
+
local = await loadRegistry(deps.schemaEntry);
|
|
538
|
+
state = await deps.client.get("/schema/state");
|
|
539
|
+
} catch (error) {
|
|
540
|
+
return { exitCode: 2, errorMessage: messageOf(error) };
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
const report = computeDriftReport(local, state);
|
|
544
|
+
return { exitCode: report.exitCode, report, state };
|
|
545
|
+
} catch (error) {
|
|
546
|
+
return { exitCode: 2, errorMessage: messageOf(error) };
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function renderCreatedLine(view) {
|
|
550
|
+
return chalk4.dim(`+ view ${view.key} \u2014 will be created`);
|
|
551
|
+
}
|
|
552
|
+
function renderOrphanLine(view) {
|
|
553
|
+
return chalk4.dim(`\u25CB view ${view.key} \u2014 removed from code, still in DB`);
|
|
554
|
+
}
|
|
555
|
+
function renderViewLine(entry) {
|
|
556
|
+
const fields = entry.changedFields.join(", ");
|
|
557
|
+
if (entry.state === "code-changed") {
|
|
558
|
+
return chalk4.green(`~ view ${entry.key} \u2014 will apply on next deploy (code changed: ${fields})`);
|
|
559
|
+
}
|
|
560
|
+
if (entry.state === "runtime-diverged") {
|
|
561
|
+
return chalk4.yellow(
|
|
562
|
+
`\u2260 view ${entry.key} \u2014 runtime divergence (users changed: ${fields}) \u2014 promote or leave`
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (entry.state === "conflict") {
|
|
566
|
+
return chalk4.red(
|
|
567
|
+
`\u26A0 CONFLICT view ${entry.key} \u2014 next deploy WOULD DESTROY runtime changes (${fields}) \u2014 run "standards pull"`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
return chalk4.green(`\u2713 resolved conflict view ${entry.key} \u2014 code will apply on next deploy`);
|
|
571
|
+
}
|
|
572
|
+
function relationTag(attr) {
|
|
573
|
+
return attr.isRelation ? " [relation]" : "";
|
|
574
|
+
}
|
|
575
|
+
function attrInline(attr) {
|
|
576
|
+
const suffix = attr.tolerated ? ", tolerated" : `${relationTag(attr)}, label: "${attr.label}"`;
|
|
577
|
+
return ` \xB7 ${attr.name} (${attr.type}${suffix})`;
|
|
578
|
+
}
|
|
579
|
+
function renderSchemaDriftLines(schema) {
|
|
580
|
+
const lines = [];
|
|
581
|
+
for (const obj of schema.customObjects) {
|
|
582
|
+
lines.push(chalk4.yellow(`\u26A0 ${obj.name} (${obj.label}) \u2014 custom object, not in code`));
|
|
583
|
+
lines.push(chalk4.dim(' \u2192 Run "standards pull" to promote or leave.'));
|
|
584
|
+
}
|
|
585
|
+
for (const entry of schema.systemObjectDrift) {
|
|
586
|
+
const unexpected = entry.sealed ? entry.customAttributes.filter((a) => !a.tolerated) : [];
|
|
587
|
+
const tolerated = entry.customAttributes.filter((a) => a.tolerated);
|
|
588
|
+
if (entry.sealed && unexpected.length > 0) {
|
|
589
|
+
lines.push(
|
|
590
|
+
chalk4.red(
|
|
591
|
+
`\u2717 ${entry.objectName} \u2014 ${unexpected.length} unexpected custom attribute(s) (sealed object)`
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
for (const a of unexpected) lines.push(chalk4.red(attrInline(a)));
|
|
595
|
+
lines.push(chalk4.dim(' \u2192 Run "standards pull" to promote or tolerate these attributes.'));
|
|
596
|
+
if (tolerated.length > 0) {
|
|
597
|
+
lines.push(chalk4.dim(` Also tolerated: ${tolerated.length} attribute(s)`));
|
|
598
|
+
for (const a of tolerated) lines.push(chalk4.dim(attrInline(a)));
|
|
599
|
+
}
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const okAttrs = entry.sealed ? tolerated : entry.customAttributes;
|
|
603
|
+
if (okAttrs.length === 0) continue;
|
|
604
|
+
const okSuffix = entry.sealed ? "tolerated custom attribute(s) (sealed)" : "custom attribute(s) (extensible)";
|
|
605
|
+
lines.push(chalk4.green(`\u2713 ${entry.objectName} \u2014 ${okAttrs.length} ${okSuffix}`));
|
|
606
|
+
for (const a of okAttrs) lines.push(chalk4.dim(attrInline(a)));
|
|
607
|
+
}
|
|
608
|
+
return lines;
|
|
609
|
+
}
|
|
610
|
+
function renderDiffLines(report) {
|
|
611
|
+
const lines = [];
|
|
612
|
+
for (const view of report.created) lines.push(renderCreatedLine(view));
|
|
613
|
+
for (const view of report.views) {
|
|
614
|
+
if (view.state === "in-sync") continue;
|
|
615
|
+
lines.push(renderViewLine(view));
|
|
616
|
+
}
|
|
617
|
+
for (const view of report.orphans) lines.push(renderOrphanLine(view));
|
|
618
|
+
lines.push(...renderSchemaDriftLines(report.schema));
|
|
619
|
+
return lines;
|
|
620
|
+
}
|
|
621
|
+
function formatDiffReport(report, format) {
|
|
622
|
+
if (format === "json") {
|
|
623
|
+
return JSON.stringify(report, null, 2);
|
|
624
|
+
}
|
|
625
|
+
const lines = renderDiffLines(report);
|
|
626
|
+
return lines.length > 0 ? lines.join("\n") : chalk4.green("\u2713 No drift detected \u2014 next deploy is a no-op.");
|
|
627
|
+
}
|
|
628
|
+
function resolveEffectiveFormat(cmd) {
|
|
629
|
+
const format = getFormat(cmd);
|
|
630
|
+
const formatExplicit = cmd.getOptionValueSourceWithGlobals("format") !== "default";
|
|
631
|
+
return format === "json" && !formatExplicit ? "table" : format;
|
|
632
|
+
}
|
|
633
|
+
function registerDiffCommand(program) {
|
|
634
|
+
program.command("diff").description("Show what the next deploy will do to the runtime schema and views").action(async (_opts, cmd) => {
|
|
635
|
+
let client;
|
|
636
|
+
let schemaEntry;
|
|
637
|
+
try {
|
|
638
|
+
client = getClientFromCommand(cmd);
|
|
639
|
+
schemaEntry = readStandardsProjectConfig().schemaEntry;
|
|
640
|
+
} catch (error) {
|
|
641
|
+
console.error(chalk4.red(`\u2717 ${messageOf(error)}`));
|
|
642
|
+
process.exit(2);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const result = await runDiffCommand({ client, schemaEntry });
|
|
646
|
+
if (result.exitCode === 2) {
|
|
647
|
+
console.error(chalk4.red(`\u2717 ${result.errorMessage}`));
|
|
648
|
+
process.exit(2);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
console.info(formatDiffReport(result.report, resolveEffectiveFormat(cmd)));
|
|
652
|
+
process.exit(result.exitCode);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
281
656
|
// src/commands/documents.ts
|
|
282
657
|
var BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"];
|
|
283
658
|
function formatFileSize(bytes) {
|
|
@@ -353,6 +728,12 @@ function registerDocumentsCommand(program) {
|
|
|
353
728
|
const client = getClientFromCommand(cmd);
|
|
354
729
|
await client.delete(`/documents/${id}/files/${fileId}`);
|
|
355
730
|
process.stdout.write(`File ${fileId} removed.
|
|
731
|
+
`);
|
|
732
|
+
});
|
|
733
|
+
documents.command("content").description("Print a document's aggregated OCR text (every pack file, position order)").argument("<id>", "document ID").action(async (id, _opts, cmd) => {
|
|
734
|
+
const client = getClientFromCommand(cmd);
|
|
735
|
+
const text = await client.getText(`/documents/${id}/content`);
|
|
736
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
356
737
|
`);
|
|
357
738
|
});
|
|
358
739
|
}
|
|
@@ -389,8 +770,101 @@ function registerFoldersCommand(program) {
|
|
|
389
770
|
});
|
|
390
771
|
}
|
|
391
772
|
|
|
773
|
+
// src/commands/init.ts
|
|
774
|
+
import { copyFileSync, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
775
|
+
import { dirname as dirname3, join as join3, relative, resolve as resolve3 } from "path";
|
|
776
|
+
import { fileURLToPath } from "url";
|
|
777
|
+
import { ValidationError as ValidationError4 } from "@stndrds/schema";
|
|
778
|
+
import chalk5 from "chalk";
|
|
779
|
+
var PROMOTIONS_GITIGNORE_LINE = ".standards/promotions/";
|
|
780
|
+
function resolveAssetPath(filename) {
|
|
781
|
+
const moduleDir = dirname3(fileURLToPath(import.meta.url));
|
|
782
|
+
const candidates = [
|
|
783
|
+
join3(moduleDir, "..", "assets", filename),
|
|
784
|
+
join3(moduleDir, "assets", filename)
|
|
785
|
+
];
|
|
786
|
+
const found = candidates.find((candidate) => existsSync3(candidate));
|
|
787
|
+
if (!found) {
|
|
788
|
+
throw new ValidationError4(
|
|
789
|
+
`Could not locate bundled asset "${filename}" (checked: ${candidates.join(", ")}). This is a packaging bug in @stndrds/cli.`,
|
|
790
|
+
[]
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
return found;
|
|
794
|
+
}
|
|
795
|
+
async function runInitCommand(options, deps = {}) {
|
|
796
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
797
|
+
await loadLocalRegistry(options.entry);
|
|
798
|
+
const standardsDir = resolveStandardsDir(cwd);
|
|
799
|
+
const projectRoot = dirname3(standardsDir);
|
|
800
|
+
const entryAbsolute = resolve3(cwd, options.entry);
|
|
801
|
+
const schemaEntry = relative(projectRoot, entryAbsolute);
|
|
802
|
+
mkdirSync(standardsDir, { recursive: true });
|
|
803
|
+
const configPath = join3(standardsDir, "config.json");
|
|
804
|
+
writeFileSync(configPath, `${JSON.stringify({ schemaEntry }, null, 2)}
|
|
805
|
+
`, "utf-8");
|
|
806
|
+
const gitignorePath = join3(projectRoot, ".gitignore");
|
|
807
|
+
const gitignore = updateGitignore(gitignorePath);
|
|
808
|
+
const skillDir = join3(projectRoot, ".claude", "skills", "standards-pull");
|
|
809
|
+
mkdirSync(skillDir, { recursive: true });
|
|
810
|
+
const skillPath = join3(skillDir, "SKILL.md");
|
|
811
|
+
copyFileSync(resolveAssetPath("standards-pull-skill.md"), skillPath);
|
|
812
|
+
let workflowPath;
|
|
813
|
+
if (options.ci) {
|
|
814
|
+
const workflowDir = join3(projectRoot, ".github", "workflows");
|
|
815
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
816
|
+
workflowPath = join3(workflowDir, "standards-drift.yml");
|
|
817
|
+
copyFileSync(resolveAssetPath("standards-drift.yml"), workflowPath);
|
|
818
|
+
}
|
|
819
|
+
return { standardsDir, configPath, skillPath, gitignore, workflowPath };
|
|
820
|
+
}
|
|
821
|
+
function updateGitignore(gitignorePath) {
|
|
822
|
+
if (!existsSync3(gitignorePath)) {
|
|
823
|
+
return "not-found";
|
|
824
|
+
}
|
|
825
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
826
|
+
const alreadyPresent = content.split("\n").some((line) => line.trim() === PROMOTIONS_GITIGNORE_LINE);
|
|
827
|
+
if (alreadyPresent) {
|
|
828
|
+
return "already-present";
|
|
829
|
+
}
|
|
830
|
+
const withTrailingNewline = content.length === 0 || content.endsWith("\n") ? content : `${content}
|
|
831
|
+
`;
|
|
832
|
+
writeFileSync(gitignorePath, `${withTrailingNewline}${PROMOTIONS_GITIGNORE_LINE}
|
|
833
|
+
`, "utf-8");
|
|
834
|
+
return "updated";
|
|
835
|
+
}
|
|
836
|
+
function registerInitCommand(program) {
|
|
837
|
+
program.command("init").description(
|
|
838
|
+
"Bootstrap this project: .standards/config.json, the standards-pull skill, and (optionally) a CI drift check. Re-running restores the managed skill/workflow assets verbatim (overwriting local edits to them); .gitignore is the only step that's merge-safe."
|
|
839
|
+
).requiredOption("--entry <path>", "Path to the schema entry file (e.g. src/schema/index.ts)").option("--ci", "Also write .github/workflows/standards-drift.yml").action(async (opts) => {
|
|
840
|
+
let result;
|
|
841
|
+
try {
|
|
842
|
+
result = await runInitCommand({ entry: opts.entry, ci: opts.ci });
|
|
843
|
+
} catch (error) {
|
|
844
|
+
console.error(chalk5.red(`\u2717 ${messageOf(error)}`));
|
|
845
|
+
process.exit(2);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
console.info(chalk5.green(`\u2713 Wrote ${result.configPath}`));
|
|
849
|
+
console.info(chalk5.green(`\u2713 Installed skill at ${result.skillPath}`));
|
|
850
|
+
if (result.gitignore === "updated") {
|
|
851
|
+
console.info(chalk5.green(`\u2713 Added "${PROMOTIONS_GITIGNORE_LINE}" to .gitignore`));
|
|
852
|
+
} else if (result.gitignore === "not-found") {
|
|
853
|
+
console.info(
|
|
854
|
+
chalk5.dim(
|
|
855
|
+
` No .gitignore found \u2014 remember to ignore "${PROMOTIONS_GITIGNORE_LINE}" yourself.`
|
|
856
|
+
)
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
if (result.workflowPath) {
|
|
860
|
+
console.info(chalk5.green(`\u2713 Wrote ${result.workflowPath}`));
|
|
861
|
+
}
|
|
862
|
+
console.info(chalk5.dim('Run "standards diff" to check for drift.'));
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
|
|
392
866
|
// src/commands/keys.ts
|
|
393
|
-
import
|
|
867
|
+
import chalk6 from "chalk";
|
|
394
868
|
function registerKeysCommand(program) {
|
|
395
869
|
const keys = program.command("keys").description("Manage Standards API keys");
|
|
396
870
|
keys.command("list").description("List API keys").action(async (_opts, cmd) => {
|
|
@@ -417,11 +891,270 @@ function registerKeysCommand(program) {
|
|
|
417
891
|
}
|
|
418
892
|
const client = getClientFromCommand(cmd);
|
|
419
893
|
await client.delete(`/api-keys/${id}`);
|
|
420
|
-
process.stdout.write(`${
|
|
894
|
+
process.stdout.write(`${chalk6.green("\u2713")} API key ${id} revoked.
|
|
421
895
|
`);
|
|
422
896
|
});
|
|
423
897
|
}
|
|
424
898
|
|
|
899
|
+
// src/commands/pull.ts
|
|
900
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
901
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
902
|
+
import * as prompts from "@clack/prompts";
|
|
903
|
+
import chalk7 from "chalk";
|
|
904
|
+
function handlePromote(entry, ctx) {
|
|
905
|
+
const viewState = ctx.viewStateByKey.get(entry.key);
|
|
906
|
+
if (!viewState) {
|
|
907
|
+
return {
|
|
908
|
+
ok: false,
|
|
909
|
+
message: `No runtime state found for view "${entry.key}" \u2014 cannot promote. The pull context must be built from the same schema state as the drift report.`
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
const runtimeConfig = {
|
|
914
|
+
object: viewState.object,
|
|
915
|
+
name: viewState.name,
|
|
916
|
+
type: viewState.type,
|
|
917
|
+
label: viewState.label,
|
|
918
|
+
description: viewState.description,
|
|
919
|
+
icon: viewState.icon,
|
|
920
|
+
default: viewState.default,
|
|
921
|
+
metadata: viewState.metadata,
|
|
922
|
+
config: viewState.config
|
|
923
|
+
};
|
|
924
|
+
const builderHint = findBuilderFileHint(
|
|
925
|
+
ctx.schemaEntry,
|
|
926
|
+
viewState.object,
|
|
927
|
+
viewState.name,
|
|
928
|
+
viewState.type
|
|
929
|
+
);
|
|
930
|
+
const promotionsDir = join4(ctx.standardsDir, "promotions");
|
|
931
|
+
mkdirSync2(promotionsDir, { recursive: true });
|
|
932
|
+
const filePath = join4(
|
|
933
|
+
promotionsDir,
|
|
934
|
+
`${viewState.object}-${viewState.name}-${viewState.type}.md`
|
|
935
|
+
);
|
|
936
|
+
const content = [
|
|
937
|
+
`# Promote view \`${entry.key}\``,
|
|
938
|
+
"",
|
|
939
|
+
"Runtime users changed this view since it was last synced from code. To keep",
|
|
940
|
+
"their changes, fold this configuration into the view's builder call below.",
|
|
941
|
+
"",
|
|
942
|
+
"## Runtime config",
|
|
943
|
+
"",
|
|
944
|
+
"```json",
|
|
945
|
+
JSON.stringify(runtimeConfig, null, 2),
|
|
946
|
+
"```",
|
|
947
|
+
"",
|
|
948
|
+
"## Builder file",
|
|
949
|
+
"",
|
|
950
|
+
builderHint,
|
|
951
|
+
"",
|
|
952
|
+
"---",
|
|
953
|
+
"",
|
|
954
|
+
"Hand this file to your agent, then re-run `standards diff`.",
|
|
955
|
+
""
|
|
956
|
+
].join("\n");
|
|
957
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
958
|
+
return { ok: true, filePath };
|
|
959
|
+
} catch (error) {
|
|
960
|
+
return {
|
|
961
|
+
ok: false,
|
|
962
|
+
message: `Failed to write promotion file for ${entry.key}: ${messageOf(error)}`
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async function handleOverwrite(entry, _ctx, client) {
|
|
967
|
+
try {
|
|
968
|
+
await client.post("/schema/resolutions", {
|
|
969
|
+
viewId: entry.viewId,
|
|
970
|
+
codeHash: entry.codeHash,
|
|
971
|
+
dbHash: entry.dbHash
|
|
972
|
+
});
|
|
973
|
+
return { ok: true };
|
|
974
|
+
} catch (error) {
|
|
975
|
+
if (error instanceof ApiClientError && error.statusCode === 400) {
|
|
976
|
+
return {
|
|
977
|
+
ok: false,
|
|
978
|
+
reason: "stale",
|
|
979
|
+
message: `View ${entry.key} changed since the diff \u2014 re-run "standards diff".`
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
ok: false,
|
|
984
|
+
reason: "infra",
|
|
985
|
+
message: `Failed to overwrite ${entry.key}: ${messageOf(error)}`
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
var BUILDER_SOURCE_EXTENSIONS = [".ts", ".tsx"];
|
|
990
|
+
var BUILDER_SEARCH_IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".git"]);
|
|
991
|
+
function collectSourceFiles(dir) {
|
|
992
|
+
let entries;
|
|
993
|
+
try {
|
|
994
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
995
|
+
} catch {
|
|
996
|
+
return [];
|
|
997
|
+
}
|
|
998
|
+
const files = [];
|
|
999
|
+
for (const entry of entries) {
|
|
1000
|
+
if (BUILDER_SEARCH_IGNORED_DIRS.has(entry.name)) continue;
|
|
1001
|
+
const fullPath = join4(dir, entry.name);
|
|
1002
|
+
if (entry.isDirectory()) {
|
|
1003
|
+
files.push(...collectSourceFiles(fullPath));
|
|
1004
|
+
} else if (BUILDER_SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) {
|
|
1005
|
+
files.push(fullPath);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return files;
|
|
1009
|
+
}
|
|
1010
|
+
function escapeRegExp(value) {
|
|
1011
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1012
|
+
}
|
|
1013
|
+
function findBuilderFileHint(schemaEntry, object, name, type) {
|
|
1014
|
+
const builderCall = type === "detail" ? "detailView" : "listView";
|
|
1015
|
+
const pattern = new RegExp(`${builderCall}\\(\\s*["'\`]${escapeRegExp(name)}["'\`]`);
|
|
1016
|
+
const searchDir = dirname4(schemaEntry);
|
|
1017
|
+
for (const filePath of collectSourceFiles(searchDir)) {
|
|
1018
|
+
const lines = readFileSync3(filePath, "utf-8").split("\n");
|
|
1019
|
+
const lineIndex = lines.findIndex((line) => pattern.test(line));
|
|
1020
|
+
if (lineIndex !== -1) {
|
|
1021
|
+
return `\`${filePath}:${lineIndex + 1}\`
|
|
1022
|
+
|
|
1023
|
+
\`\`\`ts
|
|
1024
|
+
${lines[lineIndex]?.trim()}
|
|
1025
|
+
\`\`\``;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
return `No \`${builderCall}("${name}", ...)\` call found under ${searchDir} \u2014 search manually for the "${object}" object's view builders.`;
|
|
1029
|
+
}
|
|
1030
|
+
function entriesNeedingDecisionOf(result) {
|
|
1031
|
+
if (result.exitCode === 2) return [];
|
|
1032
|
+
const conflicts = result.report.views.filter((view) => view.state === "conflict");
|
|
1033
|
+
const diverged = result.report.views.filter((view) => view.state === "runtime-diverged");
|
|
1034
|
+
return [...conflicts, ...diverged];
|
|
1035
|
+
}
|
|
1036
|
+
async function runPullCommand(deps, decide) {
|
|
1037
|
+
const initial = await runDiffCommand(deps);
|
|
1038
|
+
if (initial.exitCode === 2) {
|
|
1039
|
+
return { initial, outcomes: [], residual: initial };
|
|
1040
|
+
}
|
|
1041
|
+
const entriesNeedingDecision = entriesNeedingDecisionOf(initial);
|
|
1042
|
+
const viewStateByKey = new Map(
|
|
1043
|
+
initial.state.views.map((entry) => [viewKey(entry.object, entry.name, entry.type), entry])
|
|
1044
|
+
);
|
|
1045
|
+
const ctx = {
|
|
1046
|
+
viewStateByKey,
|
|
1047
|
+
schemaEntry: deps.schemaEntry,
|
|
1048
|
+
standardsDir: deps.standardsDir
|
|
1049
|
+
};
|
|
1050
|
+
const outcomes = [];
|
|
1051
|
+
for (const entry of entriesNeedingDecision) {
|
|
1052
|
+
const decision = await decide(entry);
|
|
1053
|
+
if (decision === "promote") {
|
|
1054
|
+
const promote = handlePromote(entry, ctx);
|
|
1055
|
+
outcomes.push({ entry, decision, promote });
|
|
1056
|
+
if (!promote.ok) {
|
|
1057
|
+
return { initial, outcomes, residual: { exitCode: 2, errorMessage: promote.message } };
|
|
1058
|
+
}
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
if (decision === "overwrite") {
|
|
1062
|
+
const overwrite = await handleOverwrite(entry, ctx, deps.client);
|
|
1063
|
+
outcomes.push({ entry, decision, overwrite });
|
|
1064
|
+
if (!overwrite.ok && overwrite.reason === "infra") {
|
|
1065
|
+
return { initial, outcomes, residual: { exitCode: 2, errorMessage: overwrite.message } };
|
|
1066
|
+
}
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
outcomes.push({ entry, decision: "skip" });
|
|
1070
|
+
}
|
|
1071
|
+
const residual = await runDiffCommand(deps);
|
|
1072
|
+
return { initial, outcomes, residual };
|
|
1073
|
+
}
|
|
1074
|
+
function decisionOptionsFor(entry) {
|
|
1075
|
+
const options = [
|
|
1076
|
+
{
|
|
1077
|
+
value: "promote",
|
|
1078
|
+
label: "Promote",
|
|
1079
|
+
hint: "write the runtime config to .standards/promotions/"
|
|
1080
|
+
}
|
|
1081
|
+
];
|
|
1082
|
+
if (entry.state === "conflict") {
|
|
1083
|
+
options.push({
|
|
1084
|
+
value: "overwrite",
|
|
1085
|
+
label: "Overwrite",
|
|
1086
|
+
hint: "accept the code version, discard the runtime change"
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
options.push({ value: "skip", label: "Skip", hint: "leave this view as-is for now" });
|
|
1090
|
+
return options;
|
|
1091
|
+
}
|
|
1092
|
+
async function promptDecision(entry) {
|
|
1093
|
+
const label = entry.state === "conflict" ? "CONFLICT" : "runtime divergence";
|
|
1094
|
+
const answer = await prompts.select({
|
|
1095
|
+
message: `${entry.key} \u2014 ${label} (${entry.changedFields.join(", ")})`,
|
|
1096
|
+
options: decisionOptionsFor(entry)
|
|
1097
|
+
});
|
|
1098
|
+
if (prompts.isCancel(answer)) {
|
|
1099
|
+
return "skip";
|
|
1100
|
+
}
|
|
1101
|
+
return answer;
|
|
1102
|
+
}
|
|
1103
|
+
function registerPullCommand(program) {
|
|
1104
|
+
program.command("pull").description("Interactively resolve view drift: promote runtime changes or overwrite them").action(async (_opts, cmd) => {
|
|
1105
|
+
let client;
|
|
1106
|
+
let schemaEntry;
|
|
1107
|
+
let standardsDir;
|
|
1108
|
+
try {
|
|
1109
|
+
client = getClientFromCommand(cmd);
|
|
1110
|
+
schemaEntry = readStandardsProjectConfig().schemaEntry;
|
|
1111
|
+
standardsDir = resolveStandardsDir();
|
|
1112
|
+
} catch (error) {
|
|
1113
|
+
console.error(chalk7.red(`\u2717 ${messageOf(error)}`));
|
|
1114
|
+
process.exit(2);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
let result;
|
|
1118
|
+
try {
|
|
1119
|
+
result = await runPullCommand({ client, schemaEntry, standardsDir }, promptDecision);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
console.error(chalk7.red(`\u2717 ${messageOf(error)}`));
|
|
1122
|
+
process.exit(2);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (result.initial.exitCode === 2) {
|
|
1126
|
+
console.error(chalk7.red(`\u2717 ${result.initial.errorMessage}`));
|
|
1127
|
+
process.exit(2);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
if (result.outcomes.length === 0) {
|
|
1131
|
+
console.info(chalk7.dim("No conflicts or runtime-diverged views need a decision."));
|
|
1132
|
+
}
|
|
1133
|
+
for (const outcome of result.outcomes) {
|
|
1134
|
+
if (outcome.decision === "promote") {
|
|
1135
|
+
if (outcome.promote.ok) {
|
|
1136
|
+
console.info(chalk7.green(`\u2713 Wrote ${outcome.promote.filePath}`));
|
|
1137
|
+
} else {
|
|
1138
|
+
console.error(chalk7.red(`\u2717 ${outcome.promote.message}`));
|
|
1139
|
+
}
|
|
1140
|
+
} else if (outcome.decision === "overwrite") {
|
|
1141
|
+
if (outcome.overwrite.ok) {
|
|
1142
|
+
console.info(chalk7.green(`\u2713 Overwrote ${outcome.entry.key}`));
|
|
1143
|
+
} else {
|
|
1144
|
+
console.error(chalk7.red(`\u2717 ${outcome.overwrite.message}`));
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (result.residual.exitCode === 2) {
|
|
1149
|
+
console.error(chalk7.red(`\u2717 ${result.residual.errorMessage}`));
|
|
1150
|
+
process.exit(2);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
console.info(formatDiffReport(result.residual.report, resolveEffectiveFormat(cmd)));
|
|
1154
|
+
process.exit(result.residual.exitCode);
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
|
|
425
1158
|
// src/commands/records.ts
|
|
426
1159
|
import { readFile as readFile2 } from "fs/promises";
|
|
427
1160
|
import { basename } from "path";
|
|
@@ -502,20 +1235,20 @@ function registerRecordsCommand(program) {
|
|
|
502
1235
|
// src/commands/root.ts
|
|
503
1236
|
import { stdin as input, stdout as output } from "process";
|
|
504
1237
|
import { createInterface } from "readline/promises";
|
|
505
|
-
import
|
|
1238
|
+
import chalk8 from "chalk";
|
|
506
1239
|
|
|
507
1240
|
// src/config.ts
|
|
508
1241
|
import { mkdir, readFile as readFile3, rm, writeFile } from "fs/promises";
|
|
509
1242
|
import { homedir } from "os";
|
|
510
|
-
import { dirname, join } from "path";
|
|
1243
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
511
1244
|
var DEFAULT_API_URL = "http://localhost:4100/v1";
|
|
512
1245
|
var ENV_PROFILE_NAME = "sandbox";
|
|
513
1246
|
function getDefaultApiUrl() {
|
|
514
1247
|
return DEFAULT_API_URL;
|
|
515
1248
|
}
|
|
516
1249
|
function getConfigPath() {
|
|
517
|
-
const configDir = process.env.STANDARDS_CONFIG_DIR ??
|
|
518
|
-
return
|
|
1250
|
+
const configDir = process.env.STANDARDS_CONFIG_DIR ?? join5(homedir(), ".standards");
|
|
1251
|
+
return join5(configDir, "config.json");
|
|
519
1252
|
}
|
|
520
1253
|
async function readConfig() {
|
|
521
1254
|
try {
|
|
@@ -534,7 +1267,7 @@ async function readConfig() {
|
|
|
534
1267
|
}
|
|
535
1268
|
async function writeConfig(config) {
|
|
536
1269
|
const path = getConfigPath();
|
|
537
|
-
await mkdir(
|
|
1270
|
+
await mkdir(dirname5(path), { recursive: true, mode: 448 });
|
|
538
1271
|
await writeFile(path, `${JSON.stringify(config, null, 2)}
|
|
539
1272
|
`, { mode: 384 });
|
|
540
1273
|
}
|
|
@@ -642,7 +1375,7 @@ function registerRootCommands(program) {
|
|
|
642
1375
|
await client.get("/api-keys");
|
|
643
1376
|
await upsertProfile({ name: opts.name, apiUrl: opts.url, apiKey });
|
|
644
1377
|
process.stdout.write(
|
|
645
|
-
`${
|
|
1378
|
+
`${chalk8.green("\u2713")} Standards instance "${opts.name}" saved and selected.
|
|
646
1379
|
`
|
|
647
1380
|
);
|
|
648
1381
|
process.stdout.write(` API URL: ${opts.url}
|
|
@@ -654,7 +1387,7 @@ function registerRootCommands(program) {
|
|
|
654
1387
|
});
|
|
655
1388
|
program.command("use").description("Select the active Standards instance").argument("<name>", "instance name").action(async (name) => {
|
|
656
1389
|
await setCurrentProfile(name);
|
|
657
|
-
process.stdout.write(`${
|
|
1390
|
+
process.stdout.write(`${chalk8.green("\u2713")} Standards instance "${name}" selected.
|
|
658
1391
|
`);
|
|
659
1392
|
});
|
|
660
1393
|
program.command("instances").description("List configured Standards instances").action(async (_opts, cmd) => {
|
|
@@ -673,17 +1406,12 @@ function registerRootCommands(program) {
|
|
|
673
1406
|
});
|
|
674
1407
|
program.command("logout").description("Remove a Standards instance from local CLI config").argument("[name]", "instance name, defaults to current").action(async (name) => {
|
|
675
1408
|
await removeProfile(name);
|
|
676
|
-
process.stdout.write(`${
|
|
1409
|
+
process.stdout.write(`${chalk8.green("\u2713")} Standards instance removed.
|
|
677
1410
|
`);
|
|
678
1411
|
});
|
|
679
1412
|
}
|
|
680
1413
|
|
|
681
1414
|
// src/commands/schema.ts
|
|
682
|
-
import { createHash } from "crypto";
|
|
683
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
684
|
-
import { join as join2 } from "path";
|
|
685
|
-
import { fileURLToPath } from "url";
|
|
686
|
-
import chalk6 from "chalk";
|
|
687
1415
|
function registerSchemaCommand(program) {
|
|
688
1416
|
const schema = program.command("schema").description("Inspect schema objects and attributes");
|
|
689
1417
|
schema.command("list").description("List all schema objects").action(async (_opts, cmd) => {
|
|
@@ -696,201 +1424,10 @@ function registerSchemaCommand(program) {
|
|
|
696
1424
|
const result = await client.get(`/schema/objects/${objectName}`);
|
|
697
1425
|
formatOutput(result, getFormat(cmd));
|
|
698
1426
|
});
|
|
699
|
-
schema.command("diff").description("Show runtime drift between DB and code schema").option("--object <name>", "Scope to a single object").option("--quiet", "Exit code only, no output").action(async (opts, cmd) => {
|
|
700
|
-
const format = getFormat(cmd);
|
|
701
|
-
const result = await fetchDrift(getClientFromCommand(cmd), opts.object);
|
|
702
|
-
if (format === "json" && cmd.getOptionValueSourceWithGlobals("format") !== "default") {
|
|
703
|
-
formatOutput(result, "json");
|
|
704
|
-
} else if (!opts.quiet) {
|
|
705
|
-
printDiffOutput(result);
|
|
706
|
-
}
|
|
707
|
-
process.exit(result.hasUnexpectedDrift ? 1 : 0);
|
|
708
|
-
});
|
|
709
|
-
schema.command("pull").description("Generate an AI-ready prompt to resolve schema drift").option("--object <name>", "Scope to a single object").option("--no-save", "Print to stdout only, do not write to .standards/").option("--output <file>", "Write prompt to a specific file path").action(async (opts, cmd) => {
|
|
710
|
-
const result = await fetchDrift(getClientFromCommand(cmd), opts.object);
|
|
711
|
-
const totalDrift = result.summary.totalCustomObjects + result.summary.totalCustomAttributes;
|
|
712
|
-
if (totalDrift === 0) {
|
|
713
|
-
console.info(chalk6.green("\u2713 No drift detected \u2014 nothing to pull."));
|
|
714
|
-
process.exit(0);
|
|
715
|
-
}
|
|
716
|
-
const globalOpts = cmd.optsWithGlobals();
|
|
717
|
-
const prompt = buildPullPrompt(result, globalOpts.apiUrl ?? "unknown");
|
|
718
|
-
const shouldSave = opts.save !== false;
|
|
719
|
-
if (opts.output) {
|
|
720
|
-
writeFileSync(opts.output, prompt, "utf-8");
|
|
721
|
-
console.info(chalk6.green(`\u2713 Prompt written to ${opts.output}`));
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
if (shouldSave) {
|
|
725
|
-
const standardsDir = resolveStandardsDir();
|
|
726
|
-
const pullsDir = join2(standardsDir, "pulls");
|
|
727
|
-
const diffDir = join2(standardsDir, "diff");
|
|
728
|
-
mkdirSync(pullsDir, { recursive: true });
|
|
729
|
-
mkdirSync(diffDir, { recursive: true });
|
|
730
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
731
|
-
const hash = createHash("sha1").update(JSON.stringify(result)).digest("hex").slice(0, 6);
|
|
732
|
-
const filename = join2(pullsDir, `${date}-${hash}.md`);
|
|
733
|
-
writeFileSync(filename, prompt, "utf-8");
|
|
734
|
-
writeFileSync(join2(diffDir, "latest.json"), JSON.stringify(result, null, 2), "utf-8");
|
|
735
|
-
console.info(chalk6.green(`\u2713 Pull prompt written to ${filename}`));
|
|
736
|
-
console.info(chalk6.dim(" Copy-paste its contents into your LLM to resolve the drift."));
|
|
737
|
-
} else {
|
|
738
|
-
process.stdout.write(prompt);
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
var fetchDrift = (client, object) => client.get("/schema/drift", object ? { object } : {});
|
|
743
|
-
var relationTag = (attr) => attr.isRelation ? " [relation]" : "";
|
|
744
|
-
var attrInline = (attr) => {
|
|
745
|
-
const suffix = attr.tolerated ? ", tolerated" : `${relationTag(attr)}, label: "${attr.label}"`;
|
|
746
|
-
return ` \xB7 ${attr.name} (${attr.type}${suffix})`;
|
|
747
|
-
};
|
|
748
|
-
function printDiffOutput(result) {
|
|
749
|
-
const lines = [];
|
|
750
|
-
for (const obj of result.customObjects) {
|
|
751
|
-
lines.push(chalk6.yellow(`\u26A0 ${obj.name} (${obj.label}) \u2014 custom object, not in code`));
|
|
752
|
-
lines.push(chalk6.dim(' \u2192 Run "standards pull" to promote or leave.'));
|
|
753
|
-
}
|
|
754
|
-
for (const entry of result.systemObjectDrift) {
|
|
755
|
-
const unexpected = entry.sealed ? entry.customAttributes.filter((a) => !a.tolerated) : [];
|
|
756
|
-
const tolerated = entry.customAttributes.filter((a) => a.tolerated);
|
|
757
|
-
if (entry.sealed && unexpected.length > 0) {
|
|
758
|
-
lines.push(
|
|
759
|
-
chalk6.red(
|
|
760
|
-
`\u2717 ${entry.objectName} \u2014 ${unexpected.length} unexpected custom attribute(s) (sealed object)`
|
|
761
|
-
)
|
|
762
|
-
);
|
|
763
|
-
for (const attr of unexpected) lines.push(chalk6.red(attrInline(attr)));
|
|
764
|
-
lines.push(chalk6.dim(' \u2192 Run "standards pull" to promote or tolerate these attributes.'));
|
|
765
|
-
if (tolerated.length > 0) {
|
|
766
|
-
lines.push(chalk6.dim(` Also tolerated: ${tolerated.length} attribute(s)`));
|
|
767
|
-
for (const attr of tolerated) lines.push(chalk6.dim(attrInline(attr)));
|
|
768
|
-
}
|
|
769
|
-
continue;
|
|
770
|
-
}
|
|
771
|
-
const okAttrs = entry.sealed ? tolerated : entry.customAttributes;
|
|
772
|
-
if (okAttrs.length === 0) continue;
|
|
773
|
-
const okSuffix = entry.sealed ? "tolerated custom attribute(s) (sealed)" : "custom attribute(s) (extensible)";
|
|
774
|
-
lines.push(chalk6.green(`\u2713 ${entry.objectName} \u2014 ${okAttrs.length} ${okSuffix}`));
|
|
775
|
-
for (const attr of okAttrs) lines.push(chalk6.dim(attrInline(attr)));
|
|
776
|
-
}
|
|
777
|
-
if (lines.length === 0) {
|
|
778
|
-
console.info(chalk6.green("\u2713 No drift detected."));
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
console.info(lines.join("\n"));
|
|
782
|
-
if (result.hasUnexpectedDrift) {
|
|
783
|
-
console.info(
|
|
784
|
-
chalk6.red(
|
|
785
|
-
`
|
|
786
|
-
\u2717 Unexpected drift: ${result.summary.totalUnexpected} attribute(s) on sealed object(s).`
|
|
787
|
-
)
|
|
788
|
-
);
|
|
789
|
-
} else {
|
|
790
|
-
console.info(chalk6.green("\n\u2713 No unexpected drift."));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
function resolveStandardsDir() {
|
|
794
|
-
let dir = process.cwd();
|
|
795
|
-
while (true) {
|
|
796
|
-
if (existsSync(join2(dir, "package.json"))) {
|
|
797
|
-
return join2(dir, ".standards");
|
|
798
|
-
}
|
|
799
|
-
const parent = join2(dir, "..");
|
|
800
|
-
if (parent === dir) break;
|
|
801
|
-
dir = parent;
|
|
802
|
-
}
|
|
803
|
-
return join2(process.cwd(), ".standards");
|
|
804
|
-
}
|
|
805
|
-
function buildPullPrompt(result, instanceUrl) {
|
|
806
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
807
|
-
const builderRef = loadBuilderReference();
|
|
808
|
-
const formatAttr = (attr) => {
|
|
809
|
-
const toleratedNote = attr.tolerated ? " *(already tolerated)*" : "";
|
|
810
|
-
return ` - ${attr.name}${relationTag(attr)}${toleratedNote} \u2014 type: ${attr.type}, label: "${attr.label}"`;
|
|
811
|
-
};
|
|
812
|
-
const lines = [
|
|
813
|
-
"# Standards Schema Pull",
|
|
814
|
-
"generated-with: standards-cli",
|
|
815
|
-
`generated-at: ${now}`,
|
|
816
|
-
`instance: ${instanceUrl}`,
|
|
817
|
-
"",
|
|
818
|
-
"---",
|
|
819
|
-
"",
|
|
820
|
-
"## Standards Schema Builder \u2014 Reference",
|
|
821
|
-
"",
|
|
822
|
-
builderRef,
|
|
823
|
-
"",
|
|
824
|
-
"---",
|
|
825
|
-
"",
|
|
826
|
-
"## Runtime Drift",
|
|
827
|
-
"",
|
|
828
|
-
"The following attributes and objects exist in the database but are not declared",
|
|
829
|
-
"in the code schema. For each one, the user will decide what to do.",
|
|
830
|
-
""
|
|
831
|
-
];
|
|
832
|
-
if (result.customObjects.length > 0) {
|
|
833
|
-
lines.push("### Custom Objects (not declared in code)");
|
|
834
|
-
lines.push("");
|
|
835
|
-
for (const obj of result.customObjects) {
|
|
836
|
-
lines.push(`**${obj.name}** \u2014 label: "${obj.label}"`);
|
|
837
|
-
for (const attr of obj.attributes) lines.push(formatAttr(attr));
|
|
838
|
-
lines.push("");
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if (result.systemObjectDrift.length > 0) {
|
|
842
|
-
lines.push("### Custom Attributes on Existing Objects");
|
|
843
|
-
lines.push("");
|
|
844
|
-
for (const entry of result.systemObjectDrift) {
|
|
845
|
-
const sealedNote = entry.sealed ? " **(sealed \u2014 CI fails until resolved)**" : " (extensible)";
|
|
846
|
-
lines.push(`**${entry.objectName}**${sealedNote} \u2014 label: "${entry.objectLabel}"`);
|
|
847
|
-
for (const attr of entry.customAttributes) lines.push(formatAttr(attr));
|
|
848
|
-
lines.push("");
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
lines.push("---");
|
|
852
|
-
lines.push("");
|
|
853
|
-
lines.push("## Your Task");
|
|
854
|
-
lines.push("");
|
|
855
|
-
lines.push("1. **Explore the codebase** to find the schema files where each object is declared");
|
|
856
|
-
lines.push(' (look for `object("name")` calls importing from `@stndrds/schema`)');
|
|
857
|
-
lines.push("");
|
|
858
|
-
lines.push("2. **Present each attribute and custom object to the user**, one object at a time.");
|
|
859
|
-
lines.push(" For each, explain what it is and ask the user to choose:");
|
|
860
|
-
lines.push(
|
|
861
|
-
" - **Promote** \u2192 will add `.attribute(...)` to the object builder (becomes system on next boot)"
|
|
862
|
-
);
|
|
863
|
-
lines.push(
|
|
864
|
-
' - **Tolerate** \u2192 will add to `.tolerate(["name"])` (required on sealed objects to fix CI)'
|
|
865
|
-
);
|
|
866
|
-
lines.push(" - **Leave** \u2192 do nothing (will keep appearing in future `standards diff` runs)");
|
|
867
|
-
lines.push("");
|
|
868
|
-
lines.push("3. **Collect all decisions** before writing any code. Once the user has decided");
|
|
869
|
-
lines.push(" for every attribute, **apply all changes in one pass**.");
|
|
870
|
-
lines.push("");
|
|
871
|
-
lines.push("4. **Verify** by running: `standards diff`");
|
|
872
|
-
lines.push("");
|
|
873
|
-
lines.push("Refer to the Standards Schema Builder Reference above for the exact API.");
|
|
874
|
-
lines.push(
|
|
875
|
-
"For relation attributes, use the `relation()` builder with bilateral config if needed."
|
|
876
|
-
);
|
|
877
|
-
return lines.join("\n");
|
|
878
|
-
}
|
|
879
|
-
function loadBuilderReference() {
|
|
880
|
-
const candidates = [
|
|
881
|
-
join2(fileURLToPath(new URL(".", import.meta.url)), "../assets/schema-builder-reference.md"),
|
|
882
|
-
join2(process.cwd(), "packages/cli/src/assets/schema-builder-reference.md")
|
|
883
|
-
];
|
|
884
|
-
for (const candidate of candidates) {
|
|
885
|
-
if (existsSync(candidate)) {
|
|
886
|
-
return readFileSync(candidate, "utf-8");
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
return "[Standards Schema Builder Reference \u2014 see packages/cli/src/assets/schema-builder-reference.md]";
|
|
890
1427
|
}
|
|
891
1428
|
|
|
892
1429
|
// src/program.ts
|
|
893
|
-
var PUBLIC_COMMANDS = /* @__PURE__ */ new Set(["login", "use", "instances", "current", "logout", "help"]);
|
|
1430
|
+
var PUBLIC_COMMANDS = /* @__PURE__ */ new Set(["login", "use", "instances", "current", "logout", "help", "init"]);
|
|
894
1431
|
function isPublicCommand(actionCommand) {
|
|
895
1432
|
if (PUBLIC_COMMANDS.has(actionCommand.name())) return true;
|
|
896
1433
|
return actionCommand.parent?.name() === "auth" && actionCommand.name() === "help";
|
|
@@ -901,6 +1438,9 @@ function createProgram() {
|
|
|
901
1438
|
registerRootCommands(program);
|
|
902
1439
|
registerRecordsCommand(program);
|
|
903
1440
|
registerSchemaCommand(program);
|
|
1441
|
+
registerDiffCommand(program);
|
|
1442
|
+
registerPullCommand(program);
|
|
1443
|
+
registerInitCommand(program);
|
|
904
1444
|
registerDocumentsCommand(program);
|
|
905
1445
|
registerFoldersCommand(program);
|
|
906
1446
|
registerKeysCommand(program);
|
|
@@ -921,11 +1461,11 @@ function createProgram() {
|
|
|
921
1461
|
program.setOptionValue("tenant", raw.tenant);
|
|
922
1462
|
if (!(resolved.apiKey || isPublicCommand(actionCommand))) {
|
|
923
1463
|
console.error(
|
|
924
|
-
|
|
1464
|
+
chalk9.red(
|
|
925
1465
|
`\u2717 Error: No Standards instance configured. Run "standards login" or pass --api-key.`
|
|
926
1466
|
)
|
|
927
1467
|
);
|
|
928
|
-
process.exit(
|
|
1468
|
+
process.exit(2);
|
|
929
1469
|
}
|
|
930
1470
|
});
|
|
931
1471
|
return program;
|
|
@@ -935,12 +1475,12 @@ async function runProgram(argv = process.argv) {
|
|
|
935
1475
|
await program.parseAsync(argv).catch((error) => {
|
|
936
1476
|
if (error instanceof ApiClientError) {
|
|
937
1477
|
if (error.statusCode > 0) {
|
|
938
|
-
console.error(
|
|
1478
|
+
console.error(chalk9.red(`\u2717 Error (${error.statusCode}): ${error.message}`));
|
|
939
1479
|
} else {
|
|
940
|
-
console.error(
|
|
1480
|
+
console.error(chalk9.red(`\u2717 Error: ${error.message}`));
|
|
941
1481
|
}
|
|
942
1482
|
} else if (error instanceof Error) {
|
|
943
|
-
console.error(
|
|
1483
|
+
console.error(chalk9.red(`\u2717 Error: ${error.message}`));
|
|
944
1484
|
}
|
|
945
1485
|
process.exit(1);
|
|
946
1486
|
});
|