ai-lens 0.6.9 → 0.7.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/.commithash +1 -1
- package/bin/ai-lens.js +6 -0
- package/cli/init.js +59 -0
- package/cli/status.js +466 -0
- package/package.json +1 -1
package/.commithash
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
7773594
|
package/bin/ai-lens.js
CHANGED
|
@@ -13,6 +13,11 @@ switch (command) {
|
|
|
13
13
|
await remove();
|
|
14
14
|
break;
|
|
15
15
|
}
|
|
16
|
+
case 'status': {
|
|
17
|
+
const { default: status } = await import('../cli/status.js');
|
|
18
|
+
await status();
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
16
21
|
case 'version':
|
|
17
22
|
case '--version':
|
|
18
23
|
case '-v': {
|
|
@@ -37,6 +42,7 @@ switch (command) {
|
|
|
37
42
|
console.log(' --no-mcp Skip MCP server registration');
|
|
38
43
|
console.log(' --mcp-scope S MCP scope: user, local, or project (default: user)');
|
|
39
44
|
console.log(' remove Remove AI Lens hooks and client files');
|
|
45
|
+
console.log(' status Run diagnostics and generate a status report');
|
|
40
46
|
console.log(' version Show package version and commit hash');
|
|
41
47
|
process.exit(command ? 1 : 0);
|
|
42
48
|
}
|
package/cli/init.js
CHANGED
|
@@ -529,5 +529,64 @@ export default async function init() {
|
|
|
529
529
|
blank();
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
+
// Quick verification
|
|
533
|
+
heading('Verification');
|
|
534
|
+
const finalConfig = readLensConfig();
|
|
535
|
+
const verifyUrl = finalConfig.serverUrl;
|
|
536
|
+
|
|
537
|
+
if (verifyUrl) {
|
|
538
|
+
// 1. Server reachable?
|
|
539
|
+
try {
|
|
540
|
+
const start = Date.now();
|
|
541
|
+
const health = await getJson(`${verifyUrl}/api/health`);
|
|
542
|
+
const ms = Date.now() - start;
|
|
543
|
+
if (health.status === 'ok') {
|
|
544
|
+
success(` Server: reachable (${ms}ms)`);
|
|
545
|
+
} else {
|
|
546
|
+
warn(` Server: responded but unexpected status: ${JSON.stringify(health)}`);
|
|
547
|
+
}
|
|
548
|
+
} catch (err) {
|
|
549
|
+
error(` Server: unreachable — ${err.message}`);
|
|
550
|
+
info(' Check server URL and make sure the server is running.');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 2. Token valid?
|
|
554
|
+
if (finalConfig.authToken) {
|
|
555
|
+
try {
|
|
556
|
+
const parsed = new URL(`${verifyUrl}/api/dashboard/overview`);
|
|
557
|
+
const isHttps = parsed.protocol === 'https:';
|
|
558
|
+
const requestFn = isHttps ? httpsRequest : httpRequest;
|
|
559
|
+
const status = await new Promise((resolve, reject) => {
|
|
560
|
+
const req = requestFn({
|
|
561
|
+
hostname: parsed.hostname,
|
|
562
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
563
|
+
path: parsed.pathname,
|
|
564
|
+
method: 'GET',
|
|
565
|
+
headers: { 'X-Auth-Token': finalConfig.authToken },
|
|
566
|
+
timeout: 10_000,
|
|
567
|
+
}, (res) => {
|
|
568
|
+
res.resume(); // drain
|
|
569
|
+
resolve(res.statusCode);
|
|
570
|
+
});
|
|
571
|
+
req.on('error', reject);
|
|
572
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
573
|
+
req.end();
|
|
574
|
+
});
|
|
575
|
+
if (status === 200) {
|
|
576
|
+
success(' Token: valid');
|
|
577
|
+
} else if (status === 401) {
|
|
578
|
+
error(' Token: invalid (401) — re-run init to re-authenticate');
|
|
579
|
+
} else {
|
|
580
|
+
warn(` Token: unexpected response (HTTP ${status})`);
|
|
581
|
+
}
|
|
582
|
+
} catch (err) {
|
|
583
|
+
warn(` Token: could not verify — ${err.message}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
warn(' No server URL configured — skipping verification');
|
|
588
|
+
}
|
|
589
|
+
blank();
|
|
590
|
+
|
|
532
591
|
detail(`Log: ${getLogPath()}`);
|
|
533
592
|
}
|
package/cli/status.js
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
|
|
6
|
+
import { getVersionInfo, readLensConfig, detectInstalledTools, analyzeToolHooks, CAPTURE_PATH, TOOL_CONFIGS } from './hooks.js';
|
|
7
|
+
import { DATA_DIR, QUEUE_PATH, LOG_PATH, getGitIdentity } from '../client/config.js';
|
|
8
|
+
import { initLogger, info, success, warn, error, heading, blank } from './logger.js';
|
|
9
|
+
|
|
10
|
+
// ANSI helpers
|
|
11
|
+
const RESET = '\x1b[0m';
|
|
12
|
+
const BOLD = '\x1b[1m';
|
|
13
|
+
const DIM = '\x1b[2m';
|
|
14
|
+
const GREEN = '\x1b[32m';
|
|
15
|
+
const RED = '\x1b[31m';
|
|
16
|
+
|
|
17
|
+
const CHECK = `${GREEN}\u2713${RESET}`;
|
|
18
|
+
const CROSS = `${RED}\u2717${RESET}`;
|
|
19
|
+
|
|
20
|
+
const REPORT_PATH = join(DATA_DIR, 'status-report.txt');
|
|
21
|
+
|
|
22
|
+
function maskToken(token) {
|
|
23
|
+
if (!token || token.length < 20) return token || '(none)';
|
|
24
|
+
return token.slice(0, 14) + '...' + token.slice(-4);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function execSafe(cmd, opts = {}) {
|
|
28
|
+
try {
|
|
29
|
+
return execSync(cmd, { encoding: 'utf-8', timeout: 5000, ...opts }).trim();
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function relativeTime(dateStr) {
|
|
36
|
+
const diff = Date.now() - new Date(dateStr).getTime();
|
|
37
|
+
if (diff < 0) return 'just now';
|
|
38
|
+
const secs = Math.floor(diff / 1000);
|
|
39
|
+
if (secs < 60) return `${secs}s ago`;
|
|
40
|
+
const mins = Math.floor(secs / 60);
|
|
41
|
+
if (mins < 60) return `${mins}m ago`;
|
|
42
|
+
const hours = Math.floor(mins / 60);
|
|
43
|
+
if (hours < 24) return `${hours}h ago`;
|
|
44
|
+
const days = Math.floor(hours / 24);
|
|
45
|
+
return `${days}d ago`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Individual checks — each returns { ok, summary, detail }
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
function checkVersion() {
|
|
53
|
+
const { version, commit } = getVersionInfo();
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
summary: `v${version} (${commit})`,
|
|
57
|
+
detail: `Version: ${version}\nCommit: ${commit}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function checkSystem() {
|
|
62
|
+
const osVersion = execSafe('sw_vers -productVersion') || execSafe('uname -r') || 'unknown';
|
|
63
|
+
const arch = execSafe('uname -m') || 'unknown';
|
|
64
|
+
const nodeVersion = execSafe('node --version') || 'unknown';
|
|
65
|
+
const platform = process.platform === 'darwin' ? 'macOS' : process.platform;
|
|
66
|
+
const summary = `${platform} ${osVersion} ${arch}, Node ${nodeVersion}`;
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
summary,
|
|
70
|
+
detail: `Platform: ${platform}\nOS version: ${osVersion}\nArchitecture: ${arch}\nNode: ${nodeVersion}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function checkToolVersion(appName) {
|
|
75
|
+
const plistPath = `/Applications/${appName}.app/Contents/Info.plist`;
|
|
76
|
+
const version = execSafe(`defaults read "${plistPath}" CFBundleShortVersionString 2>/dev/null`);
|
|
77
|
+
if (version) {
|
|
78
|
+
return { ok: true, summary: `v${version}`, detail: `${appName}: v${version}` };
|
|
79
|
+
}
|
|
80
|
+
return { ok: null, summary: 'not installed', detail: `${appName}: not installed` };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function checkGitIdentity() {
|
|
84
|
+
const { name, email } = getGitIdentity();
|
|
85
|
+
if (name && email) {
|
|
86
|
+
return { ok: true, summary: `${name} <${email}>`, detail: `Name: ${name}\nEmail: ${email}` };
|
|
87
|
+
}
|
|
88
|
+
if (email) {
|
|
89
|
+
return { ok: true, summary: email, detail: `Email: ${email}\nName: (not set)` };
|
|
90
|
+
}
|
|
91
|
+
return { ok: false, summary: 'not configured', detail: 'Git identity not configured (git config user.email)' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function checkClientFiles() {
|
|
95
|
+
const dir = join(homedir(), '.ai-lens', 'client');
|
|
96
|
+
const files = ['capture.js', 'sender.js', 'config.js'];
|
|
97
|
+
const results = files.map(f => ({ name: f, exists: existsSync(join(dir, f)) }));
|
|
98
|
+
const found = results.filter(r => r.exists).length;
|
|
99
|
+
const ok = found === files.length;
|
|
100
|
+
const detail = results.map(r => ` ${r.exists ? '\u2713' : '\u2717'} ${r.name}`).join('\n');
|
|
101
|
+
return {
|
|
102
|
+
ok,
|
|
103
|
+
summary: ok ? `installed (${found}/${files.length})` : `missing (${found}/${files.length})`,
|
|
104
|
+
detail: `Client files in ${dir}:\n${detail}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function checkConfig() {
|
|
109
|
+
const config = readLensConfig();
|
|
110
|
+
const serverUrl = config.serverUrl || '(default: http://localhost:3000)';
|
|
111
|
+
const token = maskToken(config.authToken);
|
|
112
|
+
const projects = config.projects || '(all)';
|
|
113
|
+
const hasServer = !!config.serverUrl;
|
|
114
|
+
const hasToken = !!config.authToken;
|
|
115
|
+
const ok = hasServer && hasToken;
|
|
116
|
+
return {
|
|
117
|
+
ok,
|
|
118
|
+
summary: `serverUrl=${hasServer ? config.serverUrl : '(not set)'}, token=${token}`,
|
|
119
|
+
detail: `Server URL: ${serverUrl}\nAuth token: ${token}\nProjects: ${projects}\nRaw config: ${JSON.stringify(config, (k, v) => k === 'authToken' && v ? maskToken(v) : v, 2)}`,
|
|
120
|
+
serverUrl: config.serverUrl,
|
|
121
|
+
authToken: config.authToken,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function checkHooks(tool) {
|
|
126
|
+
const analysis = analyzeToolHooks(tool);
|
|
127
|
+
const statusMap = {
|
|
128
|
+
fresh: { ok: null, label: 'no config file' },
|
|
129
|
+
current: { ok: true, label: 'hooks current' },
|
|
130
|
+
outdated: { ok: false, label: 'hooks outdated' },
|
|
131
|
+
absent: { ok: false, label: 'hooks absent' },
|
|
132
|
+
malformed: { ok: false, label: 'hooks malformed' },
|
|
133
|
+
};
|
|
134
|
+
const mapped = statusMap[analysis.status] || { ok: false, label: analysis.status };
|
|
135
|
+
|
|
136
|
+
let detail = `Config path: ${tool.configPath}\nStatus: ${analysis.status}`;
|
|
137
|
+
if (analysis.error) detail += `\nError: ${analysis.error}`;
|
|
138
|
+
|
|
139
|
+
// Include full hook config content
|
|
140
|
+
try {
|
|
141
|
+
const raw = readFileSync(tool.configPath, 'utf-8');
|
|
142
|
+
const parsed = JSON.parse(raw);
|
|
143
|
+
if (parsed.hooks) {
|
|
144
|
+
detail += `\nHook config:\n${JSON.stringify(parsed.hooks, null, 2)}`;
|
|
145
|
+
}
|
|
146
|
+
} catch { /* file doesn't exist or isn't valid */ }
|
|
147
|
+
|
|
148
|
+
return { ok: mapped.ok, summary: mapped.label, detail };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function checkQueue() {
|
|
152
|
+
if (!existsSync(QUEUE_PATH)) {
|
|
153
|
+
return { ok: true, summary: 'empty (0 events)', detail: 'Queue file does not exist (no pending events)' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let lines;
|
|
157
|
+
try {
|
|
158
|
+
lines = readFileSync(QUEUE_PATH, 'utf-8').split('\n').filter(Boolean);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
return { ok: false, summary: `error reading queue: ${err.message}`, detail: `Error: ${err.message}` };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const count = lines.length;
|
|
164
|
+
const size = statSync(QUEUE_PATH).size;
|
|
165
|
+
const sizeStr = size < 1024 ? `${size}B` : `${(size / 1024).toFixed(1)}KB`;
|
|
166
|
+
|
|
167
|
+
let detail = `Queue: ${QUEUE_PATH}\nEvents: ${count}\nSize: ${sizeStr}`;
|
|
168
|
+
|
|
169
|
+
// Show last 3 events (truncated)
|
|
170
|
+
const last3 = lines.slice(-3);
|
|
171
|
+
if (last3.length > 0) {
|
|
172
|
+
detail += '\n\nLast events:';
|
|
173
|
+
for (const line of last3) {
|
|
174
|
+
try {
|
|
175
|
+
const evt = JSON.parse(line);
|
|
176
|
+
detail += `\n ${evt.type || 'unknown'} @ ${evt.timestamp || '?'} [${evt.session_id?.slice(0, 8) || '?'}]`;
|
|
177
|
+
} catch {
|
|
178
|
+
detail += `\n (unparseable line, ${line.length} chars)`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Queue with events = something may be stuck
|
|
184
|
+
const ok = count === 0;
|
|
185
|
+
return {
|
|
186
|
+
ok,
|
|
187
|
+
summary: ok ? 'empty (0 events)' : `${count} events pending (${sizeStr})`,
|
|
188
|
+
detail,
|
|
189
|
+
lineCount: count,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function checkSenderLog() {
|
|
194
|
+
if (!existsSync(LOG_PATH)) {
|
|
195
|
+
return { ok: null, summary: 'no log file', detail: 'Sender log does not exist (sender has not run yet)' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let lines;
|
|
199
|
+
try {
|
|
200
|
+
lines = readFileSync(LOG_PATH, 'utf-8').split('\n').filter(Boolean);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return { ok: false, summary: `error reading log: ${err.message}`, detail: `Error: ${err.message}` };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const last20 = lines.slice(-20);
|
|
206
|
+
let lastSend = null;
|
|
207
|
+
let hasErrors = false;
|
|
208
|
+
|
|
209
|
+
for (const line of last20) {
|
|
210
|
+
try {
|
|
211
|
+
const entry = JSON.parse(line);
|
|
212
|
+
if (entry.msg === 'batch_sent' || entry.msg === 'send_ok') lastSend = entry.ts;
|
|
213
|
+
if (entry.msg === 'send_error' || entry.msg === 'send_fail' || entry.level === 'error') hasErrors = true;
|
|
214
|
+
} catch { /* non-JSON line */ }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let summary;
|
|
218
|
+
if (lastSend) {
|
|
219
|
+
summary = `last send ${relativeTime(lastSend)}`;
|
|
220
|
+
if (hasErrors) summary += ', recent errors';
|
|
221
|
+
} else if (hasErrors) {
|
|
222
|
+
summary = 'recent errors in log';
|
|
223
|
+
} else {
|
|
224
|
+
summary = `${lines.length} entries`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
ok: !hasErrors,
|
|
229
|
+
summary,
|
|
230
|
+
detail: `Log: ${LOG_PATH}\nTotal entries: ${lines.length}\nLast 20 entries:\n${last20.join('\n')}`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function checkServer(serverUrl) {
|
|
235
|
+
if (!serverUrl) {
|
|
236
|
+
return { ok: false, summary: 'no server URL configured', detail: 'Cannot check server: no serverUrl in config' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const url = `${serverUrl}/api/health`;
|
|
240
|
+
const start = Date.now();
|
|
241
|
+
try {
|
|
242
|
+
const res = await fetch(url);
|
|
243
|
+
const latency = Date.now() - start;
|
|
244
|
+
const body = await res.text();
|
|
245
|
+
if (res.ok) {
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
summary: `reachable (${latency}ms)`,
|
|
249
|
+
detail: `URL: ${url}\nStatus: ${res.status}\nLatency: ${latency}ms\nResponse: ${body}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
summary: `HTTP ${res.status} (${latency}ms)`,
|
|
255
|
+
detail: `URL: ${url}\nStatus: ${res.status}\nLatency: ${latency}ms\nResponse: ${body}`,
|
|
256
|
+
};
|
|
257
|
+
} catch (err) {
|
|
258
|
+
const latency = Date.now() - start;
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
summary: `unreachable (${err.cause?.code || err.message})`,
|
|
262
|
+
detail: `URL: ${url}\nError: ${err.message}\nLatency: ${latency}ms`,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function checkToken(serverUrl, authToken) {
|
|
268
|
+
if (!serverUrl || !authToken) {
|
|
269
|
+
const missing = !serverUrl ? 'server URL' : 'auth token';
|
|
270
|
+
return { ok: false, summary: `no ${missing} configured`, detail: `Cannot validate token: missing ${missing}` };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const url = `${serverUrl}/api/dashboard/overview`;
|
|
274
|
+
try {
|
|
275
|
+
const res = await fetch(url, { headers: { 'X-Auth-Token': authToken } });
|
|
276
|
+
if (res.ok) {
|
|
277
|
+
return { ok: true, summary: 'valid', detail: `Token validation: ${res.status} OK` };
|
|
278
|
+
}
|
|
279
|
+
if (res.status === 401) {
|
|
280
|
+
return { ok: false, summary: 'invalid (401)', detail: `Token validation: 401 Unauthorized` };
|
|
281
|
+
}
|
|
282
|
+
return { ok: false, summary: `HTTP ${res.status}`, detail: `Token validation: unexpected status ${res.status}` };
|
|
283
|
+
} catch (err) {
|
|
284
|
+
return { ok: false, summary: `error (${err.message})`, detail: `Token validation error: ${err.message}` };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Report file generation
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
function buildReport(results, timestamp) {
|
|
293
|
+
const lines = [
|
|
294
|
+
`AI Lens Status Report`,
|
|
295
|
+
`Generated: ${timestamp}`,
|
|
296
|
+
`${'='.repeat(60)}`,
|
|
297
|
+
'',
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
for (const r of results) {
|
|
301
|
+
const icon = r.ok === true ? '\u2713' : r.ok === false ? '\u2717' : '-';
|
|
302
|
+
lines.push(`[${icon}] ${r.label}: ${r.summary}`);
|
|
303
|
+
if (r.detail) {
|
|
304
|
+
for (const dl of r.detail.split('\n')) {
|
|
305
|
+
lines.push(` ${dl}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
lines.push('');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Full config (masked)
|
|
312
|
+
lines.push(`${'='.repeat(60)}`);
|
|
313
|
+
lines.push('Full config (~/.ai-lens/config.json):');
|
|
314
|
+
try {
|
|
315
|
+
const raw = readFileSync(join(homedir(), '.ai-lens', 'config.json'), 'utf-8');
|
|
316
|
+
const config = JSON.parse(raw);
|
|
317
|
+
if (config.authToken) config.authToken = maskToken(config.authToken);
|
|
318
|
+
lines.push(JSON.stringify(config, null, 2));
|
|
319
|
+
} catch {
|
|
320
|
+
lines.push('(not found or unreadable)');
|
|
321
|
+
}
|
|
322
|
+
lines.push('');
|
|
323
|
+
|
|
324
|
+
// Full hook configs
|
|
325
|
+
for (const tool of TOOL_CONFIGS) {
|
|
326
|
+
lines.push(`${'='.repeat(60)}`);
|
|
327
|
+
lines.push(`${tool.name} hook config (${tool.configPath}):`);
|
|
328
|
+
try {
|
|
329
|
+
const raw = readFileSync(tool.configPath, 'utf-8');
|
|
330
|
+
const parsed = JSON.parse(raw);
|
|
331
|
+
if (parsed.hooks) {
|
|
332
|
+
lines.push(JSON.stringify(parsed.hooks, null, 2));
|
|
333
|
+
} else {
|
|
334
|
+
lines.push('(no hooks section)');
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
lines.push('(not found or unreadable)');
|
|
338
|
+
}
|
|
339
|
+
lines.push('');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Queue content (up to 100 lines)
|
|
343
|
+
lines.push(`${'='.repeat(60)}`);
|
|
344
|
+
lines.push(`Queue (${QUEUE_PATH}):`);
|
|
345
|
+
try {
|
|
346
|
+
const queueLines = readFileSync(QUEUE_PATH, 'utf-8').split('\n').filter(Boolean);
|
|
347
|
+
lines.push(`Total: ${queueLines.length} events`);
|
|
348
|
+
for (const ql of queueLines.slice(0, 100)) {
|
|
349
|
+
lines.push(ql);
|
|
350
|
+
}
|
|
351
|
+
if (queueLines.length > 100) lines.push(`... (${queueLines.length - 100} more)`);
|
|
352
|
+
} catch {
|
|
353
|
+
lines.push('(empty or not found)');
|
|
354
|
+
}
|
|
355
|
+
lines.push('');
|
|
356
|
+
|
|
357
|
+
// Sender log (last 100 lines)
|
|
358
|
+
lines.push(`${'='.repeat(60)}`);
|
|
359
|
+
lines.push(`Sender log (${LOG_PATH}):`);
|
|
360
|
+
try {
|
|
361
|
+
const logLines = readFileSync(LOG_PATH, 'utf-8').split('\n').filter(Boolean);
|
|
362
|
+
lines.push(`Total: ${logLines.length} entries`);
|
|
363
|
+
for (const ll of logLines.slice(-100)) {
|
|
364
|
+
lines.push(ll);
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
lines.push('(not found)');
|
|
368
|
+
}
|
|
369
|
+
lines.push('');
|
|
370
|
+
|
|
371
|
+
return lines.join('\n');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Main
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
|
|
378
|
+
export default async function status() {
|
|
379
|
+
const versionResult = checkVersion();
|
|
380
|
+
initLogger(versionResult.summary);
|
|
381
|
+
|
|
382
|
+
const { version, commit } = getVersionInfo();
|
|
383
|
+
blank();
|
|
384
|
+
info(`${BOLD}AI Lens Status v${version} (${commit})${RESET}`);
|
|
385
|
+
info('='.repeat(40));
|
|
386
|
+
blank();
|
|
387
|
+
|
|
388
|
+
const results = [];
|
|
389
|
+
|
|
390
|
+
function printLine(label, result) {
|
|
391
|
+
const icon = result.ok === true ? CHECK : result.ok === false ? CROSS : `${DIM}-${RESET}`;
|
|
392
|
+
const pad = ' '.repeat(Math.max(1, 17 - label.length));
|
|
393
|
+
info(`${label}:${pad}${icon} ${result.summary}`);
|
|
394
|
+
results.push({ label, ...result });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 1. System info
|
|
398
|
+
const sys = checkSystem();
|
|
399
|
+
info(`${'System:'} ${sys.summary}`);
|
|
400
|
+
results.push({ label: 'System', ...sys });
|
|
401
|
+
|
|
402
|
+
// 2. Tool versions
|
|
403
|
+
const claude = checkToolVersion('Claude');
|
|
404
|
+
const cursor = checkToolVersion('Cursor');
|
|
405
|
+
if (claude.ok !== null) {
|
|
406
|
+
info(`${'Claude Code:'} ${claude.summary}`);
|
|
407
|
+
results.push({ label: 'Claude Code version', ...claude });
|
|
408
|
+
}
|
|
409
|
+
if (cursor.ok !== null) {
|
|
410
|
+
info(`${'Cursor:'} ${cursor.summary}`);
|
|
411
|
+
results.push({ label: 'Cursor version', ...cursor });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 3. Git identity
|
|
415
|
+
printLine('Git identity', checkGitIdentity());
|
|
416
|
+
|
|
417
|
+
// 4. Client files
|
|
418
|
+
printLine('Client files', checkClientFiles());
|
|
419
|
+
|
|
420
|
+
// 5. Config
|
|
421
|
+
const configResult = checkConfig();
|
|
422
|
+
printLine('Config', configResult);
|
|
423
|
+
|
|
424
|
+
// 6. Hooks per installed tool
|
|
425
|
+
const installedTools = detectInstalledTools();
|
|
426
|
+
for (const tool of installedTools) {
|
|
427
|
+
printLine(tool.name, checkHooks(tool));
|
|
428
|
+
}
|
|
429
|
+
// Also report tools from TOOL_CONFIGS that aren't installed
|
|
430
|
+
for (const tool of TOOL_CONFIGS) {
|
|
431
|
+
if (!installedTools.includes(tool)) {
|
|
432
|
+
const r = { ok: null, summary: 'not installed', detail: `${tool.name} directory not found at ${tool.dirPath}` };
|
|
433
|
+
printLine(tool.name, r);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 7. Queue
|
|
438
|
+
printLine('Queue', checkQueue());
|
|
439
|
+
|
|
440
|
+
// 8. Sender log
|
|
441
|
+
printLine('Sender log', checkSenderLog());
|
|
442
|
+
|
|
443
|
+
// 9. Server connectivity
|
|
444
|
+
const serverUrl = configResult.serverUrl || readLensConfig().serverUrl;
|
|
445
|
+
const serverResult = await checkServer(serverUrl);
|
|
446
|
+
printLine('Server', serverResult);
|
|
447
|
+
|
|
448
|
+
// 10. Token validity
|
|
449
|
+
const authToken = configResult.authToken || readLensConfig().authToken;
|
|
450
|
+
const tokenResult = await checkToken(serverUrl, authToken);
|
|
451
|
+
printLine('Token', tokenResult);
|
|
452
|
+
|
|
453
|
+
// Write report file
|
|
454
|
+
const timestamp = new Date().toISOString();
|
|
455
|
+
const report = buildReport(results, timestamp);
|
|
456
|
+
try {
|
|
457
|
+
writeFileSync(REPORT_PATH, report);
|
|
458
|
+
blank();
|
|
459
|
+
info(`${DIM}Full report \u2192 ${REPORT_PATH}${RESET}`);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
blank();
|
|
462
|
+
error(`Could not write report: ${err.message}`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
blank();
|
|
466
|
+
}
|