@vibelet/cli 0.1.35 → 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 -1035
- package/dist/index.cjs +0 -125
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
sanitizeSessionTitle,
|
|
6
|
+
isIdLikeSessionTitle,
|
|
7
|
+
isFallbackSessionTitle,
|
|
8
|
+
titleFromFirstSentence,
|
|
9
|
+
preferSessionTitle,
|
|
10
|
+
} from './session-title.js';
|
|
11
|
+
|
|
12
|
+
describe('sanitizeSessionTitle', () => {
|
|
13
|
+
it('returns undefined for null', () => {
|
|
14
|
+
assert.equal(sanitizeSessionTitle(null), undefined);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns undefined for undefined', () => {
|
|
18
|
+
assert.equal(sanitizeSessionTitle(undefined), undefined);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns undefined for non-string values', () => {
|
|
22
|
+
assert.equal(sanitizeSessionTitle(42 as any), undefined);
|
|
23
|
+
assert.equal(sanitizeSessionTitle(true as any), undefined);
|
|
24
|
+
assert.equal(sanitizeSessionTitle({} as any), undefined);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns undefined for empty string', () => {
|
|
28
|
+
assert.equal(sanitizeSessionTitle(''), undefined);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns undefined for whitespace-only string', () => {
|
|
32
|
+
assert.equal(sanitizeSessionTitle(' '), undefined);
|
|
33
|
+
assert.equal(sanitizeSessionTitle('\t\n'), undefined);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('collapses internal whitespace', () => {
|
|
37
|
+
assert.equal(sanitizeSessionTitle('hello world'), 'hello world');
|
|
38
|
+
assert.equal(sanitizeSessionTitle('a\n\nb\tc'), 'a b c');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('trims leading and trailing whitespace', () => {
|
|
42
|
+
assert.equal(sanitizeSessionTitle(' hello '), 'hello');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns normalized string for valid input', () => {
|
|
46
|
+
assert.equal(sanitizeSessionTitle('Fix the bug'), 'Fix the bug');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('isIdLikeSessionTitle', () => {
|
|
51
|
+
it('returns false for null/undefined', () => {
|
|
52
|
+
assert.equal(isIdLikeSessionTitle(null), false);
|
|
53
|
+
assert.equal(isIdLikeSessionTitle(undefined), false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('returns false for empty string', () => {
|
|
57
|
+
assert.equal(isIdLikeSessionTitle(''), false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('detects long hex strings (8+ chars)', () => {
|
|
61
|
+
assert.equal(isIdLikeSessionTitle('abcdef01'), true);
|
|
62
|
+
assert.equal(isIdLikeSessionTitle('0123456789abcdef'), true);
|
|
63
|
+
assert.equal(isIdLikeSessionTitle('ABCDEF01'), true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not match short hex strings (<8 chars)', () => {
|
|
67
|
+
assert.equal(isIdLikeSessionTitle('abcdef0'), false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('detects UUID format', () => {
|
|
71
|
+
assert.equal(isIdLikeSessionTitle('550e8400-e29b-41d4-a716-446655440000'), true);
|
|
72
|
+
assert.equal(isIdLikeSessionTitle('ABCDEF00-1234-5678-9ABC-DEF012345678'), true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('detects agent-prefixed IDs', () => {
|
|
76
|
+
assert.equal(isIdLikeSessionTitle('agent-abc123'), true);
|
|
77
|
+
assert.equal(isIdLikeSessionTitle('agent-some_long-id'), true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rejects agent prefix with short suffix', () => {
|
|
81
|
+
assert.equal(isIdLikeSessionTitle('agent-abc'), false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('returns false for normal titles', () => {
|
|
85
|
+
assert.equal(isIdLikeSessionTitle('Fix the login bug'), false);
|
|
86
|
+
assert.equal(isIdLikeSessionTitle('Refactor utils'), false);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('isFallbackSessionTitle', () => {
|
|
91
|
+
it('returns true for null/undefined/empty', () => {
|
|
92
|
+
assert.equal(isFallbackSessionTitle(null), true);
|
|
93
|
+
assert.equal(isFallbackSessionTitle(undefined), true);
|
|
94
|
+
assert.equal(isFallbackSessionTitle(''), true);
|
|
95
|
+
assert.equal(isFallbackSessionTitle(' '), true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns true for generic titles', () => {
|
|
99
|
+
assert.equal(isFallbackSessionTitle('New session'), true);
|
|
100
|
+
assert.equal(isFallbackSessionTitle('Resumed session'), true);
|
|
101
|
+
assert.equal(isFallbackSessionTitle('Untitled session'), true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('returns true for UUID-like titles', () => {
|
|
105
|
+
assert.equal(isFallbackSessionTitle('550e8400-e29b-41d4-a716-446655440000'), true);
|
|
106
|
+
assert.equal(isFallbackSessionTitle('abcdef0123456789'), true);
|
|
107
|
+
assert.equal(isFallbackSessionTitle('agent-abc123def'), true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns false for real titles', () => {
|
|
111
|
+
assert.equal(isFallbackSessionTitle('Fix the login bug'), false);
|
|
112
|
+
assert.equal(isFallbackSessionTitle('Add dark mode support'), false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('titleFromFirstSentence', () => {
|
|
117
|
+
it('returns undefined for null/undefined', () => {
|
|
118
|
+
assert.equal(titleFromFirstSentence(null), undefined);
|
|
119
|
+
assert.equal(titleFromFirstSentence(undefined), undefined);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('returns undefined for empty/whitespace string', () => {
|
|
123
|
+
assert.equal(titleFromFirstSentence(''), undefined);
|
|
124
|
+
assert.equal(titleFromFirstSentence(' '), undefined);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('extracts first sentence ending with period', () => {
|
|
128
|
+
assert.equal(titleFromFirstSentence('Fix the bug. Then deploy.'), 'Fix the bug');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('extracts first sentence ending with exclamation mark', () => {
|
|
132
|
+
assert.equal(titleFromFirstSentence('Great feature! It works.'), 'Great feature');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('extracts first sentence ending with question mark', () => {
|
|
136
|
+
assert.equal(titleFromFirstSentence('Does this work? I think so.'), 'Does this work');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('extracts first sentence ending with semicolon', () => {
|
|
140
|
+
assert.equal(titleFromFirstSentence('First part; second part'), 'First part');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('handles Chinese sentence markers', () => {
|
|
144
|
+
assert.equal(titleFromFirstSentence('修复了登录问题。然后部署。'), '修复了登录问题');
|
|
145
|
+
assert.equal(titleFromFirstSentence('这是什么?我不知道。'), '这是什么');
|
|
146
|
+
assert.equal(titleFromFirstSentence('太好了!继续。'), '太好了');
|
|
147
|
+
assert.equal(titleFromFirstSentence('第一部分;第二部分'), '第一部分');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('does not split on period without trailing space (e.g. file.ts)', () => {
|
|
151
|
+
assert.equal(titleFromFirstSentence('Update file.ts for production'), 'Update file.ts for production');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('does split on period at end of string', () => {
|
|
155
|
+
assert.equal(titleFromFirstSentence('Fix the bug.'), 'Fix the bug');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns the whole line if no sentence boundary found', () => {
|
|
159
|
+
assert.equal(titleFromFirstSentence('Fix the bug'), 'Fix the bug');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('skips meta noise lines', () => {
|
|
163
|
+
assert.equal(
|
|
164
|
+
titleFromFirstSentence('<environment_context>some context\nActual title here'),
|
|
165
|
+
'Actual title here',
|
|
166
|
+
);
|
|
167
|
+
assert.equal(
|
|
168
|
+
titleFromFirstSentence('<command-name>run\nReal title'),
|
|
169
|
+
'Real title',
|
|
170
|
+
);
|
|
171
|
+
assert.equal(
|
|
172
|
+
titleFromFirstSentence('<command-message>msg\nReal title'),
|
|
173
|
+
'Real title',
|
|
174
|
+
);
|
|
175
|
+
assert.equal(
|
|
176
|
+
titleFromFirstSentence('<command-args>args\nReal title'),
|
|
177
|
+
'Real title',
|
|
178
|
+
);
|
|
179
|
+
assert.equal(
|
|
180
|
+
titleFromFirstSentence('<local-command-caveat>caveat\nReal title'),
|
|
181
|
+
'Real title',
|
|
182
|
+
);
|
|
183
|
+
assert.equal(
|
|
184
|
+
titleFromFirstSentence('<local-command-stdout>out\nReal title'),
|
|
185
|
+
'Real title',
|
|
186
|
+
);
|
|
187
|
+
assert.equal(
|
|
188
|
+
titleFromFirstSentence('<local-command-stderr>err\nReal title'),
|
|
189
|
+
'Real title',
|
|
190
|
+
);
|
|
191
|
+
assert.equal(
|
|
192
|
+
titleFromFirstSentence('# AGENTS.md instructions for something\nReal title'),
|
|
193
|
+
'Real title',
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('skips path-like lines', () => {
|
|
198
|
+
assert.equal(titleFromFirstSentence('/usr/bin/node\nActual title'), 'Actual title');
|
|
199
|
+
assert.equal(titleFromFirstSentence('~/projects/foo\nActual title'), 'Actual title');
|
|
200
|
+
assert.equal(titleFromFirstSentence('./src/index.ts\nActual title'), 'Actual title');
|
|
201
|
+
assert.equal(titleFromFirstSentence('../parent/file\nActual title'), 'Actual title');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('skips Windows path-like lines', () => {
|
|
205
|
+
assert.equal(titleFromFirstSentence('C:\\Users\\foo\nActual title'), 'Actual title');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('skips quoted path-like lines', () => {
|
|
209
|
+
assert.equal(titleFromFirstSentence('"/usr/bin/node"\nActual title'), 'Actual title');
|
|
210
|
+
assert.equal(titleFromFirstSentence("'~/foo'\nActual title"), 'Actual title');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('truncates long titles with ellipsis', () => {
|
|
214
|
+
const longText = 'This is a very long title that keeps going and going and should be truncated at some reasonable length by the function';
|
|
215
|
+
const result = titleFromFirstSentence(longText);
|
|
216
|
+
assert.ok(result!.length <= 80);
|
|
217
|
+
assert.ok(result!.endsWith('...'));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('respects custom maxLength', () => {
|
|
221
|
+
const result = titleFromFirstSentence('This is a somewhat long title for testing', 20);
|
|
222
|
+
assert.ok(result!.length <= 20);
|
|
223
|
+
assert.ok(result!.endsWith('...'));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('does not truncate titles within maxLength', () => {
|
|
227
|
+
assert.equal(titleFromFirstSentence('Short title', 80), 'Short title');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('returns undefined if extracted title is ID-like', () => {
|
|
231
|
+
assert.equal(titleFromFirstSentence('550e8400-e29b-41d4-a716-446655440000'), undefined);
|
|
232
|
+
assert.equal(titleFromFirstSentence('abcdef0123456789'), undefined);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('skips empty lines to find the first meaningful line', () => {
|
|
236
|
+
assert.equal(titleFromFirstSentence('\n\n\nActual title'), 'Actual title');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('handles \\r\\n line endings', () => {
|
|
240
|
+
assert.equal(titleFromFirstSentence('\r\n\r\nActual title'), 'Actual title');
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('preferSessionTitle', () => {
|
|
245
|
+
it('returns first non-fallback candidate', () => {
|
|
246
|
+
assert.equal(preferSessionTitle(['Fix the bug', 'Other title']), 'Fix the bug');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('skips null and undefined candidates', () => {
|
|
250
|
+
assert.equal(preferSessionTitle([null, undefined, 'Fix the bug']), 'Fix the bug');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('skips generic fallback titles', () => {
|
|
254
|
+
assert.equal(
|
|
255
|
+
preferSessionTitle(['New session', 'Resumed session', 'Real title']),
|
|
256
|
+
'Real title',
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('skips ID-like candidates', () => {
|
|
261
|
+
assert.equal(
|
|
262
|
+
preferSessionTitle(['550e8400-e29b-41d4-a716-446655440000', 'Real title']),
|
|
263
|
+
'Real title',
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('falls back to titleFromFirstSentence when all candidates are fallback', () => {
|
|
268
|
+
assert.equal(
|
|
269
|
+
preferSessionTitle(['New session', null], 'Extract this title from text'),
|
|
270
|
+
'Extract this title from text',
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('returns undefined when no candidates and no fallback', () => {
|
|
275
|
+
assert.equal(preferSessionTitle([null, undefined]), undefined);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns undefined when all candidates are fallback and fallback text is empty', () => {
|
|
279
|
+
assert.equal(preferSessionTitle(['New session'], ''), undefined);
|
|
280
|
+
assert.equal(preferSessionTitle(['New session'], null), undefined);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('returns undefined for empty candidates array with no fallback', () => {
|
|
284
|
+
assert.equal(preferSessionTitle([]), undefined);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const GENERIC_SESSION_TITLES = new Set([
|
|
2
|
+
'New session',
|
|
3
|
+
'Resumed session',
|
|
4
|
+
'Untitled session',
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
const KNOWN_META_PREFIXES = [
|
|
8
|
+
'<environment_context>',
|
|
9
|
+
'<local-command-caveat>',
|
|
10
|
+
'<local-command-stdout>',
|
|
11
|
+
'<local-command-stderr>',
|
|
12
|
+
'<command-name>',
|
|
13
|
+
'<command-message>',
|
|
14
|
+
'<command-args>',
|
|
15
|
+
'# AGENTS.md instructions for ',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function collapseWhitespace(text: string): string {
|
|
19
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function sanitizeSessionTitle(title?: string | null): string | undefined {
|
|
23
|
+
if (typeof title !== 'string') return undefined;
|
|
24
|
+
const normalized = collapseWhitespace(title);
|
|
25
|
+
return normalized || undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isIdLikeSessionTitle(title?: string | null): boolean {
|
|
29
|
+
const normalized = sanitizeSessionTitle(title);
|
|
30
|
+
if (!normalized) return false;
|
|
31
|
+
return (
|
|
32
|
+
/^[0-9a-f]{8,}$/i.test(normalized) ||
|
|
33
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalized) ||
|
|
34
|
+
/^agent-[a-z0-9_-]{6,}$/i.test(normalized)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isFallbackSessionTitle(title?: string | null): boolean {
|
|
39
|
+
const normalized = sanitizeSessionTitle(title);
|
|
40
|
+
return !normalized || GENERIC_SESSION_TITLES.has(normalized) || isIdLikeSessionTitle(normalized);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function firstNonEmptyLine(text: string): string | undefined {
|
|
44
|
+
for (const line of text.replace(/\r/g, '').split('\n')) {
|
|
45
|
+
const normalized = sanitizeSessionTitle(line);
|
|
46
|
+
if (!normalized) continue;
|
|
47
|
+
if (isMetaTitleNoise(normalized)) continue;
|
|
48
|
+
if (isPathLikeTitleLine(normalized)) continue;
|
|
49
|
+
return normalized;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isMetaTitleNoise(text: string): boolean {
|
|
55
|
+
return KNOWN_META_PREFIXES.some(prefix => text.startsWith(prefix));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isPathLikeTitleLine(text: string): boolean {
|
|
59
|
+
const stripped = text
|
|
60
|
+
.replace(/^['"`]+/, '')
|
|
61
|
+
.replace(/['"`\\]+$/, '')
|
|
62
|
+
.trim();
|
|
63
|
+
if (!stripped) return true;
|
|
64
|
+
if (/^(\/|~\/|\.\/|\.\.\/)/.test(stripped)) return true;
|
|
65
|
+
if (/^[A-Za-z]:\\/.test(stripped)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findSentenceBoundary(text: string): number {
|
|
70
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
71
|
+
const char = text[index];
|
|
72
|
+
if (char === '。' || char === '!' || char === '?' || char === ';') {
|
|
73
|
+
return index + 1;
|
|
74
|
+
}
|
|
75
|
+
if (char === '.' || char === '!' || char === '?' || char === ';') {
|
|
76
|
+
const next = text[index + 1];
|
|
77
|
+
if (!next || /\s/.test(next)) {
|
|
78
|
+
return index + 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return -1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function titleFromFirstSentence(text?: string | null, maxLength = 80): string | undefined {
|
|
86
|
+
if (typeof text !== 'string') return undefined;
|
|
87
|
+
const line = firstNonEmptyLine(text);
|
|
88
|
+
if (!line) return undefined;
|
|
89
|
+
|
|
90
|
+
const boundary = findSentenceBoundary(line);
|
|
91
|
+
const sentence = boundary > 0 ? line.slice(0, boundary) : line;
|
|
92
|
+
const stripped = sentence.replace(/[。!?;.!?;]+$/gu, '').trim();
|
|
93
|
+
if (!stripped || isIdLikeSessionTitle(stripped)) return undefined;
|
|
94
|
+
if (stripped.length <= maxLength) return stripped;
|
|
95
|
+
return `${stripped.slice(0, Math.max(1, maxLength - 3)).trimEnd()}...`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function preferSessionTitle(
|
|
99
|
+
candidates: Array<string | null | undefined>,
|
|
100
|
+
fallbackText?: string | null,
|
|
101
|
+
): string | undefined {
|
|
102
|
+
for (const candidate of candidates) {
|
|
103
|
+
const normalized = sanitizeSessionTitle(candidate);
|
|
104
|
+
if (!normalized || isFallbackSessionTitle(normalized)) continue;
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
return titleFromFirstSentence(fallbackText);
|
|
108
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
|
|
8
|
+
// The bundled daemon entry is at the project root's dist/index.cjs
|
|
9
|
+
const daemonEntry = resolve(import.meta.dirname, '..', '..', '..', 'dist', 'index.cjs');
|
|
10
|
+
|
|
11
|
+
function waitForHealth(port: number, timeoutMs = 10000): Promise<boolean> {
|
|
12
|
+
const deadline = Date.now() + timeoutMs;
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
const poll = async () => {
|
|
15
|
+
if (Date.now() > deadline) { resolve(false); return; }
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
18
|
+
if (res.ok) { resolve(true); return; }
|
|
19
|
+
} catch { /* retry */ }
|
|
20
|
+
setTimeout(poll, 200);
|
|
21
|
+
};
|
|
22
|
+
poll();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function waitForExit(child: ReturnType<typeof spawn>, timeoutMs = 5000): Promise<number | null> {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
const timer = setTimeout(() => {
|
|
29
|
+
child.kill('SIGKILL');
|
|
30
|
+
resolve(null);
|
|
31
|
+
}, timeoutMs);
|
|
32
|
+
child.on('exit', (code) => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
resolve(code);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function spawnDaemon(port: number): { child: ReturnType<typeof spawn>; homeDir: string } {
|
|
40
|
+
const homeDir = mkdtempSync(join(tmpdir(), 'vibelet-shutdown-test-'));
|
|
41
|
+
const child = spawn(process.execPath, [daemonEntry], {
|
|
42
|
+
env: {
|
|
43
|
+
...process.env,
|
|
44
|
+
HOME: homeDir,
|
|
45
|
+
VIBE_PORT: String(port),
|
|
46
|
+
VIBE_TOKEN: 'test-token',
|
|
47
|
+
CLAUDE_PATH: process.execPath,
|
|
48
|
+
CODEX_PATH: process.execPath,
|
|
49
|
+
},
|
|
50
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
51
|
+
});
|
|
52
|
+
return { child, homeDir };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
test('POST /shutdown returns stopping and daemon exits', async () => {
|
|
56
|
+
const testPort = 29000 + Math.floor(Math.random() * 1000);
|
|
57
|
+
const { child, homeDir } = spawnDaemon(testPort);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const healthy = await waitForHealth(testPort);
|
|
61
|
+
assert.ok(healthy, 'Daemon should become healthy');
|
|
62
|
+
|
|
63
|
+
const res = await fetch(`http://127.0.0.1:${testPort}/shutdown`, { method: 'POST' });
|
|
64
|
+
assert.equal(res.status, 200);
|
|
65
|
+
const body = await res.json() as { status: string };
|
|
66
|
+
assert.equal(body.status, 'stopping');
|
|
67
|
+
|
|
68
|
+
const exitCode = await waitForExit(child);
|
|
69
|
+
assert.notEqual(exitCode, null, 'Daemon should exit within timeout');
|
|
70
|
+
} finally {
|
|
71
|
+
child.kill('SIGKILL');
|
|
72
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('GET /shutdown does not trigger shutdown', async () => {
|
|
77
|
+
const testPort = 29000 + Math.floor(Math.random() * 1000);
|
|
78
|
+
const { child, homeDir } = spawnDaemon(testPort);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const healthy = await waitForHealth(testPort);
|
|
82
|
+
assert.ok(healthy, 'Daemon should become healthy');
|
|
83
|
+
|
|
84
|
+
// GET /shutdown should not match the POST-only route
|
|
85
|
+
const res = await fetch(`http://127.0.0.1:${testPort}/shutdown`);
|
|
86
|
+
assert.notEqual(res.status, 200, 'GET /shutdown should not return 200');
|
|
87
|
+
|
|
88
|
+
// Daemon should still be alive
|
|
89
|
+
const stillHealthy = await waitForHealth(testPort, 1500);
|
|
90
|
+
assert.ok(stillHealthy, 'Daemon should still be running');
|
|
91
|
+
} finally {
|
|
92
|
+
child.kill('SIGKILL');
|
|
93
|
+
rmSync(homeDir, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { runStorageHousekeepingSync, trimFileTailSync } from './storage-housekeeping.js';
|
|
7
|
+
|
|
8
|
+
test('trimFileTailSync leaves files under the cap unchanged', () => {
|
|
9
|
+
const dir = mkdtempSync(join(tmpdir(), 'vibelet-storage-'));
|
|
10
|
+
const filePath = join(dir, 'audit.jsonl');
|
|
11
|
+
writeFileSync(filePath, 'one\ntwo\n', 'utf8');
|
|
12
|
+
|
|
13
|
+
const result = trimFileTailSync(filePath, { maxBytes: 64 });
|
|
14
|
+
|
|
15
|
+
assert.equal(result.trimmed, false);
|
|
16
|
+
assert.equal(readFileSync(filePath, 'utf8'), 'one\ntwo\n');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('trimFileTailSync keeps the newest tail aligned to a newline when trimming', () => {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), 'vibelet-storage-'));
|
|
21
|
+
const filePath = join(dir, 'daemon.stdout.log');
|
|
22
|
+
writeFileSync(filePath, 'alpha\nbravo\ncharlie\ndelta\n', 'utf8');
|
|
23
|
+
|
|
24
|
+
const result = trimFileTailSync(filePath, {
|
|
25
|
+
maxBytes: 20,
|
|
26
|
+
retainBytes: 20,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.equal(result.trimmed, true);
|
|
30
|
+
assert.equal(readFileSync(filePath, 'utf8'), 'charlie\ndelta\n');
|
|
31
|
+
assert.equal(result.afterBytes, Buffer.byteLength('charlie\ndelta\n'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('trimFileTailSync reserves space for an incoming append', () => {
|
|
35
|
+
const dir = mkdtempSync(join(tmpdir(), 'vibelet-storage-'));
|
|
36
|
+
const filePath = join(dir, 'audit.jsonl');
|
|
37
|
+
writeFileSync(filePath, '1111\n2222\n3333\n4444\n', 'utf8');
|
|
38
|
+
|
|
39
|
+
const result = trimFileTailSync(filePath, {
|
|
40
|
+
maxBytes: 18,
|
|
41
|
+
incomingBytes: 6,
|
|
42
|
+
retainBytes: 18,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
assert.equal(result.trimmed, true);
|
|
46
|
+
assert.equal(readFileSync(filePath, 'utf8'), '3333\n4444\n');
|
|
47
|
+
assert.ok(result.afterBytes + 6 <= 18);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('runStorageHousekeepingSync trims only oversized audit and log files', () => {
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), 'vibelet-storage-'));
|
|
52
|
+
const dataDir = join(dir, 'data');
|
|
53
|
+
const logDir = join(dir, 'logs');
|
|
54
|
+
mkdirSync(dataDir, { recursive: true });
|
|
55
|
+
mkdirSync(logDir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
const auditPath = join(dataDir, 'audit.jsonl');
|
|
58
|
+
const stdoutLogPath = join(logDir, 'daemon.stdout.log');
|
|
59
|
+
const stderrLogPath = join(logDir, 'daemon.stderr.log');
|
|
60
|
+
|
|
61
|
+
writeFileSync(auditPath, 'old-a\nold-b\nkeep-a\nkeep-b\n', 'utf8');
|
|
62
|
+
writeFileSync(stdoutLogPath, 'stdout-old\nstdout-keep\n', 'utf8');
|
|
63
|
+
writeFileSync(stderrLogPath, 'stderr-ok\n', 'utf8');
|
|
64
|
+
|
|
65
|
+
const result = runStorageHousekeepingSync({
|
|
66
|
+
auditPath,
|
|
67
|
+
stdoutLogPath,
|
|
68
|
+
stderrLogPath,
|
|
69
|
+
auditMaxBytes: 20,
|
|
70
|
+
logMaxBytes: 20,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
assert.equal(result.trimmedFiles, 2);
|
|
74
|
+
assert.ok(result.reclaimedBytes > 0);
|
|
75
|
+
assert.equal(readFileSync(auditPath, 'utf8'), 'keep-a\nkeep-b\n');
|
|
76
|
+
assert.equal(readFileSync(stdoutLogPath, 'utf8'), 'stdout-keep\n');
|
|
77
|
+
assert.equal(readFileSync(stderrLogPath, 'utf8'), 'stderr-ok\n');
|
|
78
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { closeSync, existsSync, ftruncateSync, openSync, readSync, statSync, writeSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
export interface TrimFileOptions {
|
|
4
|
+
maxBytes: number;
|
|
5
|
+
incomingBytes?: number;
|
|
6
|
+
retainBytes?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TrimFileResult {
|
|
10
|
+
path: string;
|
|
11
|
+
existed: boolean;
|
|
12
|
+
trimmed: boolean;
|
|
13
|
+
beforeBytes: number;
|
|
14
|
+
afterBytes: number;
|
|
15
|
+
reclaimedBytes: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StorageHousekeepingOptions {
|
|
19
|
+
auditPath: string;
|
|
20
|
+
stdoutLogPath: string;
|
|
21
|
+
stderrLogPath: string;
|
|
22
|
+
auditMaxBytes: number;
|
|
23
|
+
logMaxBytes: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StorageHousekeepingResult {
|
|
27
|
+
files: TrimFileResult[];
|
|
28
|
+
trimmedFiles: number;
|
|
29
|
+
reclaimedBytes: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function clampRetainBytes(beforeBytes: number, maxBytes: number, incomingBytes: number, retainBytes?: number): number {
|
|
33
|
+
const appendBudget = Math.max(0, maxBytes - incomingBytes);
|
|
34
|
+
const defaultRetain = Math.floor(maxBytes * 0.75);
|
|
35
|
+
return Math.max(0, Math.min(beforeBytes, appendBudget, retainBytes ?? defaultRetain));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function trimFileTailSync(filePath: string, options: TrimFileOptions): TrimFileResult {
|
|
39
|
+
const existed = existsSync(filePath);
|
|
40
|
+
if (!existed) {
|
|
41
|
+
return {
|
|
42
|
+
path: filePath,
|
|
43
|
+
existed: false,
|
|
44
|
+
trimmed: false,
|
|
45
|
+
beforeBytes: 0,
|
|
46
|
+
afterBytes: 0,
|
|
47
|
+
reclaimedBytes: 0,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const beforeBytes = statSync(filePath).size;
|
|
52
|
+
const incomingBytes = Math.max(0, options.incomingBytes ?? 0);
|
|
53
|
+
if (options.maxBytes <= 0 || beforeBytes + incomingBytes <= options.maxBytes) {
|
|
54
|
+
return {
|
|
55
|
+
path: filePath,
|
|
56
|
+
existed: true,
|
|
57
|
+
trimmed: false,
|
|
58
|
+
beforeBytes,
|
|
59
|
+
afterBytes: beforeBytes,
|
|
60
|
+
reclaimedBytes: 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const keepBytes = clampRetainBytes(beforeBytes, options.maxBytes, incomingBytes, options.retainBytes);
|
|
65
|
+
const fd = openSync(filePath, 'r+');
|
|
66
|
+
try {
|
|
67
|
+
let tail = Buffer.alloc(0);
|
|
68
|
+
if (keepBytes > 0) {
|
|
69
|
+
const buffer = Buffer.alloc(keepBytes);
|
|
70
|
+
readSync(fd, buffer, 0, keepBytes, beforeBytes - keepBytes);
|
|
71
|
+
let start = 0;
|
|
72
|
+
if (beforeBytes > keepBytes) {
|
|
73
|
+
const newlineIndex = buffer.indexOf(0x0a);
|
|
74
|
+
if (newlineIndex >= 0 && newlineIndex + 1 < buffer.length) {
|
|
75
|
+
start = newlineIndex + 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
tail = buffer.subarray(start);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ftruncateSync(fd, 0);
|
|
82
|
+
if (tail.length > 0) {
|
|
83
|
+
writeSync(fd, tail, 0, tail.length, 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
path: filePath,
|
|
88
|
+
existed: true,
|
|
89
|
+
trimmed: true,
|
|
90
|
+
beforeBytes,
|
|
91
|
+
afterBytes: tail.length,
|
|
92
|
+
reclaimedBytes: beforeBytes - tail.length,
|
|
93
|
+
};
|
|
94
|
+
} finally {
|
|
95
|
+
closeSync(fd);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function runStorageHousekeepingSync(options: StorageHousekeepingOptions): StorageHousekeepingResult {
|
|
100
|
+
const files = [
|
|
101
|
+
trimFileTailSync(options.auditPath, { maxBytes: options.auditMaxBytes }),
|
|
102
|
+
trimFileTailSync(options.stdoutLogPath, { maxBytes: options.logMaxBytes }),
|
|
103
|
+
trimFileTailSync(options.stderrLogPath, { maxBytes: options.logMaxBytes }),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
files,
|
|
108
|
+
trimmedFiles: files.filter(file => file.trimmed).length,
|
|
109
|
+
reclaimedBytes: files.reduce((sum, file) => sum + file.reclaimedBytes, 0),
|
|
110
|
+
};
|
|
111
|
+
}
|