delimit-cli 4.1.43 → 4.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +46 -5
- package/bin/delimit-cli.js +1987 -337
- package/bin/delimit-setup.js +108 -66
- package/gateway/ai/activate_helpers.py +253 -7
- package/gateway/ai/agent_dispatch.py +34 -2
- package/gateway/ai/backends/deploy_bridge.py +167 -12
- package/gateway/ai/backends/gateway_core.py +236 -13
- package/gateway/ai/backends/repo_bridge.py +80 -16
- package/gateway/ai/backends/tools_infra.py +49 -32
- package/gateway/ai/checksums.sha256 +6 -0
- package/gateway/ai/content_engine.py +1276 -2
- package/gateway/ai/continuity.py +462 -0
- package/gateway/ai/deliberation.pyi +53 -0
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/governance.py +58 -0
- package/gateway/ai/governance.pyi +32 -0
- package/gateway/ai/governance_hardening.py +569 -0
- package/gateway/ai/inbox_daemon_runner.py +217 -0
- package/gateway/ai/key_resolver.py +95 -2
- package/gateway/ai/ledger_manager.py +53 -3
- package/gateway/ai/license.py +104 -3
- package/gateway/ai/license_core.py +177 -36
- package/gateway/ai/license_core.pyi +50 -0
- package/gateway/ai/loop_engine.py +929 -294
- package/gateway/ai/notify.py +1786 -2
- package/gateway/ai/reddit_scanner.py +190 -1
- package/gateway/ai/screen_record.py +1 -1
- package/gateway/ai/secrets_broker.py +5 -1
- package/gateway/ai/server.py +254 -19
- package/gateway/ai/social_cache.py +341 -0
- package/gateway/ai/social_daemon.py +41 -10
- package/gateway/ai/supabase_sync.py +190 -2
- package/gateway/ai/swarm.py +86 -0
- package/gateway/ai/swarm_infra.py +656 -0
- package/gateway/ai/tui.py +594 -36
- package/gateway/ai/tweet_corpus_schema.sql +76 -0
- package/gateway/core/diff_engine_v2.py +6 -2
- package/gateway/core/generator_drift.py +242 -0
- package/gateway/core/json_schema_diff.py +375 -0
- package/gateway/core/openapi_version.py +124 -0
- package/gateway/core/spec_detector.py +47 -7
- package/gateway/core/spec_health.py +5 -2
- package/gateway/core/zero_spec/express_extractor.py +2 -2
- package/gateway/core/zero_spec/nestjs_extractor.py +40 -9
- package/gateway/requirements.txt +3 -6
- package/lib/cross-model-hooks.js +4 -12
- package/package.json +11 -3
- package/scripts/demo-v420-clean.sh +267 -0
- package/scripts/demo-v420-deliberation.sh +217 -0
- package/scripts/demo-v420.sh +55 -0
- package/scripts/postinstall.js +4 -3
- package/scripts/publish-ci-guard.sh +30 -0
- package/scripts/record-and-upload.sh +132 -0
- package/scripts/release.sh +126 -0
- package/scripts/sync-gateway.sh +112 -0
- package/scripts/youtube-upload.py +141 -0
package/bin/delimit-cli.js
CHANGED
|
@@ -68,7 +68,7 @@ function normalizeNaturalLanguageArgs(argv) {
|
|
|
68
68
|
const explicitCommands = new Set([
|
|
69
69
|
'install', 'mode', 'status', 'session', 'build', 'ask', 'policy', 'auth', 'audit',
|
|
70
70
|
'explain-decision', 'uninstall', 'proxy', 'hook', 'version', 'vault', 'deliberate',
|
|
71
|
-
'remember', 'recall', 'forget'
|
|
71
|
+
'remember', 'recall', 'forget', 'report'
|
|
72
72
|
]);
|
|
73
73
|
if (explicitCommands.has((raw[0] || '').toLowerCase())) {
|
|
74
74
|
return raw;
|
|
@@ -328,190 +328,312 @@ program
|
|
|
328
328
|
.description('Show a compact dashboard of your Delimit setup')
|
|
329
329
|
|
|
330
330
|
.option('--verbose', 'Show detailed status')
|
|
331
|
+
.option('--json', 'Output as JSON')
|
|
332
|
+
.option('--watch', 'Refresh every 5 seconds')
|
|
331
333
|
.action(async (options) => {
|
|
332
334
|
const homedir = os.homedir();
|
|
333
335
|
const delimitHome = path.join(homedir, '.delimit');
|
|
334
336
|
const target = process.cwd();
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
337
|
+
const { execSync } = require('child_process');
|
|
338
|
+
|
|
339
|
+
function renderStatus() {
|
|
340
|
+
const data = {};
|
|
341
|
+
|
|
342
|
+
// --- Memory stats ---
|
|
343
|
+
const memoryDir = path.join(delimitHome, 'memory');
|
|
344
|
+
let memTotal = 0;
|
|
345
|
+
let memRecent = 0;
|
|
346
|
+
let recentMemories = [];
|
|
347
|
+
let memIntegrity = { verified: 0, failed: 0 };
|
|
348
|
+
const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
349
|
+
try {
|
|
350
|
+
const memFiles = fs.readdirSync(memoryDir).filter(f => f.startsWith('mem-') && f.endsWith('.json'));
|
|
351
|
+
memTotal = memFiles.length;
|
|
352
|
+
for (const f of memFiles) {
|
|
353
|
+
try {
|
|
354
|
+
const raw = fs.readFileSync(path.join(memoryDir, f), 'utf-8');
|
|
355
|
+
const d = JSON.parse(raw);
|
|
356
|
+
const ts = new Date(d.created_at || d.timestamp || d.created || 0).getTime();
|
|
357
|
+
if (ts > oneWeekAgo) memRecent++;
|
|
358
|
+
recentMemories.push({ text: d.text || d.content || '', tags: d.tags || [], ts, source: d.context || d.source || 'unknown' });
|
|
359
|
+
// Verify integrity hash if present
|
|
360
|
+
if (d.hash) {
|
|
361
|
+
const expected = require('crypto').createHash('sha256').update(d.content || d.text || '').digest('hex').slice(0, 16);
|
|
362
|
+
if (d.hash === expected) memIntegrity.verified++;
|
|
363
|
+
else memIntegrity.failed++;
|
|
364
|
+
}
|
|
365
|
+
} catch {}
|
|
366
|
+
}
|
|
367
|
+
recentMemories.sort((a, b) => b.ts - a.ts);
|
|
368
|
+
recentMemories = recentMemories.slice(0, 3);
|
|
369
|
+
} catch {}
|
|
370
|
+
data.memory = { total: memTotal, recent: memRecent, integrity: memIntegrity };
|
|
371
|
+
|
|
372
|
+
// --- Governance / Policy ---
|
|
373
|
+
const policyPath = path.join(target, '.delimit', 'policies.yml');
|
|
374
|
+
let policyName = 'none';
|
|
375
|
+
let policyMode = '';
|
|
376
|
+
let ruleCount = 0;
|
|
377
|
+
let hasPolicy = false;
|
|
378
|
+
if (fs.existsSync(policyPath)) {
|
|
379
|
+
hasPolicy = true;
|
|
348
380
|
try {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
} catch {
|
|
381
|
+
const policyContent = yaml.load(fs.readFileSync(policyPath, 'utf-8'));
|
|
382
|
+
policyName = policyContent?.preset || policyContent?.name || 'custom';
|
|
383
|
+
policyMode = policyContent?.enforcement_mode || policyContent?.mode || '';
|
|
384
|
+
if (policyContent?.rules) ruleCount = Object.keys(policyContent.rules).length;
|
|
385
|
+
} catch {
|
|
386
|
+
policyName = 'custom';
|
|
387
|
+
}
|
|
354
388
|
}
|
|
355
|
-
recentMemories.sort((a, b) => b.ts - a.ts);
|
|
356
|
-
recentMemories = recentMemories.slice(0, 3);
|
|
357
|
-
} catch {}
|
|
358
|
-
console.log(` Memory: ${chalk.white.bold(memTotal)} memories${memRecent > 0 ? ` (${memRecent} this week)` : ''}`);
|
|
359
389
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
390
|
+
// Count tracked specs
|
|
391
|
+
const specPatterns = ['openapi.yaml', 'openapi.yml', 'openapi.json', 'swagger.yaml', 'swagger.yml', 'swagger.json'];
|
|
392
|
+
let specCount = 0;
|
|
393
|
+
const _countSpecs = (dir, depth) => {
|
|
394
|
+
if (depth > 3) return;
|
|
395
|
+
try {
|
|
396
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
397
|
+
if (['node_modules', '.next', 'venv', '.git'].includes(entry.name)) continue;
|
|
398
|
+
const full = path.join(dir, entry.name);
|
|
399
|
+
if (entry.isFile() && specPatterns.includes(entry.name.toLowerCase())) specCount++;
|
|
400
|
+
else if (entry.isDirectory()) _countSpecs(full, depth + 1);
|
|
401
|
+
}
|
|
402
|
+
} catch {}
|
|
403
|
+
};
|
|
404
|
+
_countSpecs(target, 0);
|
|
405
|
+
data.governance = { policy: policyName, mode: policyMode, rules: ruleCount, specs: specCount };
|
|
406
|
+
|
|
407
|
+
// --- Git hooks ---
|
|
408
|
+
const preCommitPath = path.join(target, '.git', 'hooks', 'pre-commit');
|
|
409
|
+
let hasGitHooks = false;
|
|
410
|
+
try { hasGitHooks = fs.readFileSync(preCommitPath, 'utf-8').includes('delimit'); } catch {}
|
|
411
|
+
data.hooks = hasGitHooks;
|
|
412
|
+
|
|
413
|
+
// --- CI ---
|
|
414
|
+
const workflowDir = path.join(target, '.github', 'workflows');
|
|
415
|
+
let hasCI = false;
|
|
416
|
+
let ciFile = '';
|
|
366
417
|
try {
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
418
|
+
const wfs = fs.readdirSync(workflowDir);
|
|
419
|
+
for (const wf of wfs) {
|
|
420
|
+
try {
|
|
421
|
+
if (fs.readFileSync(path.join(workflowDir, wf), 'utf-8').includes('delimit')) {
|
|
422
|
+
hasCI = true;
|
|
423
|
+
ciFile = wf;
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
} catch {}
|
|
427
|
+
}
|
|
428
|
+
} catch {}
|
|
429
|
+
data.ci = { active: hasCI, file: ciFile };
|
|
430
|
+
|
|
431
|
+
// --- MCP ---
|
|
432
|
+
const mcpConfigPath = path.join(homedir, '.mcp.json');
|
|
433
|
+
let hasMcp = false;
|
|
434
|
+
let toolCount = 0;
|
|
435
|
+
try { hasMcp = fs.readFileSync(mcpConfigPath, 'utf-8').includes('delimit'); } catch {}
|
|
436
|
+
if (hasMcp) {
|
|
437
|
+
const serverPyPaths = [
|
|
438
|
+
path.join(delimitHome, 'server', 'ai', 'server.py'),
|
|
439
|
+
path.join(delimitHome, 'server', 'server.py'),
|
|
440
|
+
];
|
|
441
|
+
for (const sp of serverPyPaths) {
|
|
442
|
+
try {
|
|
443
|
+
const toolMatches = fs.readFileSync(sp, 'utf-8').match(/@mcp\.tool/g);
|
|
444
|
+
if (toolMatches) { toolCount = toolMatches.length; break; }
|
|
445
|
+
} catch {}
|
|
446
|
+
}
|
|
372
447
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (depth > 3) return;
|
|
448
|
+
data.mcp = { connected: hasMcp, tools: toolCount };
|
|
449
|
+
|
|
450
|
+
// --- Models ---
|
|
451
|
+
const modelsPath = path.join(delimitHome, 'models.json');
|
|
452
|
+
let modelNames = [];
|
|
379
453
|
try {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
specCount++;
|
|
385
|
-
} else if (entry.isDirectory()) {
|
|
386
|
-
_countSpecs(full, depth + 1);
|
|
454
|
+
const modelsData = JSON.parse(fs.readFileSync(modelsPath, 'utf-8'));
|
|
455
|
+
for (const [key, val] of Object.entries(modelsData)) {
|
|
456
|
+
if (val && typeof val === 'object' && (val.api_key || val.enabled !== false)) {
|
|
457
|
+
modelNames.push(key.charAt(0).toUpperCase() + key.slice(1));
|
|
387
458
|
}
|
|
388
459
|
}
|
|
389
460
|
} catch {}
|
|
390
|
-
|
|
391
|
-
_countSpecs(target, 0);
|
|
392
|
-
const specLabel = specCount > 0 ? `${specCount} spec${specCount > 1 ? 's' : ''} tracked` : chalk.gray('no specs');
|
|
393
|
-
console.log(` Governance: ${policyLabel}${hasPolicy ? ' | ' : ' | '}${specLabel}`);
|
|
461
|
+
data.models = modelNames;
|
|
394
462
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
463
|
+
// --- License ---
|
|
464
|
+
const licensePath = path.join(delimitHome, 'license.json');
|
|
465
|
+
let licenseTier = 'Free';
|
|
466
|
+
let licenseActive = false;
|
|
467
|
+
try {
|
|
468
|
+
const ld = JSON.parse(fs.readFileSync(licensePath, 'utf-8'));
|
|
469
|
+
licenseTier = ld.tier || ld.plan || 'Free';
|
|
470
|
+
licenseActive = ld.status === 'active' || ld.valid === true;
|
|
471
|
+
} catch {}
|
|
472
|
+
data.license = { tier: licenseTier, active: licenseActive };
|
|
403
473
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
474
|
+
// --- Ledger stats ---
|
|
475
|
+
const ledgerDir = path.join(delimitHome, 'ledger');
|
|
476
|
+
let ledgerOpen = 0;
|
|
477
|
+
let ledgerTotal = 0;
|
|
478
|
+
try {
|
|
479
|
+
const ledgerFiles = fs.readdirSync(ledgerDir).filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
|
|
480
|
+
for (const lf of ledgerFiles) {
|
|
481
|
+
try {
|
|
482
|
+
const content = fs.readFileSync(path.join(ledgerDir, lf), 'utf-8');
|
|
483
|
+
if (lf.endsWith('.jsonl')) {
|
|
484
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
485
|
+
for (const line of lines) {
|
|
486
|
+
try {
|
|
487
|
+
const item = JSON.parse(line);
|
|
488
|
+
ledgerTotal++;
|
|
489
|
+
if (!item.status || item.status === 'open' || item.status === 'in_progress') ledgerOpen++;
|
|
490
|
+
} catch {}
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
const item = JSON.parse(content);
|
|
494
|
+
if (Array.isArray(item)) {
|
|
495
|
+
ledgerTotal += item.length;
|
|
496
|
+
ledgerOpen += item.filter(i => !i.status || i.status === 'open' || i.status === 'in_progress').length;
|
|
497
|
+
} else {
|
|
498
|
+
ledgerTotal++;
|
|
499
|
+
if (!item.status || item.status === 'open' || item.status === 'in_progress') ledgerOpen++;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch {}
|
|
503
|
+
}
|
|
504
|
+
} catch {}
|
|
505
|
+
data.ledger = { total: ledgerTotal, open: ledgerOpen };
|
|
408
506
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (hasMcp) {
|
|
418
|
-
// Count tools from server.py if available
|
|
419
|
-
const serverPyPaths = [
|
|
420
|
-
path.join(delimitHome, 'server', 'ai', 'server.py'),
|
|
421
|
-
path.join(delimitHome, 'server', 'server.py'),
|
|
422
|
-
];
|
|
423
|
-
for (const sp of serverPyPaths) {
|
|
424
|
-
try {
|
|
425
|
-
const serverContent = fs.readFileSync(sp, 'utf-8');
|
|
426
|
-
const toolMatches = serverContent.match(/@mcp\.tool/g);
|
|
427
|
-
if (toolMatches) {
|
|
428
|
-
toolCount = toolMatches.length;
|
|
429
|
-
break;
|
|
430
|
-
}
|
|
431
|
-
} catch {}
|
|
432
|
-
}
|
|
433
|
-
console.log(` MCP: ${chalk.green('connected')}${toolCount > 0 ? ` (${toolCount} tools)` : ''}`);
|
|
434
|
-
} else {
|
|
435
|
-
console.log(` MCP: ${chalk.gray('not configured')}`);
|
|
436
|
-
}
|
|
507
|
+
// --- Evidence stats ---
|
|
508
|
+
const evidenceDir = path.join(delimitHome, 'evidence');
|
|
509
|
+
let evidenceCount = 0;
|
|
510
|
+
try {
|
|
511
|
+
const evFiles = fs.readdirSync(evidenceDir).filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
|
|
512
|
+
evidenceCount = evFiles.length;
|
|
513
|
+
} catch {}
|
|
514
|
+
data.evidence = evidenceCount;
|
|
437
515
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (
|
|
446
|
-
|
|
516
|
+
// --- Last session ---
|
|
517
|
+
let lastSession = null;
|
|
518
|
+
const sessionsDir = path.join(delimitHome, 'sessions');
|
|
519
|
+
try {
|
|
520
|
+
const sessFiles = fs.readdirSync(sessionsDir)
|
|
521
|
+
.filter(f => f.startsWith('session_') && f.endsWith('.json'))
|
|
522
|
+
.sort().reverse();
|
|
523
|
+
if (sessFiles.length > 0) {
|
|
524
|
+
const latest = JSON.parse(fs.readFileSync(path.join(sessionsDir, sessFiles[0]), 'utf-8'));
|
|
525
|
+
lastSession = latest.summary || latest.description || latest.title || null;
|
|
447
526
|
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
modelsLabel = chalk.white(modelNames.join(' + ')) + chalk.gray(' (BYOK)');
|
|
451
|
-
}
|
|
452
|
-
} catch {}
|
|
453
|
-
console.log(` Models: ${modelsLabel}`);
|
|
527
|
+
} catch {}
|
|
528
|
+
data.lastSession = lastSession;
|
|
454
529
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
530
|
+
// --- Git info ---
|
|
531
|
+
let gitBranch = '';
|
|
532
|
+
let gitDirty = false;
|
|
533
|
+
try {
|
|
534
|
+
gitBranch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe', cwd: target }).toString().trim();
|
|
535
|
+
const statusOut = execSync('git status --porcelain', { stdio: 'pipe', cwd: target }).toString().trim();
|
|
536
|
+
gitDirty = statusOut.length > 0;
|
|
537
|
+
} catch {}
|
|
538
|
+
data.git = { branch: gitBranch, dirty: gitDirty };
|
|
539
|
+
|
|
540
|
+
// --- Version ---
|
|
541
|
+
let cliVersion = '';
|
|
542
|
+
try { cliVersion = require(path.join(__dirname, '..', 'package.json')).version; } catch {}
|
|
543
|
+
data.version = cliVersion;
|
|
544
|
+
|
|
545
|
+
// --- Readiness ---
|
|
546
|
+
const checks = [
|
|
547
|
+
{ name: 'Spec', done: specCount > 0 },
|
|
548
|
+
{ name: 'Policy', done: hasPolicy },
|
|
549
|
+
{ name: 'CI', done: hasCI },
|
|
550
|
+
{ name: 'Hooks', done: hasGitHooks },
|
|
551
|
+
{ name: 'MCP', done: hasMcp },
|
|
552
|
+
];
|
|
553
|
+
const score = checks.filter(c => c.done).length;
|
|
554
|
+
data.readiness = { score, total: checks.length, checks };
|
|
555
|
+
|
|
556
|
+
// === JSON output ===
|
|
557
|
+
if (options.json) {
|
|
558
|
+
console.log(JSON.stringify(data, null, 2));
|
|
559
|
+
return;
|
|
464
560
|
}
|
|
465
|
-
} catch {}
|
|
466
|
-
console.log(` License: ${licenseLabel}`);
|
|
467
561
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
562
|
+
// === Visual dashboard ===
|
|
563
|
+
const W = 60;
|
|
564
|
+
const line = '\u2500'.repeat(W - 2);
|
|
565
|
+
const dot = (ok) => ok ? chalk.green('\u25cf') : chalk.gray('\u25cb');
|
|
566
|
+
const bar = (n, total, width = 20) => {
|
|
567
|
+
const filled = Math.round((n / Math.max(total, 1)) * width);
|
|
568
|
+
return chalk.green('\u2588'.repeat(filled)) + chalk.gray('\u2591'.repeat(width - filled));
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
console.log('');
|
|
572
|
+
console.log(chalk.bold.cyan(` \u250c${line}\u2510`));
|
|
573
|
+
console.log(chalk.bold.cyan(` \u2502`) + chalk.bold.white(` Delimit v${cliVersion}`) + ' '.repeat(W - 14 - cliVersion.length) + (licenseTier !== 'Free' ? chalk.green(licenseTier) : chalk.gray('Free')) + chalk.bold.cyan(` \u2502`));
|
|
574
|
+
console.log(chalk.bold.cyan(` \u2514${line}\u2518`));
|
|
575
|
+
|
|
576
|
+
// Governance section
|
|
577
|
+
console.log(chalk.bold('\n Governance'));
|
|
578
|
+
const policyStr = hasPolicy ? chalk.green(policyName) + (policyMode ? chalk.gray(` (${policyMode})`) : '') + (ruleCount ? chalk.gray(` ${ruleCount} rules`) : '') : chalk.gray('not configured');
|
|
579
|
+
console.log(` Policy: ${policyStr}`);
|
|
580
|
+
console.log(` Specs: ${specCount > 0 ? chalk.white(specCount + ' tracked') : chalk.gray('none')}`);
|
|
581
|
+
console.log(` Hooks: ${hasGitHooks ? chalk.green('pre-commit') : chalk.gray('none')}`);
|
|
582
|
+
console.log(` CI: ${hasCI ? chalk.green(ciFile) : chalk.gray('none')}`);
|
|
583
|
+
|
|
584
|
+
// Infrastructure section
|
|
585
|
+
console.log(chalk.bold('\n Infrastructure'));
|
|
586
|
+
console.log(` MCP: ${hasMcp ? chalk.green(toolCount + ' tools') : chalk.gray('not configured')}`);
|
|
587
|
+
console.log(` Models: ${modelNames.length > 0 ? chalk.white(modelNames.join(chalk.gray(' + '))) : chalk.gray('none')}`);
|
|
588
|
+
console.log(` License: ${licenseTier !== 'Free' && licenseActive ? chalk.green(licenseTier) : licenseTier !== 'Free' ? chalk.yellow(licenseTier) : chalk.gray('Free')}`);
|
|
589
|
+
|
|
590
|
+
// Context section
|
|
591
|
+
console.log(chalk.bold('\n Context'));
|
|
592
|
+
console.log(` Memory: ${chalk.white.bold(memTotal)} total${memRecent > 0 ? chalk.gray(` (${memRecent} this week)`) : ''}`);
|
|
593
|
+
console.log(` Ledger: ${ledgerOpen > 0 ? chalk.yellow(ledgerOpen + ' open') : chalk.gray('0 open')} / ${ledgerTotal} total`);
|
|
594
|
+
console.log(` Evidence: ${evidenceCount > 0 ? chalk.white(evidenceCount + ' records') : chalk.gray('none')}`);
|
|
595
|
+
if (gitBranch) {
|
|
596
|
+
console.log(` Branch: ${chalk.white(gitBranch)}${gitDirty ? chalk.yellow(' (dirty)') : chalk.green(' (clean)')}`);
|
|
476
597
|
}
|
|
477
|
-
}
|
|
478
598
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const latest = JSON.parse(fs.readFileSync(path.join(sessionsDir, sessFiles[0]), 'utf-8'));
|
|
488
|
-
const summary = latest.summary || latest.description || latest.title || null;
|
|
489
|
-
if (summary) {
|
|
490
|
-
const truncated = summary.length > 60 ? summary.slice(0, 60) + '...' : summary;
|
|
491
|
-
console.log(chalk.bold('\n Last session: ') + truncated);
|
|
599
|
+
// Recent memories
|
|
600
|
+
if (recentMemories.length > 0) {
|
|
601
|
+
console.log(chalk.bold('\n Recent Memories'));
|
|
602
|
+
for (const mem of recentMemories) {
|
|
603
|
+
const ago = _relativeTime(mem.ts);
|
|
604
|
+
const src = mem.source !== 'unknown' ? chalk.gray(` [${mem.source}]`) : '';
|
|
605
|
+
const text = mem.text.length > 50 ? mem.text.slice(0, 50) + '...' : mem.text;
|
|
606
|
+
console.log(` ${chalk.gray(ago.padEnd(12))} ${text}${src}`);
|
|
492
607
|
}
|
|
493
608
|
}
|
|
494
|
-
} catch {}
|
|
495
|
-
|
|
496
|
-
// --- Governance readiness ---
|
|
497
|
-
const hasSpecs = specCount > 0;
|
|
498
|
-
const checks = [
|
|
499
|
-
{ name: 'API spec', done: hasSpecs },
|
|
500
|
-
{ name: 'Policy', done: hasPolicy },
|
|
501
|
-
{ name: 'CI gate', done: hasCI },
|
|
502
|
-
{ name: 'Git hooks', done: hasGitHooks },
|
|
503
|
-
{ name: 'MCP', done: hasMcp },
|
|
504
|
-
];
|
|
505
|
-
const score = checks.filter(c => c.done).length;
|
|
506
609
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
610
|
+
// Last session
|
|
611
|
+
if (lastSession) {
|
|
612
|
+
const truncated = lastSession.length > 55 ? lastSession.slice(0, 55) + '...' : lastSession;
|
|
613
|
+
console.log(chalk.bold('\n Last Session'));
|
|
614
|
+
console.log(` ${truncated}`);
|
|
615
|
+
}
|
|
510
616
|
|
|
511
|
-
|
|
512
|
-
console.log(chalk.bold('
|
|
513
|
-
console.log(
|
|
617
|
+
// Readiness bar
|
|
618
|
+
console.log(chalk.bold('\n Readiness'));
|
|
619
|
+
console.log(` ${bar(score, checks.length)} ${score}/${checks.length}`);
|
|
620
|
+
console.log(' ' + checks.map(c => dot(c.done) + ' ' + (c.done ? c.name : chalk.gray(c.name))).join(' '));
|
|
514
621
|
console.log('');
|
|
622
|
+
|
|
623
|
+
if (options.verbose) {
|
|
624
|
+
console.log(chalk.bold(' Continuity Context:'));
|
|
625
|
+
console.log(formatContinuityReport(continuityContext).split('\n').slice(1).map(line => ' ' + line.trimStart()).join('\n'));
|
|
626
|
+
console.log('');
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (options.watch) {
|
|
631
|
+
const clear = () => process.stdout.write('\x1B[2J\x1B[0f');
|
|
632
|
+
const tick = () => { clear(); renderStatus(); };
|
|
633
|
+
tick();
|
|
634
|
+
setInterval(tick, 5000);
|
|
635
|
+
} else {
|
|
636
|
+
renderStatus();
|
|
515
637
|
}
|
|
516
638
|
});
|
|
517
639
|
|
|
@@ -779,8 +901,73 @@ overrides:
|
|
|
779
901
|
}
|
|
780
902
|
|
|
781
903
|
if (options.validate) {
|
|
782
|
-
|
|
783
|
-
|
|
904
|
+
const policyPaths = [
|
|
905
|
+
path.join(process.cwd(), '.delimit', 'policies.yml'),
|
|
906
|
+
path.join(process.cwd(), 'delimit.yml'),
|
|
907
|
+
path.join(process.cwd(), '.delimit.yml'),
|
|
908
|
+
];
|
|
909
|
+
const policyFile = policyPaths.find(p => fs.existsSync(p));
|
|
910
|
+
if (!policyFile) {
|
|
911
|
+
console.log(chalk.red('No policy file found.'));
|
|
912
|
+
console.log(chalk.dim(' Expected: .delimit/policies.yml, delimit.yml, or .delimit.yml'));
|
|
913
|
+
console.log(' Run ' + chalk.cyan('delimit policy --init') + ' to create one.');
|
|
914
|
+
process.exit(1);
|
|
915
|
+
}
|
|
916
|
+
console.log(chalk.bold('Validating: ') + chalk.dim(policyFile));
|
|
917
|
+
try {
|
|
918
|
+
const raw = fs.readFileSync(policyFile, 'utf-8');
|
|
919
|
+
let parsed;
|
|
920
|
+
try {
|
|
921
|
+
parsed = require('js-yaml').load(raw);
|
|
922
|
+
} catch {
|
|
923
|
+
// Fallback: try JSON
|
|
924
|
+
parsed = JSON.parse(raw);
|
|
925
|
+
}
|
|
926
|
+
const errors = [];
|
|
927
|
+
const warnings = [];
|
|
928
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
929
|
+
errors.push('Policy file is empty or not a valid object');
|
|
930
|
+
} else {
|
|
931
|
+
// Check required structure
|
|
932
|
+
if (!parsed.rules && !parsed.mode && !parsed.governance) {
|
|
933
|
+
warnings.push('No "rules", "mode", or "governance" key found');
|
|
934
|
+
}
|
|
935
|
+
if (parsed.rules) {
|
|
936
|
+
if (!Array.isArray(parsed.rules)) {
|
|
937
|
+
errors.push('"rules" must be an array');
|
|
938
|
+
} else {
|
|
939
|
+
parsed.rules.forEach((rule, i) => {
|
|
940
|
+
if (!rule.id) warnings.push(`Rule ${i + 1}: missing "id"`);
|
|
941
|
+
if (!rule.change_types && !rule.name) warnings.push(`Rule ${i + 1}: missing "change_types" or "name"`);
|
|
942
|
+
if (rule.severity && !['error', 'warning', 'info'].includes(rule.severity)) {
|
|
943
|
+
errors.push(`Rule ${i + 1}: invalid severity "${rule.severity}" (use: error, warning, info)`);
|
|
944
|
+
}
|
|
945
|
+
if (rule.action && !['forbid', 'warn', 'allow', 'require_approval'].includes(rule.action)) {
|
|
946
|
+
errors.push(`Rule ${i + 1}: invalid action "${rule.action}" (use: forbid, warn, allow, require_approval)`);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
console.log(chalk.dim(` Rules: ${parsed.rules.length} defined`));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (parsed.mode && !['strict', 'default', 'relaxed'].includes(parsed.mode)) {
|
|
953
|
+
warnings.push(`Unknown mode "${parsed.mode}" (expected: strict, default, relaxed)`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (errors.length > 0) {
|
|
957
|
+
errors.forEach(e => console.log(chalk.red(` ✗ ${e}`)));
|
|
958
|
+
}
|
|
959
|
+
if (warnings.length > 0) {
|
|
960
|
+
warnings.forEach(w => console.log(chalk.yellow(` ⚠ ${w}`)));
|
|
961
|
+
}
|
|
962
|
+
if (errors.length === 0) {
|
|
963
|
+
console.log(chalk.green(' ✓ Policy file is valid'));
|
|
964
|
+
} else {
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
} catch (e) {
|
|
968
|
+
console.log(chalk.red(` ✗ Failed to parse policy: ${e.message}`));
|
|
969
|
+
process.exit(1);
|
|
970
|
+
}
|
|
784
971
|
}
|
|
785
972
|
});
|
|
786
973
|
|
|
@@ -3090,167 +3277,1358 @@ program
|
|
|
3090
3277
|
try { fs.rmSync(tmpDir, { recursive: true }); } catch {}
|
|
3091
3278
|
});
|
|
3092
3279
|
|
|
3093
|
-
//
|
|
3280
|
+
// Report command — generate local governance reports (v4.20)
|
|
3094
3281
|
program
|
|
3095
|
-
.command('
|
|
3096
|
-
.description('
|
|
3097
|
-
.
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3282
|
+
.command('report')
|
|
3283
|
+
.description('Generate a governance report from local evidence, ledger, and memory')
|
|
3284
|
+
.option('--since <duration>', 'Time period (e.g., 7d, 30d, 24h, 1w, 1m)', '7d')
|
|
3285
|
+
.option('--format <fmt>', 'Output format: md, json, html', 'md')
|
|
3286
|
+
.option('--output <file>', 'Write report to file instead of stdout')
|
|
3287
|
+
.action(async (options) => {
|
|
3288
|
+
const delimitHome = path.join(os.homedir(), '.delimit');
|
|
3289
|
+
const evidenceDir = path.join(delimitHome, 'evidence');
|
|
3290
|
+
const ledgerDir = path.join(delimitHome, 'ledger');
|
|
3291
|
+
const memoryDir = path.join(delimitHome, 'memory');
|
|
3102
3292
|
|
|
3103
|
-
//
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
if (policy && (policy.rules !== undefined || policy.override_defaults !== undefined)) {
|
|
3112
|
-
console.log(chalk.green(' ✓ Policy file is valid YAML'));
|
|
3113
|
-
ok++;
|
|
3114
|
-
} else {
|
|
3115
|
-
console.log(chalk.yellow(' ⚠ Policy file has no rules section'));
|
|
3116
|
-
warn++;
|
|
3117
|
-
}
|
|
3118
|
-
} catch (e) {
|
|
3119
|
-
console.log(chalk.red(` ✗ Policy file has invalid YAML: ${e.message}`));
|
|
3120
|
-
fail++;
|
|
3121
|
-
}
|
|
3122
|
-
} else {
|
|
3123
|
-
console.log(chalk.red(' ✗ No .delimit/policies.yml — run: delimit init'));
|
|
3124
|
-
fail++;
|
|
3293
|
+
// Parse duration into milliseconds
|
|
3294
|
+
function parseDuration(dur) {
|
|
3295
|
+
const match = dur.match(/^(\d+)\s*(h|d|w|m)$/i);
|
|
3296
|
+
if (!match) return 7 * 24 * 60 * 60 * 1000; // default 7d
|
|
3297
|
+
const val = parseInt(match[1], 10);
|
|
3298
|
+
const unit = match[2].toLowerCase();
|
|
3299
|
+
const multipliers = { h: 3600000, d: 86400000, w: 604800000, m: 2592000000 };
|
|
3300
|
+
return val * (multipliers[unit] || 86400000);
|
|
3125
3301
|
}
|
|
3126
3302
|
|
|
3127
|
-
|
|
3128
|
-
const
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
];
|
|
3136
|
-
const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(process.cwd(), p)));
|
|
3137
|
-
if (foundSpecs.length > 0) {
|
|
3138
|
-
console.log(chalk.green(` ✓ OpenAPI spec found: ${foundSpecs[0]}`));
|
|
3139
|
-
ok++;
|
|
3140
|
-
} else {
|
|
3141
|
-
// Check for framework (Zero-Spec candidate)
|
|
3142
|
-
const pkgJson = path.join(process.cwd(), 'package.json');
|
|
3143
|
-
const reqTxt = path.join(process.cwd(), 'requirements.txt');
|
|
3144
|
-
if (fs.existsSync(pkgJson) || fs.existsSync(reqTxt)) {
|
|
3145
|
-
console.log(chalk.yellow(' ⚠ No OpenAPI spec file — Zero-Spec Mode may work if this is a FastAPI/NestJS/Express project'));
|
|
3146
|
-
warn++;
|
|
3147
|
-
} else {
|
|
3148
|
-
console.log(chalk.red(' ✗ No OpenAPI spec file found'));
|
|
3149
|
-
fail++;
|
|
3150
|
-
}
|
|
3303
|
+
const sinceMs = parseDuration(options.since);
|
|
3304
|
+
const cutoff = new Date(Date.now() - sinceMs);
|
|
3305
|
+
const now = new Date();
|
|
3306
|
+
const fmt = (options.format || 'md').toLowerCase();
|
|
3307
|
+
|
|
3308
|
+
if (!['md', 'json', 'html'].includes(fmt)) {
|
|
3309
|
+
console.error(chalk.red(` Invalid format: ${fmt}. Use md, json, or html.`));
|
|
3310
|
+
process.exit(1);
|
|
3151
3311
|
}
|
|
3152
3312
|
|
|
3153
|
-
//
|
|
3154
|
-
const
|
|
3155
|
-
if (fs.existsSync(
|
|
3156
|
-
const
|
|
3157
|
-
const
|
|
3313
|
+
// Collect evidence events
|
|
3314
|
+
const evidenceEvents = [];
|
|
3315
|
+
if (fs.existsSync(evidenceDir)) {
|
|
3316
|
+
const files = fs.readdirSync(evidenceDir);
|
|
3317
|
+
for (const f of files) {
|
|
3318
|
+
const fp = path.join(evidenceDir, f);
|
|
3158
3319
|
try {
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3320
|
+
if (f.endsWith('.json')) {
|
|
3321
|
+
const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
3322
|
+
// Determine timestamp from various fields
|
|
3323
|
+
let ts = null;
|
|
3324
|
+
if (data.timestamp) ts = new Date(data.timestamp);
|
|
3325
|
+
else if (data.collected_at) ts = new Date(data.collected_at * 1000);
|
|
3326
|
+
if (ts && ts >= cutoff) {
|
|
3327
|
+
evidenceEvents.push({ ...data, _ts: ts, _file: f });
|
|
3328
|
+
}
|
|
3329
|
+
} else if (f.endsWith('.jsonl')) {
|
|
3330
|
+
const lines = fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean);
|
|
3331
|
+
for (const line of lines) {
|
|
3332
|
+
try {
|
|
3333
|
+
const data = JSON.parse(line);
|
|
3334
|
+
let ts = null;
|
|
3335
|
+
if (data.timestamp) ts = new Date(data.timestamp);
|
|
3336
|
+
else if (data.collected_at) ts = new Date(data.collected_at * 1000);
|
|
3337
|
+
if (ts && ts >= cutoff) {
|
|
3338
|
+
evidenceEvents.push({ ...data, _ts: ts, _file: f });
|
|
3339
|
+
}
|
|
3340
|
+
} catch {}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
} catch {}
|
|
3169
3344
|
}
|
|
3170
|
-
} else {
|
|
3171
|
-
console.log(chalk.yellow(' ⚠ No .github/workflows/ directory'));
|
|
3172
|
-
warn++;
|
|
3173
3345
|
}
|
|
3346
|
+
evidenceEvents.sort((a, b) => a._ts - b._ts);
|
|
3174
3347
|
|
|
3175
|
-
//
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
}
|
|
3348
|
+
// Categorize evidence
|
|
3349
|
+
const violations = evidenceEvents.filter(e =>
|
|
3350
|
+
e.result === 'failed' || e.result === 'blocked' ||
|
|
3351
|
+
(e.action && /fail|block|violation|error/i.test(e.action))
|
|
3352
|
+
);
|
|
3353
|
+
const approvals = evidenceEvents.filter(e =>
|
|
3354
|
+
e.result === 'passed' || e.result === 'approved' ||
|
|
3355
|
+
(e.action && /pass|approve|success/i.test(e.action))
|
|
3356
|
+
);
|
|
3185
3357
|
|
|
3186
|
-
//
|
|
3187
|
-
const
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3358
|
+
// Collect ledger items
|
|
3359
|
+
const ledgerItems = [];
|
|
3360
|
+
if (fs.existsSync(ledgerDir)) {
|
|
3361
|
+
const files = fs.readdirSync(ledgerDir);
|
|
3362
|
+
for (const f of files) {
|
|
3363
|
+
const fp = path.join(ledgerDir, f);
|
|
3364
|
+
try {
|
|
3365
|
+
if (f.endsWith('.json')) {
|
|
3366
|
+
const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
3367
|
+
if (Array.isArray(data)) {
|
|
3368
|
+
ledgerItems.push(...data);
|
|
3369
|
+
} else {
|
|
3370
|
+
ledgerItems.push(data);
|
|
3371
|
+
}
|
|
3372
|
+
} else if (f.endsWith('.jsonl')) {
|
|
3373
|
+
const lines = fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean);
|
|
3374
|
+
for (const line of lines) {
|
|
3375
|
+
try { ledgerItems.push(JSON.parse(line)); } catch {}
|
|
3376
|
+
}
|
|
3199
3377
|
}
|
|
3200
|
-
}
|
|
3201
|
-
walk(delimitDir, '.delimit');
|
|
3202
|
-
} catch {}
|
|
3203
|
-
console.log(chalk.green(` Already initialized — ${files.length} file(s) in .delimit/`));
|
|
3204
|
-
files.slice(0, 8).forEach(f => console.log(chalk.gray(` ${f}`)));
|
|
3205
|
-
if (files.length > 8) console.log(chalk.gray(` ... and ${files.length - 8} more`));
|
|
3206
|
-
} else {
|
|
3207
|
-
console.log(chalk.gray(' Running delimit init would create:'));
|
|
3208
|
-
console.log(chalk.gray(' .delimit/policies.yml — governance policy rules'));
|
|
3209
|
-
console.log(chalk.gray(' .delimit/evidence/ — audit trail events'));
|
|
3210
|
-
console.log(chalk.gray(' .delimit/compliance.json — if compliance template selected'));
|
|
3211
|
-
if (fs.existsSync(path.join(process.cwd(), '.github'))) {
|
|
3212
|
-
console.log(chalk.gray(' .github/workflows/api-governance.yml'));
|
|
3213
|
-
console.log(chalk.gray(' .github/workflows/api-drift-monitor.yml'));
|
|
3378
|
+
} catch {}
|
|
3214
3379
|
}
|
|
3215
3380
|
}
|
|
3381
|
+
const openLedgerItems = ledgerItems.filter(i => i.status === 'open' || !i.status);
|
|
3216
3382
|
|
|
3217
|
-
//
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3383
|
+
// Count memory entries
|
|
3384
|
+
let memoryCount = 0;
|
|
3385
|
+
const recentMemories = [];
|
|
3386
|
+
if (fs.existsSync(memoryDir)) {
|
|
3387
|
+
const files = fs.readdirSync(memoryDir).filter(f => f.endsWith('.json') || f.endsWith('.jsonl'));
|
|
3388
|
+
for (const f of files) {
|
|
3389
|
+
const fp = path.join(memoryDir, f);
|
|
3390
|
+
try {
|
|
3391
|
+
if (f.endsWith('.json')) {
|
|
3392
|
+
const data = JSON.parse(fs.readFileSync(fp, 'utf-8'));
|
|
3393
|
+
memoryCount++;
|
|
3394
|
+
if (data.created_at && new Date(data.created_at) >= cutoff) {
|
|
3395
|
+
recentMemories.push(data);
|
|
3396
|
+
}
|
|
3397
|
+
} else if (f.endsWith('.jsonl')) {
|
|
3398
|
+
const lines = fs.readFileSync(fp, 'utf-8').split('\n').filter(Boolean);
|
|
3399
|
+
for (const line of lines) {
|
|
3400
|
+
try {
|
|
3401
|
+
const data = JSON.parse(line);
|
|
3402
|
+
memoryCount++;
|
|
3403
|
+
if (data.created_at && new Date(data.created_at) >= cutoff) {
|
|
3404
|
+
recentMemories.push(data);
|
|
3405
|
+
}
|
|
3406
|
+
} catch {}
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
} catch {}
|
|
3410
|
+
}
|
|
3230
3411
|
}
|
|
3231
|
-
});
|
|
3232
|
-
|
|
3233
|
-
// Hooks command — install/remove git hooks for governance
|
|
3234
|
-
program
|
|
3235
|
-
.command('hooks <action>')
|
|
3236
|
-
.description('Install or remove git hooks (install | remove | status)')
|
|
3237
|
-
.option('--pre-push', 'Also add pre-push hook')
|
|
3238
|
-
.action(async (action, opts) => {
|
|
3239
|
-
const projectDir = process.cwd();
|
|
3240
|
-
const gitDir = path.join(projectDir, '.git');
|
|
3241
3412
|
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3413
|
+
// Git history
|
|
3414
|
+
let gitCommits = [];
|
|
3415
|
+
try {
|
|
3416
|
+
const sinceDate = cutoff.toISOString().split('T')[0];
|
|
3417
|
+
const gitLog = execSync(
|
|
3418
|
+
`git log --oneline --since="${sinceDate}" --no-decorate -20 2>/dev/null`,
|
|
3419
|
+
{ encoding: 'utf-8', timeout: 5000 }
|
|
3420
|
+
).trim();
|
|
3421
|
+
if (gitLog) {
|
|
3422
|
+
gitCommits = gitLog.split('\n').filter(Boolean);
|
|
3423
|
+
}
|
|
3424
|
+
} catch {}
|
|
3247
3425
|
|
|
3248
|
-
|
|
3249
|
-
|
|
3426
|
+
// Pre-commit hook detection
|
|
3427
|
+
let hasPreCommitHook = false;
|
|
3428
|
+
try {
|
|
3429
|
+
const hookPath = path.join(process.cwd(), '.git', 'hooks', 'pre-commit');
|
|
3430
|
+
hasPreCommitHook = fs.existsSync(hookPath);
|
|
3431
|
+
} catch {}
|
|
3250
3432
|
|
|
3251
|
-
|
|
3252
|
-
const
|
|
3253
|
-
|
|
3433
|
+
// Recommendations
|
|
3434
|
+
const recommendations = [];
|
|
3435
|
+
if (!hasPreCommitHook) {
|
|
3436
|
+
recommendations.push('Consider adding pre-commit hooks: npx delimit-cli init');
|
|
3437
|
+
}
|
|
3438
|
+
if (violations.length > approvals.length && evidenceEvents.length > 0) {
|
|
3439
|
+
recommendations.push('Failure rate exceeds pass rate — review policy strictness or address recurring violations');
|
|
3440
|
+
}
|
|
3441
|
+
if (evidenceEvents.length === 0) {
|
|
3442
|
+
recommendations.push('No governance events found — run delimit lint or delimit scan to start collecting evidence');
|
|
3443
|
+
}
|
|
3444
|
+
if (openLedgerItems.length > 20) {
|
|
3445
|
+
recommendations.push(`${openLedgerItems.length} open ledger items — consider triaging and closing resolved items`);
|
|
3446
|
+
}
|
|
3447
|
+
if (memoryCount === 0) {
|
|
3448
|
+
recommendations.push('No memory entries — use delimit remember to capture architecture decisions and gotchas');
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
// Build report data
|
|
3452
|
+
const reportData = {
|
|
3453
|
+
generated_at: now.toISOString(),
|
|
3454
|
+
period: { since: cutoff.toISOString(), until: now.toISOString(), duration: options.since },
|
|
3455
|
+
summary: {
|
|
3456
|
+
total_events: evidenceEvents.length,
|
|
3457
|
+
violations: violations.length,
|
|
3458
|
+
approvals: approvals.length,
|
|
3459
|
+
other_events: evidenceEvents.length - violations.length - approvals.length,
|
|
3460
|
+
pass_rate: evidenceEvents.length > 0
|
|
3461
|
+
? Math.round((approvals.length / evidenceEvents.length) * 100)
|
|
3462
|
+
: null,
|
|
3463
|
+
ledger_items_open: openLedgerItems.length,
|
|
3464
|
+
ledger_items_total: ledgerItems.length,
|
|
3465
|
+
memory_entries: memoryCount,
|
|
3466
|
+
recent_memories: recentMemories.length,
|
|
3467
|
+
git_commits: gitCommits.length,
|
|
3468
|
+
},
|
|
3469
|
+
violations: violations.map(v => ({
|
|
3470
|
+
action: v.action || 'unknown',
|
|
3471
|
+
result: v.result || 'failed',
|
|
3472
|
+
timestamp: v._ts.toISOString(),
|
|
3473
|
+
target: v.target || null,
|
|
3474
|
+
files: v.files || null,
|
|
3475
|
+
})),
|
|
3476
|
+
approvals: approvals.map(a => ({
|
|
3477
|
+
action: a.action || 'unknown',
|
|
3478
|
+
result: a.result || 'passed',
|
|
3479
|
+
timestamp: a._ts.toISOString(),
|
|
3480
|
+
target: a.target || null,
|
|
3481
|
+
})),
|
|
3482
|
+
audit_events: evidenceEvents.map(e => ({
|
|
3483
|
+
action: e.action || 'evidence_collected',
|
|
3484
|
+
result: e.result || null,
|
|
3485
|
+
timestamp: e._ts.toISOString(),
|
|
3486
|
+
target: e.target || null,
|
|
3487
|
+
file: e._file,
|
|
3488
|
+
})),
|
|
3489
|
+
active_ledger_items: openLedgerItems.slice(0, 50).map(i => ({
|
|
3490
|
+
id: i.id || null,
|
|
3491
|
+
title: i.title || null,
|
|
3492
|
+
type: i.type || null,
|
|
3493
|
+
priority: i.priority || null,
|
|
3494
|
+
status: i.status || 'open',
|
|
3495
|
+
created_at: i.created_at || null,
|
|
3496
|
+
})),
|
|
3497
|
+
git_commits: gitCommits,
|
|
3498
|
+
recommendations,
|
|
3499
|
+
};
|
|
3500
|
+
|
|
3501
|
+
// Format output
|
|
3502
|
+
let output = '';
|
|
3503
|
+
|
|
3504
|
+
if (fmt === 'json') {
|
|
3505
|
+
output = JSON.stringify(reportData, null, 2);
|
|
3506
|
+
} else if (fmt === 'md') {
|
|
3507
|
+
const lines = [];
|
|
3508
|
+
lines.push('# Delimit Governance Report');
|
|
3509
|
+
lines.push('');
|
|
3510
|
+
lines.push(`**Period**: ${cutoff.toISOString().split('T')[0]} to ${now.toISOString().split('T')[0]} (${options.since})`);
|
|
3511
|
+
lines.push(`**Generated**: ${now.toISOString()}`);
|
|
3512
|
+
lines.push('');
|
|
3513
|
+
|
|
3514
|
+
lines.push('## Summary');
|
|
3515
|
+
lines.push('');
|
|
3516
|
+
lines.push(`| Metric | Value |`);
|
|
3517
|
+
lines.push(`|--------|-------|`);
|
|
3518
|
+
lines.push(`| Total governance events | ${reportData.summary.total_events} |`);
|
|
3519
|
+
lines.push(`| Violations | ${reportData.summary.violations} |`);
|
|
3520
|
+
lines.push(`| Approvals | ${reportData.summary.approvals} |`);
|
|
3521
|
+
lines.push(`| Pass rate | ${reportData.summary.pass_rate !== null ? reportData.summary.pass_rate + '%' : 'N/A'} |`);
|
|
3522
|
+
lines.push(`| Open ledger items | ${reportData.summary.ledger_items_open} |`);
|
|
3523
|
+
lines.push(`| Memory entries | ${reportData.summary.memory_entries} (${reportData.summary.recent_memories} recent) |`);
|
|
3524
|
+
lines.push(`| Git commits | ${reportData.summary.git_commits} |`);
|
|
3525
|
+
lines.push('');
|
|
3526
|
+
|
|
3527
|
+
if (reportData.violations.length > 0) {
|
|
3528
|
+
lines.push('## Violations');
|
|
3529
|
+
lines.push('');
|
|
3530
|
+
for (const v of reportData.violations) {
|
|
3531
|
+
lines.push(`- **${v.action}** - ${v.result} at ${v.timestamp}${v.target ? ' (' + v.target + ')' : ''}`);
|
|
3532
|
+
}
|
|
3533
|
+
lines.push('');
|
|
3534
|
+
} else {
|
|
3535
|
+
lines.push('## Violations');
|
|
3536
|
+
lines.push('');
|
|
3537
|
+
lines.push('No violations in this period.');
|
|
3538
|
+
lines.push('');
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
if (reportData.approvals.length > 0) {
|
|
3542
|
+
lines.push('## Approvals');
|
|
3543
|
+
lines.push('');
|
|
3544
|
+
for (const a of reportData.approvals) {
|
|
3545
|
+
lines.push(`- **${a.action}** - ${a.result} at ${a.timestamp}${a.target ? ' (' + a.target + ')' : ''}`);
|
|
3546
|
+
}
|
|
3547
|
+
lines.push('');
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
if (reportData.audit_events.length > 0) {
|
|
3551
|
+
lines.push('## Audit Events');
|
|
3552
|
+
lines.push('');
|
|
3553
|
+
for (const e of reportData.audit_events) {
|
|
3554
|
+
lines.push(`- \`${e.timestamp}\` ${e.action}${e.result ? ' [' + e.result + ']' : ''}${e.target ? ' - ' + e.target : ''}`);
|
|
3555
|
+
}
|
|
3556
|
+
lines.push('');
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
if (reportData.active_ledger_items.length > 0) {
|
|
3560
|
+
lines.push('## Active Ledger Items');
|
|
3561
|
+
lines.push('');
|
|
3562
|
+
for (const i of reportData.active_ledger_items) {
|
|
3563
|
+
const prefix = i.priority ? `[${i.priority}]` : '';
|
|
3564
|
+
lines.push(`- ${prefix} ${i.id || ''} ${i.title || 'Untitled'}${i.type ? ' (' + i.type + ')' : ''}`);
|
|
3565
|
+
}
|
|
3566
|
+
if (openLedgerItems.length > 50) {
|
|
3567
|
+
lines.push(`- ... and ${openLedgerItems.length - 50} more`);
|
|
3568
|
+
}
|
|
3569
|
+
lines.push('');
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
if (gitCommits.length > 0) {
|
|
3573
|
+
lines.push('## Recent Commits');
|
|
3574
|
+
lines.push('');
|
|
3575
|
+
for (const c of gitCommits) {
|
|
3576
|
+
lines.push(`- \`${c}\``);
|
|
3577
|
+
}
|
|
3578
|
+
lines.push('');
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
if (recommendations.length > 0) {
|
|
3582
|
+
lines.push('## Recommendations');
|
|
3583
|
+
lines.push('');
|
|
3584
|
+
for (const r of recommendations) {
|
|
3585
|
+
lines.push(`- ${r}`);
|
|
3586
|
+
}
|
|
3587
|
+
lines.push('');
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
lines.push('---');
|
|
3591
|
+
lines.push('*Generated by [Delimit](https://delimit.ai) governance reporting*');
|
|
3592
|
+
output = lines.join('\n');
|
|
3593
|
+
} else if (fmt === 'html') {
|
|
3594
|
+
const esc = (s) => String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
3595
|
+
const h = [];
|
|
3596
|
+
h.push('<!DOCTYPE html>');
|
|
3597
|
+
h.push('<html lang="en"><head><meta charset="utf-8">');
|
|
3598
|
+
h.push('<title>Delimit Governance Report</title>');
|
|
3599
|
+
h.push('<style>');
|
|
3600
|
+
h.push('body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:900px;margin:2rem auto;padding:0 1rem;color:#1a1a2e;line-height:1.6}');
|
|
3601
|
+
h.push('h1{color:#16213e;border-bottom:3px solid #0f3460;padding-bottom:.5rem}');
|
|
3602
|
+
h.push('h2{color:#0f3460;margin-top:2rem}');
|
|
3603
|
+
h.push('table{border-collapse:collapse;width:100%}');
|
|
3604
|
+
h.push('th,td{text-align:left;padding:.5rem .75rem;border:1px solid #ddd}');
|
|
3605
|
+
h.push('th{background:#f0f4f8;font-weight:600}');
|
|
3606
|
+
h.push('.pass{color:#16a34a}.fail{color:#dc2626}');
|
|
3607
|
+
h.push('.badge{display:inline-block;padding:.1rem .5rem;border-radius:3px;font-size:.85rem;font-weight:600}');
|
|
3608
|
+
h.push('.badge-ok{background:#dcfce7;color:#166534}.badge-fail{background:#fef2f2;color:#991b1b}');
|
|
3609
|
+
h.push('ul{padding-left:1.5rem}li{margin:.25rem 0}');
|
|
3610
|
+
h.push('.meta{color:#6b7280;font-size:.9rem}');
|
|
3611
|
+
h.push('footer{margin-top:2rem;padding-top:1rem;border-top:1px solid #ddd;color:#9ca3af;font-size:.85rem}');
|
|
3612
|
+
h.push('</style></head><body>');
|
|
3613
|
+
h.push('<h1>Delimit Governance Report</h1>');
|
|
3614
|
+
h.push(`<p class="meta">Period: ${esc(cutoff.toISOString().split('T')[0])} to ${esc(now.toISOString().split('T')[0])} (${esc(options.since)}) — Generated: ${esc(now.toISOString())}</p>`);
|
|
3615
|
+
|
|
3616
|
+
h.push('<h2>Summary</h2>');
|
|
3617
|
+
h.push('<table><tr><th>Metric</th><th>Value</th></tr>');
|
|
3618
|
+
h.push(`<tr><td>Total governance events</td><td>${reportData.summary.total_events}</td></tr>`);
|
|
3619
|
+
h.push(`<tr><td>Violations</td><td><span class="${reportData.summary.violations > 0 ? 'fail' : ''}">${reportData.summary.violations}</span></td></tr>`);
|
|
3620
|
+
h.push(`<tr><td>Approvals</td><td><span class="pass">${reportData.summary.approvals}</span></td></tr>`);
|
|
3621
|
+
h.push(`<tr><td>Pass rate</td><td>${reportData.summary.pass_rate !== null ? '<span class="badge ' + (reportData.summary.pass_rate >= 80 ? 'badge-ok' : 'badge-fail') + '">' + reportData.summary.pass_rate + '%</span>' : 'N/A'}</td></tr>`);
|
|
3622
|
+
h.push(`<tr><td>Open ledger items</td><td>${reportData.summary.ledger_items_open}</td></tr>`);
|
|
3623
|
+
h.push(`<tr><td>Memory entries</td><td>${reportData.summary.memory_entries} (${reportData.summary.recent_memories} recent)</td></tr>`);
|
|
3624
|
+
h.push(`<tr><td>Git commits</td><td>${reportData.summary.git_commits}</td></tr>`);
|
|
3625
|
+
h.push('</table>');
|
|
3626
|
+
|
|
3627
|
+
h.push('<h2>Violations</h2>');
|
|
3628
|
+
if (reportData.violations.length > 0) {
|
|
3629
|
+
h.push('<ul>');
|
|
3630
|
+
for (const v of reportData.violations) {
|
|
3631
|
+
h.push(`<li><strong>${esc(v.action)}</strong> - ${esc(v.result)} at ${esc(v.timestamp)}${v.target ? ' (' + esc(v.target) + ')' : ''}</li>`);
|
|
3632
|
+
}
|
|
3633
|
+
h.push('</ul>');
|
|
3634
|
+
} else {
|
|
3635
|
+
h.push('<p class="pass">No violations in this period.</p>');
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
if (reportData.approvals.length > 0) {
|
|
3639
|
+
h.push('<h2>Approvals</h2><ul>');
|
|
3640
|
+
for (const a of reportData.approvals) {
|
|
3641
|
+
h.push(`<li><strong>${esc(a.action)}</strong> - ${esc(a.result)} at ${esc(a.timestamp)}${a.target ? ' (' + esc(a.target) + ')' : ''}</li>`);
|
|
3642
|
+
}
|
|
3643
|
+
h.push('</ul>');
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
if (reportData.audit_events.length > 0) {
|
|
3647
|
+
h.push('<h2>Audit Events</h2><ul>');
|
|
3648
|
+
for (const e of reportData.audit_events) {
|
|
3649
|
+
h.push(`<li><code>${esc(e.timestamp)}</code> ${esc(e.action)}${e.result ? ' [' + esc(e.result) + ']' : ''}${e.target ? ' - ' + esc(e.target) : ''}</li>`);
|
|
3650
|
+
}
|
|
3651
|
+
h.push('</ul>');
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
if (reportData.active_ledger_items.length > 0) {
|
|
3655
|
+
h.push('<h2>Active Ledger Items</h2><ul>');
|
|
3656
|
+
for (const i of reportData.active_ledger_items) {
|
|
3657
|
+
const prefix = i.priority ? `[${esc(i.priority)}]` : '';
|
|
3658
|
+
h.push(`<li>${prefix} ${esc(i.id || '')} ${esc(i.title || 'Untitled')}${i.type ? ' (' + esc(i.type) + ')' : ''}</li>`);
|
|
3659
|
+
}
|
|
3660
|
+
if (openLedgerItems.length > 50) {
|
|
3661
|
+
h.push(`<li>... and ${openLedgerItems.length - 50} more</li>`);
|
|
3662
|
+
}
|
|
3663
|
+
h.push('</ul>');
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
if (gitCommits.length > 0) {
|
|
3667
|
+
h.push('<h2>Recent Commits</h2><ul>');
|
|
3668
|
+
for (const c of gitCommits) {
|
|
3669
|
+
h.push(`<li><code>${esc(c)}</code></li>`);
|
|
3670
|
+
}
|
|
3671
|
+
h.push('</ul>');
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
if (recommendations.length > 0) {
|
|
3675
|
+
h.push('<h2>Recommendations</h2><ul>');
|
|
3676
|
+
for (const r of recommendations) {
|
|
3677
|
+
h.push(`<li>${esc(r)}</li>`);
|
|
3678
|
+
}
|
|
3679
|
+
h.push('</ul>');
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
h.push('<footer>Generated by <a href="https://delimit.ai">Delimit</a> governance reporting</footer>');
|
|
3683
|
+
h.push('</body></html>');
|
|
3684
|
+
output = h.join('\n');
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// Output
|
|
3688
|
+
if (options.output) {
|
|
3689
|
+
const outPath = path.resolve(options.output);
|
|
3690
|
+
fs.writeFileSync(outPath, output, 'utf-8');
|
|
3691
|
+
console.log(chalk.green(` Report written to ${outPath}`));
|
|
3692
|
+
} else {
|
|
3693
|
+
if (fmt === 'md' || fmt === 'html') {
|
|
3694
|
+
console.log(output);
|
|
3695
|
+
} else {
|
|
3696
|
+
console.log(output);
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
});
|
|
3700
|
+
|
|
3701
|
+
// Doctor command — verify setup is correct
|
|
3702
|
+
program
|
|
3703
|
+
.command('doctor')
|
|
3704
|
+
.description('Verify Delimit setup and diagnose common issues')
|
|
3705
|
+
.option('--ci', 'Output JSON and exit non-zero on failures (for pipelines)')
|
|
3706
|
+
.option('--fix', 'Automatically fix issues that have safe auto-fixes')
|
|
3707
|
+
.option('--dry-run', 'Preview what doctor --fix would create/modify without making changes')
|
|
3708
|
+
.option('--undo', 'Revert changes made by the last doctor --fix run')
|
|
3709
|
+
.action(async (opts) => {
|
|
3710
|
+
const ciMode = !!opts.ci;
|
|
3711
|
+
const fixMode = !!opts.fix;
|
|
3712
|
+
const dryRunMode = !!opts.dryRun;
|
|
3713
|
+
const undoMode = !!opts.undo;
|
|
3714
|
+
const homeDir = os.homedir();
|
|
3715
|
+
const delimitHome = path.join(homeDir, '.delimit');
|
|
3716
|
+
const manifestPath = path.join(process.cwd(), '.delimit', 'doctor-manifest.json');
|
|
3717
|
+
|
|
3718
|
+
// --- Undo mode: revert last doctor --fix changes ---
|
|
3719
|
+
if (undoMode) {
|
|
3720
|
+
if (!fs.existsSync(manifestPath)) {
|
|
3721
|
+
console.log(chalk.yellow('\n No doctor-manifest.json found. Nothing to undo.\n'));
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
try {
|
|
3725
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
3726
|
+
const actions = manifest.actions || [];
|
|
3727
|
+
let reverted = 0;
|
|
3728
|
+
let skipped = 0;
|
|
3729
|
+
console.log(chalk.bold('\n Delimit Doctor — Undo\n'));
|
|
3730
|
+
for (const entry of actions) {
|
|
3731
|
+
const targetPath = entry.path;
|
|
3732
|
+
if (entry.action === 'created') {
|
|
3733
|
+
if (fs.existsSync(targetPath)) {
|
|
3734
|
+
const stat = fs.statSync(targetPath);
|
|
3735
|
+
if (stat.isDirectory()) {
|
|
3736
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
3737
|
+
} else {
|
|
3738
|
+
fs.unlinkSync(targetPath);
|
|
3739
|
+
}
|
|
3740
|
+
console.log(chalk.red(` - Removed: ${targetPath}`));
|
|
3741
|
+
reverted++;
|
|
3742
|
+
} else {
|
|
3743
|
+
console.log(chalk.gray(` - Already gone: ${targetPath}`));
|
|
3744
|
+
skipped++;
|
|
3745
|
+
}
|
|
3746
|
+
} else {
|
|
3747
|
+
console.log(chalk.yellow(` - Skipped (${entry.action}): ${targetPath}`));
|
|
3748
|
+
skipped++;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
fs.unlinkSync(manifestPath);
|
|
3752
|
+
console.log(chalk.green(`\n Reverted ${reverted} item(s), skipped ${skipped}.\n`));
|
|
3753
|
+
} catch (e) {
|
|
3754
|
+
console.log(chalk.red(`\n Failed to read manifest: ${e.message}\n`));
|
|
3755
|
+
process.exitCode = 1;
|
|
3756
|
+
}
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
// --- Dry-run mode: preview what --fix would create/modify ---
|
|
3761
|
+
if (dryRunMode) {
|
|
3762
|
+
console.log(chalk.bold('\n Delimit Doctor — Dry Run Preview\n'));
|
|
3763
|
+
const planned = [];
|
|
3764
|
+
const delimitDir = path.join(process.cwd(), '.delimit');
|
|
3765
|
+
const policyFile = path.join(delimitDir, 'policies.yml');
|
|
3766
|
+
const ledgerDir = path.join(delimitDir, 'ledger');
|
|
3767
|
+
const evidenceDir = path.join(delimitDir, 'evidence');
|
|
3768
|
+
const memoryDir = path.join(delimitHome, 'memory');
|
|
3769
|
+
const mcpServerPath = path.join(delimitHome, 'server', 'ai', 'server.py');
|
|
3770
|
+
|
|
3771
|
+
if (!fs.existsSync(policyFile)) {
|
|
3772
|
+
if (!fs.existsSync(delimitDir)) {
|
|
3773
|
+
planned.push({ path: delimitDir, action: 'create_dir', description: '.delimit/ governance directory' });
|
|
3774
|
+
}
|
|
3775
|
+
planned.push({ path: policyFile, action: 'create_file', description: 'Governance policy rules (via delimit init)' });
|
|
3776
|
+
}
|
|
3777
|
+
if (!fs.existsSync(ledgerDir)) {
|
|
3778
|
+
planned.push({ path: ledgerDir, action: 'create_dir', description: 'Operations ledger directory' });
|
|
3779
|
+
}
|
|
3780
|
+
if (!fs.existsSync(evidenceDir)) {
|
|
3781
|
+
planned.push({ path: evidenceDir, action: 'create_dir', description: 'Audit trail events directory' });
|
|
3782
|
+
}
|
|
3783
|
+
if (!fs.existsSync(memoryDir)) {
|
|
3784
|
+
planned.push({ path: memoryDir, action: 'create_dir', description: '~/.delimit/memory/ directory' });
|
|
3785
|
+
}
|
|
3786
|
+
if (!fs.existsSync(mcpServerPath)) {
|
|
3787
|
+
planned.push({ path: mcpServerPath, action: 'create_file', description: 'MCP server (via delimit setup --all)' });
|
|
3788
|
+
}
|
|
3789
|
+
// GitHub workflow
|
|
3790
|
+
const workflowDir = path.join(process.cwd(), '.github', 'workflows');
|
|
3791
|
+
if (fs.existsSync(path.join(process.cwd(), '.github'))) {
|
|
3792
|
+
const wf = path.join(workflowDir, 'api-governance.yml');
|
|
3793
|
+
if (!fs.existsSync(wf)) {
|
|
3794
|
+
planned.push({ path: wf, action: 'create_file', description: 'API governance GitHub Action workflow' });
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
if (planned.length === 0) {
|
|
3799
|
+
console.log(chalk.green(' No changes needed. Everything looks good.\n'));
|
|
3800
|
+
} else {
|
|
3801
|
+
console.log(chalk.gray(` doctor --fix would create/modify ${planned.length} item(s):\n`));
|
|
3802
|
+
for (const p of planned) {
|
|
3803
|
+
const icon = p.action.startsWith('create') ? '+' : '~';
|
|
3804
|
+
console.log(chalk.gray(` ${icon} ${p.path}`));
|
|
3805
|
+
console.log(chalk.gray(` ${p.description}`));
|
|
3806
|
+
}
|
|
3807
|
+
console.log(chalk.gray(`\n Run ${chalk.bold('delimit doctor --fix')} to apply these changes.\n`));
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
if (ciMode) {
|
|
3811
|
+
console.log(JSON.stringify({ status: 'dry_run', planned_changes: planned, change_count: planned.length }, null, 2));
|
|
3812
|
+
}
|
|
3813
|
+
return;
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
const results = []; // { name, status: 'pass'|'warn'|'fail', message, fix? }
|
|
3817
|
+
const manifestActions = []; // track what --fix creates
|
|
3818
|
+
|
|
3819
|
+
function addResult(name, status, message, fix) {
|
|
3820
|
+
results.push({ name, status, message, fix: fix || null });
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
// Helper: record a created file/dir in the manifest
|
|
3824
|
+
function trackCreated(filePath) {
|
|
3825
|
+
manifestActions.push({ path: filePath, action: 'created', timestamp: new Date().toISOString() });
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
// --- Check 1: Policy file ---
|
|
3829
|
+
const policyPath = path.join(process.cwd(), '.delimit', 'policies.yml');
|
|
3830
|
+
if (fs.existsSync(policyPath)) {
|
|
3831
|
+
addResult('policy-file', 'pass', '.delimit/policies.yml found');
|
|
3832
|
+
try {
|
|
3833
|
+
const policy = yaml.load(fs.readFileSync(policyPath, 'utf8'));
|
|
3834
|
+
if (policy && (policy.rules !== undefined || policy.override_defaults !== undefined)) {
|
|
3835
|
+
addResult('policy-valid', 'pass', 'Policy file is valid YAML');
|
|
3836
|
+
} else {
|
|
3837
|
+
addResult('policy-valid', 'warn', 'Policy file has no rules section — add rules to .delimit/policies.yml');
|
|
3838
|
+
}
|
|
3839
|
+
} catch (e) {
|
|
3840
|
+
addResult('policy-valid', 'fail', `Policy file has invalid YAML: ${e.message}`, 'delimit init --force');
|
|
3841
|
+
}
|
|
3842
|
+
} else {
|
|
3843
|
+
addResult('policy-file', 'fail', 'No .delimit/policies.yml', 'delimit init');
|
|
3844
|
+
if (fixMode) {
|
|
3845
|
+
try {
|
|
3846
|
+
const delimitDirPre = fs.existsSync(path.join(process.cwd(), '.delimit'));
|
|
3847
|
+
execSync('delimit init --dry-run', { stdio: 'pipe', cwd: process.cwd() });
|
|
3848
|
+
// If dry-run works, run real init
|
|
3849
|
+
execSync('delimit init', { stdio: 'pipe', cwd: process.cwd() });
|
|
3850
|
+
addResult('policy-file-fix', 'pass', 'Auto-fixed: ran delimit init');
|
|
3851
|
+
if (!delimitDirPre) trackCreated(path.join(process.cwd(), '.delimit'));
|
|
3852
|
+
trackCreated(policyPath);
|
|
3853
|
+
} catch {
|
|
3854
|
+
addResult('policy-file-fix', 'warn', 'Auto-fix failed: run delimit init manually');
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
// --- Check 2: OpenAPI spec ---
|
|
3860
|
+
const specPatterns = [
|
|
3861
|
+
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
3862
|
+
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
3863
|
+
'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json',
|
|
3864
|
+
'spec/openapi.yaml', 'spec/openapi.json',
|
|
3865
|
+
'api/openapi.yaml', 'api/openapi.json',
|
|
3866
|
+
'contrib/openapi.json',
|
|
3867
|
+
];
|
|
3868
|
+
const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(process.cwd(), p)));
|
|
3869
|
+
if (foundSpecs.length > 0) {
|
|
3870
|
+
addResult('openapi-spec', 'pass', `OpenAPI spec found: ${foundSpecs[0]}`);
|
|
3871
|
+
} else {
|
|
3872
|
+
const pkgJson = path.join(process.cwd(), 'package.json');
|
|
3873
|
+
const reqTxt = path.join(process.cwd(), 'requirements.txt');
|
|
3874
|
+
if (fs.existsSync(pkgJson) || fs.existsSync(reqTxt)) {
|
|
3875
|
+
addResult('openapi-spec', 'warn', 'No OpenAPI spec file — Zero-Spec Mode may work if this is a FastAPI/NestJS/Express project');
|
|
3876
|
+
} else {
|
|
3877
|
+
addResult('openapi-spec', 'fail', 'No OpenAPI spec file found', 'Create openapi.yaml in project root or run: delimit scan');
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
|
|
3881
|
+
// --- Check 3: GitHub workflow ---
|
|
3882
|
+
const workflowDir = path.join(process.cwd(), '.github', 'workflows');
|
|
3883
|
+
if (fs.existsSync(workflowDir)) {
|
|
3884
|
+
const workflows = fs.readdirSync(workflowDir);
|
|
3885
|
+
const hasDelimit = workflows.some(f => {
|
|
3886
|
+
try {
|
|
3887
|
+
const content = fs.readFileSync(path.join(workflowDir, f), 'utf8');
|
|
3888
|
+
return content.includes('delimit-ai/delimit') || content.includes('delimit');
|
|
3889
|
+
} catch { return false; }
|
|
3890
|
+
});
|
|
3891
|
+
if (hasDelimit) {
|
|
3892
|
+
addResult('github-action', 'pass', 'GitHub Action workflow found');
|
|
3893
|
+
} else {
|
|
3894
|
+
addResult('github-action', 'warn', 'No Delimit GitHub Action workflow', 'delimit init');
|
|
3895
|
+
}
|
|
3896
|
+
} else {
|
|
3897
|
+
addResult('github-action', 'warn', 'No .github/workflows/ directory', 'mkdir -p .github/workflows && delimit init');
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// --- Check 4: Git repository ---
|
|
3901
|
+
try {
|
|
3902
|
+
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
3903
|
+
addResult('git-repo', 'pass', 'Git repository detected');
|
|
3904
|
+
} catch {
|
|
3905
|
+
addResult('git-repo', 'warn', 'Not a git repository', 'git init');
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3908
|
+
// --- Check 5: Node.js version ---
|
|
3909
|
+
const nodeVersion = parseInt(process.versions.node.split('.')[0], 10);
|
|
3910
|
+
if (nodeVersion >= 18) {
|
|
3911
|
+
addResult('node-version', 'pass', `Node.js v${process.versions.node}`);
|
|
3912
|
+
} else {
|
|
3913
|
+
addResult('node-version', 'warn', `Node.js v${process.versions.node} — v18+ recommended`, 'nvm install 18 && nvm use 18');
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
// --- Check 6: Python availability ---
|
|
3917
|
+
try {
|
|
3918
|
+
const pyVersion = execSync('python3 --version', { stdio: 'pipe' }).toString().trim();
|
|
3919
|
+
addResult('python', 'pass', `${pyVersion} available (needed for MCP server)`);
|
|
3920
|
+
} catch {
|
|
3921
|
+
addResult('python', 'fail', 'python3 not found on PATH — required for MCP server', 'Install Python 3: https://python.org/downloads/');
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3924
|
+
// --- Check 7: MCP server connectivity ---
|
|
3925
|
+
const mcpJsonPath = path.join(homeDir, '.mcp.json');
|
|
3926
|
+
const mcpServerPath = path.join(delimitHome, 'server', 'ai', 'server.py');
|
|
3927
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
3928
|
+
try {
|
|
3929
|
+
const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
3930
|
+
const hasDelimitMcp = mcpConfig.mcpServers && mcpConfig.mcpServers.delimit;
|
|
3931
|
+
if (hasDelimitMcp) {
|
|
3932
|
+
addResult('mcp-config', 'pass', 'Delimit configured in ~/.mcp.json');
|
|
3933
|
+
} else {
|
|
3934
|
+
addResult('mcp-config', 'warn', 'Delimit not configured in ~/.mcp.json', 'delimit setup --all');
|
|
3935
|
+
}
|
|
3936
|
+
} catch {
|
|
3937
|
+
addResult('mcp-config', 'warn', '~/.mcp.json exists but failed to parse', 'Check ~/.mcp.json for valid JSON');
|
|
3938
|
+
}
|
|
3939
|
+
} else {
|
|
3940
|
+
addResult('mcp-config', 'warn', 'No ~/.mcp.json found', 'delimit setup --all');
|
|
3941
|
+
}
|
|
3942
|
+
if (fs.existsSync(mcpServerPath)) {
|
|
3943
|
+
addResult('mcp-server', 'pass', 'MCP server file exists at ~/.delimit/server/ai/server.py');
|
|
3944
|
+
} else {
|
|
3945
|
+
addResult('mcp-server', 'fail', 'MCP server not installed at ~/.delimit/server/ai/server.py', 'delimit setup --all');
|
|
3946
|
+
if (fixMode) {
|
|
3947
|
+
try {
|
|
3948
|
+
execSync('delimit setup --all', { stdio: 'pipe' });
|
|
3949
|
+
addResult('mcp-server-fix', 'pass', 'Auto-fixed: ran delimit setup --all');
|
|
3950
|
+
trackCreated(mcpServerPath);
|
|
3951
|
+
} catch {
|
|
3952
|
+
addResult('mcp-server-fix', 'warn', 'Auto-fix failed: run delimit setup --all manually');
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
// --- Check 8: Memory health ---
|
|
3958
|
+
const memoryDir = path.join(delimitHome, 'memory');
|
|
3959
|
+
if (fs.existsSync(memoryDir)) {
|
|
3960
|
+
let memoryCount = 0;
|
|
3961
|
+
try {
|
|
3962
|
+
const memFiles = fs.readdirSync(memoryDir).filter(f => f.endsWith('.jsonl'));
|
|
3963
|
+
for (const mf of memFiles) {
|
|
3964
|
+
const content = fs.readFileSync(path.join(memoryDir, mf), 'utf8');
|
|
3965
|
+
memoryCount += content.split('\n').filter(l => l.trim()).length;
|
|
3966
|
+
}
|
|
3967
|
+
} catch {}
|
|
3968
|
+
if (memoryCount > 1000) {
|
|
3969
|
+
addResult('memory-health', 'warn', `Memory store has ${memoryCount} entries (>1000) — consider pruning`, 'delimit memory --prune');
|
|
3970
|
+
} else {
|
|
3971
|
+
addResult('memory-health', 'pass', `Memory store: ${memoryCount} entries`);
|
|
3972
|
+
}
|
|
3973
|
+
} else {
|
|
3974
|
+
addResult('memory-health', 'warn', 'No ~/.delimit/memory/ directory', `mkdir -p ${memoryDir}`);
|
|
3975
|
+
if (fixMode) {
|
|
3976
|
+
try {
|
|
3977
|
+
fs.mkdirSync(memoryDir, { recursive: true });
|
|
3978
|
+
addResult('memory-health-fix', 'pass', 'Auto-fixed: created ~/.delimit/memory/');
|
|
3979
|
+
trackCreated(memoryDir);
|
|
3980
|
+
} catch {
|
|
3981
|
+
addResult('memory-health-fix', 'warn', `Auto-fix failed: run mkdir -p ${memoryDir}`);
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
// --- Check 9: Models configured ---
|
|
3987
|
+
const modelsPath = path.join(delimitHome, 'models.json');
|
|
3988
|
+
if (fs.existsSync(modelsPath)) {
|
|
3989
|
+
try {
|
|
3990
|
+
const models = JSON.parse(fs.readFileSync(modelsPath, 'utf8'));
|
|
3991
|
+
const configured = Array.isArray(models)
|
|
3992
|
+
? models.filter(m => m.api_key)
|
|
3993
|
+
: Object.values(models).filter(m => m && m.api_key);
|
|
3994
|
+
if (configured.length > 0) {
|
|
3995
|
+
addResult('models', 'pass', `${configured.length} model(s) configured with API keys`);
|
|
3996
|
+
} else {
|
|
3997
|
+
addResult('models', 'warn', 'models.json exists but no models have api_key set', 'Edit ~/.delimit/models.json and add your API keys');
|
|
3998
|
+
}
|
|
3999
|
+
} catch {
|
|
4000
|
+
addResult('models', 'warn', '~/.delimit/models.json exists but failed to parse', 'Check ~/.delimit/models.json for valid JSON');
|
|
4001
|
+
}
|
|
4002
|
+
} else {
|
|
4003
|
+
addResult('models', 'warn', 'No ~/.delimit/models.json — multi-model features unavailable', 'delimit setup --all');
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
// --- Check 10: License status ---
|
|
4007
|
+
const licensePath = path.join(delimitHome, 'license.json');
|
|
4008
|
+
if (fs.existsSync(licensePath)) {
|
|
4009
|
+
try {
|
|
4010
|
+
const license = JSON.parse(fs.readFileSync(licensePath, 'utf8'));
|
|
4011
|
+
const tier = license.tier || license.plan || 'Unknown';
|
|
4012
|
+
addResult('license', 'pass', `License: ${tier}`);
|
|
4013
|
+
} catch {
|
|
4014
|
+
addResult('license', 'warn', '~/.delimit/license.json exists but failed to parse', 'Check ~/.delimit/license.json for valid JSON');
|
|
4015
|
+
}
|
|
4016
|
+
} else {
|
|
4017
|
+
addResult('license', 'pass', 'License: Free tier (upgrade at delimit.ai/pricing)');
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
// --- Check 11: Cross-model hooks ---
|
|
4021
|
+
const claudeSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
|
|
4022
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
4023
|
+
try {
|
|
4024
|
+
const settings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8'));
|
|
4025
|
+
const hasHooks = settings.hooks && settings.hooks.PostToolUse;
|
|
4026
|
+
if (hasHooks) {
|
|
4027
|
+
addResult('cross-model-hooks', 'pass', 'Claude Code PostToolUse hooks installed');
|
|
4028
|
+
} else {
|
|
4029
|
+
addResult('cross-model-hooks', 'warn', 'Claude Code hooks not configured in .claude/settings.json', 'delimit hooks install');
|
|
4030
|
+
}
|
|
4031
|
+
} catch {
|
|
4032
|
+
addResult('cross-model-hooks', 'warn', '.claude/settings.json exists but failed to parse', 'Check .claude/settings.json for valid JSON');
|
|
4033
|
+
}
|
|
4034
|
+
} else {
|
|
4035
|
+
addResult('cross-model-hooks', 'warn', 'No .claude/settings.json — cross-model hooks not installed', 'delimit hooks install');
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// --- Check 12: Disk space ---
|
|
4039
|
+
if (fs.existsSync(delimitHome)) {
|
|
4040
|
+
try {
|
|
4041
|
+
const duOutput = execSync(`du -sm "${delimitHome}"`, { stdio: 'pipe' }).toString().trim();
|
|
4042
|
+
const sizeMb = parseInt(duOutput.split('\t')[0], 10);
|
|
4043
|
+
if (sizeMb > 500) {
|
|
4044
|
+
addResult('disk-space', 'warn', `~/.delimit/ is ${sizeMb}MB (>500MB) — consider cleanup`, `du -sh ~/.delimit/*/`);
|
|
4045
|
+
} else {
|
|
4046
|
+
addResult('disk-space', 'pass', `~/.delimit/ disk usage: ${sizeMb}MB`);
|
|
4047
|
+
}
|
|
4048
|
+
} catch {
|
|
4049
|
+
addResult('disk-space', 'pass', '~/.delimit/ disk usage: unknown (du not available)');
|
|
4050
|
+
}
|
|
4051
|
+
} else {
|
|
4052
|
+
addResult('disk-space', 'pass', '~/.delimit/ does not exist yet');
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
// --- CI mode: output JSON and exit ---
|
|
4056
|
+
if (ciMode) {
|
|
4057
|
+
const ok = results.filter(r => r.status === 'pass').length;
|
|
4058
|
+
const warn = results.filter(r => r.status === 'warn').length;
|
|
4059
|
+
const fail = results.filter(r => r.status === 'fail').length;
|
|
4060
|
+
const total = results.length;
|
|
4061
|
+
const score = total > 0 ? Math.round((ok / total) * 10) : 0;
|
|
4062
|
+
const output = {
|
|
4063
|
+
version: '4.20',
|
|
4064
|
+
health_score: `${score}/10`,
|
|
4065
|
+
summary: { pass: ok, warn, fail, total },
|
|
4066
|
+
checks: results,
|
|
4067
|
+
};
|
|
4068
|
+
console.log(JSON.stringify(output, null, 2));
|
|
4069
|
+
if (fail > 0) {
|
|
4070
|
+
process.exitCode = 1;
|
|
4071
|
+
}
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
// --- Human-readable output ---
|
|
4076
|
+
console.log(chalk.bold('\n Delimit Doctor v4.20\n'));
|
|
4077
|
+
|
|
4078
|
+
const icons = { pass: chalk.green(' ✓'), warn: chalk.yellow(' ⚠'), fail: chalk.red(' ✗') };
|
|
4079
|
+
const colors = { pass: chalk.green, warn: chalk.yellow, fail: chalk.red };
|
|
4080
|
+
for (const r of results) {
|
|
4081
|
+
console.log(`${icons[r.status]} ${colors[r.status](r.message)}`);
|
|
4082
|
+
if (r.fix && r.status !== 'pass') {
|
|
4083
|
+
console.log(chalk.gray(` Run: ${r.fix}`));
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
// Preview what init would create (LED-265)
|
|
4088
|
+
const delimitDir = path.join(process.cwd(), '.delimit');
|
|
4089
|
+
const hasDelimitDir = fs.existsSync(delimitDir);
|
|
4090
|
+
console.log(chalk.bold('\n Init Preview:'));
|
|
4091
|
+
if (hasDelimitDir) {
|
|
4092
|
+
const files = [];
|
|
4093
|
+
try {
|
|
4094
|
+
const walk = (dir, prefix) => {
|
|
4095
|
+
for (const f of fs.readdirSync(dir)) {
|
|
4096
|
+
const full = path.join(dir, f);
|
|
4097
|
+
const rel = prefix ? `${prefix}/${f}` : f;
|
|
4098
|
+
if (fs.statSync(full).isDirectory()) walk(full, rel);
|
|
4099
|
+
else files.push(rel);
|
|
4100
|
+
}
|
|
4101
|
+
};
|
|
4102
|
+
walk(delimitDir, '.delimit');
|
|
4103
|
+
} catch {}
|
|
4104
|
+
console.log(chalk.green(` Already initialized — ${files.length} file(s) in .delimit/`));
|
|
4105
|
+
files.slice(0, 8).forEach(f => console.log(chalk.gray(` ${f}`)));
|
|
4106
|
+
if (files.length > 8) console.log(chalk.gray(` ... and ${files.length - 8} more`));
|
|
4107
|
+
} else {
|
|
4108
|
+
console.log(chalk.gray(' Running delimit init would create:'));
|
|
4109
|
+
console.log(chalk.gray(' .delimit/policies.yml — governance policy rules'));
|
|
4110
|
+
console.log(chalk.gray(' .delimit/evidence/ — audit trail events'));
|
|
4111
|
+
console.log(chalk.gray(' .delimit/compliance.json — if compliance template selected'));
|
|
4112
|
+
if (fs.existsSync(path.join(process.cwd(), '.github'))) {
|
|
4113
|
+
console.log(chalk.gray(' .github/workflows/api-governance.yml'));
|
|
4114
|
+
console.log(chalk.gray(' .github/workflows/api-drift-monitor.yml'));
|
|
4115
|
+
}
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
// Save manifest if --fix made changes (LED-265)
|
|
4119
|
+
if (fixMode && manifestActions.length > 0) {
|
|
4120
|
+
const manifestDir = path.join(process.cwd(), '.delimit');
|
|
4121
|
+
if (!fs.existsSync(manifestDir)) {
|
|
4122
|
+
fs.mkdirSync(manifestDir, { recursive: true });
|
|
4123
|
+
}
|
|
4124
|
+
const manifest = {
|
|
4125
|
+
version: 1,
|
|
4126
|
+
created: new Date().toISOString(),
|
|
4127
|
+
actions: manifestActions,
|
|
4128
|
+
};
|
|
4129
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
4130
|
+
console.log(chalk.bold('\n Manifest:'));
|
|
4131
|
+
console.log(chalk.gray(` Saved ${manifestActions.length} action(s) to .delimit/doctor-manifest.json`));
|
|
4132
|
+
console.log(chalk.gray(' Run: delimit doctor --undo to revert\n'));
|
|
4133
|
+
} else {
|
|
4134
|
+
// Undo instruction (LED-265)
|
|
4135
|
+
console.log(chalk.bold('\n Undo:'));
|
|
4136
|
+
if (fs.existsSync(manifestPath)) {
|
|
4137
|
+
console.log(chalk.gray(' delimit doctor --undo — revert last doctor --fix changes'));
|
|
4138
|
+
}
|
|
4139
|
+
console.log(chalk.gray(' rm -rf .delimit — remove all Delimit files'));
|
|
4140
|
+
console.log(chalk.gray(' delimit uninstall --dry-run — preview MCP removal\n'));
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
// Health score and summary
|
|
4144
|
+
const ok = results.filter(r => r.status === 'pass').length;
|
|
4145
|
+
const warn = results.filter(r => r.status === 'warn').length;
|
|
4146
|
+
const fail = results.filter(r => r.status === 'fail').length;
|
|
4147
|
+
const total = results.length;
|
|
4148
|
+
const score = total > 0 ? Math.round((ok / total) * 10) : 0;
|
|
4149
|
+
|
|
4150
|
+
console.log(chalk.bold(` Health: ${score}/10`));
|
|
4151
|
+
console.log('');
|
|
4152
|
+
if (fail === 0 && warn === 0) {
|
|
4153
|
+
console.log(chalk.green.bold(' All checks passed! Ready to lint.\n'));
|
|
4154
|
+
} else if (fail === 0) {
|
|
4155
|
+
console.log(chalk.yellow.bold(` ${ok} passed, ${warn} warning(s). Setup looks good.\n`));
|
|
4156
|
+
} else {
|
|
4157
|
+
console.log(chalk.red.bold(` ${ok} passed, ${warn} warning(s), ${fail} error(s). Fix errors above.\n`));
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4160
|
+
if (fail > 0) {
|
|
4161
|
+
process.exitCode = 1;
|
|
4162
|
+
}
|
|
4163
|
+
|
|
4164
|
+
// Occasional star nudge (show ~20% of the time on success)
|
|
4165
|
+
if (fail === 0 && Math.random() < 0.2) {
|
|
4166
|
+
console.log(chalk.gray(' Useful? Star us: https://github.com/delimit-ai/delimit-mcp-server\n'));
|
|
4167
|
+
}
|
|
4168
|
+
});
|
|
4169
|
+
|
|
4170
|
+
// Simulate command — dry-run governance preview ("terraform plan" for API governance)
|
|
4171
|
+
program
|
|
4172
|
+
.command('simulate')
|
|
4173
|
+
.description('Show what governance would block or allow without making changes')
|
|
4174
|
+
.option('--spec <path>', 'Path to OpenAPI spec to simulate lint against')
|
|
4175
|
+
.option('--policy <path>', 'Path to policies.yml (default: .delimit/policies.yml)')
|
|
4176
|
+
.option('--commit', 'Simulate a pre-commit governance check on staged changes')
|
|
4177
|
+
.option('--verbose', 'Show detailed rule breakdown')
|
|
4178
|
+
.action(async (opts) => {
|
|
4179
|
+
const projectDir = process.cwd();
|
|
4180
|
+
const configDir = path.join(projectDir, '.delimit');
|
|
4181
|
+
const policyPath = opts.policy
|
|
4182
|
+
? path.resolve(opts.policy)
|
|
4183
|
+
: path.join(configDir, 'policies.yml');
|
|
4184
|
+
|
|
4185
|
+
console.log(chalk.bold('\n Delimit Simulate \u2014 Dry Run\n'));
|
|
4186
|
+
|
|
4187
|
+
// Load and parse policy
|
|
4188
|
+
let policy = null;
|
|
4189
|
+
let preset = 'default';
|
|
4190
|
+
let ruleCount = 0;
|
|
4191
|
+
let policyRules = [];
|
|
4192
|
+
|
|
4193
|
+
if (fs.existsSync(policyPath)) {
|
|
4194
|
+
try {
|
|
4195
|
+
const yaml = require('js-yaml');
|
|
4196
|
+
policy = yaml.load(fs.readFileSync(policyPath, 'utf8'));
|
|
4197
|
+
|
|
4198
|
+
// Detect preset from content
|
|
4199
|
+
const policyContent = fs.readFileSync(policyPath, 'utf-8');
|
|
4200
|
+
if (policyContent.includes('action: forbid') && !policyContent.includes('action: warn')) preset = 'strict';
|
|
4201
|
+
else if (!policyContent.includes('action: forbid') && policyContent.includes('action: warn')) preset = 'relaxed';
|
|
4202
|
+
|
|
4203
|
+
// Count rules from various policy formats
|
|
4204
|
+
if (policy && policy.rules && Array.isArray(policy.rules)) {
|
|
4205
|
+
policyRules = policy.rules;
|
|
4206
|
+
ruleCount = policyRules.length;
|
|
4207
|
+
} else if (policy && policy.override_defaults && Array.isArray(policy.override_defaults)) {
|
|
4208
|
+
policyRules = policy.override_defaults;
|
|
4209
|
+
ruleCount = policyRules.length;
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
// Also count top-level change-type keys as implicit rules
|
|
4213
|
+
if (policy) {
|
|
4214
|
+
const changeTypeKeys = Object.keys(policy).filter(k =>
|
|
4215
|
+
!['rules', 'override_defaults', 'defaultMode', 'overrides', 'version', 'preset'].includes(k)
|
|
4216
|
+
);
|
|
4217
|
+
if (changeTypeKeys.length > 0 && ruleCount === 0) {
|
|
4218
|
+
ruleCount = changeTypeKeys.length;
|
|
4219
|
+
policyRules = changeTypeKeys.map(k => ({
|
|
4220
|
+
name: k,
|
|
4221
|
+
action: typeof policy[k] === 'object' ? (policy[k].action || 'warn') : String(policy[k]),
|
|
4222
|
+
}));
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
// Default mode from policy
|
|
4227
|
+
const mode = (policy && policy.defaultMode) || 'enforce';
|
|
4228
|
+
console.log(chalk.gray(` Policy: ${preset} (${mode} mode)`));
|
|
4229
|
+
console.log(chalk.gray(` Source: ${path.relative(projectDir, policyPath) || policyPath}`));
|
|
4230
|
+
console.log(chalk.gray(` Rules active: ${ruleCount}`));
|
|
4231
|
+
} catch (e) {
|
|
4232
|
+
console.log(chalk.red(` Policy file has invalid YAML: ${e.message}\n`));
|
|
4233
|
+
process.exitCode = 1;
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
} else {
|
|
4237
|
+
console.log(chalk.yellow(' No .delimit/policies.yml found \u2014 using built-in defaults'));
|
|
4238
|
+
console.log(chalk.gray(' Rules active: built-in (12 default change-type rules)'));
|
|
4239
|
+
preset = 'default';
|
|
4240
|
+
ruleCount = 12;
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
console.log('');
|
|
4244
|
+
|
|
4245
|
+
// Show rule details in verbose mode
|
|
4246
|
+
if (opts.verbose && policyRules.length > 0) {
|
|
4247
|
+
console.log(chalk.bold(' Rule Breakdown:'));
|
|
4248
|
+
console.log(chalk.gray(' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'));
|
|
4249
|
+
for (const rule of policyRules) {
|
|
4250
|
+
const name = rule.name || rule.change_type || '(unnamed)';
|
|
4251
|
+
const action = rule.action || rule.mode || 'warn';
|
|
4252
|
+
const icon = action === 'forbid' || action === 'enforce' || action === 'error'
|
|
4253
|
+
? chalk.red('\u2717')
|
|
4254
|
+
: action === 'warn' || action === 'advisory' || action === 'guarded'
|
|
4255
|
+
? chalk.yellow('\u26a0')
|
|
4256
|
+
: chalk.green('\u2713');
|
|
4257
|
+
console.log(` ${icon} ${name} ${chalk.gray(`(${action})`)}`);
|
|
4258
|
+
if (opts.verbose && rule.triggers) {
|
|
4259
|
+
for (const trigger of rule.triggers) {
|
|
4260
|
+
const triggerStr = Object.entries(trigger).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(', ');
|
|
4261
|
+
console.log(chalk.gray(` trigger: ${triggerStr}`));
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
}
|
|
4265
|
+
console.log('');
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
console.log(chalk.bold(' Simulation Results:'));
|
|
4269
|
+
console.log(chalk.gray(' \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'));
|
|
4270
|
+
|
|
4271
|
+
let totalBlocking = 0;
|
|
4272
|
+
let totalWarnings = 0;
|
|
4273
|
+
let totalPassed = 0;
|
|
4274
|
+
|
|
4275
|
+
// --- Mode 1: --spec — simulate lint against a specific spec ---
|
|
4276
|
+
if (opts.spec) {
|
|
4277
|
+
const specPath = path.resolve(opts.spec);
|
|
4278
|
+
if (!fs.existsSync(specPath)) {
|
|
4279
|
+
console.log(chalk.red(`\n File not found: ${specPath}\n`));
|
|
4280
|
+
process.exitCode = 1;
|
|
4281
|
+
return;
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
// Try to find a baseline to compare against
|
|
4285
|
+
const baselinePath = path.join(configDir, 'baseline.yaml');
|
|
4286
|
+
let basePath = null;
|
|
4287
|
+
|
|
4288
|
+
// Check git for the previous version of this spec
|
|
4289
|
+
const relSpec = path.relative(projectDir, specPath);
|
|
4290
|
+
try {
|
|
4291
|
+
const baseContent = execSync(`git show HEAD:${relSpec}`, {
|
|
4292
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4293
|
+
});
|
|
4294
|
+
const tmpBase = path.join(os.tmpdir(), `delimit-sim-base-${Date.now()}.yaml`);
|
|
4295
|
+
fs.writeFileSync(tmpBase, baseContent);
|
|
4296
|
+
basePath = tmpBase;
|
|
4297
|
+
} catch {
|
|
4298
|
+
// No git history for this file; try baseline
|
|
4299
|
+
if (fs.existsSync(baselinePath)) {
|
|
4300
|
+
basePath = baselinePath;
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
|
|
4304
|
+
if (!basePath) {
|
|
4305
|
+
console.log(chalk.gray(' No baseline found to compare against (new spec or no git history).'));
|
|
4306
|
+
console.log(chalk.green(' \u2713 PASS Spec exists and is parseable'));
|
|
4307
|
+
// Validate that the spec is valid YAML/JSON
|
|
4308
|
+
try {
|
|
4309
|
+
const yaml = require('js-yaml');
|
|
4310
|
+
const content = fs.readFileSync(specPath, 'utf8');
|
|
4311
|
+
const parsed = specPath.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
|
|
4312
|
+
if (parsed && (parsed.openapi || parsed.swagger)) {
|
|
4313
|
+
console.log(chalk.green(` \u2713 PASS Valid OpenAPI ${parsed.openapi || parsed.swagger} spec`));
|
|
4314
|
+
totalPassed += 2;
|
|
4315
|
+
} else {
|
|
4316
|
+
console.log(chalk.yellow(' \u26a0 WARN File parsed but no openapi/swagger version key found'));
|
|
4317
|
+
totalWarnings++;
|
|
4318
|
+
totalPassed++;
|
|
4319
|
+
}
|
|
4320
|
+
} catch (e) {
|
|
4321
|
+
console.log(chalk.red(` \u2717 BLOCK Spec file is not valid YAML/JSON: ${e.message}`));
|
|
4322
|
+
totalBlocking++;
|
|
4323
|
+
}
|
|
4324
|
+
} else {
|
|
4325
|
+
// Run the lint engine in dry-run mode
|
|
4326
|
+
try {
|
|
4327
|
+
const result = apiEngine.lint(basePath, specPath, { policy: preset });
|
|
4328
|
+
|
|
4329
|
+
if (result && result.summary) {
|
|
4330
|
+
const breaking = result.summary.breaking_changes || result.summary.breaking || 0;
|
|
4331
|
+
const warnings = result.summary.warnings || 0;
|
|
4332
|
+
const violations = result.violations || [];
|
|
4333
|
+
|
|
4334
|
+
if (breaking === 0 && warnings === 0) {
|
|
4335
|
+
console.log(chalk.green(' \u2713 PASS No breaking changes detected'));
|
|
4336
|
+
totalPassed++;
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4339
|
+
for (const v of violations) {
|
|
4340
|
+
if (v.severity === 'error') {
|
|
4341
|
+
console.log(chalk.red(` \u2717 BLOCK ${v.message}`));
|
|
4342
|
+
if (v.path) console.log(chalk.gray(` ${v.path}`));
|
|
4343
|
+
totalBlocking++;
|
|
4344
|
+
} else {
|
|
4345
|
+
console.log(chalk.yellow(` \u26a0 WARN ${v.message}`));
|
|
4346
|
+
if (v.path) console.log(chalk.gray(` ${v.path}`));
|
|
4347
|
+
totalWarnings++;
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
// Show safe changes
|
|
4352
|
+
const safe = (result.all_changes || []).filter(c => !c.is_breaking);
|
|
4353
|
+
if (safe.length > 0) {
|
|
4354
|
+
for (const c of safe) {
|
|
4355
|
+
console.log(chalk.green(` \u2713 PASS ${c.message}`));
|
|
4356
|
+
totalPassed++;
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
// Semver info
|
|
4361
|
+
if (result.semver && result.semver.bump && result.semver.bump !== 'none') {
|
|
4362
|
+
const bump = result.semver.bump.toUpperCase();
|
|
4363
|
+
console.log(chalk.gray(`\n Semver bump: ${bump}`));
|
|
4364
|
+
}
|
|
4365
|
+
} else {
|
|
4366
|
+
console.log(chalk.green(' \u2713 PASS No breaking changes detected'));
|
|
4367
|
+
totalPassed++;
|
|
4368
|
+
}
|
|
4369
|
+
} catch (err) {
|
|
4370
|
+
console.log(chalk.green(' \u2713 PASS No issues detected'));
|
|
4371
|
+
totalPassed++;
|
|
4372
|
+
} finally {
|
|
4373
|
+
// Clean up temp base file if we created one
|
|
4374
|
+
if (basePath && basePath.startsWith(os.tmpdir())) {
|
|
4375
|
+
try { fs.unlinkSync(basePath); } catch {}
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4379
|
+
|
|
4380
|
+
// --- Mode 2: --commit — simulate pre-commit check on staged changes ---
|
|
4381
|
+
} else if (opts.commit) {
|
|
4382
|
+
let stagedFiles = [];
|
|
4383
|
+
try {
|
|
4384
|
+
const output = execSync('git diff --cached --name-only', {
|
|
4385
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4386
|
+
}).trim();
|
|
4387
|
+
if (output) stagedFiles = output.split('\n');
|
|
4388
|
+
} catch {
|
|
4389
|
+
console.log(chalk.red(' \u2717 BLOCK Not a git repository or git not available'));
|
|
4390
|
+
totalBlocking++;
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
if (stagedFiles.length === 0 && totalBlocking === 0) {
|
|
4394
|
+
console.log(chalk.gray(' No staged files. Stage changes with git add first.\n'));
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
// Filter to spec files
|
|
4399
|
+
const specExtensions = ['.yaml', '.yml', '.json'];
|
|
4400
|
+
const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec'];
|
|
4401
|
+
const specFiles = stagedFiles.filter(f => {
|
|
4402
|
+
const ext = path.extname(f).toLowerCase();
|
|
4403
|
+
const name = path.basename(f).toLowerCase();
|
|
4404
|
+
if (!specExtensions.includes(ext)) return false;
|
|
4405
|
+
if (specKeywords.some(kw => name.includes(kw))) return true;
|
|
4406
|
+
try {
|
|
4407
|
+
const head = fs.readFileSync(path.join(projectDir, f), 'utf-8').slice(0, 512);
|
|
4408
|
+
return head.includes('"openapi"') || head.includes('openapi:') || head.includes('"swagger"') || head.includes('swagger:');
|
|
4409
|
+
} catch { return false; }
|
|
4410
|
+
});
|
|
4411
|
+
|
|
4412
|
+
// Report on staged files
|
|
4413
|
+
console.log(chalk.gray(` Staged files: ${stagedFiles.length} total, ${specFiles.length} API spec(s)`));
|
|
4414
|
+
console.log('');
|
|
4415
|
+
|
|
4416
|
+
if (specFiles.length === 0) {
|
|
4417
|
+
console.log(chalk.green(' \u2713 PASS No API spec changes in staged files'));
|
|
4418
|
+
totalPassed++;
|
|
4419
|
+
} else {
|
|
4420
|
+
for (const specFile of specFiles) {
|
|
4421
|
+
const fullPath = path.join(projectDir, specFile);
|
|
4422
|
+
let baseContent = null;
|
|
4423
|
+
try {
|
|
4424
|
+
baseContent = execSync(`git show HEAD:${specFile}`, {
|
|
4425
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4426
|
+
});
|
|
4427
|
+
} catch {
|
|
4428
|
+
console.log(chalk.green(` \u2713 PASS ${specFile} (new file \u2014 no base to compare)`));
|
|
4429
|
+
totalPassed++;
|
|
4430
|
+
continue;
|
|
4431
|
+
}
|
|
4432
|
+
|
|
4433
|
+
const tmpBase = path.join(os.tmpdir(), `delimit-sim-commit-${Date.now()}.yaml`);
|
|
4434
|
+
try {
|
|
4435
|
+
fs.writeFileSync(tmpBase, baseContent);
|
|
4436
|
+
const result = apiEngine.lint(tmpBase, fullPath, { policy: preset });
|
|
4437
|
+
|
|
4438
|
+
if (result && result.summary) {
|
|
4439
|
+
const breaking = result.summary.breaking_changes || result.summary.breaking || 0;
|
|
4440
|
+
const warnings = result.summary.warnings || 0;
|
|
4441
|
+
const violations = result.violations || [];
|
|
4442
|
+
|
|
4443
|
+
if (breaking === 0 && warnings === 0) {
|
|
4444
|
+
console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no breaking changes`));
|
|
4445
|
+
totalPassed++;
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
for (const v of violations) {
|
|
4449
|
+
if (v.severity === 'error') {
|
|
4450
|
+
console.log(chalk.red(` \u2717 BLOCK ${v.message}`));
|
|
4451
|
+
if (v.path) console.log(chalk.gray(` ${v.path}`));
|
|
4452
|
+
totalBlocking++;
|
|
4453
|
+
} else {
|
|
4454
|
+
console.log(chalk.yellow(` \u26a0 WARN ${v.message}`));
|
|
4455
|
+
if (v.path) console.log(chalk.gray(` ${v.path}`));
|
|
4456
|
+
totalWarnings++;
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
} else {
|
|
4460
|
+
console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no issues`));
|
|
4461
|
+
totalPassed++;
|
|
4462
|
+
}
|
|
4463
|
+
} catch {
|
|
4464
|
+
console.log(chalk.green(` \u2713 PASS ${specFile} \u2014 no issues`));
|
|
4465
|
+
totalPassed++;
|
|
4466
|
+
} finally {
|
|
4467
|
+
try { fs.unlinkSync(tmpBase); } catch {}
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
// Check for non-spec governance signals
|
|
4473
|
+
const hasPaymentFiles = stagedFiles.some(f => f.includes('payment') || f.includes('billing') || f.includes('stripe'));
|
|
4474
|
+
if (hasPaymentFiles) {
|
|
4475
|
+
const paymentRule = policyRules.find(r => r.name && r.name.toLowerCase().includes('payment'));
|
|
4476
|
+
if (paymentRule) {
|
|
4477
|
+
const action = paymentRule.mode || paymentRule.action || 'warn';
|
|
4478
|
+
if (action === 'enforce' || action === 'forbid') {
|
|
4479
|
+
console.log(chalk.red(` \u2717 BLOCK Payment code change detected \u2014 "${paymentRule.name}" rule is in ${action} mode`));
|
|
4480
|
+
totalBlocking++;
|
|
4481
|
+
} else {
|
|
4482
|
+
console.log(chalk.yellow(` \u26a0 WARN Payment code change detected \u2014 "${paymentRule.name}" rule (${action} mode)`));
|
|
4483
|
+
totalWarnings++;
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
// --- Mode 3: Default — show policy overview and what would happen ---
|
|
4489
|
+
} else {
|
|
4490
|
+
// Find all specs in the project
|
|
4491
|
+
const specPatterns = [
|
|
4492
|
+
'openapi.yaml', 'openapi.yml', 'openapi.json',
|
|
4493
|
+
'swagger.yaml', 'swagger.yml', 'swagger.json',
|
|
4494
|
+
'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json',
|
|
4495
|
+
'spec/openapi.yaml', 'spec/openapi.json',
|
|
4496
|
+
'api/openapi.yaml', 'api/openapi.json',
|
|
4497
|
+
'contrib/openapi.json',
|
|
4498
|
+
];
|
|
4499
|
+
const foundSpecs = specPatterns.filter(p => fs.existsSync(path.join(projectDir, p)));
|
|
4500
|
+
|
|
4501
|
+
if (foundSpecs.length > 0) {
|
|
4502
|
+
console.log(chalk.green(` \u2713 PASS API spec(s) found: ${foundSpecs.join(', ')}`));
|
|
4503
|
+
totalPassed++;
|
|
4504
|
+
} else {
|
|
4505
|
+
console.log(chalk.yellow(' \u26a0 WARN No API spec files found in project'));
|
|
4506
|
+
totalWarnings++;
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
// Check git status for uncommitted spec changes
|
|
4510
|
+
try {
|
|
4511
|
+
const output = execSync('git diff --name-only', {
|
|
4512
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4513
|
+
}).trim();
|
|
4514
|
+
const stagedOutput = execSync('git diff --cached --name-only', {
|
|
4515
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 5000
|
|
4516
|
+
}).trim();
|
|
4517
|
+
|
|
4518
|
+
const allChanged = [...new Set([
|
|
4519
|
+
...(output ? output.split('\n') : []),
|
|
4520
|
+
...(stagedOutput ? stagedOutput.split('\n') : []),
|
|
4521
|
+
])];
|
|
4522
|
+
|
|
4523
|
+
const specKeywords = ['openapi', 'swagger', 'api-spec', 'api_spec'];
|
|
4524
|
+
const changedSpecs = allChanged.filter(f => {
|
|
4525
|
+
const name = path.basename(f).toLowerCase();
|
|
4526
|
+
return specKeywords.some(kw => name.includes(kw));
|
|
4527
|
+
});
|
|
4528
|
+
|
|
4529
|
+
if (changedSpecs.length > 0) {
|
|
4530
|
+
console.log(chalk.yellow(` \u26a0 WARN ${changedSpecs.length} uncommitted spec change(s): ${changedSpecs.join(', ')}`));
|
|
4531
|
+
console.log(chalk.gray(' Run: delimit simulate --commit to check staged changes'));
|
|
4532
|
+
totalWarnings++;
|
|
4533
|
+
} else {
|
|
4534
|
+
console.log(chalk.green(' \u2713 PASS No uncommitted API spec changes'));
|
|
4535
|
+
totalPassed++;
|
|
4536
|
+
}
|
|
4537
|
+
} catch {
|
|
4538
|
+
console.log(chalk.gray(' \u2500 SKIP Not a git repository'));
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
// Check governance hooks
|
|
4542
|
+
const gitHooksDir = path.join(projectDir, '.git', 'hooks');
|
|
4543
|
+
const preCommitHook = path.join(gitHooksDir, 'pre-commit');
|
|
4544
|
+
if (fs.existsSync(preCommitHook)) {
|
|
4545
|
+
try {
|
|
4546
|
+
const hookContent = fs.readFileSync(preCommitHook, 'utf8');
|
|
4547
|
+
if (hookContent.includes('delimit')) {
|
|
4548
|
+
console.log(chalk.green(' \u2713 PASS Delimit pre-commit hook installed'));
|
|
4549
|
+
totalPassed++;
|
|
4550
|
+
} else {
|
|
4551
|
+
console.log(chalk.yellow(' \u26a0 WARN Pre-commit hook exists but does not reference Delimit'));
|
|
4552
|
+
totalWarnings++;
|
|
4553
|
+
}
|
|
4554
|
+
} catch {
|
|
4555
|
+
console.log(chalk.yellow(' \u26a0 WARN Could not read pre-commit hook'));
|
|
4556
|
+
totalWarnings++;
|
|
4557
|
+
}
|
|
4558
|
+
} else {
|
|
4559
|
+
console.log(chalk.yellow(' \u26a0 WARN No pre-commit hook \u2014 governance only runs manually'));
|
|
4560
|
+
console.log(chalk.gray(' Run: delimit hooks install'));
|
|
4561
|
+
totalWarnings++;
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4564
|
+
// GitHub Action check
|
|
4565
|
+
const workflowDir = path.join(projectDir, '.github', 'workflows');
|
|
4566
|
+
if (fs.existsSync(workflowDir)) {
|
|
4567
|
+
try {
|
|
4568
|
+
const workflows = fs.readdirSync(workflowDir);
|
|
4569
|
+
const hasDelimit = workflows.some(f => {
|
|
4570
|
+
try {
|
|
4571
|
+
const content = fs.readFileSync(path.join(workflowDir, f), 'utf8');
|
|
4572
|
+
return content.includes('delimit-ai/delimit') || content.includes('delimit');
|
|
4573
|
+
} catch { return false; }
|
|
4574
|
+
});
|
|
4575
|
+
if (hasDelimit) {
|
|
4576
|
+
console.log(chalk.green(' \u2713 PASS GitHub Action governance workflow found'));
|
|
4577
|
+
totalPassed++;
|
|
4578
|
+
} else {
|
|
4579
|
+
console.log(chalk.yellow(' \u26a0 WARN No Delimit GitHub Action \u2014 CI governance not enabled'));
|
|
4580
|
+
console.log(chalk.gray(' Run: delimit ci'));
|
|
4581
|
+
totalWarnings++;
|
|
4582
|
+
}
|
|
4583
|
+
} catch {}
|
|
4584
|
+
} else {
|
|
4585
|
+
console.log(chalk.yellow(' \u26a0 WARN No .github/workflows/ directory'));
|
|
4586
|
+
totalWarnings++;
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
// --- Verdict ---
|
|
4591
|
+
console.log('');
|
|
4592
|
+
if (totalBlocking > 0) {
|
|
4593
|
+
const parts = [];
|
|
4594
|
+
if (totalBlocking > 0) parts.push(`${totalBlocking} blocking`);
|
|
4595
|
+
if (totalWarnings > 0) parts.push(`${totalWarnings} warning(s)`);
|
|
4596
|
+
if (totalPassed > 0) parts.push(`${totalPassed} passed`);
|
|
4597
|
+
console.log(chalk.gray(` Verdict: ${parts.join(', ')}`));
|
|
4598
|
+
console.log(chalk.red.bold(' A real commit would be BLOCKED.\n'));
|
|
4599
|
+
} else if (totalWarnings > 0) {
|
|
4600
|
+
const parts = [];
|
|
4601
|
+
if (totalWarnings > 0) parts.push(`${totalWarnings} warning(s)`);
|
|
4602
|
+
if (totalPassed > 0) parts.push(`${totalPassed} passed`);
|
|
4603
|
+
console.log(chalk.gray(` Verdict: ${parts.join(', ')}`));
|
|
4604
|
+
console.log(chalk.yellow.bold(' A real commit would PASS with warnings.\n'));
|
|
4605
|
+
} else {
|
|
4606
|
+
console.log(chalk.gray(` Verdict: ${totalPassed} passed, 0 warnings, 0 blocking`));
|
|
4607
|
+
console.log(chalk.green.bold(' A real commit would PASS cleanly.\n'));
|
|
4608
|
+
}
|
|
4609
|
+
});
|
|
4610
|
+
|
|
4611
|
+
// Hooks command — install/remove git hooks for governance
|
|
4612
|
+
program
|
|
4613
|
+
.command('hooks <action>')
|
|
4614
|
+
.description('Install or remove git hooks (install | remove | status)')
|
|
4615
|
+
.option('--pre-push', 'Also add pre-push hook')
|
|
4616
|
+
.action(async (action, opts) => {
|
|
4617
|
+
const projectDir = process.cwd();
|
|
4618
|
+
const gitDir = path.join(projectDir, '.git');
|
|
4619
|
+
|
|
4620
|
+
if (!fs.existsSync(gitDir)) {
|
|
4621
|
+
console.log(chalk.red('\n Not a git repository. Run git init first.\n'));
|
|
4622
|
+
process.exitCode = 1;
|
|
4623
|
+
return;
|
|
4624
|
+
}
|
|
4625
|
+
|
|
4626
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
4627
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
4628
|
+
|
|
4629
|
+
const preCommitPath = path.join(hooksDir, 'pre-commit');
|
|
4630
|
+
const prePushPath = path.join(hooksDir, 'pre-push');
|
|
4631
|
+
const marker = '# delimit-governance-hook';
|
|
3254
4632
|
|
|
3255
4633
|
const preCommitHook = `#!/bin/sh
|
|
3256
4634
|
${marker}
|
|
@@ -3712,50 +5090,250 @@ program
|
|
|
3712
5090
|
return;
|
|
3713
5091
|
}
|
|
3714
5092
|
|
|
3715
|
-
//
|
|
5093
|
+
// Detect CI environment — use plain output (no color) when not a TTY
|
|
5094
|
+
const isCI = !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.JENKINS_URL || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS);
|
|
5095
|
+
const isTTY = process.stdout.isTTY;
|
|
5096
|
+
const useColor = isTTY && !isCI && !process.env.NO_COLOR;
|
|
5097
|
+
|
|
5098
|
+
// Severity classification for violations (mirrors Action's ci_formatter.py)
|
|
5099
|
+
const SEVERITY_MAP = {
|
|
5100
|
+
'no_endpoint_removal': { label: 'Critical', color: 'red' },
|
|
5101
|
+
'no_method_removal': { label: 'Critical', color: 'red' },
|
|
5102
|
+
'no_field_removal': { label: 'Critical', color: 'red' },
|
|
5103
|
+
'no_response_field_removal': { label: 'Critical', color: 'red' },
|
|
5104
|
+
'no_required_param_addition': { label: 'High', color: 'yellow' },
|
|
5105
|
+
'no_type_changes': { label: 'High', color: 'yellow' },
|
|
5106
|
+
'warn_type_change': { label: 'High', color: 'yellow' },
|
|
5107
|
+
'no_enum_removal': { label: 'High', color: 'yellow' },
|
|
5108
|
+
};
|
|
5109
|
+
|
|
5110
|
+
// Teachings — WHY each rule matters (mirrors Action's ci_formatter.py TEACHINGS)
|
|
5111
|
+
const TEACHINGS = {
|
|
5112
|
+
'no_endpoint_removal': 'Removing an endpoint breaks existing clients actively calling it. Their requests will return 404.',
|
|
5113
|
+
'no_method_removal': 'Removing an HTTP method breaks clients using that verb. They will receive 405 Method Not Allowed.',
|
|
5114
|
+
'no_required_param_addition': 'Adding a required parameter breaks every existing request that omits it. Clients get 400 Bad Request.',
|
|
5115
|
+
'no_field_removal': 'Removing a request field breaks clients sending it if the server rejects the payload or silently drops data.',
|
|
5116
|
+
'no_response_field_removal': 'Removing a response field breaks clients reading it. Their code hits undefined/null.',
|
|
5117
|
+
'no_type_changes': 'Changing a field type breaks serialization. Clients parsing the old type will fail.',
|
|
5118
|
+
'warn_type_change': 'Changing a field type breaks serialization. Clients parsing the old type will fail.',
|
|
5119
|
+
'no_enum_removal': 'Removing an enum value breaks clients that send or compare against it.',
|
|
5120
|
+
};
|
|
5121
|
+
|
|
5122
|
+
// Fix hints — HOW to fix each rule (mirrors Action's ci_formatter.py FIX_HINTS)
|
|
5123
|
+
const FIX_HINTS = {
|
|
5124
|
+
'no_endpoint_removal': 'Deprecate the endpoint first, then remove in a future major version.',
|
|
5125
|
+
'no_method_removal': 'Keep the old method available or redirect it. Remove only after a deprecation period.',
|
|
5126
|
+
'no_required_param_addition': 'Make the new parameter optional with a sensible default value.',
|
|
5127
|
+
'no_field_removal': 'Keep the field in the schema. Mark it deprecated and stop populating in a future version.',
|
|
5128
|
+
'no_response_field_removal': 'Restore the field. If removing is intentional, version the endpoint (e.g., /v2/).',
|
|
5129
|
+
'no_type_changes': 'Revert the type change, or introduce a new field with the desired type and deprecate the old one.',
|
|
5130
|
+
'warn_type_change': 'Revert the type change, or introduce a new field with the desired type and deprecate the old one.',
|
|
5131
|
+
'no_enum_removal': 'Keep the enum value and mark it deprecated. Remove only in a coordinated major release.',
|
|
5132
|
+
};
|
|
5133
|
+
|
|
5134
|
+
// Helper: colorize or plain text
|
|
5135
|
+
const c = {
|
|
5136
|
+
red: (s) => useColor ? chalk.red(s) : s,
|
|
5137
|
+
green: (s) => useColor ? chalk.green(s) : s,
|
|
5138
|
+
yellow: (s) => useColor ? chalk.yellow(s) : s,
|
|
5139
|
+
gray: (s) => useColor ? chalk.gray(s) : s,
|
|
5140
|
+
bold: (s) => useColor ? chalk.bold(s) : s,
|
|
5141
|
+
redBold: (s) => useColor ? chalk.red.bold(s) : s,
|
|
5142
|
+
greenBold: (s) => useColor ? chalk.green.bold(s) : s,
|
|
5143
|
+
yellowBold: (s) => useColor ? chalk.yellow.bold(s) : s,
|
|
5144
|
+
dim: (s) => useColor ? chalk.dim(s) : s,
|
|
5145
|
+
cyan: (s) => useColor ? chalk.cyan(s) : s,
|
|
5146
|
+
};
|
|
5147
|
+
|
|
3716
5148
|
const decision = result.decision;
|
|
3717
5149
|
const semver = result.semver;
|
|
3718
|
-
const
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
5150
|
+
const s = result.summary;
|
|
5151
|
+
const violations = result.violations || [];
|
|
5152
|
+
const allChanges = result.all_changes || [];
|
|
5153
|
+
const errors = violations.filter(v => v.severity === 'error');
|
|
5154
|
+
const warnings = violations.filter(v => v.severity === 'warning');
|
|
5155
|
+
const safe = allChanges.filter(ch => !ch.is_breaking);
|
|
3723
5156
|
|
|
3724
|
-
|
|
3725
|
-
const
|
|
5157
|
+
// ── Header Banner ──
|
|
5158
|
+
const divider = useColor ? chalk.dim('─'.repeat(60)) : '-'.repeat(60);
|
|
5159
|
+
console.log('');
|
|
5160
|
+
console.log(divider);
|
|
3726
5161
|
|
|
3727
|
-
|
|
5162
|
+
if (decision === 'fail') {
|
|
5163
|
+
console.log(c.redBold(' GOVERNANCE FAILED'));
|
|
5164
|
+
} else if (decision === 'warn') {
|
|
5165
|
+
console.log(c.yellowBold(' GOVERNANCE PASSED WITH WARNINGS'));
|
|
5166
|
+
} else {
|
|
5167
|
+
console.log(c.greenBold(' GOVERNANCE PASSED'));
|
|
5168
|
+
}
|
|
3728
5169
|
|
|
3729
|
-
//
|
|
3730
|
-
const
|
|
3731
|
-
|
|
5170
|
+
// Semver line
|
|
5171
|
+
const bumpLabel = semver ? semver.bump.toUpperCase() : 'NONE';
|
|
5172
|
+
const nextVerStr = semver && semver.next_version ? ` Next: ${semver.next_version}` : '';
|
|
5173
|
+
console.log(` Semver: ${c.bold(bumpLabel)}${nextVerStr}`);
|
|
5174
|
+
console.log(divider);
|
|
5175
|
+
|
|
5176
|
+
// ── Summary Stats ──
|
|
5177
|
+
console.log('');
|
|
5178
|
+
console.log(` Total changes: ${s.total_changes}`);
|
|
5179
|
+
console.log(` Breaking changes: ${s.breaking_changes > 0 ? c.red(String(s.breaking_changes)) : c.green('0')}`);
|
|
5180
|
+
console.log(` Policy violations: ${s.violations > 0 ? c.red(String(s.violations)) : c.green('0')}`);
|
|
3732
5181
|
if (s.violations > 0) {
|
|
3733
|
-
console.log(`
|
|
5182
|
+
console.log(` Errors: ${s.errors}`);
|
|
5183
|
+
console.log(` Warnings: ${s.warnings}`);
|
|
3734
5184
|
}
|
|
3735
5185
|
console.log('');
|
|
3736
5186
|
|
|
3737
|
-
//
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
5187
|
+
// ── Breaking Changes Table ──
|
|
5188
|
+
if (errors.length > 0 || warnings.length > 0) {
|
|
5189
|
+
console.log(c.bold(' Breaking Changes'));
|
|
5190
|
+
console.log(divider);
|
|
5191
|
+
console.log('');
|
|
5192
|
+
|
|
5193
|
+
// Table header
|
|
5194
|
+
const colSev = 10;
|
|
5195
|
+
const colLoc = 32;
|
|
5196
|
+
const colMsg = 50;
|
|
5197
|
+
const pad = (str, len) => {
|
|
5198
|
+
const stripped = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
5199
|
+
const diff = len - stripped.length;
|
|
5200
|
+
return diff > 0 ? str + ' '.repeat(diff) : str;
|
|
5201
|
+
};
|
|
5202
|
+
|
|
5203
|
+
console.log(` ${pad(c.bold('Severity'), colSev)} ${pad(c.bold('Location'), colLoc)} ${c.bold('Description')}`);
|
|
5204
|
+
console.log(` ${'-'.repeat(colSev)} ${'-'.repeat(colLoc)} ${'-'.repeat(colMsg)}`);
|
|
5205
|
+
|
|
5206
|
+
errors.forEach(v => {
|
|
5207
|
+
const sev = SEVERITY_MAP[v.rule] || { label: 'Error', color: 'red' };
|
|
5208
|
+
const sevStr = sev.color === 'red' ? c.red(sev.label) : c.yellow(sev.label);
|
|
5209
|
+
const loc = v.path || '-';
|
|
5210
|
+
const truncLoc = loc.length > colLoc ? loc.substring(0, colLoc - 3) + '...' : loc;
|
|
5211
|
+
console.log(` ${pad(sevStr, colSev)} ${pad(c.cyan(truncLoc), colLoc)} ${v.message}`);
|
|
5212
|
+
});
|
|
5213
|
+
|
|
5214
|
+
warnings.forEach(v => {
|
|
5215
|
+
const sev = SEVERITY_MAP[v.rule] || { label: 'Medium', color: 'yellow' };
|
|
5216
|
+
const sevStr = c.yellow(sev.label);
|
|
5217
|
+
const loc = v.path || '-';
|
|
5218
|
+
const truncLoc = loc.length > colLoc ? loc.substring(0, colLoc - 3) + '...' : loc;
|
|
5219
|
+
console.log(` ${pad(sevStr, colSev)} ${pad(c.cyan(truncLoc), colLoc)} ${v.message}`);
|
|
5220
|
+
});
|
|
5221
|
+
|
|
5222
|
+
console.log('');
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
// ── Why This Breaks (Teachings) ──
|
|
5226
|
+
if (errors.length > 0) {
|
|
5227
|
+
console.log(c.bold(' Why This Breaks'));
|
|
5228
|
+
console.log(divider);
|
|
5229
|
+
console.log('');
|
|
5230
|
+
|
|
5231
|
+
// Deduplicate by rule
|
|
5232
|
+
const seenRules = new Set();
|
|
5233
|
+
errors.forEach(v => {
|
|
5234
|
+
if (v.rule && TEACHINGS[v.rule] && !seenRules.has(v.rule)) {
|
|
5235
|
+
seenRules.add(v.rule);
|
|
5236
|
+
const ruleName = v.rule.replace(/^no_/, '').replace(/_/g, ' ');
|
|
5237
|
+
console.log(` ${c.red('*')} ${c.bold(ruleName)}`);
|
|
5238
|
+
console.log(` ${c.gray(TEACHINGS[v.rule])}`);
|
|
5239
|
+
console.log('');
|
|
5240
|
+
}
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
// ── How to Fix (Migration Hints) ──
|
|
5245
|
+
if (errors.length > 0) {
|
|
5246
|
+
console.log(c.bold(' How to Fix'));
|
|
5247
|
+
console.log(divider);
|
|
5248
|
+
console.log('');
|
|
5249
|
+
|
|
5250
|
+
errors.forEach((v, i) => {
|
|
5251
|
+
const loc = v.path || '-';
|
|
5252
|
+
const hint = FIX_HINTS[v.rule] || 'Review this change and update consumers accordingly.';
|
|
5253
|
+
console.log(` ${c.bold(`${i + 1}. ${loc}`)}`);
|
|
5254
|
+
console.log(` ${hint}`);
|
|
5255
|
+
console.log('');
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
5258
|
+
|
|
5259
|
+
// ── Migration Guide (if available from engine) ──
|
|
5260
|
+
if (result.migration && decision === 'fail') {
|
|
5261
|
+
console.log(c.bold(' Migration Guide'));
|
|
5262
|
+
console.log(divider);
|
|
5263
|
+
console.log('');
|
|
5264
|
+
// Indent migration text
|
|
5265
|
+
const migrationLines = result.migration.split('\n');
|
|
5266
|
+
migrationLines.forEach(line => {
|
|
5267
|
+
console.log(` ${line}`);
|
|
5268
|
+
});
|
|
5269
|
+
console.log('');
|
|
5270
|
+
}
|
|
5271
|
+
|
|
5272
|
+
// ── Non-Breaking Additions ──
|
|
5273
|
+
if (safe.length > 0 && safe.length <= 20) {
|
|
5274
|
+
console.log(c.bold(` Non-Breaking Additions (${safe.length})`));
|
|
5275
|
+
console.log(divider);
|
|
5276
|
+
console.log('');
|
|
5277
|
+
safe.forEach(ch => {
|
|
5278
|
+
console.log(` ${c.green('+')} ${ch.message}`);
|
|
5279
|
+
if (ch.path) console.log(` ${c.gray(ch.path)}`);
|
|
5280
|
+
});
|
|
5281
|
+
console.log('');
|
|
5282
|
+
} else if (safe.length > 20) {
|
|
5283
|
+
console.log(c.bold(` Non-Breaking Additions (${safe.length})`));
|
|
5284
|
+
console.log(divider);
|
|
5285
|
+
console.log('');
|
|
5286
|
+
safe.slice(0, 10).forEach(ch => {
|
|
5287
|
+
console.log(` ${c.green('+')} ${ch.message}`);
|
|
3744
5288
|
});
|
|
5289
|
+
console.log(c.gray(` ... and ${safe.length - 10} more additions`));
|
|
3745
5290
|
console.log('');
|
|
3746
5291
|
}
|
|
3747
5292
|
|
|
3748
|
-
//
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
5293
|
+
// ── Governance Gates ──
|
|
5294
|
+
console.log(c.bold(' Governance Gates'));
|
|
5295
|
+
console.log(divider);
|
|
5296
|
+
console.log('');
|
|
5297
|
+
|
|
5298
|
+
const lintPass = s.breaking_changes === 0;
|
|
5299
|
+
const policyPass = violations.length === 0;
|
|
5300
|
+
const deployReady = lintPass && policyPass;
|
|
5301
|
+
|
|
5302
|
+
const gateIcon = (pass) => pass ? c.green('PASS') : c.red('FAIL');
|
|
5303
|
+
const gates = [
|
|
5304
|
+
['API Lint', lintPass],
|
|
5305
|
+
['Policy Compliance', policyPass],
|
|
5306
|
+
['Deploy Readiness', deployReady],
|
|
5307
|
+
];
|
|
5308
|
+
|
|
5309
|
+
const gateCol = 22;
|
|
5310
|
+
console.log(` ${c.bold('Gate'.padEnd(gateCol))} ${c.bold('Status')}`);
|
|
5311
|
+
console.log(` ${'-'.repeat(gateCol)} ${'-'.repeat(10)}`);
|
|
5312
|
+
gates.forEach(([name, pass]) => {
|
|
5313
|
+
const status = pass ? gateIcon(true) : gateIcon(false);
|
|
5314
|
+
if (name === 'Policy Compliance' && !policyPass) {
|
|
5315
|
+
console.log(` ${name.padEnd(gateCol)} ${status} (${violations.length} violation${violations.length !== 1 ? 's' : ''})`);
|
|
5316
|
+
} else if (name === 'Deploy Readiness' && !deployReady) {
|
|
5317
|
+
console.log(` ${name.padEnd(gateCol)} ${c.yellow('BLOCKED')}`);
|
|
5318
|
+
} else {
|
|
5319
|
+
console.log(` ${name.padEnd(gateCol)} ${status}`);
|
|
5320
|
+
}
|
|
5321
|
+
});
|
|
5322
|
+
console.log('');
|
|
5323
|
+
|
|
5324
|
+
if (!deployReady) {
|
|
5325
|
+
console.log(c.yellow(' Deploy blocked until all gates pass.'));
|
|
3753
5326
|
console.log('');
|
|
3754
5327
|
}
|
|
3755
5328
|
|
|
5329
|
+
// ── Footer ──
|
|
5330
|
+
console.log(divider);
|
|
3756
5331
|
if (decision === 'pass') {
|
|
3757
|
-
console.log('Keep Building
|
|
5332
|
+
console.log(c.green(' Keep Building.'));
|
|
5333
|
+
} else {
|
|
5334
|
+
console.log(c.gray(' Fix the issues above, then re-run: npx delimit-cli lint'));
|
|
3758
5335
|
}
|
|
5336
|
+
console.log('');
|
|
3759
5337
|
|
|
3760
5338
|
process.exit(result.exit_code || 0);
|
|
3761
5339
|
} catch (err) {
|
|
@@ -4551,7 +6129,7 @@ program
|
|
|
4551
6129
|
.argument("[action]", "Action: status | set | list | reveal", "status")
|
|
4552
6130
|
.option("--verbose", "Show encryption details and backend status")
|
|
4553
6131
|
.action(async (action, options) => {
|
|
4554
|
-
console.log(chalk.
|
|
6132
|
+
console.log(chalk.magenta.bold("\n Delimit Vault\n"));
|
|
4555
6133
|
|
|
4556
6134
|
if (action === "status") {
|
|
4557
6135
|
console.log(chalk.bold("Backend Status:"));
|
|
@@ -4566,13 +6144,70 @@ program
|
|
|
4566
6144
|
console.log("\nUse " + chalk.cyan("delimit vault list") + " to see configured secrets.");
|
|
4567
6145
|
} else if (action === "list") {
|
|
4568
6146
|
console.log(chalk.bold("Configured Secrets:"));
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
6147
|
+
const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
|
|
6148
|
+
if (fs.existsSync(secretsDir)) {
|
|
6149
|
+
const files = fs.readdirSync(secretsDir).filter(f => f.endsWith('.json') && !f.startsWith('.'));
|
|
6150
|
+
if (files.length === 0) {
|
|
6151
|
+
console.log(chalk.dim(" No secrets configured yet."));
|
|
6152
|
+
} else {
|
|
6153
|
+
files.forEach(f => {
|
|
6154
|
+
const name = f.replace('.json', '');
|
|
6155
|
+
console.log(` • ${name} ${chalk.gray("********")}`);
|
|
6156
|
+
});
|
|
6157
|
+
}
|
|
6158
|
+
} else {
|
|
6159
|
+
console.log(chalk.dim(" No secrets directory found."));
|
|
6160
|
+
}
|
|
6161
|
+
console.log("\nRun " + chalk.cyan("delimit vault set <NAME>") + " to add a secret.");
|
|
6162
|
+
} else if (action === "set") {
|
|
6163
|
+
const name = process.argv[4];
|
|
6164
|
+
if (!name) {
|
|
6165
|
+
console.log(chalk.red("Usage: delimit vault set <NAME>"));
|
|
6166
|
+
console.log(chalk.dim(" Example: delimit vault set OPENAI_API_KEY"));
|
|
6167
|
+
process.exit(1);
|
|
6168
|
+
}
|
|
6169
|
+
const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
|
|
6170
|
+
fs.mkdirSync(secretsDir, { recursive: true });
|
|
6171
|
+
const filePath = path.join(secretsDir, `${name}.json`);
|
|
6172
|
+
const existing = fs.existsSync(filePath);
|
|
6173
|
+
// Read value from stdin or prompt
|
|
6174
|
+
const readline = require('readline');
|
|
6175
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
6176
|
+
rl.question(` Enter value for ${chalk.bold(name)}: `, (value) => {
|
|
6177
|
+
rl.close();
|
|
6178
|
+
if (!value || !value.trim()) {
|
|
6179
|
+
console.log(chalk.red(" Empty value. Aborted."));
|
|
6180
|
+
return;
|
|
6181
|
+
}
|
|
6182
|
+
fs.writeFileSync(filePath, JSON.stringify({ key: name, value: value.trim(), updated: new Date().toISOString() }), 'utf-8');
|
|
6183
|
+
fs.chmodSync(filePath, 0o600);
|
|
6184
|
+
console.log(chalk.green(` ${existing ? 'Updated' : 'Saved'}: ${name}`));
|
|
6185
|
+
console.log(chalk.dim(` Location: ${filePath}`));
|
|
6186
|
+
});
|
|
6187
|
+
} else if (action === "reveal") {
|
|
6188
|
+
const name = process.argv[4];
|
|
6189
|
+
if (!name) {
|
|
6190
|
+
console.log(chalk.red("Usage: delimit vault reveal <NAME>"));
|
|
6191
|
+
process.exit(1);
|
|
6192
|
+
}
|
|
6193
|
+
const secretsDir = path.join(os.homedir(), '.delimit', 'secrets');
|
|
6194
|
+
const filePath = path.join(secretsDir, `${name}.json`);
|
|
6195
|
+
if (!fs.existsSync(filePath)) {
|
|
6196
|
+
console.log(chalk.red(` Secret "${name}" not found.`));
|
|
6197
|
+
console.log(chalk.dim(" Run " + chalk.cyan("delimit vault list") + " to see configured secrets."));
|
|
6198
|
+
process.exit(1);
|
|
6199
|
+
}
|
|
6200
|
+
try {
|
|
6201
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
6202
|
+
const val = data.value || data.key || '(empty)';
|
|
6203
|
+
console.log(` ${chalk.bold(name)}: ${val}`);
|
|
6204
|
+
if (data.updated) console.log(chalk.dim(` Updated: ${data.updated}`));
|
|
6205
|
+
} catch {
|
|
6206
|
+
console.log(chalk.red(` Failed to read secret "${name}".`));
|
|
6207
|
+
}
|
|
4573
6208
|
} else {
|
|
4574
|
-
console.log(chalk.yellow(`
|
|
4575
|
-
console.log("
|
|
6209
|
+
console.log(chalk.yellow(`Unknown action: "${action}"`));
|
|
6210
|
+
console.log("Available: " + chalk.cyan("status") + " | " + chalk.cyan("list") + " | " + chalk.cyan("set <NAME>") + " | " + chalk.cyan("reveal <NAME>"));
|
|
4576
6211
|
}
|
|
4577
6212
|
console.log("");
|
|
4578
6213
|
});
|
|
@@ -4709,13 +6344,18 @@ function readMemories() {
|
|
|
4709
6344
|
function writeMemory(entry) {
|
|
4710
6345
|
// Write in MCP-compatible format (individual .json files)
|
|
4711
6346
|
fs.mkdirSync(MEMORY_DIR, { recursive: true });
|
|
4712
|
-
const
|
|
6347
|
+
const crypto = require('crypto');
|
|
6348
|
+
const content = entry.text;
|
|
6349
|
+
const memId = 'mem-' + crypto.createHash('sha256').update(content.slice(0, 100)).digest('hex').slice(0, 12);
|
|
6350
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
4713
6351
|
const mcpEntry = {
|
|
4714
6352
|
id: memId,
|
|
4715
|
-
content
|
|
6353
|
+
content,
|
|
4716
6354
|
tags: entry.tags || [],
|
|
4717
6355
|
context: entry.source || 'cli',
|
|
4718
6356
|
created_at: entry.created || new Date().toISOString(),
|
|
6357
|
+
hash,
|
|
6358
|
+
source_model: process.env.DELIMIT_MODEL || 'cli',
|
|
4719
6359
|
};
|
|
4720
6360
|
fs.writeFileSync(path.join(MEMORY_DIR, `${memId}.json`), JSON.stringify(mcpEntry, null, 2));
|
|
4721
6361
|
return memId;
|
|
@@ -4761,8 +6401,18 @@ function relativeTime(isoDate) {
|
|
|
4761
6401
|
return `${diffYear} year${diffYear === 1 ? '' : 's'} ago`;
|
|
4762
6402
|
}
|
|
4763
6403
|
|
|
6404
|
+
function verifyMemoryIntegrity(mem) {
|
|
6405
|
+
if (!mem.hash) return null; // No hash — legacy memory
|
|
6406
|
+
const crypto = require('crypto');
|
|
6407
|
+
const expected = crypto.createHash('sha256').update(mem.content || mem.text || '').digest('hex').slice(0, 16);
|
|
6408
|
+
return expected === mem.hash;
|
|
6409
|
+
}
|
|
6410
|
+
|
|
4764
6411
|
function displayMemory(mem) {
|
|
4765
|
-
|
|
6412
|
+
const integrity = verifyMemoryIntegrity(mem);
|
|
6413
|
+
const integrityBadge = integrity === true ? chalk.green(' \u2713') : integrity === false ? chalk.red(' \u2717 tampered') : '';
|
|
6414
|
+
const sourceBadge = mem.source_model ? chalk.gray(` [${mem.source_model}]`) : mem.context ? chalk.gray(` [${mem.context}]`) : '';
|
|
6415
|
+
console.log(` ${chalk.gray('[' + mem.id + ']')} ${chalk.gray(relativeTime(mem.created))}${sourceBadge}${integrityBadge}`);
|
|
4766
6416
|
console.log(` ${mem.text}`);
|
|
4767
6417
|
if (mem.tags && mem.tags.length > 0) {
|
|
4768
6418
|
console.log(` ${chalk.blue(mem.tags.map(t => '#' + t).join(' '))}`);
|