@vibelet/cli 0.1.34 → 0.1.36
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/app.json +5 -0
- package/dist/advertised-hosts.d.ts +34 -0
- package/dist/advertised-hosts.d.ts.map +1 -0
- package/dist/advertised-hosts.js +176 -0
- package/dist/advertised-hosts.js.map +1 -0
- package/dist/advertised-hosts.test.d.ts +2 -0
- package/dist/advertised-hosts.test.d.ts.map +1 -0
- package/dist/advertised-hosts.test.js +96 -0
- package/dist/advertised-hosts.test.js.map +1 -0
- package/dist/audit.d.ts +30 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +73 -0
- package/dist/audit.js.map +1 -0
- package/dist/audit.test.d.ts +2 -0
- package/dist/audit.test.d.ts.map +1 -0
- package/dist/audit.test.js +33 -0
- package/dist/audit.test.js.map +1 -0
- package/dist/auth.d.ts +6 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +27 -0
- package/dist/auth.js.map +1 -0
- package/dist/claude-hooks.d.ts +58 -0
- package/dist/claude-hooks.d.ts.map +1 -0
- package/dist/claude-hooks.js +129 -0
- package/dist/claude-hooks.js.map +1 -0
- package/dist/cli-version.d.ts +3 -0
- package/dist/cli-version.d.ts.map +1 -0
- package/dist/cli-version.js +35 -0
- package/dist/cli-version.js.map +1 -0
- package/dist/cli-version.test.d.ts +2 -0
- package/dist/cli-version.test.d.ts.map +1 -0
- package/dist/cli-version.test.js +38 -0
- package/dist/cli-version.test.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +327 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +184 -0
- package/dist/config.test.js.map +1 -0
- package/dist/dev-auth.test.d.ts +2 -0
- package/dist/dev-auth.test.d.ts.map +1 -0
- package/dist/dev-auth.test.js +154 -0
- package/dist/dev-auth.test.js.map +1 -0
- package/dist/dev-script.test.d.ts +2 -0
- package/dist/dev-script.test.d.ts.map +1 -0
- package/dist/dev-script.test.js +412 -0
- package/dist/dev-script.test.js.map +1 -0
- package/dist/drivers/claude.d.ts +34 -0
- package/dist/drivers/claude.d.ts.map +1 -0
- package/dist/drivers/claude.js +413 -0
- package/dist/drivers/claude.js.map +1 -0
- package/dist/drivers/claude.test.d.ts +2 -0
- package/dist/drivers/claude.test.d.ts.map +1 -0
- package/dist/drivers/claude.test.js +951 -0
- package/dist/drivers/claude.test.js.map +1 -0
- package/dist/drivers/codex.d.ts +38 -0
- package/dist/drivers/codex.d.ts.map +1 -0
- package/dist/drivers/codex.js +771 -0
- package/dist/drivers/codex.js.map +1 -0
- package/dist/drivers/codex.test.d.ts +2 -0
- package/dist/drivers/codex.test.d.ts.map +1 -0
- package/dist/drivers/codex.test.js +939 -0
- package/dist/drivers/codex.test.js.map +1 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/drivers/types.d.ts.map +1 -0
- package/dist/drivers/types.js +2 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/e2e.test.d.ts +2 -0
- package/dist/e2e.test.d.ts.map +1 -0
- package/dist/e2e.test.js +111 -0
- package/dist/e2e.test.js.map +1 -0
- package/dist/identity.d.ts +10 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +66 -0
- package/dist/identity.js.map +1 -0
- package/dist/identity.test.d.ts +2 -0
- package/dist/identity.test.d.ts.map +1 -0
- package/dist/identity.test.js +25 -0
- package/dist/identity.test.js.map +1 -0
- package/dist/index-entry.test.d.ts +2 -0
- package/dist/index-entry.test.d.ts.map +1 -0
- package/dist/index-entry.test.js +272 -0
- package/dist/index-entry.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +707 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +31 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +75 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +52 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +89 -0
- package/dist/metrics.js.map +1 -0
- package/dist/pairing-store.d.ts +29 -0
- package/dist/pairing-store.d.ts.map +1 -0
- package/dist/pairing-store.js +131 -0
- package/dist/pairing-store.js.map +1 -0
- package/dist/pairing-store.test.d.ts +2 -0
- package/dist/pairing-store.test.d.ts.map +1 -0
- package/dist/pairing-store.test.js +47 -0
- package/dist/pairing-store.test.js.map +1 -0
- package/dist/paths.d.ts +16 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +18 -0
- package/dist/paths.js.map +1 -0
- package/dist/perf-compare.d.ts +13 -0
- package/dist/perf-compare.d.ts.map +1 -0
- package/dist/perf-compare.js +125 -0
- package/dist/perf-compare.js.map +1 -0
- package/dist/port-conflict.d.ts +9 -0
- package/dist/port-conflict.d.ts.map +1 -0
- package/dist/port-conflict.js +33 -0
- package/dist/port-conflict.js.map +1 -0
- package/dist/port-conflict.test.d.ts +2 -0
- package/dist/port-conflict.test.d.ts.map +1 -0
- package/dist/port-conflict.test.js +38 -0
- package/dist/port-conflict.test.js.map +1 -0
- package/dist/process-scanner.d.ts +43 -0
- package/dist/process-scanner.d.ts.map +1 -0
- package/dist/process-scanner.js +453 -0
- package/dist/process-scanner.js.map +1 -0
- package/dist/process-scanner.perf.test.d.ts +2 -0
- package/dist/process-scanner.perf.test.d.ts.map +1 -0
- package/dist/process-scanner.perf.test.js +186 -0
- package/dist/process-scanner.perf.test.js.map +1 -0
- package/dist/process-scanner.test.d.ts +2 -0
- package/dist/process-scanner.test.d.ts.map +1 -0
- package/dist/process-scanner.test.js +399 -0
- package/dist/process-scanner.test.js.map +1 -0
- package/dist/push-protocol.d.ts +15 -0
- package/dist/push-protocol.d.ts.map +1 -0
- package/dist/push-protocol.js +23 -0
- package/dist/push-protocol.js.map +1 -0
- package/dist/push-protocol.test.d.ts +2 -0
- package/dist/push-protocol.test.d.ts.map +1 -0
- package/dist/push-protocol.test.js +57 -0
- package/dist/push-protocol.test.js.map +1 -0
- package/dist/push-store.d.ts +22 -0
- package/dist/push-store.d.ts.map +1 -0
- package/dist/push-store.js +103 -0
- package/dist/push-store.js.map +1 -0
- package/dist/push-store.test.d.ts +2 -0
- package/dist/push-store.test.d.ts.map +1 -0
- package/dist/push-store.test.js +79 -0
- package/dist/push-store.test.js.map +1 -0
- package/dist/push.d.ts +65 -0
- package/dist/push.d.ts.map +1 -0
- package/dist/push.js +202 -0
- package/dist/push.js.map +1 -0
- package/dist/push.test.d.ts +2 -0
- package/dist/push.test.d.ts.map +1 -0
- package/dist/push.test.js +199 -0
- package/dist/push.test.js.map +1 -0
- package/dist/safe-stdio.d.ts +3 -0
- package/dist/safe-stdio.d.ts.map +1 -0
- package/dist/safe-stdio.js +46 -0
- package/dist/safe-stdio.js.map +1 -0
- package/dist/scanner.d.ts +30 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +859 -0
- package/dist/scanner.js.map +1 -0
- package/dist/scanner.perf.test.d.ts +2 -0
- package/dist/scanner.perf.test.d.ts.map +1 -0
- package/dist/scanner.perf.test.js +320 -0
- package/dist/scanner.perf.test.js.map +1 -0
- package/dist/scanner.test.d.ts +2 -0
- package/dist/scanner.test.d.ts.map +1 -0
- package/dist/scanner.test.js +948 -0
- package/dist/scanner.test.js.map +1 -0
- package/dist/session-inventory.d.ts +63 -0
- package/dist/session-inventory.d.ts.map +1 -0
- package/dist/session-inventory.js +525 -0
- package/dist/session-inventory.js.map +1 -0
- package/dist/session-inventory.perf.test.d.ts +2 -0
- package/dist/session-inventory.perf.test.d.ts.map +1 -0
- package/dist/session-inventory.perf.test.js +220 -0
- package/dist/session-inventory.perf.test.js.map +1 -0
- package/dist/session-inventory.test.d.ts +2 -0
- package/dist/session-inventory.test.d.ts.map +1 -0
- package/dist/session-inventory.test.js +712 -0
- package/dist/session-inventory.test.js.map +1 -0
- package/dist/session-manager.d.ts +75 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +1515 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/session-manager.test.d.ts +2 -0
- package/dist/session-manager.test.d.ts.map +1 -0
- package/dist/session-manager.test.js +2861 -0
- package/dist/session-manager.test.js.map +1 -0
- package/dist/session-store.d.ts +42 -0
- package/dist/session-store.d.ts.map +1 -0
- package/dist/session-store.js +163 -0
- package/dist/session-store.js.map +1 -0
- package/dist/session-store.test.d.ts +2 -0
- package/dist/session-store.test.d.ts.map +1 -0
- package/dist/session-store.test.js +236 -0
- package/dist/session-store.test.js.map +1 -0
- package/dist/session-title.d.ts +6 -0
- package/dist/session-title.d.ts.map +1 -0
- package/dist/session-title.js +105 -0
- package/dist/session-title.js.map +1 -0
- package/dist/session-title.perf.test.d.ts +2 -0
- package/dist/session-title.perf.test.d.ts.map +1 -0
- package/dist/session-title.perf.test.js +99 -0
- package/dist/session-title.perf.test.js.map +1 -0
- package/dist/session-title.test.d.ts +2 -0
- package/dist/session-title.test.d.ts.map +1 -0
- package/dist/session-title.test.js +199 -0
- package/dist/session-title.test.js.map +1 -0
- package/dist/shutdown-endpoint.test.d.ts +2 -0
- package/dist/shutdown-endpoint.test.d.ts.map +1 -0
- package/dist/shutdown-endpoint.test.js +93 -0
- package/dist/shutdown-endpoint.test.js.map +1 -0
- package/dist/storage-housekeeping.d.ts +28 -0
- package/dist/storage-housekeeping.d.ts.map +1 -0
- package/dist/storage-housekeeping.js +76 -0
- package/dist/storage-housekeeping.js.map +1 -0
- package/dist/storage-housekeeping.test.d.ts +2 -0
- package/dist/storage-housekeeping.test.d.ts.map +1 -0
- package/dist/storage-housekeeping.test.js +65 -0
- package/dist/storage-housekeeping.test.js.map +1 -0
- package/dist/test-daemon-harness.d.ts +31 -0
- package/dist/test-daemon-harness.d.ts.map +1 -0
- package/dist/test-daemon-harness.js +337 -0
- package/dist/test-daemon-harness.js.map +1 -0
- package/dist/token-auth.test.d.ts +2 -0
- package/dist/token-auth.test.d.ts.map +1 -0
- package/dist/token-auth.test.js +52 -0
- package/dist/token-auth.test.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +40 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.test.d.ts +2 -0
- package/dist/utils.test.d.ts.map +1 -0
- package/dist/utils.test.js +54 -0
- package/dist/utils.test.js.map +1 -0
- package/dist/ws-data.d.ts +4 -0
- package/dist/ws-data.d.ts.map +1 -0
- package/dist/ws-data.js +20 -0
- package/dist/ws-data.js.map +1 -0
- package/dist/ws-data.test.d.ts +2 -0
- package/dist/ws-data.test.d.ts.map +1 -0
- package/dist/ws-data.test.js +17 -0
- package/dist/ws-data.test.js.map +1 -0
- package/package.json +24 -27
- package/perf-reporter.mjs +138 -0
- package/scripts/build-release.mjs +41 -0
- package/scripts/dev.mjs +537 -0
- package/src/advertised-hosts.test.ts +125 -0
- package/src/advertised-hosts.ts +225 -0
- package/src/audit.test.ts +38 -0
- package/src/audit.ts +117 -0
- package/src/auth.ts +31 -0
- package/src/claude-hooks.ts +195 -0
- package/src/cli-version.test.ts +36 -0
- package/src/cli-version.ts +46 -0
- package/src/config.test.ts +254 -0
- package/src/config.ts +324 -0
- package/src/dev-auth.test.ts +183 -0
- package/src/dev-script.test.ts +511 -0
- package/src/drivers/claude.test.ts +1186 -0
- package/src/drivers/claude.ts +443 -0
- package/src/drivers/codex.test.ts +1096 -0
- package/src/drivers/codex.ts +879 -0
- package/src/drivers/types.ts +15 -0
- package/src/e2e.test.ts +139 -0
- package/src/identity.test.ts +26 -0
- package/src/identity.ts +82 -0
- package/src/index-entry.test.ts +336 -0
- package/src/index.ts +781 -0
- package/src/logger.ts +112 -0
- package/src/metrics.ts +117 -0
- package/src/pairing-store.test.ts +53 -0
- package/src/pairing-store.ts +154 -0
- package/src/paths.ts +19 -0
- package/src/perf-compare.ts +164 -0
- package/src/port-conflict.test.ts +45 -0
- package/src/port-conflict.ts +44 -0
- package/src/process-scanner.perf.test.ts +222 -0
- package/src/process-scanner.test.ts +575 -0
- package/src/process-scanner.ts +514 -0
- package/src/push-protocol.test.ts +74 -0
- package/src/push-protocol.ts +36 -0
- package/src/push-store.test.ts +89 -0
- package/src/push-store.ts +126 -0
- package/src/push.test.ts +234 -0
- package/src/push.ts +318 -0
- package/src/safe-stdio.ts +51 -0
- package/src/scanner.perf.test.ts +359 -0
- package/src/scanner.test.ts +1045 -0
- package/src/scanner.ts +924 -0
- package/src/session-inventory.perf.test.ts +250 -0
- package/src/session-inventory.test.ts +1002 -0
- package/src/session-inventory.ts +721 -0
- package/src/session-manager.test.ts +3430 -0
- package/src/session-manager.ts +1775 -0
- package/src/session-store.test.ts +276 -0
- package/src/session-store.ts +202 -0
- package/src/session-title.perf.test.ts +118 -0
- package/src/session-title.test.ts +286 -0
- package/src/session-title.ts +108 -0
- package/src/shutdown-endpoint.test.ts +95 -0
- package/src/storage-housekeeping.test.ts +78 -0
- package/src/storage-housekeeping.ts +111 -0
- package/src/test-daemon-harness.ts +410 -0
- package/src/token-auth.test.ts +67 -0
- package/src/utils.test.ts +65 -0
- package/src/utils.ts +47 -0
- package/src/ws-data.test.ts +20 -0
- package/src/ws-data.ts +26 -0
- package/tsconfig.json +12 -0
- package/README.md +0 -80
- package/bin/cloudflared-quick-tunnel.mjs +0 -11
- package/bin/cloudflared-resolver.mjs +0 -68
- package/bin/vibelet-runtime-policy.mjs +0 -36
- package/bin/vibelet.cjs +0 -12
- package/bin/vibelet.mjs +0 -1019
- package/dist/index.cjs +0 -123
package/src/logger.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import {
|
|
3
|
+
DAEMON_STDERR_LOG_PATH,
|
|
4
|
+
DAEMON_STDOUT_LOG_PATH,
|
|
5
|
+
VIBELET_LOG_DIR,
|
|
6
|
+
} from './paths.js';
|
|
7
|
+
import { writeStderrSafe, writeStdoutSafe } from './safe-stdio.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Structured logger for the Vibelet daemon.
|
|
11
|
+
*
|
|
12
|
+
* Outputs one JSON object per line (JSONL) to stdout/stderr.
|
|
13
|
+
* Each entry includes: level, ts, msg, and any extra context fields.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* import { logger } from './logger.js';
|
|
17
|
+
* logger.info({ sessionId, agent }, 'session created');
|
|
18
|
+
* const child = logger.child({ sessionId, agent });
|
|
19
|
+
* child.info('driver started');
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
23
|
+
|
|
24
|
+
const LEVEL_VALUE: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
25
|
+
|
|
26
|
+
let minLevel: LogLevel = (process.env.VIBE_LOG_LEVEL as LogLevel) || 'info';
|
|
27
|
+
|
|
28
|
+
export function setLogLevel(level: LogLevel): void {
|
|
29
|
+
minLevel = level;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface LogContext {
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isTestRuntime(): boolean {
|
|
37
|
+
return process.env.VIBE_TEST === '1' || process.argv.includes('--test');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Skip appendFileSync when launchd/systemd already redirects stdout/stderr
|
|
41
|
+
// to the log files — writing to both causes every line to appear twice.
|
|
42
|
+
function stdoutAlreadyRedirected(): boolean {
|
|
43
|
+
try {
|
|
44
|
+
const { fstatSync } = require('fs') as typeof import('fs');
|
|
45
|
+
const stat = fstatSync(1);
|
|
46
|
+
return stat.isFile();
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const PERSIST_LOG_FILE = !isTestRuntime() && !stdoutAlreadyRedirected();
|
|
53
|
+
|
|
54
|
+
if (PERSIST_LOG_FILE) {
|
|
55
|
+
try { mkdirSync(VIBELET_LOG_DIR, { recursive: true }); } catch {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function write(level: LogLevel, ctx: LogContext, msg: string): void {
|
|
59
|
+
if (LEVEL_VALUE[level] < LEVEL_VALUE[minLevel]) return;
|
|
60
|
+
|
|
61
|
+
const entry = {
|
|
62
|
+
level,
|
|
63
|
+
ts: new Date().toISOString(),
|
|
64
|
+
msg,
|
|
65
|
+
...ctx,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const line = JSON.stringify(entry);
|
|
69
|
+
if (level === 'error' || level === 'warn') {
|
|
70
|
+
writeStderrSafe(line + '\n');
|
|
71
|
+
} else {
|
|
72
|
+
writeStdoutSafe(line + '\n');
|
|
73
|
+
}
|
|
74
|
+
if (PERSIST_LOG_FILE) {
|
|
75
|
+
const logFile = level === 'error' || level === 'warn'
|
|
76
|
+
? DAEMON_STDERR_LOG_PATH
|
|
77
|
+
: DAEMON_STDOUT_LOG_PATH;
|
|
78
|
+
try { appendFileSync(logFile, line + '\n'); } catch {}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Logger {
|
|
83
|
+
debug(msg: string): void;
|
|
84
|
+
debug(ctx: LogContext, msg: string): void;
|
|
85
|
+
info(msg: string): void;
|
|
86
|
+
info(ctx: LogContext, msg: string): void;
|
|
87
|
+
warn(msg: string): void;
|
|
88
|
+
warn(ctx: LogContext, msg: string): void;
|
|
89
|
+
error(msg: string): void;
|
|
90
|
+
error(ctx: LogContext, msg: string): void;
|
|
91
|
+
child(defaultCtx: LogContext): Logger;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createLogger(defaultCtx: LogContext = {}): Logger {
|
|
95
|
+
function log(level: LogLevel, ctxOrMsg: LogContext | string, maybeMsg?: string): void {
|
|
96
|
+
if (typeof ctxOrMsg === 'string') {
|
|
97
|
+
write(level, defaultCtx, ctxOrMsg);
|
|
98
|
+
} else {
|
|
99
|
+
write(level, { ...defaultCtx, ...ctxOrMsg }, maybeMsg!);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
debug: (ctxOrMsg: LogContext | string, maybeMsg?: string) => log('debug', ctxOrMsg, maybeMsg),
|
|
105
|
+
info: (ctxOrMsg: LogContext | string, maybeMsg?: string) => log('info', ctxOrMsg, maybeMsg),
|
|
106
|
+
warn: (ctxOrMsg: LogContext | string, maybeMsg?: string) => log('warn', ctxOrMsg, maybeMsg),
|
|
107
|
+
error: (ctxOrMsg: LogContext | string, maybeMsg?: string) => log('error', ctxOrMsg, maybeMsg),
|
|
108
|
+
child: (childCtx: LogContext) => createLogger({ ...defaultCtx, ...childCtx }),
|
|
109
|
+
} as Logger;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const logger = createLogger({ module: 'daemon' });
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight in-memory metrics for the Vibelet daemon.
|
|
3
|
+
*
|
|
4
|
+
* Provides counters and timers. No external dependencies.
|
|
5
|
+
* Metrics are exposed via the /health endpoint and periodically logged.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { metrics } from './metrics.js';
|
|
9
|
+
* metrics.increment('session.create', { agent: 'claude' });
|
|
10
|
+
* const end = metrics.startTimer('driver.spawn');
|
|
11
|
+
* // ... work ...
|
|
12
|
+
* end(); // records duration in ms
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { logger } from './logger.js';
|
|
16
|
+
|
|
17
|
+
interface CounterEntry {
|
|
18
|
+
value: number;
|
|
19
|
+
labels: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TimerEntry {
|
|
23
|
+
count: number;
|
|
24
|
+
totalMs: number;
|
|
25
|
+
minMs: number;
|
|
26
|
+
maxMs: number;
|
|
27
|
+
lastMs: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class MetricsRegistry {
|
|
31
|
+
private counters = new Map<string, CounterEntry[]>();
|
|
32
|
+
private timers = new Map<string, TimerEntry>();
|
|
33
|
+
private gauges = new Map<string, number>();
|
|
34
|
+
private startTime = Date.now();
|
|
35
|
+
private logInterval: ReturnType<typeof setInterval> | null = null;
|
|
36
|
+
|
|
37
|
+
/** Increment a counter, optionally with labels. */
|
|
38
|
+
increment(name: string, labels: Record<string, string> = {}): void {
|
|
39
|
+
if (!this.counters.has(name)) this.counters.set(name, []);
|
|
40
|
+
const entries = this.counters.get(name)!;
|
|
41
|
+
const key = JSON.stringify(labels);
|
|
42
|
+
const existing = entries.find(e => JSON.stringify(e.labels) === key);
|
|
43
|
+
if (existing) {
|
|
44
|
+
existing.value++;
|
|
45
|
+
} else {
|
|
46
|
+
entries.push({ value: 1, labels });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Set a gauge value (for current-state metrics). */
|
|
51
|
+
gauge(name: string, value: number): void {
|
|
52
|
+
this.gauges.set(name, value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Start a timer, returns a function to call when done. */
|
|
56
|
+
startTimer(name: string): () => number {
|
|
57
|
+
const start = performance.now();
|
|
58
|
+
return () => {
|
|
59
|
+
const durationMs = Math.round(performance.now() - start);
|
|
60
|
+
const entry = this.timers.get(name) ?? { count: 0, totalMs: 0, minMs: Infinity, maxMs: 0, lastMs: 0 };
|
|
61
|
+
entry.count++;
|
|
62
|
+
entry.totalMs += durationMs;
|
|
63
|
+
entry.minMs = Math.min(entry.minMs, durationMs);
|
|
64
|
+
entry.maxMs = Math.max(entry.maxMs, durationMs);
|
|
65
|
+
entry.lastMs = durationMs;
|
|
66
|
+
this.timers.set(name, entry);
|
|
67
|
+
return durationMs;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Get a snapshot of all metrics. */
|
|
72
|
+
snapshot(): MetricsSnapshot {
|
|
73
|
+
const counters: Record<string, CounterEntry[]> = {};
|
|
74
|
+
for (const [name, entries] of this.counters) {
|
|
75
|
+
counters[name] = entries.map(e => ({ ...e }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const timers: Record<string, TimerEntry> = {};
|
|
79
|
+
for (const [name, entry] of this.timers) {
|
|
80
|
+
timers[name] = { ...entry, minMs: entry.minMs === Infinity ? 0 : entry.minMs };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const gauges: Record<string, number> = {};
|
|
84
|
+
for (const [name, value] of this.gauges) {
|
|
85
|
+
gauges[name] = value;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { uptimeMs: Date.now() - this.startTime, counters, timers, gauges };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Start periodic metrics logging (default: every 60s). */
|
|
92
|
+
startPeriodicLog(intervalMs = 60_000): void {
|
|
93
|
+
if (this.logInterval) return;
|
|
94
|
+
this.logInterval = setInterval(() => {
|
|
95
|
+
const snap = this.snapshot();
|
|
96
|
+
logger.info({ metrics: snap }, 'periodic metrics snapshot');
|
|
97
|
+
}, intervalMs);
|
|
98
|
+
this.logInterval.unref();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Stop periodic logging. */
|
|
102
|
+
stopPeriodicLog(): void {
|
|
103
|
+
if (this.logInterval) {
|
|
104
|
+
clearInterval(this.logInterval);
|
|
105
|
+
this.logInterval = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface MetricsSnapshot {
|
|
111
|
+
uptimeMs: number;
|
|
112
|
+
counters: Record<string, CounterEntry[]>;
|
|
113
|
+
timers: Record<string, TimerEntry>;
|
|
114
|
+
gauges: Record<string, number>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const metrics = new MetricsRegistry();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, readFile, rm } from 'fs/promises';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { PairingStore } from './pairing-store.js';
|
|
7
|
+
|
|
8
|
+
test('PairingStore issues stable per-device tokens and validates them', async () => {
|
|
9
|
+
const dir = await mkdtemp(join(tmpdir(), 'vibelet-pairing-'));
|
|
10
|
+
const pairingsPath = join(dir, 'pairings.json');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const store = new PairingStore(pairingsPath);
|
|
14
|
+
const token = store.issuePairToken('device-1', 'Leyang iPhone');
|
|
15
|
+
|
|
16
|
+
assert.equal(store.pairedCount(), 1);
|
|
17
|
+
assert.equal(store.validatePairToken('device-1', token), true);
|
|
18
|
+
assert.equal(store.validateAnyPairToken(token)?.deviceId, 'device-1');
|
|
19
|
+
|
|
20
|
+
const persisted = JSON.parse(await readFile(pairingsPath, 'utf8'));
|
|
21
|
+
assert.equal(persisted.length, 1);
|
|
22
|
+
assert.equal(persisted[0].deviceId, 'device-1');
|
|
23
|
+
assert.notEqual(persisted[0].tokenHash, token);
|
|
24
|
+
} finally {
|
|
25
|
+
await rm(dir, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('PairingStore expires one-time pairing windows and can revoke/reset pairings', async () => {
|
|
30
|
+
const dir = await mkdtemp(join(tmpdir(), 'vibelet-pairing-'));
|
|
31
|
+
const pairingsPath = join(dir, 'pairings.json');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const store = new PairingStore(pairingsPath);
|
|
35
|
+
const expired = store.openWindow(-1);
|
|
36
|
+
const fresh = store.openWindow();
|
|
37
|
+
const token = store.issuePairToken('device-2', 'Office iPhone');
|
|
38
|
+
|
|
39
|
+
assert.equal(store.consumeWindow(expired.pairNonce), null);
|
|
40
|
+
assert.ok(store.consumeWindow(fresh.pairNonce));
|
|
41
|
+
assert.equal(store.consumeWindow(fresh.pairNonce), null);
|
|
42
|
+
|
|
43
|
+
assert.equal(store.revoke('device-2'), true);
|
|
44
|
+
assert.equal(store.validatePairToken('device-2', token, false), false);
|
|
45
|
+
assert.equal(store.pairedCount(), 0);
|
|
46
|
+
|
|
47
|
+
store.reset();
|
|
48
|
+
const persisted = JSON.parse(await readFile(pairingsPath, 'utf8'));
|
|
49
|
+
assert.deepEqual(persisted, []);
|
|
50
|
+
} finally {
|
|
51
|
+
await rm(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
import { PAIRINGS_PATH } from './paths.js';
|
|
5
|
+
|
|
6
|
+
export interface PairingRecord {
|
|
7
|
+
deviceId: string;
|
|
8
|
+
deviceName: string;
|
|
9
|
+
tokenHash: string;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
lastSeenAt: string;
|
|
12
|
+
revokedAt: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PairingWindow {
|
|
16
|
+
pairingId: string;
|
|
17
|
+
pairNonce: string;
|
|
18
|
+
expiresAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function base64Url(bytes: number): string {
|
|
22
|
+
return randomBytes(bytes).toString('base64url');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hashToken(token: string): string {
|
|
26
|
+
return createHash('sha256').update(token).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sameHash(left: string, right: string): boolean {
|
|
30
|
+
const leftBuffer = Buffer.from(left);
|
|
31
|
+
const rightBuffer = Buffer.from(right);
|
|
32
|
+
if (leftBuffer.length !== rightBuffer.length) return false;
|
|
33
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readRecords(pairingsPath: string): PairingRecord[] {
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(pairingsPath, 'utf8');
|
|
39
|
+
const parsed = JSON.parse(raw);
|
|
40
|
+
if (!Array.isArray(parsed)) return [];
|
|
41
|
+
return parsed.filter((entry): entry is PairingRecord => (
|
|
42
|
+
entry &&
|
|
43
|
+
typeof entry.deviceId === 'string' &&
|
|
44
|
+
typeof entry.deviceName === 'string' &&
|
|
45
|
+
typeof entry.tokenHash === 'string' &&
|
|
46
|
+
typeof entry.createdAt === 'string' &&
|
|
47
|
+
typeof entry.lastSeenAt === 'string' &&
|
|
48
|
+
(typeof entry.revokedAt === 'string' || entry.revokedAt === null)
|
|
49
|
+
));
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeRecords(pairingsPath: string, records: PairingRecord[]): void {
|
|
56
|
+
mkdirSync(dirname(pairingsPath), { recursive: true });
|
|
57
|
+
writeFileSync(pairingsPath, JSON.stringify(records, null, 2) + '\n', 'utf8');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class PairingStore {
|
|
61
|
+
private records: PairingRecord[];
|
|
62
|
+
private windows = new Map<string, PairingWindow>();
|
|
63
|
+
|
|
64
|
+
constructor(private readonly pairingsPath = PAIRINGS_PATH) {
|
|
65
|
+
this.records = readRecords(pairingsPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
list(): PairingRecord[] {
|
|
69
|
+
return this.records.slice();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pairedCount(): number {
|
|
73
|
+
return this.records.filter((record) => !record.revokedAt).length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
openWindow(windowMs = 5 * 60_000): PairingWindow {
|
|
77
|
+
const pairingId = `pair_${base64Url(8)}`;
|
|
78
|
+
const pairNonce = base64Url(24);
|
|
79
|
+
const expiresAt = new Date(Date.now() + windowMs).toISOString();
|
|
80
|
+
const window: PairingWindow = { pairingId, pairNonce, expiresAt };
|
|
81
|
+
this.windows.set(pairNonce, window);
|
|
82
|
+
return window;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
consumeWindow(pairNonce: string): PairingWindow | null {
|
|
86
|
+
const window = this.windows.get(pairNonce);
|
|
87
|
+
if (!window) return null;
|
|
88
|
+
this.windows.delete(pairNonce);
|
|
89
|
+
if (new Date(window.expiresAt).getTime() < Date.now()) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return window;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
issuePairToken(deviceId: string, deviceName: string): string {
|
|
96
|
+
const now = new Date().toISOString();
|
|
97
|
+
const pairToken = base64Url(32);
|
|
98
|
+
const nextRecord: PairingRecord = {
|
|
99
|
+
deviceId,
|
|
100
|
+
deviceName,
|
|
101
|
+
tokenHash: hashToken(pairToken),
|
|
102
|
+
createdAt: now,
|
|
103
|
+
lastSeenAt: now,
|
|
104
|
+
revokedAt: null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const existingIndex = this.records.findIndex((record) => record.deviceId === deviceId);
|
|
108
|
+
if (existingIndex >= 0) {
|
|
109
|
+
this.records[existingIndex] = nextRecord;
|
|
110
|
+
} else {
|
|
111
|
+
this.records.unshift(nextRecord);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
writeRecords(this.pairingsPath, this.records);
|
|
115
|
+
return pairToken;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
validatePairToken(deviceId: string, token: string, touch = true): boolean {
|
|
119
|
+
const record = this.records.find((entry) => entry.deviceId === deviceId && !entry.revokedAt);
|
|
120
|
+
if (!record) return false;
|
|
121
|
+
const tokenDigest = hashToken(token);
|
|
122
|
+
if (!sameHash(record.tokenHash, tokenDigest)) return false;
|
|
123
|
+
if (touch) {
|
|
124
|
+
record.lastSeenAt = new Date().toISOString();
|
|
125
|
+
writeRecords(this.pairingsPath, this.records);
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
validateAnyPairToken(token: string, touch = true): PairingRecord | null {
|
|
131
|
+
const tokenDigest = hashToken(token);
|
|
132
|
+
const record = this.records.find((entry) => !entry.revokedAt && sameHash(entry.tokenHash, tokenDigest));
|
|
133
|
+
if (!record) return null;
|
|
134
|
+
if (touch) {
|
|
135
|
+
record.lastSeenAt = new Date().toISOString();
|
|
136
|
+
writeRecords(this.pairingsPath, this.records);
|
|
137
|
+
}
|
|
138
|
+
return record;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
revoke(deviceId: string): boolean {
|
|
142
|
+
const record = this.records.find((entry) => entry.deviceId === deviceId && !entry.revokedAt);
|
|
143
|
+
if (!record) return false;
|
|
144
|
+
record.revokedAt = new Date().toISOString();
|
|
145
|
+
writeRecords(this.pairingsPath, this.records);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
reset(): void {
|
|
150
|
+
this.records = [];
|
|
151
|
+
this.windows.clear();
|
|
152
|
+
writeRecords(this.pairingsPath, this.records);
|
|
153
|
+
}
|
|
154
|
+
}
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
export const VIBELET_DIR = join(homedir(), '.vibelet');
|
|
5
|
+
export const VIBELET_LOG_DIR = join(VIBELET_DIR, 'logs');
|
|
6
|
+
export const VIBELET_DATA_DIR = join(VIBELET_DIR, 'data');
|
|
7
|
+
export const VIBELET_RUNTIME_DIR = join(VIBELET_DIR, 'runtime');
|
|
8
|
+
export const VIBELET_RUNTIME_CURRENT_DIR = join(VIBELET_RUNTIME_DIR, 'current');
|
|
9
|
+
|
|
10
|
+
export const IDENTITY_PATH = join(VIBELET_DIR, 'identity.json');
|
|
11
|
+
export const PAIRINGS_PATH = join(VIBELET_DIR, 'pairings.json');
|
|
12
|
+
export const PUSH_SUBSCRIPTIONS_PATH = join(VIBELET_DATA_DIR, 'push-subscriptions.json');
|
|
13
|
+
export const SESSION_STORE_PATH = join(VIBELET_DATA_DIR, 'sessions.json');
|
|
14
|
+
export const AUDIT_PATH = join(VIBELET_DATA_DIR, 'audit.jsonl');
|
|
15
|
+
export const VIBELET_UPLOADS_DIR = join(VIBELET_DATA_DIR, 'uploads');
|
|
16
|
+
export const DAEMON_STDOUT_LOG_PATH = join(VIBELET_LOG_DIR, 'daemon.stdout.log');
|
|
17
|
+
export const DAEMON_STDERR_LOG_PATH = join(VIBELET_LOG_DIR, 'daemon.stderr.log');
|
|
18
|
+
export const UPDATE_CHECK_PATH = join(VIBELET_DIR, 'update-check.json');
|
|
19
|
+
export const PAIRING_QR_PNG_PATH = join(VIBELET_DIR, 'pairing-qr.png');
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare two perf baselines and report regressions.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* tsx src/perf-compare.ts # compare perf-baseline.json vs perf-baseline.prev.json
|
|
6
|
+
* tsx src/perf-compare.ts <current> <previous> # compare two specific files
|
|
7
|
+
*
|
|
8
|
+
* Exit code:
|
|
9
|
+
* 0 = no regressions
|
|
10
|
+
* 1 = regressions detected (>20% slower)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync } from 'fs';
|
|
14
|
+
|
|
15
|
+
interface PerfEntry {
|
|
16
|
+
test: string;
|
|
17
|
+
suite: string;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
passed: boolean;
|
|
20
|
+
metrics: Record<string, number>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PerfBaseline {
|
|
24
|
+
timestamp: string;
|
|
25
|
+
nodeVersion: string;
|
|
26
|
+
platform: string;
|
|
27
|
+
arch: string;
|
|
28
|
+
results: PerfEntry[];
|
|
29
|
+
summary: {
|
|
30
|
+
total: number;
|
|
31
|
+
passed: number;
|
|
32
|
+
failed: number;
|
|
33
|
+
totalDurationMs: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const REGRESSION_THRESHOLD = 0.20; // 20% slower = regression
|
|
38
|
+
|
|
39
|
+
function loadBaseline(path: string): PerfBaseline | null {
|
|
40
|
+
if (!existsSync(path)) return null;
|
|
41
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatDelta(current: number, previous: number): string {
|
|
45
|
+
if (previous === 0) return 'N/A';
|
|
46
|
+
const pct = ((current - previous) / previous) * 100;
|
|
47
|
+
const sign = pct >= 0 ? '+' : '';
|
|
48
|
+
return `${sign}${pct.toFixed(1)}%`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function main() {
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
const currentPath = args[0] || 'perf-baseline.json';
|
|
54
|
+
const previousPath = args[1] || 'perf-baseline.prev.json';
|
|
55
|
+
|
|
56
|
+
const current = loadBaseline(currentPath);
|
|
57
|
+
if (!current) {
|
|
58
|
+
console.error(`Current baseline not found: ${currentPath}`);
|
|
59
|
+
console.error('Run "pnpm test:perf:baseline" to generate it.');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const previous = loadBaseline(previousPath);
|
|
64
|
+
if (!previous) {
|
|
65
|
+
console.log(`No previous baseline found at ${previousPath}.`);
|
|
66
|
+
console.log(`Current baseline: ${current.summary.total} tests, ${current.summary.totalDurationMs.toFixed(0)}ms total`);
|
|
67
|
+
console.log('To compare, save the current baseline as the previous:');
|
|
68
|
+
console.log(` cp ${currentPath} ${previousPath}`);
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('Performance Comparison');
|
|
73
|
+
console.log('='.repeat(90));
|
|
74
|
+
console.log(`Previous: ${previous.timestamp} (Node ${previous.nodeVersion})`);
|
|
75
|
+
console.log(`Current: ${current.timestamp} (Node ${current.nodeVersion})`);
|
|
76
|
+
console.log('');
|
|
77
|
+
|
|
78
|
+
// Build lookup for previous results
|
|
79
|
+
const prevMap = new Map<string, PerfEntry>();
|
|
80
|
+
for (const entry of previous.results) {
|
|
81
|
+
prevMap.set(`${entry.suite}::${entry.test}`, entry);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const regressions: string[] = [];
|
|
85
|
+
const improvements: string[] = [];
|
|
86
|
+
const newTests: string[] = [];
|
|
87
|
+
|
|
88
|
+
console.log(`${'Test'.padEnd(55)} ${'Prev'.padStart(10)} ${'Curr'.padStart(10)} ${'Delta'.padStart(10)}`);
|
|
89
|
+
console.log('-'.repeat(90));
|
|
90
|
+
|
|
91
|
+
for (const entry of current.results) {
|
|
92
|
+
const key = `${entry.suite}::${entry.test}`;
|
|
93
|
+
const prev = prevMap.get(key);
|
|
94
|
+
|
|
95
|
+
// Use the most representative metric for comparison
|
|
96
|
+
const currentMs = entry.metrics.totalMs ?? entry.metrics.coldMs ?? entry.durationMs;
|
|
97
|
+
|
|
98
|
+
if (!prev) {
|
|
99
|
+
newTests.push(entry.test);
|
|
100
|
+
const label = truncate(`${entry.suite} > ${entry.test}`, 55);
|
|
101
|
+
console.log(`${label.padEnd(55)} ${'new'.padStart(10)} ${fmt(currentMs).padStart(10)} ${''.padStart(10)}`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const prevMs = prev.metrics.totalMs ?? prev.metrics.coldMs ?? prev.durationMs;
|
|
106
|
+
const delta = formatDelta(currentMs, prevMs);
|
|
107
|
+
const ratio = prevMs > 0 ? (currentMs - prevMs) / prevMs : 0;
|
|
108
|
+
|
|
109
|
+
const label = truncate(`${entry.suite} > ${entry.test}`, 55);
|
|
110
|
+
const marker = ratio > REGRESSION_THRESHOLD ? ' ⚠️' : ratio < -REGRESSION_THRESHOLD ? ' ✨' : '';
|
|
111
|
+
|
|
112
|
+
console.log(`${label.padEnd(55)} ${fmt(prevMs).padStart(10)} ${fmt(currentMs).padStart(10)} ${delta.padStart(10)}${marker}`);
|
|
113
|
+
|
|
114
|
+
if (ratio > REGRESSION_THRESHOLD) {
|
|
115
|
+
regressions.push(`${entry.suite} > ${entry.test}: ${fmt(prevMs)} → ${fmt(currentMs)} (${delta})`);
|
|
116
|
+
} else if (ratio < -REGRESSION_THRESHOLD) {
|
|
117
|
+
improvements.push(`${entry.suite} > ${entry.test}: ${fmt(prevMs)} → ${fmt(currentMs)} (${delta})`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
prevMap.delete(key);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Removed tests
|
|
124
|
+
const removedTests = [...prevMap.keys()];
|
|
125
|
+
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log('='.repeat(90));
|
|
128
|
+
console.log(`Summary: ${current.summary.total} tests | ${fmt(current.summary.totalDurationMs)} total`);
|
|
129
|
+
|
|
130
|
+
if (improvements.length > 0) {
|
|
131
|
+
console.log(`\n✨ Improvements (>${REGRESSION_THRESHOLD * 100}% faster): ${improvements.length}`);
|
|
132
|
+
for (const imp of improvements) console.log(` ${imp}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (newTests.length > 0) {
|
|
136
|
+
console.log(`\n🆕 New tests: ${newTests.length}`);
|
|
137
|
+
for (const t of newTests) console.log(` ${t}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (removedTests.length > 0) {
|
|
141
|
+
console.log(`\n🗑️ Removed tests: ${removedTests.length}`);
|
|
142
|
+
for (const t of removedTests) console.log(` ${t}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (regressions.length > 0) {
|
|
146
|
+
console.log(`\n⚠️ Regressions (>${REGRESSION_THRESHOLD * 100}% slower): ${regressions.length}`);
|
|
147
|
+
for (const reg of regressions) console.log(` ${reg}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
} else {
|
|
150
|
+
console.log('\n✅ No performance regressions detected.');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fmt(ms: number): string {
|
|
155
|
+
if (ms < 1) return `${(ms * 1000).toFixed(0)}µs`;
|
|
156
|
+
if (ms < 1000) return `${ms.toFixed(2)}ms`;
|
|
157
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function truncate(str: string, maxLen: number): string {
|
|
161
|
+
return str.length <= maxLen ? str : str.slice(0, maxLen - 3) + '...';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { collectPortOccupants, parsePidList } from './port-conflict.js';
|
|
4
|
+
|
|
5
|
+
test('parsePidList keeps only valid positive numeric pids', () => {
|
|
6
|
+
assert.deepEqual(
|
|
7
|
+
parsePidList('123\nabc\n\n0\n456\n123\n'),
|
|
8
|
+
[123, 456],
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('collectPortOccupants includes command details when available', () => {
|
|
13
|
+
const occupants = collectPortOccupants(9876, (file, args) => {
|
|
14
|
+
if (file === 'lsof') {
|
|
15
|
+
assert.deepEqual(args, ['-ti', 'tcp:9876']);
|
|
16
|
+
return '123\n456';
|
|
17
|
+
}
|
|
18
|
+
if (file === 'ps' && args[1] === '123') {
|
|
19
|
+
return 'node dist/index.js';
|
|
20
|
+
}
|
|
21
|
+
if (file === 'ps' && args[1] === '456') {
|
|
22
|
+
return 'python -m http.server';
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Unexpected command: ${file} ${args.join(' ')}`);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.deepEqual(occupants, [
|
|
28
|
+
{ pid: 123, command: 'node dist/index.js' },
|
|
29
|
+
{ pid: 456, command: 'python -m http.server' },
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('collectPortOccupants falls back to pid-only entries when ps lookup fails', () => {
|
|
34
|
+
const occupants = collectPortOccupants(9876, (file, args) => {
|
|
35
|
+
if (file === 'lsof') {
|
|
36
|
+
return '123';
|
|
37
|
+
}
|
|
38
|
+
if (file === 'ps' && args[1] === '123') {
|
|
39
|
+
throw new Error('ps failed');
|
|
40
|
+
}
|
|
41
|
+
throw new Error(`Unexpected command: ${file} ${args.join(' ')}`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.deepEqual(occupants, [{ pid: 123 }]);
|
|
45
|
+
});
|