@veraxhq/verax 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +296 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -0,0 +1,109 @@
1
+ import { resolve } from 'path';
2
+ import { existsSync, readFileSync, readdirSync } from 'fs';
3
+ import { DataError } from '../util/errors.js';
4
+
5
+ /**
6
+ * `verax inspect` command
7
+ * Read an existing run folder and display summary
8
+ */
9
+ export async function inspectCommand(runPath, options = {}) {
10
+ const { json = false } = options;
11
+
12
+ const fullPath = resolve(runPath);
13
+
14
+ // Validate run directory exists
15
+ if (!existsSync(fullPath)) {
16
+ throw new DataError(`Run directory not found: ${fullPath}`);
17
+ }
18
+
19
+ // Check for required files
20
+ const requiredFiles = ['summary.json', 'findings.json'];
21
+ const missingFiles = [];
22
+
23
+ for (const file of requiredFiles) {
24
+ const filePath = `${fullPath}/${file}`;
25
+ if (!existsSync(filePath)) {
26
+ missingFiles.push(file);
27
+ }
28
+ }
29
+
30
+ if (missingFiles.length > 0) {
31
+ throw new DataError(
32
+ `Invalid run directory. Missing files: ${missingFiles.join(', ')}`
33
+ );
34
+ }
35
+
36
+ // Read summary and findings
37
+ let summary, findings;
38
+
39
+ try {
40
+ summary = JSON.parse(readFileSync(`${fullPath}/summary.json`, 'utf8'));
41
+ } catch (error) {
42
+ throw new DataError(`Failed to parse summary.json: ${error.message}`);
43
+ }
44
+
45
+ try {
46
+ findings = JSON.parse(readFileSync(`${fullPath}/findings.json`, 'utf8'));
47
+ } catch (error) {
48
+ throw new DataError(`Failed to parse findings.json: ${error.message}`);
49
+ }
50
+
51
+ // Check for evidence directory
52
+ const evidenceDir = `${fullPath}/evidence`;
53
+ const hasEvidence = existsSync(evidenceDir);
54
+ let evidenceCount = 0;
55
+
56
+ if (hasEvidence) {
57
+ try {
58
+ evidenceCount = readdirSync(evidenceDir).length;
59
+ } catch (error) {
60
+ evidenceCount = 0;
61
+ }
62
+ }
63
+
64
+ // Build output
65
+ const output = {
66
+ runId: summary.runId || 'unknown',
67
+ status: summary.status || 'unknown',
68
+ startedAt: summary.startedAt || null,
69
+ completedAt: summary.completedAt || null,
70
+ url: summary.url || null,
71
+ findingsCount: Array.isArray(findings) ? findings.length : 0,
72
+ evidenceDir: hasEvidence ? evidenceDir : null,
73
+ evidenceFileCount: evidenceCount,
74
+ };
75
+
76
+ if (json) {
77
+ // Output as single JSON object
78
+ console.log(JSON.stringify(output, null, 2));
79
+ } else {
80
+ // Output as human-readable summary
81
+ console.log('\n=== Run Summary ===\n');
82
+ console.log(`Run ID: ${output.runId}`);
83
+ console.log(`Status: ${output.status}`);
84
+
85
+ if (output.startedAt) {
86
+ console.log(`Started: ${output.startedAt}`);
87
+ }
88
+
89
+ if (output.completedAt) {
90
+ console.log(`Completed: ${output.completedAt}`);
91
+ }
92
+
93
+ if (output.url) {
94
+ console.log(`URL: ${output.url}`);
95
+ }
96
+
97
+ console.log(`\nFindings: ${output.findingsCount}`);
98
+
99
+ if (output.evidenceDir) {
100
+ console.log(`Evidence: ${output.evidenceDir} (${output.evidenceFileCount} files)`);
101
+ } else {
102
+ console.log(`Evidence: not found`);
103
+ }
104
+
105
+ console.log('');
106
+ }
107
+
108
+ return output;
109
+ }
@@ -0,0 +1,402 @@
1
+ import { resolve, join } from 'path';
2
+ import { existsSync, readFileSync } from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname } from 'path';
5
+ import { UsageError, DataError, CrashError } from '../util/errors.js';
6
+ import { generateRunId } from '../util/run-id.js';
7
+ import { getRunPaths, ensureRunDirectories } from '../util/paths.js';
8
+ import { atomicWriteJson, atomicWriteText } from '../util/atomic-write.js';
9
+ import { RunEventEmitter } from '../util/events.js';
10
+ import { discoverProject } from '../util/project-discovery.js';
11
+ import { writeProjectJson } from '../util/project-writer.js';
12
+ import { extractExpectations } from '../util/expectation-extractor.js';
13
+ import { writeLearnJson } from '../util/learn-writer.js';
14
+ import { observeExpectations } from '../util/observation-engine.js';
15
+ import { writeObserveJson } from '../util/observe-writer.js';
16
+ import { detectFindings } from '../util/detection-engine.js';
17
+ import { writeFindingsJson } from '../util/findings-writer.js';
18
+ import { writeSummaryJson } from '../util/summary-writer.js';
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ function getVersion() {
24
+ try {
25
+ const pkgPath = resolve(__dirname, '../../../package.json');
26
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
27
+ return pkg.version;
28
+ } catch {
29
+ return '0.2.0';
30
+ }
31
+ }
32
+
33
+ /**
34
+ * `verax run` command
35
+ * Strict, non-interactive CLI mode with explicit flags
36
+ */
37
+ export async function runCommand(options) {
38
+ const {
39
+ url,
40
+ src = '.',
41
+ out = '.verax',
42
+ json = false,
43
+ verbose = false,
44
+ } = options;
45
+
46
+ // Validate required arguments
47
+ if (!url) {
48
+ throw new UsageError('Missing required argument: --url <url>');
49
+ }
50
+
51
+ const projectRoot = resolve(process.cwd());
52
+ const srcPath = resolve(projectRoot, src);
53
+
54
+ // Validate src directory exists
55
+ if (!existsSync(srcPath)) {
56
+ throw new DataError(`Source directory not found: ${srcPath}`);
57
+ }
58
+
59
+ // Create event emitter
60
+ const events = new RunEventEmitter();
61
+
62
+ // Setup event handlers
63
+ if (json) {
64
+ events.on('*', (event) => {
65
+ console.log(JSON.stringify(event));
66
+ });
67
+ } else {
68
+ events.on('*', (event) => {
69
+ if (verbose) {
70
+ console.log(`[${event.type}] ${event.message || ''}`);
71
+ }
72
+ });
73
+ }
74
+
75
+ let runId = null;
76
+ let paths = null;
77
+ let startedAt = null;
78
+
79
+ try {
80
+ // Generate run ID
81
+ runId = generateRunId();
82
+ if (verbose && !json) console.log(`Run ID: ${runId}`);
83
+
84
+ paths = getRunPaths(projectRoot, out, runId);
85
+ ensureRunDirectories(paths);
86
+
87
+ // Discover project configuration
88
+ let projectProfile;
89
+ try {
90
+ projectProfile = await discoverProject(srcPath);
91
+ } catch (error) {
92
+ projectProfile = {
93
+ framework: 'unknown',
94
+ router: null,
95
+ sourceRoot: srcPath,
96
+ packageManager: 'unknown',
97
+ scripts: { dev: null, build: null, start: null },
98
+ detectedAt: new Date().toISOString(),
99
+ };
100
+ }
101
+
102
+ // Emit project detection events
103
+ events.emit('project:detected', {
104
+ framework: projectProfile.framework,
105
+ router: projectProfile.router,
106
+ sourceRoot: projectProfile.sourceRoot,
107
+ packageManager: projectProfile.packageManager,
108
+ });
109
+
110
+ // Emit phase events
111
+ events.emit('phase:started', {
112
+ phase: 'Detect Project',
113
+ message: 'Detecting project structure...',
114
+ });
115
+
116
+ events.emit('phase:started', {
117
+ phase: 'Resolve URL',
118
+ message: `Using URL: ${url}`,
119
+ });
120
+
121
+ events.emit('phase:started', {
122
+ phase: 'Initialize Run',
123
+ message: 'Initializing run artifacts...',
124
+ });
125
+
126
+ // Write initial status
127
+ const now = new Date();
128
+ startedAt = now.toISOString();
129
+
130
+ atomicWriteJson(paths.runStatusJson, {
131
+ status: 'RUNNING',
132
+ runId,
133
+ startedAt,
134
+ });
135
+
136
+ // Write metadata
137
+ atomicWriteJson(paths.runMetaJson, {
138
+ veraxVersion: getVersion(),
139
+ nodeVersion: process.version,
140
+ platform: process.platform,
141
+ cwd: projectRoot,
142
+ command: 'run',
143
+ args: { url, src, out },
144
+ url,
145
+ src: srcPath,
146
+ startedAt,
147
+ completedAt: null,
148
+ error: null,
149
+ });
150
+
151
+ // Simulate learning phase (placeholder)
152
+ events.emit('phase:started', {
153
+ phase: 'Learn',
154
+ message: 'Analyzing project structure...',
155
+ });
156
+
157
+ // Extract expectations
158
+ const { expectations, skipped } = await extractExpectations(projectProfile, projectProfile.sourceRoot);
159
+
160
+ // For now, emit a placeholder trace event
161
+ events.emit('phase:completed', {
162
+ phase: 'Learn',
163
+ message: 'Project analysis complete',
164
+ });
165
+
166
+ // Observe phase
167
+ events.emit('phase:started', {
168
+ phase: 'Observe',
169
+ message: 'Launching browser and observing expectations...',
170
+ });
171
+
172
+ let observeData = null;
173
+ if (expectations.length > 0) {
174
+ try {
175
+ observeData = await observeExpectations(
176
+ expectations,
177
+ url,
178
+ paths.evidenceDir,
179
+ (progress) => {
180
+ events.emit(progress.event, progress);
181
+ }
182
+ );
183
+ } catch (error) {
184
+ events.emit('observe:error', {
185
+ message: error.message,
186
+ });
187
+ observeData = {
188
+ observations: [],
189
+ stats: { attempted: 0, observed: 0, notObserved: 0 },
190
+ observedAt: new Date().toISOString(),
191
+ };
192
+ }
193
+ } else {
194
+ observeData = {
195
+ observations: [],
196
+ stats: { attempted: 0, observed: 0, notObserved: 0 },
197
+ observedAt: new Date().toISOString(),
198
+ };
199
+ }
200
+
201
+ events.emit('phase:completed', {
202
+ phase: 'Observe',
203
+ message: 'Browser observation complete',
204
+ });
205
+
206
+ // Detect phase
207
+ events.emit('phase:started', {
208
+ phase: 'Detect',
209
+ message: 'Analyzing findings and detecting silent failures...',
210
+ });
211
+
212
+ let detectData = null;
213
+ try {
214
+ // Use already-extracted expectations
215
+ const learnData = {
216
+ expectations,
217
+ skipped,
218
+ };
219
+
220
+ detectData = await detectFindings(learnData, observeData, projectRoot, (progress) => {
221
+ events.emit(progress.event, progress);
222
+ });
223
+ } catch (error) {
224
+ events.emit('detect:error', {
225
+ message: error.message,
226
+ });
227
+ detectData = {
228
+ findings: [],
229
+ stats: { total: 0, silentFailures: 0, observed: 0, coverageGaps: 0, unproven: 0, informational: 0 },
230
+ detectedAt: new Date().toISOString(),
231
+ };
232
+ }
233
+
234
+ events.emit('phase:completed', {
235
+ phase: 'Detect',
236
+ message: 'Silent failure detection complete',
237
+ });
238
+
239
+ // Emit finalize phase
240
+ events.emit('phase:started', {
241
+ phase: 'Finalize Artifacts',
242
+ message: 'Writing run results...',
243
+ });
244
+
245
+ const completedAt = new Date().toISOString();
246
+
247
+ // Write completed status
248
+ atomicWriteJson(paths.runStatusJson, {
249
+ status: 'COMPLETE',
250
+ runId,
251
+ startedAt,
252
+ completedAt,
253
+ });
254
+
255
+ // Update metadata with completion time
256
+ atomicWriteJson(paths.runMetaJson, {
257
+ veraxVersion: getVersion(),
258
+ nodeVersion: process.version,
259
+ platform: process.platform,
260
+ cwd: projectRoot,
261
+ command: 'run',
262
+ args: { url, src, out },
263
+ url,
264
+ src: srcPath,
265
+ startedAt,
266
+ completedAt,
267
+ error: null,
268
+ });
269
+
270
+ // Write summary with stable digest
271
+ writeSummaryJson(paths.summaryJson, {
272
+ runId,
273
+ status: 'COMPLETE',
274
+ startedAt,
275
+ completedAt,
276
+ command: 'run',
277
+ url,
278
+ notes: 'Run completed successfully',
279
+ }, {
280
+ expectationsTotal: expectations.length,
281
+ attempted: observeData.stats?.attempted || 0,
282
+ observed: observeData.stats?.observed || 0,
283
+ silentFailures: detectData.stats?.silentFailures || 0,
284
+ coverageGaps: detectData.stats?.coverageGaps || 0,
285
+ unproven: detectData.stats?.unproven || 0,
286
+ informational: detectData.stats?.informational || 0,
287
+ });
288
+
289
+ // Write detect results (or empty if detection failed)
290
+ writeFindingsJson(paths.baseDir, detectData);
291
+
292
+ // Write traces (at least phase events)
293
+ const traces = [
294
+ {
295
+ type: 'phase:started',
296
+ timestamp: startedAt,
297
+ phase: 'Detect Project',
298
+ },
299
+ {
300
+ type: 'phase:completed',
301
+ timestamp: new Date().toISOString(),
302
+ phase: 'Detect Project',
303
+ },
304
+ {
305
+ type: 'phase:started',
306
+ timestamp: new Date().toISOString(),
307
+ phase: 'Learn',
308
+ },
309
+ {
310
+ type: 'phase:completed',
311
+ timestamp: completedAt,
312
+ phase: 'Learn',
313
+ },
314
+ ];
315
+
316
+ const tracesContent = traces.map(t => JSON.stringify(t)).join('\n') + '\n';
317
+ atomicWriteText(paths.tracesJsonl, tracesContent);
318
+
319
+ // Write project profile
320
+ writeProjectJson(paths, projectProfile);
321
+
322
+ // Write learn results
323
+ writeLearnJson(paths, expectations, skipped);
324
+
325
+ // Write observe results
326
+ writeObserveJson(paths.baseDir, observeData);
327
+
328
+ events.emit('phase:completed', {
329
+ phase: 'Finalize Artifacts',
330
+ message: 'Run artifacts written',
331
+ });
332
+
333
+ // Print summary if not JSON mode
334
+ if (!json) {
335
+ console.log('\nRun complete.');
336
+ console.log(`Run ID: ${runId}`);
337
+ console.log(`Artifacts: ${paths.baseDir}`);
338
+ }
339
+
340
+ return { runId, paths, success: true };
341
+ } catch (error) {
342
+ // Mark run as FAILED if we have paths
343
+ if (paths && runId && startedAt) {
344
+ try {
345
+ const failedAt = new Date().toISOString();
346
+ atomicWriteJson(paths.runStatusJson, {
347
+ status: 'FAILED',
348
+ runId,
349
+ startedAt,
350
+ failedAt,
351
+ error: error.message,
352
+ });
353
+
354
+ // Update metadata
355
+ atomicWriteJson(paths.runMetaJson, {
356
+ veraxVersion: getVersion(),
357
+ nodeVersion: process.version,
358
+ platform: process.platform,
359
+ cwd: projectRoot,
360
+ command: 'run',
361
+ args: { url, src, out },
362
+ url,
363
+ src: srcPath,
364
+ startedAt,
365
+ completedAt: failedAt,
366
+ error: error.message,
367
+ });
368
+
369
+ // Write summary with digest even on failure
370
+ try {
371
+ writeSummaryJson(paths.summaryJson, {
372
+ runId,
373
+ status: 'FAILED',
374
+ startedAt,
375
+ completedAt: failedAt,
376
+ command: 'run',
377
+ url,
378
+ notes: `Run failed: ${error.message}`,
379
+ }, {
380
+ expectationsTotal: 0,
381
+ attempted: 0,
382
+ observed: 0,
383
+ silentFailures: 0,
384
+ coverageGaps: 0,
385
+ unproven: 0,
386
+ informational: 0,
387
+ });
388
+ } catch (summaryError) {
389
+ // Ignore summary write errors during failure handling
390
+ }
391
+ } catch (statusError) {
392
+ // Ignore errors when writing failure status
393
+ }
394
+ }
395
+
396
+ events.emit('error', {
397
+ message: error.message,
398
+ stack: error.stack,
399
+ });
400
+ throw error;
401
+ }
402
+ }
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * VERAX CLI Entry Point
5
+ *
6
+ * Commands:
7
+ * - verax (smart default with URL detection/prompting)
8
+ * - verax run --url <url> (strict, non-interactive)
9
+ * - verax inspect <runPath> (read and display run summary)
10
+ *
11
+ * Exit codes:
12
+ * - 0: success
13
+ * - 2: internal crash
14
+ * - 64: invalid CLI usage
15
+ * - 65: invalid input data
16
+ */
17
+
18
+ import { fileURLToPath } from 'url';
19
+ import { dirname, resolve } from 'path';
20
+ import { readFileSync } from 'fs';
21
+ import { defaultCommand } from './commands/default.js';
22
+ import { runCommand } from './commands/run.js';
23
+ import { inspectCommand } from './commands/inspect.js';
24
+ import { doctorCommand } from './commands/doctor.js';
25
+ import { getExitCode, UsageError, DataError, CrashError } from './util/errors.js';
26
+
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+
30
+ // Read package.json for version
31
+ function getVersion() {
32
+ try {
33
+ const pkgPath = resolve(__dirname, '../../package.json');
34
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
35
+ return pkg.version;
36
+ } catch {
37
+ return 'unknown';
38
+ }
39
+ }
40
+
41
+ async function main() {
42
+ const args = process.argv.slice(2);
43
+
44
+ try {
45
+ // Handle --version
46
+ if (args.includes('--version') || args.includes('-v')) {
47
+ console.log(`verax ${getVersion()}`);
48
+ process.exit(0);
49
+ }
50
+
51
+ // Handle explicit --help
52
+ if (args.includes('--help') || args.includes('-h')) {
53
+ showHelp();
54
+ process.exit(0);
55
+ }
56
+
57
+ // If no args, run default command
58
+ if (args.length === 0) {
59
+ await defaultCommand({});
60
+ process.exit(0);
61
+ }
62
+
63
+ const command = args[0];
64
+
65
+ // Handle 'run' command
66
+ if (command === 'run') {
67
+ const url = parseArg(args, '--url');
68
+ const src = parseArg(args, '--src') || '.';
69
+ const out = parseArg(args, '--out') || '.verax';
70
+ const json = args.includes('--json');
71
+ const verbose = args.includes('--verbose');
72
+
73
+ if (!url) {
74
+ throw new UsageError('run command requires --url <url> argument');
75
+ }
76
+
77
+ const result = await runCommand({ url, src, out, json, verbose });
78
+ process.exit(0);
79
+ }
80
+
81
+ // Handle 'inspect' command
82
+ if (command === 'inspect') {
83
+ if (args.length < 2) {
84
+ throw new UsageError('inspect command requires a run path argument');
85
+ }
86
+
87
+ const runPath = args[1];
88
+ const json = args.includes('--json');
89
+
90
+ const result = await inspectCommand(runPath, { json });
91
+ process.exit(0);
92
+ }
93
+
94
+ // Handle 'doctor' command
95
+ if (command === 'doctor') {
96
+ const allowedFlags = new Set(['--json']);
97
+ const extraFlags = args.slice(1).filter((a) => a.startsWith('-') && !allowedFlags.has(a));
98
+ const json = args.includes('--json');
99
+ const result = await doctorCommand({ json, extraFlags });
100
+ process.exit(0);
101
+ }
102
+
103
+ // Handle 'help' command
104
+ if (command === 'help' || command === '--help' || command === '-h') {
105
+ showHelp();
106
+ process.exit(0);
107
+ }
108
+
109
+ // Default command: smart scanning mode
110
+ // Options can be passed as flags before/after the default command position
111
+ const url = parseArg(args, '--url');
112
+ const src = parseArg(args, '--src') || '.';
113
+ const out = parseArg(args, '--out') || '.verax';
114
+ const json = args.includes('--json');
115
+ const verbose = args.includes('--verbose');
116
+
117
+ const result = await defaultCommand({ url, src, out, json, verbose });
118
+ process.exit(0);
119
+ } catch (error) {
120
+ // Print error message
121
+ if (error.message) {
122
+ console.error(`Error: ${error.message}`);
123
+ }
124
+
125
+ // Get exit code
126
+ const exitCode = getExitCode(error);
127
+ process.exit(exitCode);
128
+ }
129
+ }
130
+
131
+ function showHelp() {
132
+ const version = getVersion();
133
+ console.log(`
134
+ verax ${version}
135
+ VERAX — Silent failure detection for websites
136
+
137
+ USAGE:
138
+ verax [options] Smart mode (detects/prompts for URL)
139
+ verax run --url <url> [options] Strict mode (non-interactive, CI-friendly)
140
+ verax inspect <runPath> [--json] Inspect an existing run
141
+ verax doctor [--json] Diagnose local environment
142
+ verax --version Show version
143
+ verax --help Show this help
144
+
145
+ OPTIONS:
146
+ --url <url> Target URL to scan
147
+ --src <path> Source directory (default: .)
148
+ --out <path> Output directory for artifacts (default: .verax)
149
+ --json Output as JSON lines (progress events)
150
+ --verbose Verbose output
151
+ --help Show this help
152
+ --version Show version
153
+
154
+ EXAMPLES:
155
+ # Smart mode (interactive if needed)
156
+ verax
157
+
158
+ # Smart mode with explicit URL
159
+ verax --url https://example.com
160
+
161
+ # Strict mode (CI-friendly, non-interactive)
162
+ verax run --url https://example.com --src . --out .verax
163
+
164
+ # Inspect previous run
165
+ verax inspect .verax/runs/2026-01-11T00-59-12Z_4f2a9c
166
+
167
+ EXIT CODES:
168
+ 0 Success (tool executed)
169
+ 2 Internal crash
170
+ 64 Invalid CLI usage (missing args, invalid flags)
171
+ 65 Invalid input data (bad JSON, unreadable folder, etc.)
172
+
173
+ ARTIFACTS:
174
+ Artifacts are written to: <out>/runs/<runId>/
175
+ Required files:
176
+ - run.status.json Run status lifecycle
177
+ - run.meta.json Metadata about the run
178
+ - summary.json Summary of results
179
+ - findings.json Array of findings
180
+ - traces.jsonl JSONL traces of execution
181
+ - evidence/ Directory for evidence files
182
+ `);
183
+ }
184
+
185
+ function parseArg(args, name) {
186
+ const index = args.indexOf(name);
187
+ if (index !== -1 && index + 1 < args.length) {
188
+ return args[index + 1];
189
+ }
190
+ return null;
191
+ }
192
+
193
+ main().catch((error) => {
194
+ console.error(`Fatal error: ${error.message}`);
195
+ process.exit(2);
196
+ });