aegis_auto 1.0.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 +77 -0
- package/package.json +51 -0
- package/src/aegis-shadow.js +378 -0
- package/src/diagnostics.js +486 -0
- package/src/index.js +57 -0
- package/src/injected/interactionListener.js +410 -0
- package/src/monitor.js +438 -0
- package/src/recorder.js +362 -0
- package/src/utils/selectorGenerator.js +248 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# aegis_auto
|
|
2
|
+
|
|
3
|
+
Autonomous synthetic monitoring — record user journeys, replay with observability, diagnose failures with AI, and dispatch reports to your endpoint.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install aegis_auto
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> **Requires Node.js ≥ 18** (uses native `fetch`).
|
|
12
|
+
> Playwright browsers are installed automatically on first run.
|
|
13
|
+
|
|
14
|
+
## Quick Start — Shadow Mode (Zero Manual Steps)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx aegis-shadow https://your-app.com
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
This single command runs the full lifecycle:
|
|
21
|
+
|
|
22
|
+
| Phase | What Happens |
|
|
23
|
+
|-------|---|
|
|
24
|
+
| 1. **Record** (30s) | Opens headed Chromium, captures user interactions with a live progress bar |
|
|
25
|
+
| 2. **Simulate** | Replays the golden recording with HAR capture, console/network sentinels, latency comparison |
|
|
26
|
+
| 3. **Diagnose** | AI-powered root cause analysis via Groq LLM (skipped if PASS) |
|
|
27
|
+
| 4. **Dispatch** | POSTs `run_summary` + `incident_report` to your endpoint |
|
|
28
|
+
|
|
29
|
+
### Configuration (.env)
|
|
30
|
+
|
|
31
|
+
```env
|
|
32
|
+
GROQ_API_KEY=gsk_your_key_here # Required for AI diagnostics
|
|
33
|
+
WEBHOOK_URL=https://api.example.com # Optional — auto-POST results here
|
|
34
|
+
RECORD_DURATION=30 # Optional — recording time in seconds
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Programmatic Usage
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import { SessionRecorder } from 'aegis_auto/recorder';
|
|
41
|
+
|
|
42
|
+
const recorder = new SessionRecorder({
|
|
43
|
+
outputDir: './my-sessions',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await recorder.start('https://your-app.com');
|
|
47
|
+
// ... user interacts ...
|
|
48
|
+
const sessionPath = await recorder.stop();
|
|
49
|
+
console.log('Session saved to:', sessionPath);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## CLI Commands
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Full autonomous pipeline
|
|
56
|
+
npx aegis-shadow https://your-app.com
|
|
57
|
+
|
|
58
|
+
# Individual tools
|
|
59
|
+
npx aegis-monitor ./sessions/golden.json # Replay + observe
|
|
60
|
+
npx aegis-diagnose --logs-dir ./logs # AI analysis
|
|
61
|
+
npx aegis-diagnose --codebase ./src # AI analysis with file-level mapping
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Output Files
|
|
65
|
+
|
|
66
|
+
| File | Contents |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `sessions/session-*.json` | Golden recording (actions + deduplicated errors) |
|
|
69
|
+
| `logs/run_summary.json` | Step-by-step pass/fail with latency data |
|
|
70
|
+
| `logs/anomalies.json` | HTTP ≥ 400 responses during replay |
|
|
71
|
+
| `logs/crash_report.json` | Console errors + failing selectors |
|
|
72
|
+
| `logs/network.har` | Full HAR network trace |
|
|
73
|
+
| `logs/incident_report.md` | AI-generated incident analysis |
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aegis_auto",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Autonomous synthetic monitoring — record user journeys, replay with observability, diagnose failures with AI, and dispatch reports to your endpoint.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./recorder": "./src/recorder.js"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"aegis-shadow": "src/aegis-shadow.js",
|
|
13
|
+
"aegis-monitor": "src/monitor.js",
|
|
14
|
+
"aegis-diagnose": "src/diagnostics.js"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node src/index.js",
|
|
18
|
+
"shadow": "node src/aegis-shadow.js",
|
|
19
|
+
"monitor": "node src/monitor.js",
|
|
20
|
+
"diagnose": "node src/diagnostics.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"synthetic-monitoring",
|
|
24
|
+
"playwright",
|
|
25
|
+
"session-recorder",
|
|
26
|
+
"error-tracking",
|
|
27
|
+
"AI-diagnostics",
|
|
28
|
+
"observability",
|
|
29
|
+
"SRE"
|
|
30
|
+
],
|
|
31
|
+
"author": "Arun Kumar",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/arun-kumar-24/auto-aegis.git"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"src/",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"chalk": "^5.6.2",
|
|
47
|
+
"ora": "^9.3.0",
|
|
48
|
+
"playwright": "^1.58.2",
|
|
49
|
+
"uuid": "^11.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Aegis Shadow — Autonomous Lifecycle Orchestrator
|
|
4
|
+
*
|
|
5
|
+
* A single file that manages the full lifecycle:
|
|
6
|
+
* Start → Track (30s) → Simulate → Report → Dispatch
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node src/aegis-shadow.js https://example.com
|
|
10
|
+
* npm run shadow -- https://example.com
|
|
11
|
+
*
|
|
12
|
+
* Environment:
|
|
13
|
+
* WEBHOOK_URL — POST endpoint for dispatching results
|
|
14
|
+
* GROQ_API_KEY — For AI diagnostics (loaded from .env)
|
|
15
|
+
* RECORD_DURATION — Override recording duration in seconds (default: 30)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import ora from 'ora';
|
|
20
|
+
import { SessionRecorder } from './recorder.js';
|
|
21
|
+
import { spawn } from 'node:child_process';
|
|
22
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
23
|
+
import { resolve, join, dirname } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
// ── Paths ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = dirname(__filename);
|
|
30
|
+
const DOM_DIR = resolve(__dirname, '..');
|
|
31
|
+
|
|
32
|
+
// ── Config ─────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
loadEnvFile();
|
|
35
|
+
|
|
36
|
+
const RECORD_DURATION_S = parseInt(process.env.RECORD_DURATION || '30', 10);
|
|
37
|
+
const WEBHOOK_URL = process.env.WEBHOOK_URL || '';
|
|
38
|
+
const START_URL = process.argv[2] || null;
|
|
39
|
+
const CODEBASE_DIR = parseFlag('--codebase') || DOM_DIR;
|
|
40
|
+
|
|
41
|
+
// ── Main ───────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
console.clear();
|
|
45
|
+
printBanner();
|
|
46
|
+
|
|
47
|
+
if (!START_URL) {
|
|
48
|
+
console.log(chalk.red.bold(' ✖ No URL provided.\n'));
|
|
49
|
+
console.log(chalk.dim(' Usage: npm run shadow -- https://example.com\n'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let goldenPath = null;
|
|
54
|
+
let summaryPath = null;
|
|
55
|
+
let reportPath = null;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// ━━━ Phase 1: Shadow Recording ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
59
|
+
goldenPath = await phaseRecord(START_URL);
|
|
60
|
+
|
|
61
|
+
// Check if we have any actions to simulate
|
|
62
|
+
const golden = JSON.parse(readFileSync(goldenPath, 'utf-8'));
|
|
63
|
+
const actionCount = (golden.actions || []).length;
|
|
64
|
+
|
|
65
|
+
if (actionCount === 0) {
|
|
66
|
+
console.log(chalk.yellow.bold('\n ⚠ No interactions captured during recording.'));
|
|
67
|
+
console.log(chalk.dim(' User did not interact with the page.\n'));
|
|
68
|
+
} else {
|
|
69
|
+
// ━━━ Phase 2: Stress-Test Simulation ━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
70
|
+
summaryPath = await phaseSimulate(goldenPath);
|
|
71
|
+
|
|
72
|
+
// ━━━ Phase 3: AI Diagnostics ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
73
|
+
reportPath = await phaseDiagnose();
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
// ━━━ Phase 4: Dispatcher ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
77
|
+
await phaseDispatch(summaryPath, reportPath);
|
|
78
|
+
printFooter(goldenPath, summaryPath, reportPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
main().catch((err) => {
|
|
83
|
+
console.error(chalk.red.bold(`\n ✖ Fatal Error: ${err.message}\n`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
88
|
+
// Phase 1: Shadow Recording
|
|
89
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
90
|
+
|
|
91
|
+
async function phaseRecord(url) {
|
|
92
|
+
printPhaseHeader(1, 'Shadow Recording', 'cyan');
|
|
93
|
+
|
|
94
|
+
const recorder = new SessionRecorder({
|
|
95
|
+
outputDir: resolve(DOM_DIR, 'sessions'),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
await recorder.start(url);
|
|
99
|
+
|
|
100
|
+
// ── 30s countdown with progress bar ─────────────────────────────────
|
|
101
|
+
|
|
102
|
+
const spinner = ora({
|
|
103
|
+
text: progressText(0, RECORD_DURATION_S),
|
|
104
|
+
color: 'cyan',
|
|
105
|
+
prefixText: ' ',
|
|
106
|
+
}).start();
|
|
107
|
+
|
|
108
|
+
let elapsed = 0;
|
|
109
|
+
let browserClosed = false;
|
|
110
|
+
|
|
111
|
+
// Detect if the user closes the browser early
|
|
112
|
+
recorder.context.on('close', () => {
|
|
113
|
+
browserClosed = true;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await new Promise((done) => {
|
|
117
|
+
const tick = setInterval(() => {
|
|
118
|
+
elapsed++;
|
|
119
|
+
spinner.text = progressText(elapsed, RECORD_DURATION_S);
|
|
120
|
+
|
|
121
|
+
if (elapsed >= RECORD_DURATION_S || browserClosed) {
|
|
122
|
+
clearInterval(tick);
|
|
123
|
+
done();
|
|
124
|
+
}
|
|
125
|
+
}, 1000);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (browserClosed) {
|
|
129
|
+
spinner.info(chalk.dim('Browser closed by user — saving session.'));
|
|
130
|
+
} else {
|
|
131
|
+
spinner.succeed(chalk.green('Recording window complete.'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Stop the recorder — saves golden JSON + closes browser
|
|
135
|
+
const outputPath = await recorder.stop();
|
|
136
|
+
|
|
137
|
+
console.log(chalk.green.bold(' ✔ User Journey Captured'));
|
|
138
|
+
console.log(chalk.dim(` → ${outputPath}\n`));
|
|
139
|
+
|
|
140
|
+
return outputPath;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
144
|
+
// Phase 2: Stress-Test Simulation
|
|
145
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
146
|
+
|
|
147
|
+
async function phaseSimulate(goldenPath) {
|
|
148
|
+
printPhaseHeader(2, 'Stress-Test Simulation', 'yellow');
|
|
149
|
+
|
|
150
|
+
console.log(chalk.yellow(' 🚀 Recording Complete. Starting Stress-Test Simulation...\n'));
|
|
151
|
+
|
|
152
|
+
const spinner = ora({
|
|
153
|
+
text: chalk.yellow('Performing simulation in background...'),
|
|
154
|
+
color: 'yellow',
|
|
155
|
+
prefixText: ' ',
|
|
156
|
+
}).start();
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await runChildScript('monitor.js', [goldenPath]);
|
|
160
|
+
spinner.succeed(chalk.green('Simulation complete.'));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
spinner.warn(chalk.yellow('Simulation completed with issues.'));
|
|
163
|
+
console.log(chalk.dim(` ${err.message.split('\n')[0]}`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Show run summary stats
|
|
167
|
+
const summaryPath = resolve(DOM_DIR, 'logs', 'run_summary.json');
|
|
168
|
+
if (existsSync(summaryPath)) {
|
|
169
|
+
const summary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
170
|
+
const statusIcon = summary.status === 'PASS' ? chalk.green('✔ PASS') : chalk.red('✖ FAIL');
|
|
171
|
+
console.log(chalk.dim(` Status : ${statusIcon}`));
|
|
172
|
+
console.log(chalk.dim(` Steps : ${summary.passedSteps}/${summary.totalSteps} passed`));
|
|
173
|
+
console.log(chalk.dim(` Latency : ${summary.performanceSummary?.totalLatency}ms`));
|
|
174
|
+
console.log();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return summaryPath;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
181
|
+
// Phase 3: AI Diagnostics
|
|
182
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
183
|
+
|
|
184
|
+
async function phaseDiagnose() {
|
|
185
|
+
printPhaseHeader(3, 'AI Diagnostics', 'magenta');
|
|
186
|
+
|
|
187
|
+
const summaryPath = resolve(DOM_DIR, 'logs', 'run_summary.json');
|
|
188
|
+
const reportPath = resolve(DOM_DIR, 'logs', 'incident_report.md');
|
|
189
|
+
|
|
190
|
+
if (!existsSync(summaryPath)) {
|
|
191
|
+
console.log(chalk.dim(' No run summary found. Skipping diagnostics.\n'));
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// If the run passed, skip expensive AI call
|
|
196
|
+
const summary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
197
|
+
if (summary.status === 'PASS') {
|
|
198
|
+
console.log(chalk.green.bold(' ✔ All steps passed — no incidents to diagnose.\n'));
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const spinner = ora({
|
|
203
|
+
text: chalk.magenta('Generating AI incident report via Groq...'),
|
|
204
|
+
color: 'magenta',
|
|
205
|
+
prefixText: ' ',
|
|
206
|
+
}).start();
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await runChildScript('diagnostics.js', ['--codebase', CODEBASE_DIR]);
|
|
210
|
+
spinner.succeed(chalk.green('Incident report generated.'));
|
|
211
|
+
console.log(chalk.dim(` → ${reportPath}\n`));
|
|
212
|
+
return reportPath;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
spinner.fail(chalk.red('Diagnostics failed.'));
|
|
215
|
+
console.log(chalk.dim(` ${err.message.split('\n')[0]}\n`));
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
221
|
+
// Phase 4: Dispatcher
|
|
222
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
223
|
+
|
|
224
|
+
async function phaseDispatch(summaryPath, reportPath) {
|
|
225
|
+
if (!WEBHOOK_URL) {
|
|
226
|
+
console.log(chalk.dim('\n No WEBHOOK_URL configured — skipping dispatch.'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
printPhaseHeader(4, 'Dispatching Results', 'blue');
|
|
231
|
+
|
|
232
|
+
const spinner = ora({
|
|
233
|
+
text: chalk.blue(`Sending logs to endpoint...`),
|
|
234
|
+
color: 'blue',
|
|
235
|
+
prefixText: ' ',
|
|
236
|
+
}).start();
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const payload = { timestamp: new Date().toISOString() };
|
|
240
|
+
|
|
241
|
+
if (summaryPath && existsSync(summaryPath)) {
|
|
242
|
+
payload.runSummary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
243
|
+
}
|
|
244
|
+
if (reportPath && existsSync(reportPath)) {
|
|
245
|
+
payload.incidentReport = readFileSync(reportPath, 'utf-8');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const response = await fetch(WEBHOOK_URL, {
|
|
249
|
+
method: 'POST',
|
|
250
|
+
headers: { 'Content-Type': 'application/json' },
|
|
251
|
+
body: JSON.stringify(payload),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (response.ok) {
|
|
255
|
+
spinner.succeed(chalk.green(`Logs dispatched to endpoint (${response.status}).`));
|
|
256
|
+
} else {
|
|
257
|
+
spinner.warn(chalk.yellow(`Endpoint responded with ${response.status}.`));
|
|
258
|
+
}
|
|
259
|
+
} catch (err) {
|
|
260
|
+
spinner.fail(chalk.red(`Dispatch failed: ${err.message}`));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
265
|
+
// Terminal UX
|
|
266
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
267
|
+
|
|
268
|
+
function printBanner() {
|
|
269
|
+
console.log(chalk.bold.cyan(`
|
|
270
|
+
╔═══════════════════════════════════════════════════╗
|
|
271
|
+
║ ║
|
|
272
|
+
║ ◈ A E G I S S H A D O W ◈ ║
|
|
273
|
+
║ Autonomous Lifecycle Orchestrator ║
|
|
274
|
+
║ ║
|
|
275
|
+
╚═══════════════════════════════════════════════════╝
|
|
276
|
+
`));
|
|
277
|
+
console.log(chalk.dim(` Target : ${START_URL || 'none'}`));
|
|
278
|
+
console.log(chalk.dim(` Record : ${RECORD_DURATION_S}s`));
|
|
279
|
+
console.log(chalk.dim(` Codebase: ${CODEBASE_DIR}`));
|
|
280
|
+
if (WEBHOOK_URL) {
|
|
281
|
+
console.log(chalk.dim(` Webhook: ${WEBHOOK_URL}`));
|
|
282
|
+
}
|
|
283
|
+
console.log();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function printPhaseHeader(num, title, color) {
|
|
287
|
+
const c = chalk[color] || chalk.white;
|
|
288
|
+
console.log(c.bold(` ━━━ Phase ${num}: ${title} ${'━'.repeat(Math.max(0, 38 - title.length))}`));
|
|
289
|
+
console.log();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function printFooter(goldenPath, summaryPath, reportPath) {
|
|
293
|
+
console.log(chalk.bold.cyan(`
|
|
294
|
+
╔═══════════════════════════════════════════════════╗
|
|
295
|
+
║ Run Complete ║
|
|
296
|
+
╚═══════════════════════════════════════════════════╝`));
|
|
297
|
+
|
|
298
|
+
if (goldenPath) console.log(chalk.dim(` 📄 Golden : ${goldenPath}`));
|
|
299
|
+
if (summaryPath && existsSync(summaryPath)) console.log(chalk.dim(` 📄 Summary : ${summaryPath}`));
|
|
300
|
+
if (reportPath && existsSync(reportPath)) console.log(chalk.dim(` 📄 Report : ${reportPath}`));
|
|
301
|
+
if (WEBHOOK_URL) console.log(chalk.dim(` 🔗 Webhook : ${WEBHOOK_URL}`));
|
|
302
|
+
console.log();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function progressText(current, total) {
|
|
306
|
+
const width = 20;
|
|
307
|
+
const filled = Math.round((current / total) * width);
|
|
308
|
+
const empty = width - filled;
|
|
309
|
+
const bar = chalk.cyan('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
310
|
+
return `[${bar}] ${chalk.bold(current)}/${total}s — ${chalk.dim('Recording User Interaction...')}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
314
|
+
// Helpers
|
|
315
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Run a sibling script (monitor.js / diagnostics.js) as a child process.
|
|
319
|
+
* Pipes output to /dev/null; captures stderr for error reporting.
|
|
320
|
+
*/
|
|
321
|
+
function runChildScript(scriptName, args = []) {
|
|
322
|
+
return new Promise((resolve, reject) => {
|
|
323
|
+
const scriptPath = join(__dirname, scriptName);
|
|
324
|
+
const proc = spawn(process.execPath, [scriptPath, ...args], {
|
|
325
|
+
cwd: DOM_DIR,
|
|
326
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
327
|
+
env: { ...process.env },
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
let stderr = '';
|
|
331
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
332
|
+
|
|
333
|
+
proc.on('close', (code) => {
|
|
334
|
+
if (code === 0) resolve();
|
|
335
|
+
else reject(new Error(stderr.trim() || `${scriptName} exited with code ${code}`));
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
proc.on('error', (err) => {
|
|
339
|
+
reject(new Error(`Failed to spawn ${scriptName}: ${err.message}`));
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Load .env file from the dom directory (zero-dependency).
|
|
346
|
+
*/
|
|
347
|
+
function loadEnvFile() {
|
|
348
|
+
const envPath = resolve(DOM_DIR, '.env');
|
|
349
|
+
if (!existsSync(envPath)) return;
|
|
350
|
+
try {
|
|
351
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
352
|
+
for (const line of content.split('\n')) {
|
|
353
|
+
const trimmed = line.trim();
|
|
354
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
355
|
+
const eqIdx = trimmed.indexOf('=');
|
|
356
|
+
if (eqIdx === -1) continue;
|
|
357
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
358
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
359
|
+
if (!process.env[key]) {
|
|
360
|
+
process.env[key] = val;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
// .env is optional
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Parse a --flag value from argv.
|
|
370
|
+
*/
|
|
371
|
+
function parseFlag(flag) {
|
|
372
|
+
const args = process.argv.slice(2);
|
|
373
|
+
const idx = args.indexOf(flag);
|
|
374
|
+
if (idx !== -1 && args[idx + 1]) {
|
|
375
|
+
return resolve(args[idx + 1]);
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|