speclock 4.5.2 → 4.5.4

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/src/cli/index.js CHANGED
@@ -1,1113 +1,1113 @@
1
- import path from "path";
2
- import {
3
- ensureInit,
4
- setGoal,
5
- addLock,
6
- removeLock,
7
- addDecision,
8
- addNote,
9
- updateDeployFacts,
10
- logChange,
11
- checkConflict,
12
- watchRepo,
13
- createSpecLockMd,
14
- guardFile,
15
- unguardFile,
16
- injectPackageJsonMarker,
17
- syncLocksToPackageJson,
18
- autoGuardRelatedFiles,
19
- listTemplates,
20
- applyTemplate,
21
- generateReport,
22
- auditStagedFiles,
23
- verifyAuditChain,
24
- exportCompliance,
25
- getLicenseInfo,
26
- enforceConflictCheck,
27
- enforceConflictCheckAsync,
28
- setEnforcementMode,
29
- overrideLock,
30
- getOverrideHistory,
31
- getEnforcementConfig,
32
- semanticAudit,
33
- } from "../core/engine.js";
34
- import { generateContext } from "../core/context.js";
35
- import { readBrain } from "../core/storage.js";
36
- import { installHook, removeHook } from "../core/hooks.js";
37
- import {
38
- isAuthEnabled,
39
- enableAuth,
40
- disableAuth,
41
- createApiKey,
42
- rotateApiKey,
43
- revokeApiKey,
44
- listApiKeys,
45
- } from "../core/auth.js";
46
- import { isEncryptionEnabled } from "../core/crypto.js";
47
- import {
48
- initPolicy,
49
- addPolicyRule,
50
- removePolicyRule,
51
- listPolicyRules,
52
- evaluatePolicy,
53
- exportPolicy,
54
- importPolicy,
55
- } from "../core/policy.js";
56
- import {
57
- isTelemetryEnabled,
58
- getTelemetrySummary,
59
- } from "../core/telemetry.js";
60
- import {
61
- isSSOEnabled,
62
- getSSOConfig,
63
- saveSSOConfig,
64
- } from "../core/sso.js";
65
-
66
- // --- Argument parsing ---
67
-
68
- function parseArgs(argv) {
69
- const args = argv.slice(2);
70
- const cmd = args.shift() || "";
71
- return { cmd, args };
72
- }
73
-
74
- function parseFlags(args) {
75
- const out = { _: [] };
76
- for (let i = 0; i < args.length; i++) {
77
- const a = args[i];
78
- if (a.startsWith("--")) {
79
- const key = a.slice(2);
80
- const next = args[i + 1];
81
- if (!next || next.startsWith("--")) {
82
- out[key] = true;
83
- } else {
84
- out[key] = next;
85
- i++;
86
- }
87
- } else {
88
- out._.push(a);
89
- }
90
- }
91
- return out;
92
- }
93
-
94
- function parseTags(raw) {
95
- if (!raw) return [];
96
- return raw
97
- .split(",")
98
- .map((t) => t.trim())
99
- .filter(Boolean);
100
- }
101
-
102
- function rootDir() {
103
- return process.cwd();
104
- }
105
-
106
- // --- Auto-regenerate context after write operations ---
107
-
108
- function refreshContext(root) {
109
- try {
110
- generateContext(root);
111
- } catch (_) {
112
- // Silently skip if context generation fails
113
- }
114
- }
115
-
116
- // --- Help text ---
117
-
118
- function printHelp() {
119
- console.log(`
120
- SpecLock v4.5.2 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
121
- Developed by Sandeep Roy (github.com/sgroy10)
122
-
123
- Usage: speclock <command> [options]
124
-
125
- Commands:
126
- setup [--goal <text>] [--template <name>] Full setup: init + SPECLOCK.md + context
127
- init Initialize SpecLock in current directory
128
- goal <text> Set or update the project goal
129
- lock <text> [--tags a,b] Add a non-negotiable constraint
130
- lock remove <id> Remove a lock by ID
131
- guard <file> [--lock "text"] Inject lock warning into a file
132
- unguard <file> Remove lock warning from a file
133
- decide <text> [--tags a,b] Record a decision
134
- note <text> [--pinned] Add a pinned note
135
- log-change <text> [--files x,y] Log a significant change
136
- check <text> Check if action conflicts with locks
137
- template list List available constraint templates
138
- template apply <name> Apply a template (nextjs, react, etc.)
139
- report Show violation report + stats
140
- hook install Install git pre-commit hook
141
- hook remove Remove git pre-commit hook
142
- audit Audit staged files against locks
143
- audit-semantic Semantic audit: analyze code changes vs locks
144
- audit-verify Verify HMAC audit chain integrity
145
- enforce <advisory|hard> Set enforcement mode (advisory=warn, hard=block)
146
- override <lockId> <reason> Override a lock with justification
147
- overrides [--lock <id>] Show override history
148
- export --format <soc2|hipaa|csv> Export compliance report
149
- license Show license tier and usage info
150
- context Generate and print context pack
151
- facts deploy [--provider X] Set deployment facts
152
- watch Start file watcher (auto-track changes)
153
- serve [--project <path>] Start MCP stdio server
154
- status Show project brain summary
155
-
156
- Options:
157
- --tags <a,b,c> Comma-separated tags
158
- --source <user|agent> Who created this (default: user)
159
- --files <a.ts,b.ts> Comma-separated file paths
160
- --goal <text> Goal text (for setup command)
161
- --template <name> Template to apply during setup
162
- --lock <text> Lock text (for guard command)
163
- --format <soc2|hipaa|csv> Compliance export format
164
- --project <path> Project root (for serve)
165
-
166
- Templates: nextjs, react, express, supabase, stripe, security-hardened
167
-
168
- Policy-as-Code (v3.5):
169
- policy list List all policy rules
170
- policy init Initialize policy-as-code
171
- policy add --name <name> Add a policy rule (--files, --enforce, --severity)
172
- policy remove <ruleId> Remove a policy rule
173
- policy evaluate <action> Evaluate action against policy rules
174
- policy export Export policy as YAML
175
- telemetry [status] Show telemetry status and analytics
176
- sso status Show SSO configuration
177
- sso configure --issuer <url> Configure SSO (--client-id, --client-secret)
178
-
179
- Security (v3.0):
180
- auth status Show auth status and active keys
181
- auth create-key --role <role> Create API key (viewer/developer/architect/admin)
182
- auth rotate-key <keyId> Rotate an API key
183
- auth revoke-key <keyId> Revoke an API key
184
- auth list-keys List all API keys
185
- auth enable Enable API key authentication
186
- auth disable Disable authentication
187
- encrypt [status] Show encryption status
188
-
189
- Enterprise:
190
- SPECLOCK_AUDIT_SECRET HMAC secret for audit chain (env var)
191
- SPECLOCK_LICENSE_KEY License key for Pro/Enterprise features
192
- SPECLOCK_LLM_KEY API key for LLM-powered conflict detection
193
- SPECLOCK_ENCRYPTION_KEY Master key for AES-256-GCM encryption
194
- SPECLOCK_API_KEY API key for MCP server auth
195
-
196
- Examples:
197
- npx speclock setup --goal "Build PawPalace pet shop" --template nextjs
198
- npx speclock lock "Never modify auth files"
199
- npx speclock check "Adding social login to auth page"
200
- npx speclock audit-verify
201
- npx speclock export --format soc2
202
- npx speclock license
203
- npx speclock status
204
- `);
205
- }
206
-
207
- // --- Status display ---
208
-
209
- function showStatus(root) {
210
- const brain = readBrain(root);
211
- if (!brain) {
212
- console.log("SpecLock not initialized. Run: npx speclock setup");
213
- return;
214
- }
215
-
216
- const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
217
-
218
- console.log(`\nSpecLock Status — ${brain.project.name}`);
219
- console.log("=".repeat(50));
220
- console.log(`Goal: ${brain.goal.text || "(not set)"}`);
221
- console.log(`Locks: ${activeLocks.length} active`);
222
- console.log(`Decisions: ${brain.decisions.length}`);
223
- console.log(`Notes: ${brain.notes.length}`);
224
- console.log(`Events: ${brain.events.count}`);
225
- console.log(`Deploy: ${brain.facts.deploy.provider || "(not set)"}`);
226
-
227
- if (brain.sessions.current) {
228
- console.log(`Session: active (${brain.sessions.current.toolUsed})`);
229
- } else {
230
- console.log(`Session: none active`);
231
- }
232
-
233
- if (brain.sessions.history.length > 0) {
234
- const last = brain.sessions.history[0];
235
- console.log(
236
- `Last session: ${last.toolUsed} — ${last.summary || "(no summary)"}`
237
- );
238
- }
239
-
240
- console.log(`Recent changes: ${brain.state.recentChanges.length}`);
241
- console.log(`Auth: ${isAuthEnabled(root) ? "enabled" : "disabled"}`);
242
- console.log(`Encryption: ${isEncryptionEnabled() ? "enabled (AES-256-GCM)" : "disabled"}`);
243
- const policyRules = listPolicyRules(root);
244
- console.log(`Policy rules: ${policyRules.active}/${policyRules.total}`);
245
- console.log(`Telemetry: ${isTelemetryEnabled() ? "enabled" : "disabled"}`);
246
- console.log(`SSO: ${isSSOEnabled(root) ? "configured" : "not configured"}`);
247
- console.log("");
248
- }
249
-
250
- // --- Main ---
251
-
252
- async function main() {
253
- const { cmd, args } = parseArgs(process.argv);
254
- const root = rootDir();
255
-
256
- if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
257
- printHelp();
258
- process.exit(0);
259
- }
260
-
261
- // --- SETUP (new: one-shot full setup) ---
262
- if (cmd === "setup") {
263
- const flags = parseFlags(args);
264
- const goalText = flags.goal || flags._.join(" ").trim();
265
-
266
- // 1. Initialize
267
- ensureInit(root);
268
- console.log("Initialized .speclock/ directory.");
269
-
270
- // 2. Set goal if provided
271
- if (goalText) {
272
- setGoal(root, goalText);
273
- console.log(`Goal set: "${goalText}"`);
274
- }
275
-
276
- // 3. Create SPECLOCK.md in project root
277
- const mdPath = createSpecLockMd(root);
278
- console.log(`Created SPECLOCK.md (AI instructions file).`);
279
-
280
- // 4. Inject marker into package.json (so AI tools auto-discover SpecLock)
281
- const pkgResult = injectPackageJsonMarker(root);
282
- if (pkgResult.success) {
283
- console.log("Injected SpecLock marker into package.json.");
284
- }
285
-
286
- // 5. Apply template if specified
287
- if (flags.template) {
288
- const result = applyTemplate(root, flags.template);
289
- if (result.applied) {
290
- console.log(`Applied template "${result.displayName}": ${result.locksAdded} lock(s), ${result.decisionsAdded} decision(s).`);
291
- } else {
292
- console.error(`Template error: ${result.error}`);
293
- }
294
- }
295
-
296
- // 6. Generate context
297
- generateContext(root);
298
- console.log("Generated .speclock/context/latest.md");
299
-
300
- // 7. Print summary
301
- console.log(`
302
- SpecLock is ready!
303
-
304
- Files created/updated:
305
- .speclock/brain.json — Project memory
306
- .speclock/context/latest.md — Context for AI (read this)
307
- SPECLOCK.md — AI rules (read this)
308
- package.json — Active locks embedded (AI auto-discovery)
309
-
310
- Next steps:
311
- To add constraints: npx speclock lock "Never touch auth files"
312
- To check conflicts: npx speclock check "Modifying auth page"
313
- To log changes: npx speclock log-change "Built landing page"
314
- To see status: npx speclock status
315
-
316
- Tip: When starting a new chat, tell the AI:
317
- "Check speclock status and read the project constraints before doing anything"
318
- `);
319
- return;
320
- }
321
-
322
- // --- INIT ---
323
- if (cmd === "init") {
324
- ensureInit(root);
325
- createSpecLockMd(root);
326
- injectPackageJsonMarker(root);
327
- generateContext(root);
328
- console.log("SpecLock initialized. Created SPECLOCK.md, updated package.json, and generated context file.");
329
- return;
330
- }
331
-
332
- // --- GOAL ---
333
- if (cmd === "goal") {
334
- const text = args.join(" ").trim();
335
- if (!text) {
336
- console.error("Error: Goal text is required.");
337
- console.error("Usage: speclock goal <text>");
338
- process.exit(1);
339
- }
340
- setGoal(root, text);
341
- refreshContext(root);
342
- console.log(`Goal set: "${text}"`);
343
- return;
344
- }
345
-
346
- // --- LOCK ---
347
- if (cmd === "lock") {
348
- // Check for "lock remove <id>"
349
- if (args[0] === "remove") {
350
- const lockId = args[1];
351
- if (!lockId) {
352
- console.error("Error: Lock ID is required.");
353
- console.error("Usage: speclock lock remove <lockId>");
354
- process.exit(1);
355
- }
356
- const result = removeLock(root, lockId);
357
- if (result.removed) {
358
- // Sync updated locks to package.json
359
- syncLocksToPackageJson(root);
360
- refreshContext(root);
361
- console.log(`Lock removed: "${result.lockText}"`);
362
- } else {
363
- console.error(result.error);
364
- process.exit(1);
365
- }
366
- return;
367
- }
368
-
369
- const flags = parseFlags(args);
370
- const text = flags._.join(" ").trim();
371
- if (!text) {
372
- console.error("Error: Lock text is required.");
373
- console.error("Usage: speclock lock <text> [--tags a,b] [--source user]");
374
- process.exit(1);
375
- }
376
- const { lockId, rewritten, rewriteReason } = addLock(root, text, parseTags(flags.tags), flags.source || "user");
377
-
378
- // Auto-guard related files (Solution 1)
379
- const guardResult = autoGuardRelatedFiles(root, text);
380
- if (guardResult.guarded.length > 0) {
381
- console.log(`Auto-guarded ${guardResult.guarded.length} related file(s):`);
382
- for (const f of guardResult.guarded) {
383
- console.log(` 🔒 ${f}`);
384
- }
385
- }
386
-
387
- // Sync locks to package.json (Solution 2)
388
- const syncResult = syncLocksToPackageJson(root);
389
- if (syncResult.success) {
390
- console.log(`Synced ${syncResult.lockCount} lock(s) to package.json.`);
391
- }
392
-
393
- refreshContext(root);
394
- console.log(`Locked (${lockId}): "${text}"`);
395
- if (rewritten) {
396
- console.log(` Note: Engine optimized for detection. Your original text is preserved.`);
397
- }
398
- return;
399
- }
400
-
401
- // --- DECIDE ---
402
- if (cmd === "decide") {
403
- const flags = parseFlags(args);
404
- const text = flags._.join(" ").trim();
405
- if (!text) {
406
- console.error("Error: Decision text is required.");
407
- console.error("Usage: speclock decide <text> [--tags a,b]");
408
- process.exit(1);
409
- }
410
- const { decId } = addDecision(root, text, parseTags(flags.tags), flags.source || "user");
411
- refreshContext(root);
412
- console.log(`Decision recorded (${decId}): "${text}"`);
413
- return;
414
- }
415
-
416
- // --- NOTE ---
417
- if (cmd === "note") {
418
- const flags = parseFlags(args);
419
- const text = flags._.join(" ").trim();
420
- if (!text) {
421
- console.error("Error: Note text is required.");
422
- console.error("Usage: speclock note <text> [--pinned]");
423
- process.exit(1);
424
- }
425
- const pinned = flags.pinned !== false;
426
- const { noteId } = addNote(root, text, pinned);
427
- refreshContext(root);
428
- console.log(`Note added (${noteId}): "${text}"`);
429
- return;
430
- }
431
-
432
- // --- LOG-CHANGE (new) ---
433
- if (cmd === "log-change") {
434
- const flags = parseFlags(args);
435
- const text = flags._.join(" ").trim();
436
- if (!text) {
437
- console.error("Error: Change summary is required.");
438
- console.error('Usage: speclock log-change "what changed" --files a.ts,b.ts');
439
- process.exit(1);
440
- }
441
- const files = flags.files ? flags.files.split(",").map((f) => f.trim()).filter(Boolean) : [];
442
- logChange(root, text, files);
443
- refreshContext(root);
444
- console.log(`Change logged: "${text}"`);
445
- if (files.length > 0) {
446
- console.log(`Files: ${files.join(", ")}`);
447
- }
448
- return;
449
- }
450
-
451
- // --- CHECK (new: conflict check) ---
452
- if (cmd === "check") {
453
- const text = args.join(" ").trim();
454
- if (!text) {
455
- console.error("Error: Action description is required.");
456
- console.error('Usage: speclock check "what you plan to do"');
457
- process.exit(1);
458
- }
459
- // Use async version for Gemini proxy coverage on grey-zone cases
460
- const result = await enforceConflictCheckAsync(root, text);
461
- if (result.hasConflict) {
462
- console.log(`\n${result.blocked ? "BLOCKED" : "CONFLICT DETECTED"}`);
463
- console.log("=".repeat(50));
464
- console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
465
- console.log("");
466
- for (const lock of result.conflictingLocks) {
467
- console.log(` [${lock.level}] "${lock.text}"`);
468
- console.log(` Confidence: ${lock.confidence}%`);
469
- if (lock.reasons && lock.reasons.length > 0) {
470
- for (const reason of lock.reasons) {
471
- console.log(` - ${reason}`);
472
- }
473
- }
474
- console.log("");
475
- }
476
- console.log(result.analysis);
477
- if (result.blocked) {
478
- process.exit(1);
479
- }
480
- } else {
481
- console.log(`No conflicts found. Safe to proceed with: "${text}"`);
482
- }
483
- return;
484
- }
485
-
486
- // --- GUARD (new: file-level lock) ---
487
- if (cmd === "guard") {
488
- const flags = parseFlags(args);
489
- const filePath = flags._[0];
490
- if (!filePath) {
491
- console.error("Error: File path is required.");
492
- console.error('Usage: speclock guard <file> --lock "constraint text"');
493
- process.exit(1);
494
- }
495
- const lockText = flags.lock || "This file is locked by SpecLock. Do not modify.";
496
- const result = guardFile(root, filePath, lockText);
497
- if (result.success) {
498
- console.log(`Guarded: ${filePath}`);
499
- console.log(`Lock warning injected: "${lockText}"`);
500
- } else {
501
- console.error(result.error);
502
- process.exit(1);
503
- }
504
- return;
505
- }
506
-
507
- // --- UNGUARD ---
508
- if (cmd === "unguard") {
509
- const filePath = args[0];
510
- if (!filePath) {
511
- console.error("Error: File path is required.");
512
- console.error("Usage: speclock unguard <file>");
513
- process.exit(1);
514
- }
515
- const result = unguardFile(root, filePath);
516
- if (result.success) {
517
- console.log(`Unguarded: ${filePath}`);
518
- } else {
519
- console.error(result.error);
520
- process.exit(1);
521
- }
522
- return;
523
- }
524
-
525
- // --- FACTS ---
526
- if (cmd === "facts") {
527
- const sub = args.shift();
528
- if (sub !== "deploy") {
529
- console.error("Error: Only 'facts deploy' is supported.");
530
- console.error(
531
- "Usage: speclock facts deploy --provider X --branch Y"
532
- );
533
- process.exit(1);
534
- }
535
- const flags = parseFlags(args);
536
- const payload = {
537
- provider: flags.provider,
538
- branch: flags.branch,
539
- notes: flags.notes,
540
- url: flags.url,
541
- };
542
- if (flags.autoDeploy !== undefined) {
543
- payload.autoDeploy =
544
- String(flags.autoDeploy).toLowerCase() === "true";
545
- }
546
- updateDeployFacts(root, payload);
547
- refreshContext(root);
548
- console.log("Deploy facts updated.");
549
- return;
550
- }
551
-
552
- // --- CONTEXT ---
553
- if (cmd === "context") {
554
- const md = generateContext(root);
555
- console.log(md);
556
- return;
557
- }
558
-
559
- // --- WATCH ---
560
- if (cmd === "watch") {
561
- await watchRepo(root);
562
- return;
563
- }
564
-
565
- // --- SERVE ---
566
- if (cmd === "serve") {
567
- // Start MCP server — pass through --project if provided
568
- const flags = parseFlags(args);
569
- const projectArg = flags.project || root;
570
- process.env.SPECLOCK_PROJECT_ROOT = projectArg;
571
- await import("../mcp/server.js");
572
- return;
573
- }
574
-
575
- // --- TEMPLATE ---
576
- if (cmd === "template") {
577
- const sub = args[0];
578
- if (sub === "list" || !sub) {
579
- const templates = listTemplates();
580
- console.log("\nAvailable Templates:");
581
- console.log("=".repeat(50));
582
- for (const t of templates) {
583
- console.log(` ${t.name.padEnd(20)} ${t.displayName} — ${t.description}`);
584
- console.log(` ${"".padEnd(20)} ${t.lockCount} lock(s), ${t.decisionCount} decision(s)`);
585
- console.log("");
586
- }
587
- console.log("Apply: npx speclock template apply <name>");
588
- return;
589
- }
590
- if (sub === "apply") {
591
- const name = args[1];
592
- if (!name) {
593
- console.error("Error: Template name is required.");
594
- console.error("Usage: speclock template apply <name>");
595
- console.error("Run 'speclock template list' to see available templates.");
596
- process.exit(1);
597
- }
598
- const result = applyTemplate(root, name);
599
- if (result.applied) {
600
- refreshContext(root);
601
- console.log(`Template "${result.displayName}" applied successfully!`);
602
- console.log(` Locks added: ${result.locksAdded}`);
603
- console.log(` Decisions added: ${result.decisionsAdded}`);
604
- } else {
605
- console.error(result.error);
606
- process.exit(1);
607
- }
608
- return;
609
- }
610
- console.error(`Unknown template command: ${sub}`);
611
- console.error("Usage: speclock template list | speclock template apply <name>");
612
- process.exit(1);
613
- }
614
-
615
- // --- REPORT ---
616
- if (cmd === "report") {
617
- const report = generateReport(root);
618
- console.log("\nSpecLock Violation Report");
619
- console.log("=".repeat(50));
620
- console.log(`Total violations blocked: ${report.totalViolations}`);
621
- if (report.timeRange) {
622
- console.log(`Period: ${report.timeRange.from.substring(0, 10)} to ${report.timeRange.to.substring(0, 10)}`);
623
- }
624
- if (report.mostTestedLocks.length > 0) {
625
- console.log("\nMost tested locks:");
626
- for (const lock of report.mostTestedLocks) {
627
- console.log(` ${lock.count}x — "${lock.text}"`);
628
- }
629
- }
630
- if (report.recentViolations.length > 0) {
631
- console.log("\nRecent violations:");
632
- for (const v of report.recentViolations) {
633
- console.log(` [${v.at.substring(0, 19)}] ${v.topLevel} (${v.topConfidence}%) — "${v.action.substring(0, 60)}"`);
634
- }
635
- }
636
- console.log(`\n${report.summary}`);
637
- return;
638
- }
639
-
640
- // --- HOOK ---
641
- if (cmd === "hook") {
642
- const sub = args[0];
643
- if (sub === "install") {
644
- const result = installHook(root);
645
- if (result.success) {
646
- console.log(result.message);
647
- } else {
648
- console.error(result.error);
649
- process.exit(1);
650
- }
651
- return;
652
- }
653
- if (sub === "remove") {
654
- const result = removeHook(root);
655
- if (result.success) {
656
- console.log(result.message);
657
- } else {
658
- console.error(result.error);
659
- process.exit(1);
660
- }
661
- return;
662
- }
663
- console.error("Usage: speclock hook install | speclock hook remove");
664
- process.exit(1);
665
- }
666
-
667
- // --- AUDIT ---
668
- if (cmd === "audit") {
669
- const result = auditStagedFiles(root);
670
- if (result.passed) {
671
- console.log(result.message);
672
- process.exit(0);
673
- } else {
674
- console.log("\nSPECLOCK AUDIT FAILED");
675
- console.log("=".repeat(50));
676
- for (const v of result.violations) {
677
- console.log(` [${v.severity}] ${v.file}`);
678
- console.log(` Lock: ${v.lockText}`);
679
- console.log(` Reason: ${v.reason}`);
680
- console.log("");
681
- }
682
- console.log(result.message);
683
- console.log("Commit blocked. Unlock files or unstage them to proceed.");
684
- process.exit(1);
685
- }
686
- }
687
-
688
- // --- AUDIT-VERIFY (v2.1 enterprise) ---
689
- if (cmd === "audit-verify") {
690
- ensureInit(root);
691
- const result = verifyAuditChain(root);
692
- console.log(`\nAudit Chain Verification`);
693
- console.log("=".repeat(50));
694
- console.log(`Status: ${result.valid ? "VALID" : "BROKEN"}`);
695
- console.log(`Total events: ${result.totalEvents}`);
696
- console.log(`Hashed events: ${result.hashedEvents}`);
697
- console.log(`Legacy events (pre-v2.1): ${result.unhashedEvents}`);
698
- if (!result.valid && result.errors) {
699
- console.log(`\nErrors:`);
700
- for (const err of result.errors) {
701
- console.log(` Line ${err.line}: ${err.error}`);
702
- }
703
- }
704
- console.log(`\n${result.message}`);
705
- process.exit(result.valid ? 0 : 1);
706
- }
707
-
708
- // --- EXPORT (v2.1 enterprise) ---
709
- if (cmd === "export") {
710
- const flags = parseFlags(args);
711
- const format = flags.format;
712
- if (!format || !["soc2", "hipaa", "csv"].includes(format)) {
713
- console.error("Error: Valid format is required.");
714
- console.error("Usage: speclock export --format <soc2|hipaa|csv>");
715
- process.exit(1);
716
- }
717
- ensureInit(root);
718
- const result = exportCompliance(root, format);
719
- if (result.error) {
720
- console.error(result.error);
721
- process.exit(1);
722
- }
723
- if (format === "csv") {
724
- console.log(result.data);
725
- } else {
726
- console.log(JSON.stringify(result.data, null, 2));
727
- }
728
- return;
729
- }
730
-
731
- // --- LICENSE (v2.1 enterprise) ---
732
- if (cmd === "license") {
733
- const info = getLicenseInfo(root);
734
- console.log(`\nSpecLock License Info`);
735
- console.log("=".repeat(50));
736
- console.log(`Tier: ${info.tier} (${info.tierKey})`);
737
- if (info.expiresAt) console.log(`Expires: ${info.expiresAt}`);
738
- if (info.expired) console.log(`STATUS: EXPIRED — reverted to Free tier`);
739
- console.log(`\nUsage:`);
740
- if (info.usage) {
741
- const { locks, decisions, events } = info.usage;
742
- console.log(` Locks: ${locks.current}/${locks.max === Infinity ? "unlimited" : locks.max}`);
743
- console.log(` Decisions: ${decisions.current}/${decisions.max === Infinity ? "unlimited" : decisions.max}`);
744
- console.log(` Events: ${events.current}/${events.max === Infinity ? "unlimited" : events.max}`);
745
- }
746
- if (info.warnings && info.warnings.length > 0) {
747
- console.log(`\nWarnings:`);
748
- for (const w of info.warnings) console.log(` - ${w}`);
749
- }
750
- console.log(`\nFeatures: ${info.features.join(", ")}`);
751
- return;
752
- }
753
-
754
- // --- ENFORCE (v2.5) ---
755
- if (cmd === "enforce") {
756
- const mode = args[0];
757
- if (!mode || (mode !== "advisory" && mode !== "hard")) {
758
- console.error("Usage: speclock enforce <advisory|hard> [--threshold 70]");
759
- process.exit(1);
760
- }
761
- const flags = parseFlags(args.slice(1));
762
- const options = {};
763
- if (flags.threshold) options.blockThreshold = parseInt(flags.threshold, 10);
764
- if (flags.override !== undefined) options.allowOverride = flags.override !== "false";
765
- const result = setEnforcementMode(root, mode, options);
766
- if (!result.success) {
767
- console.error(result.error);
768
- process.exit(1);
769
- }
770
- console.log(`\nEnforcement mode: ${result.mode.toUpperCase()}`);
771
- console.log(`Block threshold: ${result.config.blockThreshold}%`);
772
- console.log(`Overrides: ${result.config.allowOverride ? "allowed" : "disabled"}`);
773
- if (result.mode === "hard") {
774
- console.log(`\nHard mode active — conflicts above ${result.config.blockThreshold}% confidence will BLOCK actions.`);
775
- }
776
- return;
777
- }
778
-
779
- // --- OVERRIDE (v2.5) ---
780
- if (cmd === "override") {
781
- const lockId = args[0];
782
- const reason = args.slice(1).join(" ");
783
- if (!lockId || !reason) {
784
- console.error("Usage: speclock override <lockId> <reason>");
785
- process.exit(1);
786
- }
787
- const result = overrideLock(root, lockId, "(CLI override)", reason);
788
- if (!result.success) {
789
- console.error(result.error);
790
- process.exit(1);
791
- }
792
- console.log(`Lock overridden: "${result.lockText}"`);
793
- console.log(`Override count: ${result.overrideCount}`);
794
- if (result.escalated) {
795
- console.log(`\n${result.escalationMessage}`);
796
- }
797
- return;
798
- }
799
-
800
- // --- OVERRIDES (v2.5) ---
801
- if (cmd === "overrides") {
802
- const flags = parseFlags(args);
803
- const result = getOverrideHistory(root, flags.lock || null);
804
- if (result.total === 0) {
805
- console.log("No overrides recorded.");
806
- return;
807
- }
808
- console.log(`\nOverride History (${result.total})`);
809
- console.log("=".repeat(50));
810
- for (const o of result.overrides) {
811
- console.log(`[${o.at.substring(0, 19)}] Lock: "${o.lockText}"`);
812
- console.log(` Action: ${o.action}`);
813
- console.log(` Reason: ${o.reason}`);
814
- console.log("");
815
- }
816
- return;
817
- }
818
-
819
- // --- AUDIT-SEMANTIC (v2.5) ---
820
- if (cmd === "audit-semantic") {
821
- const result = semanticAudit(root);
822
- console.log(`\nSemantic Pre-Commit Audit`);
823
- console.log("=".repeat(50));
824
- console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
825
- console.log(`Files analyzed: ${result.filesChecked}`);
826
- console.log(`Active locks: ${result.activeLocks}`);
827
- console.log(`Violations: ${result.violations.length}`);
828
- if (result.violations.length > 0) {
829
- console.log("");
830
- for (const v of result.violations) {
831
- console.log(` [${v.level}] ${v.file} (confidence: ${v.confidence}%)`);
832
- console.log(` Lock: "${v.lockText}"`);
833
- console.log(` Reason: ${v.reason}`);
834
- if (v.addedLines !== undefined) {
835
- console.log(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
836
- }
837
- }
838
- }
839
- console.log(`\n${result.message}`);
840
- process.exit(result.blocked ? 1 : 0);
841
- }
842
-
843
- // --- AUTH (v3.0) ---
844
- if (cmd === "auth") {
845
- const sub = args[0];
846
- if (!sub || sub === "status") {
847
- const enabled = isAuthEnabled(root);
848
- console.log(`\nAuth Status: ${enabled ? "ENABLED" : "DISABLED"}`);
849
- if (enabled) {
850
- const keys = listApiKeys(root);
851
- const active = keys.keys.filter(k => k.active);
852
- console.log(`Active keys: ${active.length}`);
853
- for (const k of active) {
854
- console.log(` ${k.id} — ${k.name} (${k.role}) — last used: ${k.lastUsed || "never"}`);
855
- }
856
- } else {
857
- console.log("Run 'speclock auth create-key --role admin' to enable auth.");
858
- }
859
- return;
860
- }
861
- if (sub === "create-key") {
862
- const flags = parseFlags(args.slice(1));
863
- const role = flags.role || "developer";
864
- const name = flags.name || flags._.join(" ") || "";
865
- const result = createApiKey(root, role, name);
866
- if (!result.success) {
867
- console.error(result.error);
868
- process.exit(1);
869
- }
870
- console.log(`\nAPI Key Created`);
871
- console.log("=".repeat(50));
872
- console.log(`Key ID: ${result.keyId}`);
873
- console.log(`Role: ${result.role}`);
874
- console.log(`Name: ${result.name}`);
875
- console.log(`\nRaw Key: ${result.rawKey}`);
876
- console.log(`\nSave this key — it CANNOT be retrieved later.`);
877
- console.log(`\nUsage:`);
878
- console.log(` HTTP: Authorization: Bearer ${result.rawKey}`);
879
- console.log(` MCP: Set SPECLOCK_API_KEY=${result.rawKey} in MCP config`);
880
- return;
881
- }
882
- if (sub === "rotate-key") {
883
- const keyId = args[1];
884
- if (!keyId) {
885
- console.error("Usage: speclock auth rotate-key <keyId>");
886
- process.exit(1);
887
- }
888
- const result = rotateApiKey(root, keyId);
889
- if (!result.success) {
890
- console.error(result.error);
891
- process.exit(1);
892
- }
893
- console.log(`\nKey Rotated`);
894
- console.log(`Old key: ${result.oldKeyId} (revoked)`);
895
- console.log(`New key: ${result.newKeyId}`);
896
- console.log(`Raw Key: ${result.rawKey}`);
897
- console.log(`\nSave this key — it CANNOT be retrieved later.`);
898
- return;
899
- }
900
- if (sub === "revoke-key") {
901
- const keyId = args[1];
902
- if (!keyId) {
903
- console.error("Usage: speclock auth revoke-key <keyId>");
904
- process.exit(1);
905
- }
906
- const reason = args.slice(2).join(" ") || "manual";
907
- const result = revokeApiKey(root, keyId, reason);
908
- if (!result.success) {
909
- console.error(result.error);
910
- process.exit(1);
911
- }
912
- console.log(`Key revoked: ${result.keyId} (${result.name}, ${result.role})`);
913
- return;
914
- }
915
- if (sub === "list-keys") {
916
- const result = listApiKeys(root);
917
- console.log(`\nAPI Keys (auth ${result.enabled ? "enabled" : "disabled"}):`);
918
- console.log("=".repeat(50));
919
- if (result.keys.length === 0) {
920
- console.log(" No keys configured.");
921
- } else {
922
- for (const k of result.keys) {
923
- const status = k.active ? "active" : `revoked (${k.revokedAt?.substring(0, 10) || "unknown"})`;
924
- console.log(` ${k.id} — ${k.name} (${k.role}) [${status}]`);
925
- }
926
- }
927
- return;
928
- }
929
- if (sub === "enable") {
930
- enableAuth(root);
931
- console.log("Auth enabled. API keys are now required for HTTP access.");
932
- return;
933
- }
934
- if (sub === "disable") {
935
- disableAuth(root);
936
- console.log("Auth disabled. All operations allowed without keys.");
937
- return;
938
- }
939
- console.error("Usage: speclock auth <create-key|rotate-key|revoke-key|list-keys|enable|disable|status>");
940
- process.exit(1);
941
- }
942
-
943
- // --- POLICY (v3.5) ---
944
- if (cmd === "policy") {
945
- const sub = args[0];
946
- if (!sub || sub === "list") {
947
- const result = listPolicyRules(root);
948
- console.log(`\nPolicy Rules (${result.active}/${result.total} active):`);
949
- console.log("=".repeat(50));
950
- if (result.rules.length === 0) {
951
- console.log(" No rules. Run 'speclock policy init' to create a policy.");
952
- } else {
953
- for (const r of result.rules) {
954
- const status = r.active !== false ? "active" : "inactive";
955
- console.log(` ${r.id} — ${r.name} [${r.enforce}/${r.severity}] (${status})`);
956
- console.log(` Files: ${(r.match?.files || []).join(", ")}`);
957
- console.log(` Actions: ${(r.match?.actions || []).join(", ")}`);
958
- console.log("");
959
- }
960
- }
961
- return;
962
- }
963
- if (sub === "init") {
964
- const result = initPolicy(root);
965
- if (!result.success) { console.error(result.error); process.exit(1); }
966
- console.log("Policy-as-code initialized. Edit .speclock/policy.yml to add rules.");
967
- return;
968
- }
969
- if (sub === "add") {
970
- const flags = parseFlags(args.slice(1));
971
- const name = flags.name || flags._.join(" ");
972
- if (!name) { console.error("Usage: speclock policy add --name <name> --files '**/*.js' --enforce block"); process.exit(1); }
973
- const rule = {
974
- name,
975
- description: flags.description || "",
976
- match: {
977
- files: flags.files ? flags.files.split(",").map(s => s.trim()) : ["**/*"],
978
- actions: flags.actions ? flags.actions.split(",").map(s => s.trim()) : ["modify", "delete"],
979
- },
980
- enforce: flags.enforce || "warn",
981
- severity: flags.severity || "medium",
982
- notify: flags.notify ? flags.notify.split(",").map(s => s.trim()) : [],
983
- };
984
- const result = addPolicyRule(root, rule);
985
- if (!result.success) { console.error(result.error); process.exit(1); }
986
- console.log(`Policy rule added: "${result.rule.name}" (${result.ruleId}) [${result.rule.enforce}]`);
987
- return;
988
- }
989
- if (sub === "remove") {
990
- const ruleId = args[1];
991
- if (!ruleId) { console.error("Usage: speclock policy remove <ruleId>"); process.exit(1); }
992
- const result = removePolicyRule(root, ruleId);
993
- if (!result.success) { console.error(result.error); process.exit(1); }
994
- console.log(`Policy rule removed: "${result.removed.name}"`);
995
- return;
996
- }
997
- if (sub === "evaluate") {
998
- const text = args.slice(1).join(" ");
999
- if (!text) { console.error("Usage: speclock policy evaluate 'what you plan to do'"); process.exit(1); }
1000
- const result = evaluatePolicy(root, { description: text, text, type: "modify" });
1001
- if (result.passed) {
1002
- console.log(`Policy check passed. ${result.rulesChecked} rules evaluated.`);
1003
- } else {
1004
- console.log(`\nPolicy Violations (${result.violations.length}):`);
1005
- for (const v of result.violations) {
1006
- console.log(` [${v.severity.toUpperCase()}] ${v.ruleName} (${v.enforce})`);
1007
- if (v.matchedFiles.length) console.log(` Files: ${v.matchedFiles.join(", ")}`);
1008
- }
1009
- if (result.blocked) process.exit(1);
1010
- }
1011
- return;
1012
- }
1013
- if (sub === "export") {
1014
- const result = exportPolicy(root);
1015
- if (!result.success) { console.error(result.error); process.exit(1); }
1016
- console.log(result.yaml);
1017
- return;
1018
- }
1019
- console.error("Usage: speclock policy <list|init|add|remove|evaluate|export>");
1020
- process.exit(1);
1021
- }
1022
-
1023
- // --- TELEMETRY (v3.5) ---
1024
- if (cmd === "telemetry") {
1025
- const sub = args[0];
1026
- if (sub === "status" || !sub) {
1027
- const enabled = isTelemetryEnabled();
1028
- console.log(`\nTelemetry: ${enabled ? "ENABLED" : "DISABLED"}`);
1029
- if (!enabled) {
1030
- console.log("Set SPECLOCK_TELEMETRY=true to enable anonymous usage analytics.");
1031
- return;
1032
- }
1033
- const summary = getTelemetrySummary(root);
1034
- console.log(`Total calls: ${summary.totalCalls}`);
1035
- console.log(`Avg response: ${summary.avgResponseMs}ms`);
1036
- console.log(`Sessions: ${summary.sessions.total}`);
1037
- console.log(`Conflicts: ${summary.conflicts.total} (blocked: ${summary.conflicts.blocked})`);
1038
- if (summary.topTools.length > 0) {
1039
- console.log(`\nTop tools:`);
1040
- for (const t of summary.topTools.slice(0, 5)) {
1041
- console.log(` ${t.name}: ${t.count} calls`);
1042
- }
1043
- }
1044
- return;
1045
- }
1046
- console.error("Usage: speclock telemetry [status]");
1047
- process.exit(1);
1048
- }
1049
-
1050
- // --- SSO (v3.5) ---
1051
- if (cmd === "sso") {
1052
- const sub = args[0];
1053
- if (sub === "status" || !sub) {
1054
- const enabled = isSSOEnabled(root);
1055
- const config = getSSOConfig(root);
1056
- console.log(`\nSSO: ${enabled ? "CONFIGURED" : "NOT CONFIGURED"}`);
1057
- if (enabled) {
1058
- console.log(`Issuer: ${config.issuer}`);
1059
- console.log(`Client ID: ${config.clientId}`);
1060
- console.log(`Redirect: ${config.redirectUri}`);
1061
- console.log(`Default role: ${config.defaultRole}`);
1062
- } else {
1063
- console.log("Set SPECLOCK_SSO_ISSUER and SPECLOCK_SSO_CLIENT_ID to enable SSO.");
1064
- }
1065
- return;
1066
- }
1067
- if (sub === "configure") {
1068
- const flags = parseFlags(args.slice(1));
1069
- const config = getSSOConfig(root);
1070
- if (flags.issuer) config.issuer = flags.issuer;
1071
- if (flags["client-id"]) config.clientId = flags["client-id"];
1072
- if (flags["client-secret"]) config.clientSecret = flags["client-secret"];
1073
- if (flags["redirect-uri"]) config.redirectUri = flags["redirect-uri"];
1074
- if (flags["default-role"]) config.defaultRole = flags["default-role"];
1075
- saveSSOConfig(root, config);
1076
- console.log("SSO configuration saved.");
1077
- return;
1078
- }
1079
- console.error("Usage: speclock sso <status|configure> [--issuer URL --client-id ID]");
1080
- process.exit(1);
1081
- }
1082
-
1083
- // --- ENCRYPT STATUS (v3.0) ---
1084
- if (cmd === "encrypt") {
1085
- const sub = args[0];
1086
- if (sub === "status" || !sub) {
1087
- const enabled = isEncryptionEnabled();
1088
- console.log(`\nEncryption: ${enabled ? "ENABLED (AES-256-GCM)" : "DISABLED"}`);
1089
- if (!enabled) {
1090
- console.log("Set SPECLOCK_ENCRYPTION_KEY env var to enable encryption.");
1091
- console.log("All data will be encrypted at rest (brain.json + events.log).");
1092
- }
1093
- return;
1094
- }
1095
- console.error("Usage: speclock encrypt [status]");
1096
- process.exit(1);
1097
- }
1098
-
1099
- // --- STATUS ---
1100
- if (cmd === "status") {
1101
- showStatus(root);
1102
- return;
1103
- }
1104
-
1105
- console.error(`Unknown command: ${cmd}`);
1106
- console.error("Run 'speclock --help' for usage.");
1107
- process.exit(1);
1108
- }
1109
-
1110
- main().catch((err) => {
1111
- console.error("SpecLock error:", err.message);
1112
- process.exit(1);
1113
- });
1
+ import path from "path";
2
+ import {
3
+ ensureInit,
4
+ setGoal,
5
+ addLock,
6
+ removeLock,
7
+ addDecision,
8
+ addNote,
9
+ updateDeployFacts,
10
+ logChange,
11
+ checkConflict,
12
+ watchRepo,
13
+ createSpecLockMd,
14
+ guardFile,
15
+ unguardFile,
16
+ injectPackageJsonMarker,
17
+ syncLocksToPackageJson,
18
+ autoGuardRelatedFiles,
19
+ listTemplates,
20
+ applyTemplate,
21
+ generateReport,
22
+ auditStagedFiles,
23
+ verifyAuditChain,
24
+ exportCompliance,
25
+ getLicenseInfo,
26
+ enforceConflictCheck,
27
+ enforceConflictCheckAsync,
28
+ setEnforcementMode,
29
+ overrideLock,
30
+ getOverrideHistory,
31
+ getEnforcementConfig,
32
+ semanticAudit,
33
+ } from "../core/engine.js";
34
+ import { generateContext } from "../core/context.js";
35
+ import { readBrain } from "../core/storage.js";
36
+ import { installHook, removeHook } from "../core/hooks.js";
37
+ import {
38
+ isAuthEnabled,
39
+ enableAuth,
40
+ disableAuth,
41
+ createApiKey,
42
+ rotateApiKey,
43
+ revokeApiKey,
44
+ listApiKeys,
45
+ } from "../core/auth.js";
46
+ import { isEncryptionEnabled } from "../core/crypto.js";
47
+ import {
48
+ initPolicy,
49
+ addPolicyRule,
50
+ removePolicyRule,
51
+ listPolicyRules,
52
+ evaluatePolicy,
53
+ exportPolicy,
54
+ importPolicy,
55
+ } from "../core/policy.js";
56
+ import {
57
+ isTelemetryEnabled,
58
+ getTelemetrySummary,
59
+ } from "../core/telemetry.js";
60
+ import {
61
+ isSSOEnabled,
62
+ getSSOConfig,
63
+ saveSSOConfig,
64
+ } from "../core/sso.js";
65
+
66
+ // --- Argument parsing ---
67
+
68
+ function parseArgs(argv) {
69
+ const args = argv.slice(2);
70
+ const cmd = args.shift() || "";
71
+ return { cmd, args };
72
+ }
73
+
74
+ function parseFlags(args) {
75
+ const out = { _: [] };
76
+ for (let i = 0; i < args.length; i++) {
77
+ const a = args[i];
78
+ if (a.startsWith("--")) {
79
+ const key = a.slice(2);
80
+ const next = args[i + 1];
81
+ if (!next || next.startsWith("--")) {
82
+ out[key] = true;
83
+ } else {
84
+ out[key] = next;
85
+ i++;
86
+ }
87
+ } else {
88
+ out._.push(a);
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ function parseTags(raw) {
95
+ if (!raw) return [];
96
+ return raw
97
+ .split(",")
98
+ .map((t) => t.trim())
99
+ .filter(Boolean);
100
+ }
101
+
102
+ function rootDir() {
103
+ return process.cwd();
104
+ }
105
+
106
+ // --- Auto-regenerate context after write operations ---
107
+
108
+ function refreshContext(root) {
109
+ try {
110
+ generateContext(root);
111
+ } catch (_) {
112
+ // Silently skip if context generation fails
113
+ }
114
+ }
115
+
116
+ // --- Help text ---
117
+
118
+ function printHelp() {
119
+ console.log(`
120
+ SpecLock v4.5.4 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
121
+ Developed by Sandeep Roy (github.com/sgroy10)
122
+
123
+ Usage: speclock <command> [options]
124
+
125
+ Commands:
126
+ setup [--goal <text>] [--template <name>] Full setup: init + SPECLOCK.md + context
127
+ init Initialize SpecLock in current directory
128
+ goal <text> Set or update the project goal
129
+ lock <text> [--tags a,b] Add a non-negotiable constraint
130
+ lock remove <id> Remove a lock by ID
131
+ guard <file> [--lock "text"] Inject lock warning into a file
132
+ unguard <file> Remove lock warning from a file
133
+ decide <text> [--tags a,b] Record a decision
134
+ note <text> [--pinned] Add a pinned note
135
+ log-change <text> [--files x,y] Log a significant change
136
+ check <text> Check if action conflicts with locks
137
+ template list List available constraint templates
138
+ template apply <name> Apply a template (nextjs, react, etc.)
139
+ report Show violation report + stats
140
+ hook install Install git pre-commit hook
141
+ hook remove Remove git pre-commit hook
142
+ audit Audit staged files against locks
143
+ audit-semantic Semantic audit: analyze code changes vs locks
144
+ audit-verify Verify HMAC audit chain integrity
145
+ enforce <advisory|hard> Set enforcement mode (advisory=warn, hard=block)
146
+ override <lockId> <reason> Override a lock with justification
147
+ overrides [--lock <id>] Show override history
148
+ export --format <soc2|hipaa|csv> Export compliance report
149
+ license Show license tier and usage info
150
+ context Generate and print context pack
151
+ facts deploy [--provider X] Set deployment facts
152
+ watch Start file watcher (auto-track changes)
153
+ serve [--project <path>] Start MCP stdio server
154
+ status Show project brain summary
155
+
156
+ Options:
157
+ --tags <a,b,c> Comma-separated tags
158
+ --source <user|agent> Who created this (default: user)
159
+ --files <a.ts,b.ts> Comma-separated file paths
160
+ --goal <text> Goal text (for setup command)
161
+ --template <name> Template to apply during setup
162
+ --lock <text> Lock text (for guard command)
163
+ --format <soc2|hipaa|csv> Compliance export format
164
+ --project <path> Project root (for serve)
165
+
166
+ Templates: nextjs, react, express, supabase, stripe, security-hardened
167
+
168
+ Policy-as-Code (v3.5):
169
+ policy list List all policy rules
170
+ policy init Initialize policy-as-code
171
+ policy add --name <name> Add a policy rule (--files, --enforce, --severity)
172
+ policy remove <ruleId> Remove a policy rule
173
+ policy evaluate <action> Evaluate action against policy rules
174
+ policy export Export policy as YAML
175
+ telemetry [status] Show telemetry status and analytics
176
+ sso status Show SSO configuration
177
+ sso configure --issuer <url> Configure SSO (--client-id, --client-secret)
178
+
179
+ Security (v3.0):
180
+ auth status Show auth status and active keys
181
+ auth create-key --role <role> Create API key (viewer/developer/architect/admin)
182
+ auth rotate-key <keyId> Rotate an API key
183
+ auth revoke-key <keyId> Revoke an API key
184
+ auth list-keys List all API keys
185
+ auth enable Enable API key authentication
186
+ auth disable Disable authentication
187
+ encrypt [status] Show encryption status
188
+
189
+ Enterprise:
190
+ SPECLOCK_AUDIT_SECRET HMAC secret for audit chain (env var)
191
+ SPECLOCK_LICENSE_KEY License key for Pro/Enterprise features
192
+ SPECLOCK_LLM_KEY API key for LLM-powered conflict detection
193
+ SPECLOCK_ENCRYPTION_KEY Master key for AES-256-GCM encryption
194
+ SPECLOCK_API_KEY API key for MCP server auth
195
+
196
+ Examples:
197
+ npx speclock setup --goal "Build PawPalace pet shop" --template nextjs
198
+ npx speclock lock "Never modify auth files"
199
+ npx speclock check "Adding social login to auth page"
200
+ npx speclock audit-verify
201
+ npx speclock export --format soc2
202
+ npx speclock license
203
+ npx speclock status
204
+ `);
205
+ }
206
+
207
+ // --- Status display ---
208
+
209
+ function showStatus(root) {
210
+ const brain = readBrain(root);
211
+ if (!brain) {
212
+ console.log("SpecLock not initialized. Run: npx speclock setup");
213
+ return;
214
+ }
215
+
216
+ const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
217
+
218
+ console.log(`\nSpecLock Status — ${brain.project.name}`);
219
+ console.log("=".repeat(50));
220
+ console.log(`Goal: ${brain.goal.text || "(not set)"}`);
221
+ console.log(`Locks: ${activeLocks.length} active`);
222
+ console.log(`Decisions: ${brain.decisions.length}`);
223
+ console.log(`Notes: ${brain.notes.length}`);
224
+ console.log(`Events: ${brain.events.count}`);
225
+ console.log(`Deploy: ${brain.facts.deploy.provider || "(not set)"}`);
226
+
227
+ if (brain.sessions.current) {
228
+ console.log(`Session: active (${brain.sessions.current.toolUsed})`);
229
+ } else {
230
+ console.log(`Session: none active`);
231
+ }
232
+
233
+ if (brain.sessions.history.length > 0) {
234
+ const last = brain.sessions.history[0];
235
+ console.log(
236
+ `Last session: ${last.toolUsed} — ${last.summary || "(no summary)"}`
237
+ );
238
+ }
239
+
240
+ console.log(`Recent changes: ${brain.state.recentChanges.length}`);
241
+ console.log(`Auth: ${isAuthEnabled(root) ? "enabled" : "disabled"}`);
242
+ console.log(`Encryption: ${isEncryptionEnabled() ? "enabled (AES-256-GCM)" : "disabled"}`);
243
+ const policyRules = listPolicyRules(root);
244
+ console.log(`Policy rules: ${policyRules.active}/${policyRules.total}`);
245
+ console.log(`Telemetry: ${isTelemetryEnabled() ? "enabled" : "disabled"}`);
246
+ console.log(`SSO: ${isSSOEnabled(root) ? "configured" : "not configured"}`);
247
+ console.log("");
248
+ }
249
+
250
+ // --- Main ---
251
+
252
+ async function main() {
253
+ const { cmd, args } = parseArgs(process.argv);
254
+ const root = rootDir();
255
+
256
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
257
+ printHelp();
258
+ process.exit(0);
259
+ }
260
+
261
+ // --- SETUP (new: one-shot full setup) ---
262
+ if (cmd === "setup") {
263
+ const flags = parseFlags(args);
264
+ const goalText = flags.goal || flags._.join(" ").trim();
265
+
266
+ // 1. Initialize
267
+ ensureInit(root);
268
+ console.log("Initialized .speclock/ directory.");
269
+
270
+ // 2. Set goal if provided
271
+ if (goalText) {
272
+ setGoal(root, goalText);
273
+ console.log(`Goal set: "${goalText}"`);
274
+ }
275
+
276
+ // 3. Create SPECLOCK.md in project root
277
+ const mdPath = createSpecLockMd(root);
278
+ console.log(`Created SPECLOCK.md (AI instructions file).`);
279
+
280
+ // 4. Inject marker into package.json (so AI tools auto-discover SpecLock)
281
+ const pkgResult = injectPackageJsonMarker(root);
282
+ if (pkgResult.success) {
283
+ console.log("Injected SpecLock marker into package.json.");
284
+ }
285
+
286
+ // 5. Apply template if specified
287
+ if (flags.template) {
288
+ const result = applyTemplate(root, flags.template);
289
+ if (result.applied) {
290
+ console.log(`Applied template "${result.displayName}": ${result.locksAdded} lock(s), ${result.decisionsAdded} decision(s).`);
291
+ } else {
292
+ console.error(`Template error: ${result.error}`);
293
+ }
294
+ }
295
+
296
+ // 6. Generate context
297
+ generateContext(root);
298
+ console.log("Generated .speclock/context/latest.md");
299
+
300
+ // 7. Print summary
301
+ console.log(`
302
+ SpecLock is ready!
303
+
304
+ Files created/updated:
305
+ .speclock/brain.json — Project memory
306
+ .speclock/context/latest.md — Context for AI (read this)
307
+ SPECLOCK.md — AI rules (read this)
308
+ package.json — Active locks embedded (AI auto-discovery)
309
+
310
+ Next steps:
311
+ To add constraints: npx speclock lock "Never touch auth files"
312
+ To check conflicts: npx speclock check "Modifying auth page"
313
+ To log changes: npx speclock log-change "Built landing page"
314
+ To see status: npx speclock status
315
+
316
+ Tip: When starting a new chat, tell the AI:
317
+ "Check speclock status and read the project constraints before doing anything"
318
+ `);
319
+ return;
320
+ }
321
+
322
+ // --- INIT ---
323
+ if (cmd === "init") {
324
+ ensureInit(root);
325
+ createSpecLockMd(root);
326
+ injectPackageJsonMarker(root);
327
+ generateContext(root);
328
+ console.log("SpecLock initialized. Created SPECLOCK.md, updated package.json, and generated context file.");
329
+ return;
330
+ }
331
+
332
+ // --- GOAL ---
333
+ if (cmd === "goal") {
334
+ const text = args.join(" ").trim();
335
+ if (!text) {
336
+ console.error("Error: Goal text is required.");
337
+ console.error("Usage: speclock goal <text>");
338
+ process.exit(1);
339
+ }
340
+ setGoal(root, text);
341
+ refreshContext(root);
342
+ console.log(`Goal set: "${text}"`);
343
+ return;
344
+ }
345
+
346
+ // --- LOCK ---
347
+ if (cmd === "lock") {
348
+ // Check for "lock remove <id>"
349
+ if (args[0] === "remove") {
350
+ const lockId = args[1];
351
+ if (!lockId) {
352
+ console.error("Error: Lock ID is required.");
353
+ console.error("Usage: speclock lock remove <lockId>");
354
+ process.exit(1);
355
+ }
356
+ const result = removeLock(root, lockId);
357
+ if (result.removed) {
358
+ // Sync updated locks to package.json
359
+ syncLocksToPackageJson(root);
360
+ refreshContext(root);
361
+ console.log(`Lock removed: "${result.lockText}"`);
362
+ } else {
363
+ console.error(result.error);
364
+ process.exit(1);
365
+ }
366
+ return;
367
+ }
368
+
369
+ const flags = parseFlags(args);
370
+ const text = flags._.join(" ").trim();
371
+ if (!text) {
372
+ console.error("Error: Lock text is required.");
373
+ console.error("Usage: speclock lock <text> [--tags a,b] [--source user]");
374
+ process.exit(1);
375
+ }
376
+ const { lockId, rewritten, rewriteReason } = addLock(root, text, parseTags(flags.tags), flags.source || "user");
377
+
378
+ // Auto-guard related files (Solution 1)
379
+ const guardResult = autoGuardRelatedFiles(root, text);
380
+ if (guardResult.guarded.length > 0) {
381
+ console.log(`Auto-guarded ${guardResult.guarded.length} related file(s):`);
382
+ for (const f of guardResult.guarded) {
383
+ console.log(` 🔒 ${f}`);
384
+ }
385
+ }
386
+
387
+ // Sync locks to package.json (Solution 2)
388
+ const syncResult = syncLocksToPackageJson(root);
389
+ if (syncResult.success) {
390
+ console.log(`Synced ${syncResult.lockCount} lock(s) to package.json.`);
391
+ }
392
+
393
+ refreshContext(root);
394
+ console.log(`Locked (${lockId}): "${text}"`);
395
+ if (rewritten) {
396
+ console.log(` Note: Engine optimized for detection. Your original text is preserved.`);
397
+ }
398
+ return;
399
+ }
400
+
401
+ // --- DECIDE ---
402
+ if (cmd === "decide") {
403
+ const flags = parseFlags(args);
404
+ const text = flags._.join(" ").trim();
405
+ if (!text) {
406
+ console.error("Error: Decision text is required.");
407
+ console.error("Usage: speclock decide <text> [--tags a,b]");
408
+ process.exit(1);
409
+ }
410
+ const { decId } = addDecision(root, text, parseTags(flags.tags), flags.source || "user");
411
+ refreshContext(root);
412
+ console.log(`Decision recorded (${decId}): "${text}"`);
413
+ return;
414
+ }
415
+
416
+ // --- NOTE ---
417
+ if (cmd === "note") {
418
+ const flags = parseFlags(args);
419
+ const text = flags._.join(" ").trim();
420
+ if (!text) {
421
+ console.error("Error: Note text is required.");
422
+ console.error("Usage: speclock note <text> [--pinned]");
423
+ process.exit(1);
424
+ }
425
+ const pinned = flags.pinned !== false;
426
+ const { noteId } = addNote(root, text, pinned);
427
+ refreshContext(root);
428
+ console.log(`Note added (${noteId}): "${text}"`);
429
+ return;
430
+ }
431
+
432
+ // --- LOG-CHANGE (new) ---
433
+ if (cmd === "log-change") {
434
+ const flags = parseFlags(args);
435
+ const text = flags._.join(" ").trim();
436
+ if (!text) {
437
+ console.error("Error: Change summary is required.");
438
+ console.error('Usage: speclock log-change "what changed" --files a.ts,b.ts');
439
+ process.exit(1);
440
+ }
441
+ const files = flags.files ? flags.files.split(",").map((f) => f.trim()).filter(Boolean) : [];
442
+ logChange(root, text, files);
443
+ refreshContext(root);
444
+ console.log(`Change logged: "${text}"`);
445
+ if (files.length > 0) {
446
+ console.log(`Files: ${files.join(", ")}`);
447
+ }
448
+ return;
449
+ }
450
+
451
+ // --- CHECK (new: conflict check) ---
452
+ if (cmd === "check") {
453
+ const text = args.join(" ").trim();
454
+ if (!text) {
455
+ console.error("Error: Action description is required.");
456
+ console.error('Usage: speclock check "what you plan to do"');
457
+ process.exit(1);
458
+ }
459
+ // Use async version for Gemini proxy coverage on grey-zone cases
460
+ const result = await enforceConflictCheckAsync(root, text);
461
+ if (result.hasConflict) {
462
+ console.log(`\n${result.blocked ? "BLOCKED" : "CONFLICT DETECTED"}`);
463
+ console.log("=".repeat(50));
464
+ console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
465
+ console.log("");
466
+ for (const lock of result.conflictingLocks) {
467
+ console.log(` [${lock.level}] "${lock.text}"`);
468
+ console.log(` Confidence: ${lock.confidence}%`);
469
+ if (lock.reasons && lock.reasons.length > 0) {
470
+ for (const reason of lock.reasons) {
471
+ console.log(` - ${reason}`);
472
+ }
473
+ }
474
+ console.log("");
475
+ }
476
+ console.log(result.analysis);
477
+ if (result.blocked) {
478
+ process.exit(1);
479
+ }
480
+ } else {
481
+ console.log(`No conflicts found. Safe to proceed with: "${text}"`);
482
+ }
483
+ return;
484
+ }
485
+
486
+ // --- GUARD (new: file-level lock) ---
487
+ if (cmd === "guard") {
488
+ const flags = parseFlags(args);
489
+ const filePath = flags._[0];
490
+ if (!filePath) {
491
+ console.error("Error: File path is required.");
492
+ console.error('Usage: speclock guard <file> --lock "constraint text"');
493
+ process.exit(1);
494
+ }
495
+ const lockText = flags.lock || "This file is locked by SpecLock. Do not modify.";
496
+ const result = guardFile(root, filePath, lockText);
497
+ if (result.success) {
498
+ console.log(`Guarded: ${filePath}`);
499
+ console.log(`Lock warning injected: "${lockText}"`);
500
+ } else {
501
+ console.error(result.error);
502
+ process.exit(1);
503
+ }
504
+ return;
505
+ }
506
+
507
+ // --- UNGUARD ---
508
+ if (cmd === "unguard") {
509
+ const filePath = args[0];
510
+ if (!filePath) {
511
+ console.error("Error: File path is required.");
512
+ console.error("Usage: speclock unguard <file>");
513
+ process.exit(1);
514
+ }
515
+ const result = unguardFile(root, filePath);
516
+ if (result.success) {
517
+ console.log(`Unguarded: ${filePath}`);
518
+ } else {
519
+ console.error(result.error);
520
+ process.exit(1);
521
+ }
522
+ return;
523
+ }
524
+
525
+ // --- FACTS ---
526
+ if (cmd === "facts") {
527
+ const sub = args.shift();
528
+ if (sub !== "deploy") {
529
+ console.error("Error: Only 'facts deploy' is supported.");
530
+ console.error(
531
+ "Usage: speclock facts deploy --provider X --branch Y"
532
+ );
533
+ process.exit(1);
534
+ }
535
+ const flags = parseFlags(args);
536
+ const payload = {
537
+ provider: flags.provider,
538
+ branch: flags.branch,
539
+ notes: flags.notes,
540
+ url: flags.url,
541
+ };
542
+ if (flags.autoDeploy !== undefined) {
543
+ payload.autoDeploy =
544
+ String(flags.autoDeploy).toLowerCase() === "true";
545
+ }
546
+ updateDeployFacts(root, payload);
547
+ refreshContext(root);
548
+ console.log("Deploy facts updated.");
549
+ return;
550
+ }
551
+
552
+ // --- CONTEXT ---
553
+ if (cmd === "context") {
554
+ const md = generateContext(root);
555
+ console.log(md);
556
+ return;
557
+ }
558
+
559
+ // --- WATCH ---
560
+ if (cmd === "watch") {
561
+ await watchRepo(root);
562
+ return;
563
+ }
564
+
565
+ // --- SERVE ---
566
+ if (cmd === "serve") {
567
+ // Start MCP server — pass through --project if provided
568
+ const flags = parseFlags(args);
569
+ const projectArg = flags.project || root;
570
+ process.env.SPECLOCK_PROJECT_ROOT = projectArg;
571
+ await import("../mcp/server.js");
572
+ return;
573
+ }
574
+
575
+ // --- TEMPLATE ---
576
+ if (cmd === "template") {
577
+ const sub = args[0];
578
+ if (sub === "list" || !sub) {
579
+ const templates = listTemplates();
580
+ console.log("\nAvailable Templates:");
581
+ console.log("=".repeat(50));
582
+ for (const t of templates) {
583
+ console.log(` ${t.name.padEnd(20)} ${t.displayName} — ${t.description}`);
584
+ console.log(` ${"".padEnd(20)} ${t.lockCount} lock(s), ${t.decisionCount} decision(s)`);
585
+ console.log("");
586
+ }
587
+ console.log("Apply: npx speclock template apply <name>");
588
+ return;
589
+ }
590
+ if (sub === "apply") {
591
+ const name = args[1];
592
+ if (!name) {
593
+ console.error("Error: Template name is required.");
594
+ console.error("Usage: speclock template apply <name>");
595
+ console.error("Run 'speclock template list' to see available templates.");
596
+ process.exit(1);
597
+ }
598
+ const result = applyTemplate(root, name);
599
+ if (result.applied) {
600
+ refreshContext(root);
601
+ console.log(`Template "${result.displayName}" applied successfully!`);
602
+ console.log(` Locks added: ${result.locksAdded}`);
603
+ console.log(` Decisions added: ${result.decisionsAdded}`);
604
+ } else {
605
+ console.error(result.error);
606
+ process.exit(1);
607
+ }
608
+ return;
609
+ }
610
+ console.error(`Unknown template command: ${sub}`);
611
+ console.error("Usage: speclock template list | speclock template apply <name>");
612
+ process.exit(1);
613
+ }
614
+
615
+ // --- REPORT ---
616
+ if (cmd === "report") {
617
+ const report = generateReport(root);
618
+ console.log("\nSpecLock Violation Report");
619
+ console.log("=".repeat(50));
620
+ console.log(`Total violations blocked: ${report.totalViolations}`);
621
+ if (report.timeRange) {
622
+ console.log(`Period: ${report.timeRange.from.substring(0, 10)} to ${report.timeRange.to.substring(0, 10)}`);
623
+ }
624
+ if (report.mostTestedLocks.length > 0) {
625
+ console.log("\nMost tested locks:");
626
+ for (const lock of report.mostTestedLocks) {
627
+ console.log(` ${lock.count}x — "${lock.text}"`);
628
+ }
629
+ }
630
+ if (report.recentViolations.length > 0) {
631
+ console.log("\nRecent violations:");
632
+ for (const v of report.recentViolations) {
633
+ console.log(` [${v.at.substring(0, 19)}] ${v.topLevel} (${v.topConfidence}%) — "${v.action.substring(0, 60)}"`);
634
+ }
635
+ }
636
+ console.log(`\n${report.summary}`);
637
+ return;
638
+ }
639
+
640
+ // --- HOOK ---
641
+ if (cmd === "hook") {
642
+ const sub = args[0];
643
+ if (sub === "install") {
644
+ const result = installHook(root);
645
+ if (result.success) {
646
+ console.log(result.message);
647
+ } else {
648
+ console.error(result.error);
649
+ process.exit(1);
650
+ }
651
+ return;
652
+ }
653
+ if (sub === "remove") {
654
+ const result = removeHook(root);
655
+ if (result.success) {
656
+ console.log(result.message);
657
+ } else {
658
+ console.error(result.error);
659
+ process.exit(1);
660
+ }
661
+ return;
662
+ }
663
+ console.error("Usage: speclock hook install | speclock hook remove");
664
+ process.exit(1);
665
+ }
666
+
667
+ // --- AUDIT ---
668
+ if (cmd === "audit") {
669
+ const result = auditStagedFiles(root);
670
+ if (result.passed) {
671
+ console.log(result.message);
672
+ process.exit(0);
673
+ } else {
674
+ console.log("\nSPECLOCK AUDIT FAILED");
675
+ console.log("=".repeat(50));
676
+ for (const v of result.violations) {
677
+ console.log(` [${v.severity}] ${v.file}`);
678
+ console.log(` Lock: ${v.lockText}`);
679
+ console.log(` Reason: ${v.reason}`);
680
+ console.log("");
681
+ }
682
+ console.log(result.message);
683
+ console.log("Commit blocked. Unlock files or unstage them to proceed.");
684
+ process.exit(1);
685
+ }
686
+ }
687
+
688
+ // --- AUDIT-VERIFY (v2.1 enterprise) ---
689
+ if (cmd === "audit-verify") {
690
+ ensureInit(root);
691
+ const result = verifyAuditChain(root);
692
+ console.log(`\nAudit Chain Verification`);
693
+ console.log("=".repeat(50));
694
+ console.log(`Status: ${result.valid ? "VALID" : "BROKEN"}`);
695
+ console.log(`Total events: ${result.totalEvents}`);
696
+ console.log(`Hashed events: ${result.hashedEvents}`);
697
+ console.log(`Legacy events (pre-v2.1): ${result.unhashedEvents}`);
698
+ if (!result.valid && result.errors) {
699
+ console.log(`\nErrors:`);
700
+ for (const err of result.errors) {
701
+ console.log(` Line ${err.line}: ${err.error}`);
702
+ }
703
+ }
704
+ console.log(`\n${result.message}`);
705
+ process.exit(result.valid ? 0 : 1);
706
+ }
707
+
708
+ // --- EXPORT (v2.1 enterprise) ---
709
+ if (cmd === "export") {
710
+ const flags = parseFlags(args);
711
+ const format = flags.format;
712
+ if (!format || !["soc2", "hipaa", "csv"].includes(format)) {
713
+ console.error("Error: Valid format is required.");
714
+ console.error("Usage: speclock export --format <soc2|hipaa|csv>");
715
+ process.exit(1);
716
+ }
717
+ ensureInit(root);
718
+ const result = exportCompliance(root, format);
719
+ if (result.error) {
720
+ console.error(result.error);
721
+ process.exit(1);
722
+ }
723
+ if (format === "csv") {
724
+ console.log(result.data);
725
+ } else {
726
+ console.log(JSON.stringify(result.data, null, 2));
727
+ }
728
+ return;
729
+ }
730
+
731
+ // --- LICENSE (v2.1 enterprise) ---
732
+ if (cmd === "license") {
733
+ const info = getLicenseInfo(root);
734
+ console.log(`\nSpecLock License Info`);
735
+ console.log("=".repeat(50));
736
+ console.log(`Tier: ${info.tier} (${info.tierKey})`);
737
+ if (info.expiresAt) console.log(`Expires: ${info.expiresAt}`);
738
+ if (info.expired) console.log(`STATUS: EXPIRED — reverted to Free tier`);
739
+ console.log(`\nUsage:`);
740
+ if (info.usage) {
741
+ const { locks, decisions, events } = info.usage;
742
+ console.log(` Locks: ${locks.current}/${locks.max === Infinity ? "unlimited" : locks.max}`);
743
+ console.log(` Decisions: ${decisions.current}/${decisions.max === Infinity ? "unlimited" : decisions.max}`);
744
+ console.log(` Events: ${events.current}/${events.max === Infinity ? "unlimited" : events.max}`);
745
+ }
746
+ if (info.warnings && info.warnings.length > 0) {
747
+ console.log(`\nWarnings:`);
748
+ for (const w of info.warnings) console.log(` - ${w}`);
749
+ }
750
+ console.log(`\nFeatures: ${info.features.join(", ")}`);
751
+ return;
752
+ }
753
+
754
+ // --- ENFORCE (v2.5) ---
755
+ if (cmd === "enforce") {
756
+ const mode = args[0];
757
+ if (!mode || (mode !== "advisory" && mode !== "hard")) {
758
+ console.error("Usage: speclock enforce <advisory|hard> [--threshold 70]");
759
+ process.exit(1);
760
+ }
761
+ const flags = parseFlags(args.slice(1));
762
+ const options = {};
763
+ if (flags.threshold) options.blockThreshold = parseInt(flags.threshold, 10);
764
+ if (flags.override !== undefined) options.allowOverride = flags.override !== "false";
765
+ const result = setEnforcementMode(root, mode, options);
766
+ if (!result.success) {
767
+ console.error(result.error);
768
+ process.exit(1);
769
+ }
770
+ console.log(`\nEnforcement mode: ${result.mode.toUpperCase()}`);
771
+ console.log(`Block threshold: ${result.config.blockThreshold}%`);
772
+ console.log(`Overrides: ${result.config.allowOverride ? "allowed" : "disabled"}`);
773
+ if (result.mode === "hard") {
774
+ console.log(`\nHard mode active — conflicts above ${result.config.blockThreshold}% confidence will BLOCK actions.`);
775
+ }
776
+ return;
777
+ }
778
+
779
+ // --- OVERRIDE (v2.5) ---
780
+ if (cmd === "override") {
781
+ const lockId = args[0];
782
+ const reason = args.slice(1).join(" ");
783
+ if (!lockId || !reason) {
784
+ console.error("Usage: speclock override <lockId> <reason>");
785
+ process.exit(1);
786
+ }
787
+ const result = overrideLock(root, lockId, "(CLI override)", reason);
788
+ if (!result.success) {
789
+ console.error(result.error);
790
+ process.exit(1);
791
+ }
792
+ console.log(`Lock overridden: "${result.lockText}"`);
793
+ console.log(`Override count: ${result.overrideCount}`);
794
+ if (result.escalated) {
795
+ console.log(`\n${result.escalationMessage}`);
796
+ }
797
+ return;
798
+ }
799
+
800
+ // --- OVERRIDES (v2.5) ---
801
+ if (cmd === "overrides") {
802
+ const flags = parseFlags(args);
803
+ const result = getOverrideHistory(root, flags.lock || null);
804
+ if (result.total === 0) {
805
+ console.log("No overrides recorded.");
806
+ return;
807
+ }
808
+ console.log(`\nOverride History (${result.total})`);
809
+ console.log("=".repeat(50));
810
+ for (const o of result.overrides) {
811
+ console.log(`[${o.at.substring(0, 19)}] Lock: "${o.lockText}"`);
812
+ console.log(` Action: ${o.action}`);
813
+ console.log(` Reason: ${o.reason}`);
814
+ console.log("");
815
+ }
816
+ return;
817
+ }
818
+
819
+ // --- AUDIT-SEMANTIC (v2.5) ---
820
+ if (cmd === "audit-semantic") {
821
+ const result = semanticAudit(root);
822
+ console.log(`\nSemantic Pre-Commit Audit`);
823
+ console.log("=".repeat(50));
824
+ console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
825
+ console.log(`Files analyzed: ${result.filesChecked}`);
826
+ console.log(`Active locks: ${result.activeLocks}`);
827
+ console.log(`Violations: ${result.violations.length}`);
828
+ if (result.violations.length > 0) {
829
+ console.log("");
830
+ for (const v of result.violations) {
831
+ console.log(` [${v.level}] ${v.file} (confidence: ${v.confidence}%)`);
832
+ console.log(` Lock: "${v.lockText}"`);
833
+ console.log(` Reason: ${v.reason}`);
834
+ if (v.addedLines !== undefined) {
835
+ console.log(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
836
+ }
837
+ }
838
+ }
839
+ console.log(`\n${result.message}`);
840
+ process.exit(result.blocked ? 1 : 0);
841
+ }
842
+
843
+ // --- AUTH (v3.0) ---
844
+ if (cmd === "auth") {
845
+ const sub = args[0];
846
+ if (!sub || sub === "status") {
847
+ const enabled = isAuthEnabled(root);
848
+ console.log(`\nAuth Status: ${enabled ? "ENABLED" : "DISABLED"}`);
849
+ if (enabled) {
850
+ const keys = listApiKeys(root);
851
+ const active = keys.keys.filter(k => k.active);
852
+ console.log(`Active keys: ${active.length}`);
853
+ for (const k of active) {
854
+ console.log(` ${k.id} — ${k.name} (${k.role}) — last used: ${k.lastUsed || "never"}`);
855
+ }
856
+ } else {
857
+ console.log("Run 'speclock auth create-key --role admin' to enable auth.");
858
+ }
859
+ return;
860
+ }
861
+ if (sub === "create-key") {
862
+ const flags = parseFlags(args.slice(1));
863
+ const role = flags.role || "developer";
864
+ const name = flags.name || flags._.join(" ") || "";
865
+ const result = createApiKey(root, role, name);
866
+ if (!result.success) {
867
+ console.error(result.error);
868
+ process.exit(1);
869
+ }
870
+ console.log(`\nAPI Key Created`);
871
+ console.log("=".repeat(50));
872
+ console.log(`Key ID: ${result.keyId}`);
873
+ console.log(`Role: ${result.role}`);
874
+ console.log(`Name: ${result.name}`);
875
+ console.log(`\nRaw Key: ${result.rawKey}`);
876
+ console.log(`\nSave this key — it CANNOT be retrieved later.`);
877
+ console.log(`\nUsage:`);
878
+ console.log(` HTTP: Authorization: Bearer ${result.rawKey}`);
879
+ console.log(` MCP: Set SPECLOCK_API_KEY=${result.rawKey} in MCP config`);
880
+ return;
881
+ }
882
+ if (sub === "rotate-key") {
883
+ const keyId = args[1];
884
+ if (!keyId) {
885
+ console.error("Usage: speclock auth rotate-key <keyId>");
886
+ process.exit(1);
887
+ }
888
+ const result = rotateApiKey(root, keyId);
889
+ if (!result.success) {
890
+ console.error(result.error);
891
+ process.exit(1);
892
+ }
893
+ console.log(`\nKey Rotated`);
894
+ console.log(`Old key: ${result.oldKeyId} (revoked)`);
895
+ console.log(`New key: ${result.newKeyId}`);
896
+ console.log(`Raw Key: ${result.rawKey}`);
897
+ console.log(`\nSave this key — it CANNOT be retrieved later.`);
898
+ return;
899
+ }
900
+ if (sub === "revoke-key") {
901
+ const keyId = args[1];
902
+ if (!keyId) {
903
+ console.error("Usage: speclock auth revoke-key <keyId>");
904
+ process.exit(1);
905
+ }
906
+ const reason = args.slice(2).join(" ") || "manual";
907
+ const result = revokeApiKey(root, keyId, reason);
908
+ if (!result.success) {
909
+ console.error(result.error);
910
+ process.exit(1);
911
+ }
912
+ console.log(`Key revoked: ${result.keyId} (${result.name}, ${result.role})`);
913
+ return;
914
+ }
915
+ if (sub === "list-keys") {
916
+ const result = listApiKeys(root);
917
+ console.log(`\nAPI Keys (auth ${result.enabled ? "enabled" : "disabled"}):`);
918
+ console.log("=".repeat(50));
919
+ if (result.keys.length === 0) {
920
+ console.log(" No keys configured.");
921
+ } else {
922
+ for (const k of result.keys) {
923
+ const status = k.active ? "active" : `revoked (${k.revokedAt?.substring(0, 10) || "unknown"})`;
924
+ console.log(` ${k.id} — ${k.name} (${k.role}) [${status}]`);
925
+ }
926
+ }
927
+ return;
928
+ }
929
+ if (sub === "enable") {
930
+ enableAuth(root);
931
+ console.log("Auth enabled. API keys are now required for HTTP access.");
932
+ return;
933
+ }
934
+ if (sub === "disable") {
935
+ disableAuth(root);
936
+ console.log("Auth disabled. All operations allowed without keys.");
937
+ return;
938
+ }
939
+ console.error("Usage: speclock auth <create-key|rotate-key|revoke-key|list-keys|enable|disable|status>");
940
+ process.exit(1);
941
+ }
942
+
943
+ // --- POLICY (v3.5) ---
944
+ if (cmd === "policy") {
945
+ const sub = args[0];
946
+ if (!sub || sub === "list") {
947
+ const result = listPolicyRules(root);
948
+ console.log(`\nPolicy Rules (${result.active}/${result.total} active):`);
949
+ console.log("=".repeat(50));
950
+ if (result.rules.length === 0) {
951
+ console.log(" No rules. Run 'speclock policy init' to create a policy.");
952
+ } else {
953
+ for (const r of result.rules) {
954
+ const status = r.active !== false ? "active" : "inactive";
955
+ console.log(` ${r.id} — ${r.name} [${r.enforce}/${r.severity}] (${status})`);
956
+ console.log(` Files: ${(r.match?.files || []).join(", ")}`);
957
+ console.log(` Actions: ${(r.match?.actions || []).join(", ")}`);
958
+ console.log("");
959
+ }
960
+ }
961
+ return;
962
+ }
963
+ if (sub === "init") {
964
+ const result = initPolicy(root);
965
+ if (!result.success) { console.error(result.error); process.exit(1); }
966
+ console.log("Policy-as-code initialized. Edit .speclock/policy.yml to add rules.");
967
+ return;
968
+ }
969
+ if (sub === "add") {
970
+ const flags = parseFlags(args.slice(1));
971
+ const name = flags.name || flags._.join(" ");
972
+ if (!name) { console.error("Usage: speclock policy add --name <name> --files '**/*.js' --enforce block"); process.exit(1); }
973
+ const rule = {
974
+ name,
975
+ description: flags.description || "",
976
+ match: {
977
+ files: flags.files ? flags.files.split(",").map(s => s.trim()) : ["**/*"],
978
+ actions: flags.actions ? flags.actions.split(",").map(s => s.trim()) : ["modify", "delete"],
979
+ },
980
+ enforce: flags.enforce || "warn",
981
+ severity: flags.severity || "medium",
982
+ notify: flags.notify ? flags.notify.split(",").map(s => s.trim()) : [],
983
+ };
984
+ const result = addPolicyRule(root, rule);
985
+ if (!result.success) { console.error(result.error); process.exit(1); }
986
+ console.log(`Policy rule added: "${result.rule.name}" (${result.ruleId}) [${result.rule.enforce}]`);
987
+ return;
988
+ }
989
+ if (sub === "remove") {
990
+ const ruleId = args[1];
991
+ if (!ruleId) { console.error("Usage: speclock policy remove <ruleId>"); process.exit(1); }
992
+ const result = removePolicyRule(root, ruleId);
993
+ if (!result.success) { console.error(result.error); process.exit(1); }
994
+ console.log(`Policy rule removed: "${result.removed.name}"`);
995
+ return;
996
+ }
997
+ if (sub === "evaluate") {
998
+ const text = args.slice(1).join(" ");
999
+ if (!text) { console.error("Usage: speclock policy evaluate 'what you plan to do'"); process.exit(1); }
1000
+ const result = evaluatePolicy(root, { description: text, text, type: "modify" });
1001
+ if (result.passed) {
1002
+ console.log(`Policy check passed. ${result.rulesChecked} rules evaluated.`);
1003
+ } else {
1004
+ console.log(`\nPolicy Violations (${result.violations.length}):`);
1005
+ for (const v of result.violations) {
1006
+ console.log(` [${v.severity.toUpperCase()}] ${v.ruleName} (${v.enforce})`);
1007
+ if (v.matchedFiles.length) console.log(` Files: ${v.matchedFiles.join(", ")}`);
1008
+ }
1009
+ if (result.blocked) process.exit(1);
1010
+ }
1011
+ return;
1012
+ }
1013
+ if (sub === "export") {
1014
+ const result = exportPolicy(root);
1015
+ if (!result.success) { console.error(result.error); process.exit(1); }
1016
+ console.log(result.yaml);
1017
+ return;
1018
+ }
1019
+ console.error("Usage: speclock policy <list|init|add|remove|evaluate|export>");
1020
+ process.exit(1);
1021
+ }
1022
+
1023
+ // --- TELEMETRY (v3.5) ---
1024
+ if (cmd === "telemetry") {
1025
+ const sub = args[0];
1026
+ if (sub === "status" || !sub) {
1027
+ const enabled = isTelemetryEnabled();
1028
+ console.log(`\nTelemetry: ${enabled ? "ENABLED" : "DISABLED"}`);
1029
+ if (!enabled) {
1030
+ console.log("Set SPECLOCK_TELEMETRY=true to enable anonymous usage analytics.");
1031
+ return;
1032
+ }
1033
+ const summary = getTelemetrySummary(root);
1034
+ console.log(`Total calls: ${summary.totalCalls}`);
1035
+ console.log(`Avg response: ${summary.avgResponseMs}ms`);
1036
+ console.log(`Sessions: ${summary.sessions.total}`);
1037
+ console.log(`Conflicts: ${summary.conflicts.total} (blocked: ${summary.conflicts.blocked})`);
1038
+ if (summary.topTools.length > 0) {
1039
+ console.log(`\nTop tools:`);
1040
+ for (const t of summary.topTools.slice(0, 5)) {
1041
+ console.log(` ${t.name}: ${t.count} calls`);
1042
+ }
1043
+ }
1044
+ return;
1045
+ }
1046
+ console.error("Usage: speclock telemetry [status]");
1047
+ process.exit(1);
1048
+ }
1049
+
1050
+ // --- SSO (v3.5) ---
1051
+ if (cmd === "sso") {
1052
+ const sub = args[0];
1053
+ if (sub === "status" || !sub) {
1054
+ const enabled = isSSOEnabled(root);
1055
+ const config = getSSOConfig(root);
1056
+ console.log(`\nSSO: ${enabled ? "CONFIGURED" : "NOT CONFIGURED"}`);
1057
+ if (enabled) {
1058
+ console.log(`Issuer: ${config.issuer}`);
1059
+ console.log(`Client ID: ${config.clientId}`);
1060
+ console.log(`Redirect: ${config.redirectUri}`);
1061
+ console.log(`Default role: ${config.defaultRole}`);
1062
+ } else {
1063
+ console.log("Set SPECLOCK_SSO_ISSUER and SPECLOCK_SSO_CLIENT_ID to enable SSO.");
1064
+ }
1065
+ return;
1066
+ }
1067
+ if (sub === "configure") {
1068
+ const flags = parseFlags(args.slice(1));
1069
+ const config = getSSOConfig(root);
1070
+ if (flags.issuer) config.issuer = flags.issuer;
1071
+ if (flags["client-id"]) config.clientId = flags["client-id"];
1072
+ if (flags["client-secret"]) config.clientSecret = flags["client-secret"];
1073
+ if (flags["redirect-uri"]) config.redirectUri = flags["redirect-uri"];
1074
+ if (flags["default-role"]) config.defaultRole = flags["default-role"];
1075
+ saveSSOConfig(root, config);
1076
+ console.log("SSO configuration saved.");
1077
+ return;
1078
+ }
1079
+ console.error("Usage: speclock sso <status|configure> [--issuer URL --client-id ID]");
1080
+ process.exit(1);
1081
+ }
1082
+
1083
+ // --- ENCRYPT STATUS (v3.0) ---
1084
+ if (cmd === "encrypt") {
1085
+ const sub = args[0];
1086
+ if (sub === "status" || !sub) {
1087
+ const enabled = isEncryptionEnabled();
1088
+ console.log(`\nEncryption: ${enabled ? "ENABLED (AES-256-GCM)" : "DISABLED"}`);
1089
+ if (!enabled) {
1090
+ console.log("Set SPECLOCK_ENCRYPTION_KEY env var to enable encryption.");
1091
+ console.log("All data will be encrypted at rest (brain.json + events.log).");
1092
+ }
1093
+ return;
1094
+ }
1095
+ console.error("Usage: speclock encrypt [status]");
1096
+ process.exit(1);
1097
+ }
1098
+
1099
+ // --- STATUS ---
1100
+ if (cmd === "status") {
1101
+ showStatus(root);
1102
+ return;
1103
+ }
1104
+
1105
+ console.error(`Unknown command: ${cmd}`);
1106
+ console.error("Run 'speclock --help' for usage.");
1107
+ process.exit(1);
1108
+ }
1109
+
1110
+ main().catch((err) => {
1111
+ console.error("SpecLock error:", err.message);
1112
+ process.exit(1);
1113
+ });