speclock 4.5.3 → 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/README.md +460 -460
- package/package.json +1 -1
- package/src/cli/index.js +1113 -1113
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +9 -8
- package/src/core/telemetry.js +1 -1
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +1 -1
- package/src/mcp/server.js +1355 -1355
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.
|
|
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
|
+
});
|