ai-lens 0.8.20 → 0.8.22
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/.commithash +1 -1
- package/cli/hooks.js +7 -1
- package/cli/status.js +306 -22
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
23120cb
|
package/cli/hooks.js
CHANGED
|
@@ -106,9 +106,15 @@ export function captureCommand() {
|
|
|
106
106
|
const capturePath = CAPTURE_PATH.replace(/\\/g, '/');
|
|
107
107
|
const escaped = shellEscape(capturePath);
|
|
108
108
|
// /usr/bin/env doesn't need shell-escaping, but named paths do
|
|
109
|
-
|
|
109
|
+
const base = nodePath === '/usr/bin/env node'
|
|
110
110
|
? `/usr/bin/env node ${escaped}`
|
|
111
111
|
: `${shellEscape(nodePath.replace(/\\/g, '/'))} ${escaped}`;
|
|
112
|
+
// On Windows, prefix with "& " so the command works in both PowerShell and cmd.exe.
|
|
113
|
+
// PowerShell treats a quoted path like "node.exe" as a string expression, not a command;
|
|
114
|
+
// the & (call) operator is required. In cmd.exe, & is a command separator — the empty
|
|
115
|
+
// first part is a no-op, and the real command runs as the second part.
|
|
116
|
+
if (process.platform === 'win32') return `& ${base}`;
|
|
117
|
+
return base;
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
// ---------------------------------------------------------------------------
|
package/cli/status.js
CHANGED
|
@@ -5,7 +5,8 @@ import { homedir, release as osRelease, arch as osArch } from 'node:os';
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
|
|
7
7
|
import { getVersionInfo, readLensConfig, detectInstalledTools, analyzeToolHooks, checkHooksDisabled, CAPTURE_PATH, TOOL_CONFIGS } from './hooks.js';
|
|
8
|
-
import { DATA_DIR, PENDING_DIR, SENDING_DIR, LOG_PATH, CAPTURE_LOG_PATH, getGitIdentity } from '../client/config.js';
|
|
8
|
+
import { DATA_DIR, PENDING_DIR, SENDING_DIR, SESSION_PATHS_DIR, LOG_PATH, CAPTURE_LOG_PATH, getGitIdentity } from '../client/config.js';
|
|
9
|
+
import { isLockStale } from '../client/sender.js';
|
|
9
10
|
import { initLogger, info, success, warn, error, heading, blank } from './logger.js';
|
|
10
11
|
|
|
11
12
|
const INIT_LOG_PATH = join(DATA_DIR, 'init.log');
|
|
@@ -18,8 +19,11 @@ const DIM = useColor ? '\x1b[2m' : '';
|
|
|
18
19
|
const GREEN = useColor ? '\x1b[32m' : '';
|
|
19
20
|
const RED = useColor ? '\x1b[31m' : '';
|
|
20
21
|
|
|
22
|
+
const YELLOW = useColor ? '\x1b[33m' : '';
|
|
23
|
+
|
|
21
24
|
const CHECK = `${GREEN}\u2713${RESET}`;
|
|
22
25
|
const CROSS = `${RED}\u2717${RESET}`;
|
|
26
|
+
const TRIANGLE = `${YELLOW}\u26a0${RESET}`;
|
|
23
27
|
|
|
24
28
|
const REPORT_PATH = join(homedir(), 'ai-lens-status.txt');
|
|
25
29
|
|
|
@@ -78,6 +82,37 @@ function extractHookCommand(tool) {
|
|
|
78
82
|
return null;
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
function validateHookCommandPaths(tool) {
|
|
86
|
+
const command = extractHookCommand(tool);
|
|
87
|
+
if (!command) return null;
|
|
88
|
+
|
|
89
|
+
const issues = [];
|
|
90
|
+
|
|
91
|
+
// Extract capture.js path using 'capture.js' as anchor
|
|
92
|
+
const captureMatch = command.match(/["']([^"']*capture\.js)["']|(\S*capture\.js)/);
|
|
93
|
+
if (captureMatch) {
|
|
94
|
+
const capturePath = captureMatch[1] || captureMatch[2];
|
|
95
|
+
if (!existsSync(capturePath)) {
|
|
96
|
+
issues.push(`capture.js not found at: ${capturePath}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Extract node path (first token). Skip if /usr/bin/env node (node resolved via PATH)
|
|
101
|
+
// Strip "& " prefix (PowerShell call operator, added on Windows) before matching.
|
|
102
|
+
const cmdForNode = command.replace(/^& /, '');
|
|
103
|
+
if (!cmdForNode.startsWith('/usr/bin/env node')) {
|
|
104
|
+
const nodeMatch = cmdForNode.match(/^["']([^"']+)["']|^(\S+)/);
|
|
105
|
+
if (nodeMatch) {
|
|
106
|
+
const nodePath = nodeMatch[1] || nodeMatch[2];
|
|
107
|
+
if (nodePath !== 'node' && !existsSync(nodePath)) {
|
|
108
|
+
issues.push(`node not found at: ${nodePath}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return issues.length > 0 ? issues : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
81
116
|
function checkCaptureRun(installedTools) {
|
|
82
117
|
// Collect commands from ALL installed tools — not just the first found (Issues 8-9).
|
|
83
118
|
// If Claude Code works but Cursor is broken, both must be tested to surface the failure.
|
|
@@ -141,21 +176,72 @@ function checkCaptureRun(installedTools) {
|
|
|
141
176
|
}
|
|
142
177
|
} catch { /* best effort */ }
|
|
143
178
|
|
|
144
|
-
|
|
179
|
+
// Shell command test: execute the actual hook command string through the shell
|
|
180
|
+
// to catch path/quoting/slash issues that direct spawn bypasses.
|
|
181
|
+
// This simulates how Claude Code / Cursor actually invoke hooks on the OS.
|
|
182
|
+
const shellSessionId = 'status-shell-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7);
|
|
183
|
+
const shellEvent = JSON.stringify({
|
|
184
|
+
hook_event_name: 'Stop',
|
|
185
|
+
session_id: shellSessionId,
|
|
186
|
+
stop_reason: 'test',
|
|
187
|
+
});
|
|
188
|
+
let shellOk = false;
|
|
189
|
+
let shellDetail = '';
|
|
190
|
+
try {
|
|
191
|
+
const shellResult = spawnSync(command, {
|
|
192
|
+
input: shellEvent,
|
|
193
|
+
shell: true,
|
|
194
|
+
encoding: 'utf-8',
|
|
195
|
+
timeout: 10_000,
|
|
196
|
+
env: { ...process.env, AI_LENS_PROJECTS: join(homedir(), '.ai-lens-status-check-nonexistent') },
|
|
197
|
+
windowsHide: true,
|
|
198
|
+
});
|
|
199
|
+
if (shellResult.error) throw shellResult.error;
|
|
200
|
+
shellOk = shellResult.status === 0;
|
|
201
|
+
shellDetail = shellOk ? 'exit 0' : `Exit code: ${shellResult.status}\nError: ${(shellResult.stderr || '').trim() || '(no stderr)'}`;
|
|
202
|
+
} catch (err) {
|
|
203
|
+
shellDetail = `Exit code: N/A\nError: ${err.message}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let shellCaptureNote = '';
|
|
207
|
+
try {
|
|
208
|
+
if (existsSync(CAPTURE_LOG_PATH)) {
|
|
209
|
+
const logLines = readFileSync(CAPTURE_LOG_PATH, 'utf-8').split(/\r?\n/).filter(Boolean);
|
|
210
|
+
for (let i = logLines.length - 1; i >= 0; i--) {
|
|
211
|
+
try {
|
|
212
|
+
const entry = JSON.parse(logLines[i]);
|
|
213
|
+
if (entry.session_id === shellSessionId) {
|
|
214
|
+
shellCaptureNote = entry.reason || entry.msg || 'unknown';
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
} catch { /* skip unparseable lines */ }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch { /* best effort */ }
|
|
221
|
+
|
|
222
|
+
toolResults.push({ name, ok: exitOk, cmd: testCmd, exitDetail, captureNote, shellOk, shellCmd: command, shellDetail, shellCaptureNote });
|
|
145
223
|
}
|
|
146
224
|
|
|
147
|
-
const allOk = toolResults.every(r => r.ok);
|
|
148
|
-
const failedTools = toolResults.filter(r => !r.ok).map(r => r.name);
|
|
225
|
+
const allOk = toolResults.every(r => r.ok && r.shellOk);
|
|
226
|
+
const failedTools = toolResults.filter(r => !r.ok || !r.shellOk).map(r => r.name);
|
|
149
227
|
const summaryParts = toolResults.map(r => {
|
|
150
|
-
const
|
|
151
|
-
|
|
228
|
+
const directStatus = r.ok ? 'OK' : 'FAILED';
|
|
229
|
+
const shellStatus = r.shellOk ? 'OK' : 'FAILED';
|
|
230
|
+
const parts = [`${r.name}: ${directStatus}`];
|
|
231
|
+
if (!r.shellOk) parts[0] += `, shell: ${shellStatus}`;
|
|
232
|
+
if (r.captureNote) parts[0] += ` (${r.captureNote})`;
|
|
233
|
+
return parts[0];
|
|
152
234
|
});
|
|
153
235
|
const summary = allOk
|
|
154
236
|
? `capture runs OK (${summaryParts.join(', ')})`
|
|
155
237
|
: `capture failed for: ${failedTools.join(', ')}`;
|
|
156
|
-
const detail = toolResults.map(r =>
|
|
157
|
-
`[${r.name}]\n Ran: ${r.cmd}\n Result: ${r.exitDetail}
|
|
158
|
-
|
|
238
|
+
const detail = toolResults.map(r => {
|
|
239
|
+
let text = `[${r.name}]\n Ran: ${r.cmd}\n Result: ${r.exitDetail}`;
|
|
240
|
+
if (r.captureNote) text += `\n Capture log: ${r.captureNote}`;
|
|
241
|
+
text += `\n Shell: ${r.shellCmd} < (test event)\n Shell result: ${r.shellDetail}`;
|
|
242
|
+
if (r.shellCaptureNote) text += `\n Shell capture log: ${r.shellCaptureNote}`;
|
|
243
|
+
return text;
|
|
244
|
+
}).join('\n\n');
|
|
159
245
|
|
|
160
246
|
return { ok: allOk, summary, detail };
|
|
161
247
|
}
|
|
@@ -277,6 +363,18 @@ function checkHooks(tool) {
|
|
|
277
363
|
};
|
|
278
364
|
}
|
|
279
365
|
|
|
366
|
+
// Path validation: when hooks are present, verify command paths exist
|
|
367
|
+
if (mapped.ok === true) {
|
|
368
|
+
const pathIssues = validateHookCommandPaths(tool);
|
|
369
|
+
if (pathIssues) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
summary: 'hooks current BUT command path missing',
|
|
373
|
+
detail: `${detail}\n\nCommand path issues:\n${pathIssues.map(i => ` \u2717 ${i}`).join('\n')}`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
280
378
|
return { ok: mapped.ok, summary: mapped.label, detail };
|
|
281
379
|
}
|
|
282
380
|
|
|
@@ -298,8 +396,15 @@ function checkQueue() {
|
|
|
298
396
|
}
|
|
299
397
|
} catch { /* best effort */ }
|
|
300
398
|
|
|
399
|
+
// Check sender lock
|
|
400
|
+
const lockPath = join(SENDING_DIR, '.sender.lock');
|
|
401
|
+
let lockStatus = 'none';
|
|
402
|
+
if (existsSync(lockPath)) {
|
|
403
|
+
lockStatus = isLockStale(lockPath) ? 'stale' : 'active';
|
|
404
|
+
}
|
|
405
|
+
|
|
301
406
|
// Show last 3 pending events (read individual files)
|
|
302
|
-
let detail = `Pending dir: ${PENDING_DIR}\nPending: ${pendingCount} events\nSending: ${sendingCount} events (in-flight)`;
|
|
407
|
+
let detail = `Pending dir: ${PENDING_DIR}\nPending: ${pendingCount} events\nSending: ${sendingCount} events (in-flight)\nSender lock: ${lockStatus}`;
|
|
303
408
|
if (pendingCount > 0) {
|
|
304
409
|
try {
|
|
305
410
|
const files = readdirSync(PENDING_DIR).filter(f => f.endsWith('.json')).sort().slice(-3);
|
|
@@ -315,11 +420,13 @@ function checkQueue() {
|
|
|
315
420
|
} catch { /* best effort */ }
|
|
316
421
|
}
|
|
317
422
|
|
|
318
|
-
const ok = pendingCount < 100
|
|
319
|
-
|
|
423
|
+
const ok = pendingCount < 100 && lockStatus !== 'stale';
|
|
424
|
+
let summary = pendingCount === 0 && sendingCount === 0
|
|
320
425
|
? 'empty (0 events)'
|
|
321
426
|
: `${pendingCount} pending, ${sendingCount} sending`;
|
|
322
|
-
|
|
427
|
+
if (lockStatus === 'stale') summary += ', STALE lock';
|
|
428
|
+
else if (lockStatus === 'active') summary += ', sender active';
|
|
429
|
+
return { ok, summary, detail, lineCount: pendingCount, lockStatus };
|
|
323
430
|
}
|
|
324
431
|
|
|
325
432
|
function checkSenderLog() {
|
|
@@ -424,6 +531,88 @@ function checkCaptureLog() {
|
|
|
424
531
|
};
|
|
425
532
|
}
|
|
426
533
|
|
|
534
|
+
function checkRealActivity() {
|
|
535
|
+
try {
|
|
536
|
+
if (!existsSync(SESSION_PATHS_DIR)) {
|
|
537
|
+
return { ok: false, summary: 'no real sessions ever captured', detail: `Session paths dir not found: ${SESSION_PATHS_DIR}`, hasEverCaptured: false, lastCaptureMs: 0 };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const files = readdirSync(SESSION_PATHS_DIR).filter(f => !f.startsWith('.'));
|
|
541
|
+
if (files.length === 0) {
|
|
542
|
+
return { ok: false, summary: 'no real sessions ever captured', detail: `Session paths dir empty: ${SESSION_PATHS_DIR}`, hasEverCaptured: false, lastCaptureMs: 0 };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Find newest file by mtime
|
|
546
|
+
let newestMtime = 0;
|
|
547
|
+
for (const file of files) {
|
|
548
|
+
try {
|
|
549
|
+
const st = statSync(join(SESSION_PATHS_DIR, file));
|
|
550
|
+
if (st.mtimeMs > newestMtime) newestMtime = st.mtimeMs;
|
|
551
|
+
} catch { /* skip unreadable */ }
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const lastCaptureAgo = newestMtime ? relativeTime(new Date(newestMtime).toISOString()) : '(unknown)';
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
ok: true,
|
|
558
|
+
summary: `${files.length} sessions, last real capture ${lastCaptureAgo}`,
|
|
559
|
+
detail: `Session paths dir: ${SESSION_PATHS_DIR}\nSession files: ${files.length}\nLast capture: ${lastCaptureAgo}`,
|
|
560
|
+
hasEverCaptured: true,
|
|
561
|
+
lastCaptureMs: newestMtime,
|
|
562
|
+
};
|
|
563
|
+
} catch (err) {
|
|
564
|
+
return { ok: null, summary: `error: ${err.message}`, detail: `Error reading session paths: ${err.message}`, hasEverCaptured: false, lastCaptureMs: 0 };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function checkServerSessions(serverUrl, authToken) {
|
|
569
|
+
if (!serverUrl || !authToken) {
|
|
570
|
+
const missing = !serverUrl ? 'server URL' : 'auth token';
|
|
571
|
+
return { ok: null, summary: `skipped (no ${missing})`, detail: `Cannot check server sessions: missing ${missing}`, sessionCount: null };
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const url = `${serverUrl}/api/sessions?days=7`;
|
|
575
|
+
try {
|
|
576
|
+
const res = await fetch(url, {
|
|
577
|
+
headers: { 'X-Auth-Token': authToken },
|
|
578
|
+
signal: AbortSignal.timeout(8000),
|
|
579
|
+
});
|
|
580
|
+
if (!res.ok) {
|
|
581
|
+
return { ok: null, summary: `HTTP ${res.status}`, detail: `GET ${url} \u2192 ${res.status}`, sessionCount: null };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const data = await res.json();
|
|
585
|
+
const sessions = Array.isArray(data) ? data : (data.sessions || []);
|
|
586
|
+
const count = sessions.length;
|
|
587
|
+
|
|
588
|
+
// Group by source
|
|
589
|
+
const bySource = {};
|
|
590
|
+
for (const s of sessions) {
|
|
591
|
+
const src = s.source || 'unknown';
|
|
592
|
+
bySource[src] = (bySource[src] || 0) + 1;
|
|
593
|
+
}
|
|
594
|
+
const breakdown = Object.entries(bySource).map(([s, n]) => `${s}: ${n}`).join(', ');
|
|
595
|
+
|
|
596
|
+
if (count === 0) {
|
|
597
|
+
return {
|
|
598
|
+
ok: false,
|
|
599
|
+
summary: 'no sessions on server in last 7 days',
|
|
600
|
+
detail: `GET ${url} \u2192 0 sessions`,
|
|
601
|
+
sessionCount: 0,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
ok: true,
|
|
607
|
+
summary: `${count} sessions in last 7 days${breakdown ? ` (${breakdown})` : ''}`,
|
|
608
|
+
detail: `GET ${url} \u2192 ${count} sessions\nBy source: ${breakdown}`,
|
|
609
|
+
sessionCount: count,
|
|
610
|
+
};
|
|
611
|
+
} catch (err) {
|
|
612
|
+
return { ok: null, summary: `error (${err.message})`, detail: `GET ${url}\nError: ${err.message}`, sessionCount: null };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
427
616
|
async function checkServer(serverUrl) {
|
|
428
617
|
if (!serverUrl) {
|
|
429
618
|
return { ok: false, summary: 'no server URL configured', detail: 'Cannot check server: no serverUrl in config' };
|
|
@@ -521,11 +710,65 @@ async function checkE2e(serverUrl, authToken) {
|
|
|
521
710
|
}
|
|
522
711
|
}
|
|
523
712
|
|
|
713
|
+
// ---------------------------------------------------------------------------
|
|
714
|
+
// Summary warnings
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
|
|
717
|
+
function buildWarnings(results, { hasEverCaptured, lastCaptureMs, serverSessionCount, queuePending, lockStatus, serverReachable }) {
|
|
718
|
+
const warnings = [];
|
|
719
|
+
|
|
720
|
+
// All checks pass but no real events captured
|
|
721
|
+
if (hasEverCaptured === false) {
|
|
722
|
+
const allChecksOk = results.every(r => r.ok !== false);
|
|
723
|
+
if (allChecksOk) {
|
|
724
|
+
warnings.push({
|
|
725
|
+
msg: 'All checks pass but no real events captured.',
|
|
726
|
+
action: 'Restart Claude Code / Cursor to activate hooks.',
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Local events exist but server has none
|
|
732
|
+
if (hasEverCaptured && serverSessionCount === 0) {
|
|
733
|
+
warnings.push({
|
|
734
|
+
msg: 'Events captured locally but server has 0 sessions in last 7 days.',
|
|
735
|
+
action: 'Check sender.log for errors.',
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Last real capture > 24h ago
|
|
740
|
+
if (lastCaptureMs && Date.now() - lastCaptureMs > 24 * 60 * 60 * 1000) {
|
|
741
|
+
const ago = relativeTime(new Date(lastCaptureMs).toISOString());
|
|
742
|
+
warnings.push({
|
|
743
|
+
msg: `Last real capture was ${ago}.`,
|
|
744
|
+
action: 'If you\'ve worked since then, hooks may not be firing. Restart your IDE.',
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Queue has many events pending while server is reachable
|
|
749
|
+
if (queuePending > 50 && serverReachable) {
|
|
750
|
+
warnings.push({
|
|
751
|
+
msg: `Queue has ${queuePending} pending events.`,
|
|
752
|
+
action: 'Sender may be stuck. Check sender.log.',
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Stale sender lock
|
|
757
|
+
if (lockStatus === 'stale') {
|
|
758
|
+
warnings.push({
|
|
759
|
+
msg: 'Sender lock is stale.',
|
|
760
|
+
action: 'Queue processing may be blocked. Delete sending/.sender.lock manually.',
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return warnings;
|
|
765
|
+
}
|
|
766
|
+
|
|
524
767
|
// ---------------------------------------------------------------------------
|
|
525
768
|
// Report file generation
|
|
526
769
|
// ---------------------------------------------------------------------------
|
|
527
770
|
|
|
528
|
-
function buildReport(results, timestamp) {
|
|
771
|
+
function buildReport(results, timestamp, warnings = []) {
|
|
529
772
|
const lines = [
|
|
530
773
|
`AI Lens Status Report`,
|
|
531
774
|
`Generated: ${timestamp}`,
|
|
@@ -544,6 +787,17 @@ function buildReport(results, timestamp) {
|
|
|
544
787
|
lines.push('');
|
|
545
788
|
}
|
|
546
789
|
|
|
790
|
+
// Warnings
|
|
791
|
+
if (warnings.length > 0) {
|
|
792
|
+
lines.push(`${'='.repeat(60)}`);
|
|
793
|
+
lines.push('Warnings:');
|
|
794
|
+
for (const w of warnings) {
|
|
795
|
+
lines.push(` \u26a0 ${w.msg}`);
|
|
796
|
+
lines.push(` \u2192 ${w.action}`);
|
|
797
|
+
}
|
|
798
|
+
lines.push('');
|
|
799
|
+
}
|
|
800
|
+
|
|
547
801
|
// Full config (masked)
|
|
548
802
|
lines.push(`${'='.repeat(60)}`);
|
|
549
803
|
lines.push('Full config (~/.ai-lens/config.json):');
|
|
@@ -706,34 +960,64 @@ export default async function status() {
|
|
|
706
960
|
}
|
|
707
961
|
|
|
708
962
|
// 7. Queue (before capture test so test event doesn't show as pending)
|
|
709
|
-
|
|
963
|
+
const queueResult = checkQueue();
|
|
964
|
+
printLine('Queue', queueResult);
|
|
710
965
|
|
|
711
|
-
//
|
|
966
|
+
// 8. Smoke-test the hook command
|
|
712
967
|
printLine('Capture test', checkCaptureRun(installedTools));
|
|
713
968
|
|
|
714
|
-
//
|
|
969
|
+
// 9. Sender log
|
|
715
970
|
printLine('Sender log', checkSenderLog());
|
|
716
971
|
|
|
717
|
-
//
|
|
972
|
+
// 10. Capture drops
|
|
718
973
|
printLine('Capture drops', checkCaptureLog());
|
|
719
974
|
|
|
720
|
-
//
|
|
975
|
+
// 11. Real activity
|
|
976
|
+
const realActivityResult = checkRealActivity();
|
|
977
|
+
printLine('Real activity', realActivityResult);
|
|
978
|
+
|
|
979
|
+
// 12. Server connectivity
|
|
721
980
|
const serverUrl = configResult.serverUrl || readLensConfig().serverUrl;
|
|
722
981
|
const serverResult = await checkServer(serverUrl);
|
|
723
982
|
printLine('Server', serverResult);
|
|
724
983
|
|
|
725
|
-
//
|
|
984
|
+
// 13. Token validity
|
|
726
985
|
const authToken = configResult.authToken || readLensConfig().authToken;
|
|
727
986
|
const tokenResult = await checkToken(serverUrl, authToken);
|
|
728
987
|
printLine('Token', tokenResult);
|
|
729
988
|
|
|
730
|
-
//
|
|
989
|
+
// 14. Server sessions
|
|
990
|
+
const serverSessionsResult = await checkServerSessions(serverUrl, authToken);
|
|
991
|
+
printLine('Server sessions', serverSessionsResult);
|
|
992
|
+
|
|
993
|
+
// 15. E2E connectivity test
|
|
731
994
|
const e2eResult = await checkE2e(serverUrl, authToken);
|
|
732
995
|
printLine('E2E test', e2eResult);
|
|
733
996
|
|
|
997
|
+
// 16. Summary warnings
|
|
998
|
+
const warnings = buildWarnings(results, {
|
|
999
|
+
hasEverCaptured: realActivityResult.hasEverCaptured,
|
|
1000
|
+
lastCaptureMs: realActivityResult.lastCaptureMs,
|
|
1001
|
+
serverSessionCount: serverSessionsResult.sessionCount,
|
|
1002
|
+
queuePending: queueResult.lineCount,
|
|
1003
|
+
lockStatus: queueResult.lockStatus,
|
|
1004
|
+
serverReachable: serverResult.ok === true,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
if (warnings.length > 0) {
|
|
1008
|
+
blank();
|
|
1009
|
+
info('='.repeat(40));
|
|
1010
|
+
info(`${BOLD}Warnings${RESET}`);
|
|
1011
|
+
blank();
|
|
1012
|
+
for (const w of warnings) {
|
|
1013
|
+
info(`${TRIANGLE} ${w.msg}`);
|
|
1014
|
+
info(` \u2192 ${w.action}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
734
1018
|
// Write report file
|
|
735
1019
|
const timestamp = new Date().toISOString();
|
|
736
|
-
const report = buildReport(results, timestamp);
|
|
1020
|
+
const report = buildReport(results, timestamp, warnings);
|
|
737
1021
|
try {
|
|
738
1022
|
writeFileSync(REPORT_PATH, report);
|
|
739
1023
|
blank();
|