@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/summary-writer.js +32 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +101 -0
- package/src/verax/cli/wizard.js +98 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +403 -0
- package/src/verax/core/incremental-store.js +237 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +521 -0
- package/src/verax/detect/comparison.js +2 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +14 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { writeFileSync, renameSync, mkdirSync } from 'fs';
|
|
2
|
+
import { dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Atomic write for JSON files
|
|
6
|
+
* Writes to a temp file and renames to prevent partial writes
|
|
7
|
+
*/
|
|
8
|
+
export function atomicWriteJson(filePath, data) {
|
|
9
|
+
const tempPath = `${filePath}.tmp`;
|
|
10
|
+
const dirPath = dirname(filePath);
|
|
11
|
+
|
|
12
|
+
// Ensure directory exists
|
|
13
|
+
mkdirSync(dirPath, { recursive: true });
|
|
14
|
+
|
|
15
|
+
// Write to temp file
|
|
16
|
+
writeFileSync(tempPath, JSON.stringify(data, null, 2), 'utf8');
|
|
17
|
+
|
|
18
|
+
// Atomic rename
|
|
19
|
+
renameSync(tempPath, filePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Atomic write for text files (JSONL, logs, etc.)
|
|
24
|
+
*/
|
|
25
|
+
export function atomicWriteText(filePath, content) {
|
|
26
|
+
const tempPath = `${filePath}.tmp`;
|
|
27
|
+
const dirPath = dirname(filePath);
|
|
28
|
+
|
|
29
|
+
// Ensure directory exists
|
|
30
|
+
mkdirSync(dirPath, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Write to temp file
|
|
33
|
+
writeFileSync(tempPath, content, 'utf8');
|
|
34
|
+
|
|
35
|
+
// Atomic rename
|
|
36
|
+
renameSync(tempPath, filePath);
|
|
37
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection Engine
|
|
3
|
+
* Compares learned expectations against observations to detect silent failures.
|
|
4
|
+
* Produces exactly one finding per expectation with deterministic confidence and impact.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export async function detectFindings(learnData, observeData, projectPath, onProgress) {
|
|
8
|
+
const findings = [];
|
|
9
|
+
const stats = {
|
|
10
|
+
total: 0,
|
|
11
|
+
silentFailures: 0,
|
|
12
|
+
observed: 0,
|
|
13
|
+
coverageGaps: 0,
|
|
14
|
+
unproven: 0,
|
|
15
|
+
informational: 0,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const observationMap = indexObservations(observeData);
|
|
19
|
+
const expectations = learnData?.expectations || [];
|
|
20
|
+
|
|
21
|
+
const narration = [];
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < expectations.length; i++) {
|
|
24
|
+
const expectation = expectations[i];
|
|
25
|
+
const index = i + 1;
|
|
26
|
+
|
|
27
|
+
if (onProgress) {
|
|
28
|
+
onProgress({
|
|
29
|
+
event: 'detect:attempt',
|
|
30
|
+
index,
|
|
31
|
+
total: expectations.length,
|
|
32
|
+
expectationId: expectation.id,
|
|
33
|
+
type: expectation.type,
|
|
34
|
+
value: expectation?.promise?.value,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const observation = getObservationForExpectation(expectation, observationMap);
|
|
39
|
+
const finding = classifyExpectation(expectation, observation);
|
|
40
|
+
|
|
41
|
+
findings.push(finding);
|
|
42
|
+
stats.total++;
|
|
43
|
+
stats[findingStatKey(finding.classification)]++;
|
|
44
|
+
|
|
45
|
+
const icon = classificationIcon(finding.classification);
|
|
46
|
+
narration.push(`${icon} ${finding.classification.toUpperCase()} ${finding.promise?.value || ''}`.trim());
|
|
47
|
+
|
|
48
|
+
if (onProgress) {
|
|
49
|
+
onProgress({
|
|
50
|
+
event: 'detect:classified',
|
|
51
|
+
index,
|
|
52
|
+
classification: finding.classification,
|
|
53
|
+
impact: finding.impact,
|
|
54
|
+
confidence: finding.confidence,
|
|
55
|
+
expectationId: expectation.id,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Terminal narration (one line per finding + summary)
|
|
61
|
+
narration.forEach((line) => console.log(line));
|
|
62
|
+
console.log(`SUMMARY findings=${findings.length} observed=${stats.observed} silent-failure=${stats.silentFailures} coverage-gap=${stats.coverageGaps} unproven=${stats.unproven}`);
|
|
63
|
+
|
|
64
|
+
// Emit completion event
|
|
65
|
+
if (onProgress) {
|
|
66
|
+
onProgress({
|
|
67
|
+
event: 'detect:completed',
|
|
68
|
+
total: findings.length,
|
|
69
|
+
stats,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
findings,
|
|
75
|
+
stats,
|
|
76
|
+
detectedAt: new Date().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function indexObservations(observeData) {
|
|
81
|
+
const map = new Map();
|
|
82
|
+
if (!observeData || !Array.isArray(observeData.observations)) return map;
|
|
83
|
+
|
|
84
|
+
observeData.observations.forEach((obs) => {
|
|
85
|
+
if (obs == null) return;
|
|
86
|
+
const keys = [];
|
|
87
|
+
if (obs.id) keys.push(obs.id);
|
|
88
|
+
if (obs.expectationId) keys.push(obs.expectationId);
|
|
89
|
+
keys.forEach((key) => {
|
|
90
|
+
if (!map.has(key)) map.set(key, obs);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
return map;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getObservationForExpectation(expectation, observationMap) {
|
|
97
|
+
if (!expectation || !expectation.id) return null;
|
|
98
|
+
return observationMap.get(expectation.id) || null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function classificationIcon(classification) {
|
|
102
|
+
switch (classification) {
|
|
103
|
+
case 'observed':
|
|
104
|
+
return '✓';
|
|
105
|
+
case 'silent-failure':
|
|
106
|
+
return '✗';
|
|
107
|
+
case 'coverage-gap':
|
|
108
|
+
return '⚠';
|
|
109
|
+
case 'unproven':
|
|
110
|
+
return '⚠';
|
|
111
|
+
default:
|
|
112
|
+
return '•';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findingStatKey(classification) {
|
|
117
|
+
switch (classification) {
|
|
118
|
+
case 'silent-failure':
|
|
119
|
+
return 'silentFailures';
|
|
120
|
+
case 'observed':
|
|
121
|
+
return 'observed';
|
|
122
|
+
case 'coverage-gap':
|
|
123
|
+
return 'coverageGaps';
|
|
124
|
+
case 'unproven':
|
|
125
|
+
return 'unproven';
|
|
126
|
+
default:
|
|
127
|
+
return 'informational';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Classify a single expectation according to deterministic rules.
|
|
133
|
+
*/
|
|
134
|
+
function classifyExpectation(expectation, observation) {
|
|
135
|
+
const finding = {
|
|
136
|
+
id: expectation.id,
|
|
137
|
+
type: expectation.type,
|
|
138
|
+
promise: expectation.promise,
|
|
139
|
+
source: expectation.source,
|
|
140
|
+
classification: 'informational',
|
|
141
|
+
impact: 'LOW',
|
|
142
|
+
confidence: 0,
|
|
143
|
+
evidence: [],
|
|
144
|
+
reason: null,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const attempted = Boolean(observation?.attempted);
|
|
148
|
+
const observed = observation?.observed === true;
|
|
149
|
+
const reason = observation?.reason || null;
|
|
150
|
+
|
|
151
|
+
const evidence = normalizeEvidence(observation?.evidenceFiles || []);
|
|
152
|
+
finding.evidence = evidence;
|
|
153
|
+
|
|
154
|
+
const evidenceSignals = analyzeEvidenceSignals(observation, evidence);
|
|
155
|
+
|
|
156
|
+
// 1) observed
|
|
157
|
+
if (observed) {
|
|
158
|
+
finding.classification = 'observed';
|
|
159
|
+
finding.reason = 'Expectation observed at runtime';
|
|
160
|
+
finding.impact = getImpact(expectation);
|
|
161
|
+
finding.confidence = 1.0;
|
|
162
|
+
return finding;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 2) coverage-gap (not attempted or explicitly blocked)
|
|
166
|
+
if (!attempted || isSafetySkip(reason)) {
|
|
167
|
+
finding.classification = 'coverage-gap';
|
|
168
|
+
finding.reason = reason || 'No observation attempt recorded';
|
|
169
|
+
finding.impact = 'LOW';
|
|
170
|
+
finding.confidence = 0;
|
|
171
|
+
return finding;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 3) silent-failure (attempted, observed === false, no safety skip)
|
|
175
|
+
if (attempted && observation?.observed === false && !isSafetySkip(reason)) {
|
|
176
|
+
finding.classification = 'silent-failure';
|
|
177
|
+
finding.reason = reason || 'Expected behavior not observed';
|
|
178
|
+
finding.impact = getImpact(expectation);
|
|
179
|
+
finding.confidence = calculateConfidence(expectation, evidenceSignals, 'silent-failure');
|
|
180
|
+
return finding;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 4) unproven (attempted, ambiguous evidence)
|
|
184
|
+
if (attempted && !observed) {
|
|
185
|
+
finding.classification = 'unproven';
|
|
186
|
+
finding.reason = reason || 'Attempted but evidence insufficient';
|
|
187
|
+
finding.impact = 'MEDIUM';
|
|
188
|
+
finding.confidence = calculateConfidence(expectation, evidenceSignals, 'unproven');
|
|
189
|
+
return finding;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 5) informational fallback
|
|
193
|
+
finding.classification = 'informational';
|
|
194
|
+
finding.reason = reason || 'No classification rule matched';
|
|
195
|
+
finding.impact = 'LOW';
|
|
196
|
+
finding.confidence = calculateConfidence(expectation, evidenceSignals, 'informational');
|
|
197
|
+
return finding;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isSafetySkip(reason) {
|
|
201
|
+
if (!reason) return false;
|
|
202
|
+
const lower = reason.toLowerCase();
|
|
203
|
+
const safetyIndicators = ['blocked', 'timeout', 'not found', 'safety', 'permission', 'denied', 'captcha', 'forbidden'];
|
|
204
|
+
return safetyIndicators.some((indicator) => lower.includes(indicator));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Deterministic confidence calculation.
|
|
209
|
+
* Screenshots + network + DOM change => >=0.8
|
|
210
|
+
* Screenshots only => ~0.6
|
|
211
|
+
* Weak signals => <0.5
|
|
212
|
+
*/
|
|
213
|
+
function calculateConfidence(expectation, evidenceSignals, classification) {
|
|
214
|
+
if (classification === 'observed') return 1.0;
|
|
215
|
+
if (classification === 'coverage-gap') return 0;
|
|
216
|
+
|
|
217
|
+
const { hasScreenshots, hasNetworkLogs, hasDomChange } = evidenceSignals;
|
|
218
|
+
|
|
219
|
+
if (classification === 'silent-failure') {
|
|
220
|
+
if (hasScreenshots && hasNetworkLogs && hasDomChange) return 0.85;
|
|
221
|
+
if (hasScreenshots && hasNetworkLogs) return 0.75;
|
|
222
|
+
if (hasScreenshots && hasDomChange) return 0.7;
|
|
223
|
+
if (hasScreenshots) return 0.6;
|
|
224
|
+
if (hasNetworkLogs || hasDomChange) return 0.5;
|
|
225
|
+
return 0.4; // weak signals, attempted but no evidence
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (classification === 'unproven') {
|
|
229
|
+
if (hasScreenshots || hasNetworkLogs || hasDomChange) return 0.45;
|
|
230
|
+
return 0.3;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return 0.3;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function analyzeEvidenceSignals(observation, evidence) {
|
|
237
|
+
const hasScreenshots = evidence.some((e) => e.type === 'screenshot');
|
|
238
|
+
const hasNetworkLogs = evidence.some((e) => e.type === 'network-log');
|
|
239
|
+
const hasDomChange = Boolean(
|
|
240
|
+
observation?.domChanged ||
|
|
241
|
+
observation?.domChange === true ||
|
|
242
|
+
observation?.sensors?.uiSignals?.diff?.changed
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return { hasScreenshots, hasNetworkLogs, hasDomChange };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeEvidence(evidenceFiles) {
|
|
249
|
+
if (!Array.isArray(evidenceFiles)) return [];
|
|
250
|
+
return evidenceFiles
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
.map((file) => ({
|
|
253
|
+
type: getEvidenceType(file),
|
|
254
|
+
path: file,
|
|
255
|
+
available: true,
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Impact classification per product spec.
|
|
261
|
+
*/
|
|
262
|
+
function getImpact(expectation) {
|
|
263
|
+
const type = expectation?.type;
|
|
264
|
+
const value = expectation?.promise?.value || '';
|
|
265
|
+
const valueStr = String(value).toLowerCase();
|
|
266
|
+
|
|
267
|
+
if (type === 'navigation') {
|
|
268
|
+
const primaryRoutes = ['/', '/home', '/about', '/contact', '/products', '/pricing', '/features', '/login', '/signup'];
|
|
269
|
+
if (primaryRoutes.includes(value)) return 'HIGH';
|
|
270
|
+
if (valueStr.includes('admin') || valueStr.includes('dashboard') || valueStr.includes('settings')) return 'MEDIUM';
|
|
271
|
+
if (valueStr.includes('privacy') || valueStr.includes('terms') || valueStr.includes('footer')) return 'LOW';
|
|
272
|
+
return 'MEDIUM';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (type === 'network') {
|
|
276
|
+
if (valueStr.includes('/api/auth') || valueStr.includes('/api/payment') || valueStr.includes('/api/user')) return 'HIGH';
|
|
277
|
+
if (valueStr.includes('api.')) return 'MEDIUM';
|
|
278
|
+
if (valueStr.includes('/api/') && !valueStr.includes('/api/analytics')) return 'MEDIUM';
|
|
279
|
+
return 'LOW';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (type === 'state') {
|
|
283
|
+
return 'MEDIUM';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return 'LOW';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getEvidenceType(filename) {
|
|
290
|
+
if (!filename) return 'unknown';
|
|
291
|
+
const lower = filename.toLowerCase();
|
|
292
|
+
if (lower.endsWith('.png') || lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'screenshot';
|
|
293
|
+
if (lower.endsWith('.json')) return 'network-log';
|
|
294
|
+
if (lower.endsWith('.log') || lower.endsWith('.txt')) return 'console-log';
|
|
295
|
+
return 'artifact';
|
|
296
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attempt to infer URL from environment variables and common dev configs
|
|
3
|
+
*/
|
|
4
|
+
export function tryResolveUrlFromEnv() {
|
|
5
|
+
// Check common environment variables
|
|
6
|
+
const candidates = [
|
|
7
|
+
process.env.VERCEL_URL,
|
|
8
|
+
process.env.NEXT_PUBLIC_SITE_URL,
|
|
9
|
+
process.env.SITE_URL,
|
|
10
|
+
process.env.PUBLIC_URL,
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
if (candidate) {
|
|
15
|
+
// Ensure it's a valid URL
|
|
16
|
+
if (candidate.startsWith('http://') || candidate.startsWith('https://')) {
|
|
17
|
+
return candidate;
|
|
18
|
+
}
|
|
19
|
+
// If it's a bare domain, assume https
|
|
20
|
+
if (candidate.includes('.') && !candidate.includes(' ')) {
|
|
21
|
+
return `https://${candidate}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for localhost/PORT
|
|
27
|
+
if (process.env.PORT) {
|
|
28
|
+
const port = process.env.PORT;
|
|
29
|
+
return `http://localhost:${port}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Error System
|
|
3
|
+
* Maps errors to exit codes:
|
|
4
|
+
* - 0: success (tool executed)
|
|
5
|
+
* - 2: internal crash
|
|
6
|
+
* - 64: invalid CLI usage
|
|
7
|
+
* - 65: invalid input data
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class CLIError extends Error {
|
|
11
|
+
constructor(message, exitCode = 2) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'CLIError';
|
|
14
|
+
this.exitCode = exitCode;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class UsageError extends CLIError {
|
|
19
|
+
constructor(message) {
|
|
20
|
+
super(message, 64);
|
|
21
|
+
this.name = 'UsageError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DataError extends CLIError {
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message, 65);
|
|
28
|
+
this.name = 'DataError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class CrashError extends CLIError {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message, 2);
|
|
35
|
+
this.name = 'CrashError';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getExitCode(error) {
|
|
40
|
+
if (error instanceof CLIError) {
|
|
41
|
+
return error.exitCode;
|
|
42
|
+
}
|
|
43
|
+
return 2; // default to crash
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event emitter for run progress
|
|
3
|
+
*/
|
|
4
|
+
export class RunEventEmitter {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.events = [];
|
|
7
|
+
this.listeners = [];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
on(event, handler) {
|
|
11
|
+
this.listeners.push({ event, handler });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
emit(type, data = {}) {
|
|
15
|
+
const event = {
|
|
16
|
+
type,
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
18
|
+
...data,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.events.push(event);
|
|
22
|
+
|
|
23
|
+
// Call registered listeners
|
|
24
|
+
this.listeners.forEach(({ event: listenEvent, handler }) => {
|
|
25
|
+
if (listenEvent === type || listenEvent === '*') {
|
|
26
|
+
handler(event);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getEvents() {
|
|
32
|
+
return this.events;
|
|
33
|
+
}
|
|
34
|
+
}
|