@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.
Files changed (323) hide show
  1. package/app.json +5 -0
  2. package/dist/advertised-hosts.d.ts +34 -0
  3. package/dist/advertised-hosts.d.ts.map +1 -0
  4. package/dist/advertised-hosts.js +176 -0
  5. package/dist/advertised-hosts.js.map +1 -0
  6. package/dist/advertised-hosts.test.d.ts +2 -0
  7. package/dist/advertised-hosts.test.d.ts.map +1 -0
  8. package/dist/advertised-hosts.test.js +96 -0
  9. package/dist/advertised-hosts.test.js.map +1 -0
  10. package/dist/audit.d.ts +30 -0
  11. package/dist/audit.d.ts.map +1 -0
  12. package/dist/audit.js +73 -0
  13. package/dist/audit.js.map +1 -0
  14. package/dist/audit.test.d.ts +2 -0
  15. package/dist/audit.test.d.ts.map +1 -0
  16. package/dist/audit.test.js +33 -0
  17. package/dist/audit.test.js.map +1 -0
  18. package/dist/auth.d.ts +6 -0
  19. package/dist/auth.d.ts.map +1 -0
  20. package/dist/auth.js +27 -0
  21. package/dist/auth.js.map +1 -0
  22. package/dist/claude-hooks.d.ts +58 -0
  23. package/dist/claude-hooks.d.ts.map +1 -0
  24. package/dist/claude-hooks.js +129 -0
  25. package/dist/claude-hooks.js.map +1 -0
  26. package/dist/cli-version.d.ts +3 -0
  27. package/dist/cli-version.d.ts.map +1 -0
  28. package/dist/cli-version.js +35 -0
  29. package/dist/cli-version.js.map +1 -0
  30. package/dist/cli-version.test.d.ts +2 -0
  31. package/dist/cli-version.test.d.ts.map +1 -0
  32. package/dist/cli-version.test.js +38 -0
  33. package/dist/cli-version.test.js.map +1 -0
  34. package/dist/config.d.ts +30 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +327 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/config.test.d.ts +2 -0
  39. package/dist/config.test.d.ts.map +1 -0
  40. package/dist/config.test.js +184 -0
  41. package/dist/config.test.js.map +1 -0
  42. package/dist/dev-auth.test.d.ts +2 -0
  43. package/dist/dev-auth.test.d.ts.map +1 -0
  44. package/dist/dev-auth.test.js +154 -0
  45. package/dist/dev-auth.test.js.map +1 -0
  46. package/dist/dev-script.test.d.ts +2 -0
  47. package/dist/dev-script.test.d.ts.map +1 -0
  48. package/dist/dev-script.test.js +412 -0
  49. package/dist/dev-script.test.js.map +1 -0
  50. package/dist/drivers/claude.d.ts +34 -0
  51. package/dist/drivers/claude.d.ts.map +1 -0
  52. package/dist/drivers/claude.js +413 -0
  53. package/dist/drivers/claude.js.map +1 -0
  54. package/dist/drivers/claude.test.d.ts +2 -0
  55. package/dist/drivers/claude.test.d.ts.map +1 -0
  56. package/dist/drivers/claude.test.js +951 -0
  57. package/dist/drivers/claude.test.js.map +1 -0
  58. package/dist/drivers/codex.d.ts +38 -0
  59. package/dist/drivers/codex.d.ts.map +1 -0
  60. package/dist/drivers/codex.js +771 -0
  61. package/dist/drivers/codex.js.map +1 -0
  62. package/dist/drivers/codex.test.d.ts +2 -0
  63. package/dist/drivers/codex.test.d.ts.map +1 -0
  64. package/dist/drivers/codex.test.js +939 -0
  65. package/dist/drivers/codex.test.js.map +1 -0
  66. package/dist/drivers/types.d.ts +14 -0
  67. package/dist/drivers/types.d.ts.map +1 -0
  68. package/dist/drivers/types.js +2 -0
  69. package/dist/drivers/types.js.map +1 -0
  70. package/dist/e2e.test.d.ts +2 -0
  71. package/dist/e2e.test.d.ts.map +1 -0
  72. package/dist/e2e.test.js +111 -0
  73. package/dist/e2e.test.js.map +1 -0
  74. package/dist/identity.d.ts +10 -0
  75. package/dist/identity.d.ts.map +1 -0
  76. package/dist/identity.js +66 -0
  77. package/dist/identity.js.map +1 -0
  78. package/dist/identity.test.d.ts +2 -0
  79. package/dist/identity.test.d.ts.map +1 -0
  80. package/dist/identity.test.js +25 -0
  81. package/dist/identity.test.js.map +1 -0
  82. package/dist/index-entry.test.d.ts +2 -0
  83. package/dist/index-entry.test.d.ts.map +1 -0
  84. package/dist/index-entry.test.js +272 -0
  85. package/dist/index-entry.test.js.map +1 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +707 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/logger.d.ts +31 -0
  91. package/dist/logger.d.ts.map +1 -0
  92. package/dist/logger.js +75 -0
  93. package/dist/logger.js.map +1 -0
  94. package/dist/metrics.d.ts +52 -0
  95. package/dist/metrics.d.ts.map +1 -0
  96. package/dist/metrics.js +89 -0
  97. package/dist/metrics.js.map +1 -0
  98. package/dist/pairing-store.d.ts +29 -0
  99. package/dist/pairing-store.d.ts.map +1 -0
  100. package/dist/pairing-store.js +131 -0
  101. package/dist/pairing-store.js.map +1 -0
  102. package/dist/pairing-store.test.d.ts +2 -0
  103. package/dist/pairing-store.test.d.ts.map +1 -0
  104. package/dist/pairing-store.test.js +47 -0
  105. package/dist/pairing-store.test.js.map +1 -0
  106. package/dist/paths.d.ts +16 -0
  107. package/dist/paths.d.ts.map +1 -0
  108. package/dist/paths.js +18 -0
  109. package/dist/paths.js.map +1 -0
  110. package/dist/perf-compare.d.ts +13 -0
  111. package/dist/perf-compare.d.ts.map +1 -0
  112. package/dist/perf-compare.js +125 -0
  113. package/dist/perf-compare.js.map +1 -0
  114. package/dist/port-conflict.d.ts +9 -0
  115. package/dist/port-conflict.d.ts.map +1 -0
  116. package/dist/port-conflict.js +33 -0
  117. package/dist/port-conflict.js.map +1 -0
  118. package/dist/port-conflict.test.d.ts +2 -0
  119. package/dist/port-conflict.test.d.ts.map +1 -0
  120. package/dist/port-conflict.test.js +38 -0
  121. package/dist/port-conflict.test.js.map +1 -0
  122. package/dist/process-scanner.d.ts +43 -0
  123. package/dist/process-scanner.d.ts.map +1 -0
  124. package/dist/process-scanner.js +453 -0
  125. package/dist/process-scanner.js.map +1 -0
  126. package/dist/process-scanner.perf.test.d.ts +2 -0
  127. package/dist/process-scanner.perf.test.d.ts.map +1 -0
  128. package/dist/process-scanner.perf.test.js +186 -0
  129. package/dist/process-scanner.perf.test.js.map +1 -0
  130. package/dist/process-scanner.test.d.ts +2 -0
  131. package/dist/process-scanner.test.d.ts.map +1 -0
  132. package/dist/process-scanner.test.js +399 -0
  133. package/dist/process-scanner.test.js.map +1 -0
  134. package/dist/push-protocol.d.ts +15 -0
  135. package/dist/push-protocol.d.ts.map +1 -0
  136. package/dist/push-protocol.js +23 -0
  137. package/dist/push-protocol.js.map +1 -0
  138. package/dist/push-protocol.test.d.ts +2 -0
  139. package/dist/push-protocol.test.d.ts.map +1 -0
  140. package/dist/push-protocol.test.js +57 -0
  141. package/dist/push-protocol.test.js.map +1 -0
  142. package/dist/push-store.d.ts +22 -0
  143. package/dist/push-store.d.ts.map +1 -0
  144. package/dist/push-store.js +103 -0
  145. package/dist/push-store.js.map +1 -0
  146. package/dist/push-store.test.d.ts +2 -0
  147. package/dist/push-store.test.d.ts.map +1 -0
  148. package/dist/push-store.test.js +79 -0
  149. package/dist/push-store.test.js.map +1 -0
  150. package/dist/push.d.ts +65 -0
  151. package/dist/push.d.ts.map +1 -0
  152. package/dist/push.js +202 -0
  153. package/dist/push.js.map +1 -0
  154. package/dist/push.test.d.ts +2 -0
  155. package/dist/push.test.d.ts.map +1 -0
  156. package/dist/push.test.js +199 -0
  157. package/dist/push.test.js.map +1 -0
  158. package/dist/safe-stdio.d.ts +3 -0
  159. package/dist/safe-stdio.d.ts.map +1 -0
  160. package/dist/safe-stdio.js +46 -0
  161. package/dist/safe-stdio.js.map +1 -0
  162. package/dist/scanner.d.ts +30 -0
  163. package/dist/scanner.d.ts.map +1 -0
  164. package/dist/scanner.js +859 -0
  165. package/dist/scanner.js.map +1 -0
  166. package/dist/scanner.perf.test.d.ts +2 -0
  167. package/dist/scanner.perf.test.d.ts.map +1 -0
  168. package/dist/scanner.perf.test.js +320 -0
  169. package/dist/scanner.perf.test.js.map +1 -0
  170. package/dist/scanner.test.d.ts +2 -0
  171. package/dist/scanner.test.d.ts.map +1 -0
  172. package/dist/scanner.test.js +948 -0
  173. package/dist/scanner.test.js.map +1 -0
  174. package/dist/session-inventory.d.ts +63 -0
  175. package/dist/session-inventory.d.ts.map +1 -0
  176. package/dist/session-inventory.js +525 -0
  177. package/dist/session-inventory.js.map +1 -0
  178. package/dist/session-inventory.perf.test.d.ts +2 -0
  179. package/dist/session-inventory.perf.test.d.ts.map +1 -0
  180. package/dist/session-inventory.perf.test.js +220 -0
  181. package/dist/session-inventory.perf.test.js.map +1 -0
  182. package/dist/session-inventory.test.d.ts +2 -0
  183. package/dist/session-inventory.test.d.ts.map +1 -0
  184. package/dist/session-inventory.test.js +712 -0
  185. package/dist/session-inventory.test.js.map +1 -0
  186. package/dist/session-manager.d.ts +75 -0
  187. package/dist/session-manager.d.ts.map +1 -0
  188. package/dist/session-manager.js +1515 -0
  189. package/dist/session-manager.js.map +1 -0
  190. package/dist/session-manager.test.d.ts +2 -0
  191. package/dist/session-manager.test.d.ts.map +1 -0
  192. package/dist/session-manager.test.js +2861 -0
  193. package/dist/session-manager.test.js.map +1 -0
  194. package/dist/session-store.d.ts +42 -0
  195. package/dist/session-store.d.ts.map +1 -0
  196. package/dist/session-store.js +163 -0
  197. package/dist/session-store.js.map +1 -0
  198. package/dist/session-store.test.d.ts +2 -0
  199. package/dist/session-store.test.d.ts.map +1 -0
  200. package/dist/session-store.test.js +236 -0
  201. package/dist/session-store.test.js.map +1 -0
  202. package/dist/session-title.d.ts +6 -0
  203. package/dist/session-title.d.ts.map +1 -0
  204. package/dist/session-title.js +105 -0
  205. package/dist/session-title.js.map +1 -0
  206. package/dist/session-title.perf.test.d.ts +2 -0
  207. package/dist/session-title.perf.test.d.ts.map +1 -0
  208. package/dist/session-title.perf.test.js +99 -0
  209. package/dist/session-title.perf.test.js.map +1 -0
  210. package/dist/session-title.test.d.ts +2 -0
  211. package/dist/session-title.test.d.ts.map +1 -0
  212. package/dist/session-title.test.js +199 -0
  213. package/dist/session-title.test.js.map +1 -0
  214. package/dist/shutdown-endpoint.test.d.ts +2 -0
  215. package/dist/shutdown-endpoint.test.d.ts.map +1 -0
  216. package/dist/shutdown-endpoint.test.js +93 -0
  217. package/dist/shutdown-endpoint.test.js.map +1 -0
  218. package/dist/storage-housekeeping.d.ts +28 -0
  219. package/dist/storage-housekeeping.d.ts.map +1 -0
  220. package/dist/storage-housekeeping.js +76 -0
  221. package/dist/storage-housekeeping.js.map +1 -0
  222. package/dist/storage-housekeeping.test.d.ts +2 -0
  223. package/dist/storage-housekeeping.test.d.ts.map +1 -0
  224. package/dist/storage-housekeeping.test.js +65 -0
  225. package/dist/storage-housekeeping.test.js.map +1 -0
  226. package/dist/test-daemon-harness.d.ts +31 -0
  227. package/dist/test-daemon-harness.d.ts.map +1 -0
  228. package/dist/test-daemon-harness.js +337 -0
  229. package/dist/test-daemon-harness.js.map +1 -0
  230. package/dist/token-auth.test.d.ts +2 -0
  231. package/dist/token-auth.test.d.ts.map +1 -0
  232. package/dist/token-auth.test.js +52 -0
  233. package/dist/token-auth.test.js.map +1 -0
  234. package/dist/utils.d.ts +4 -0
  235. package/dist/utils.d.ts.map +1 -0
  236. package/dist/utils.js +40 -0
  237. package/dist/utils.js.map +1 -0
  238. package/dist/utils.test.d.ts +2 -0
  239. package/dist/utils.test.d.ts.map +1 -0
  240. package/dist/utils.test.js +54 -0
  241. package/dist/utils.test.js.map +1 -0
  242. package/dist/ws-data.d.ts +4 -0
  243. package/dist/ws-data.d.ts.map +1 -0
  244. package/dist/ws-data.js +20 -0
  245. package/dist/ws-data.js.map +1 -0
  246. package/dist/ws-data.test.d.ts +2 -0
  247. package/dist/ws-data.test.d.ts.map +1 -0
  248. package/dist/ws-data.test.js +17 -0
  249. package/dist/ws-data.test.js.map +1 -0
  250. package/package.json +24 -27
  251. package/perf-reporter.mjs +138 -0
  252. package/scripts/build-release.mjs +41 -0
  253. package/scripts/dev.mjs +537 -0
  254. package/src/advertised-hosts.test.ts +125 -0
  255. package/src/advertised-hosts.ts +225 -0
  256. package/src/audit.test.ts +38 -0
  257. package/src/audit.ts +117 -0
  258. package/src/auth.ts +31 -0
  259. package/src/claude-hooks.ts +195 -0
  260. package/src/cli-version.test.ts +36 -0
  261. package/src/cli-version.ts +46 -0
  262. package/src/config.test.ts +254 -0
  263. package/src/config.ts +324 -0
  264. package/src/dev-auth.test.ts +183 -0
  265. package/src/dev-script.test.ts +511 -0
  266. package/src/drivers/claude.test.ts +1186 -0
  267. package/src/drivers/claude.ts +443 -0
  268. package/src/drivers/codex.test.ts +1096 -0
  269. package/src/drivers/codex.ts +879 -0
  270. package/src/drivers/types.ts +15 -0
  271. package/src/e2e.test.ts +139 -0
  272. package/src/identity.test.ts +26 -0
  273. package/src/identity.ts +82 -0
  274. package/src/index-entry.test.ts +336 -0
  275. package/src/index.ts +781 -0
  276. package/src/logger.ts +112 -0
  277. package/src/metrics.ts +117 -0
  278. package/src/pairing-store.test.ts +53 -0
  279. package/src/pairing-store.ts +154 -0
  280. package/src/paths.ts +19 -0
  281. package/src/perf-compare.ts +164 -0
  282. package/src/port-conflict.test.ts +45 -0
  283. package/src/port-conflict.ts +44 -0
  284. package/src/process-scanner.perf.test.ts +222 -0
  285. package/src/process-scanner.test.ts +575 -0
  286. package/src/process-scanner.ts +514 -0
  287. package/src/push-protocol.test.ts +74 -0
  288. package/src/push-protocol.ts +36 -0
  289. package/src/push-store.test.ts +89 -0
  290. package/src/push-store.ts +126 -0
  291. package/src/push.test.ts +234 -0
  292. package/src/push.ts +318 -0
  293. package/src/safe-stdio.ts +51 -0
  294. package/src/scanner.perf.test.ts +359 -0
  295. package/src/scanner.test.ts +1045 -0
  296. package/src/scanner.ts +924 -0
  297. package/src/session-inventory.perf.test.ts +250 -0
  298. package/src/session-inventory.test.ts +1002 -0
  299. package/src/session-inventory.ts +721 -0
  300. package/src/session-manager.test.ts +3430 -0
  301. package/src/session-manager.ts +1775 -0
  302. package/src/session-store.test.ts +276 -0
  303. package/src/session-store.ts +202 -0
  304. package/src/session-title.perf.test.ts +118 -0
  305. package/src/session-title.test.ts +286 -0
  306. package/src/session-title.ts +108 -0
  307. package/src/shutdown-endpoint.test.ts +95 -0
  308. package/src/storage-housekeeping.test.ts +78 -0
  309. package/src/storage-housekeeping.ts +111 -0
  310. package/src/test-daemon-harness.ts +410 -0
  311. package/src/token-auth.test.ts +67 -0
  312. package/src/utils.test.ts +65 -0
  313. package/src/utils.ts +47 -0
  314. package/src/ws-data.test.ts +20 -0
  315. package/src/ws-data.ts +26 -0
  316. package/tsconfig.json +12 -0
  317. package/README.md +0 -80
  318. package/bin/cloudflared-quick-tunnel.mjs +0 -11
  319. package/bin/cloudflared-resolver.mjs +0 -68
  320. package/bin/vibelet-runtime-policy.mjs +0 -36
  321. package/bin/vibelet.cjs +0 -12
  322. package/bin/vibelet.mjs +0 -1019
  323. package/dist/index.cjs +0 -123
@@ -0,0 +1,254 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+ import { isTransientPath, buildSanitizedEnv, execViaLoginShell, config } from './config.js';
7
+
8
+ const configModuleUrl = new URL('./config.ts', import.meta.url).href;
9
+
10
+ function buildChildEnv(overrides: Record<string, string | undefined>): NodeJS.ProcessEnv {
11
+ const nextEnv: NodeJS.ProcessEnv = { ...process.env };
12
+ for (const [key, value] of Object.entries(overrides)) {
13
+ if (value === undefined) {
14
+ delete nextEnv[key];
15
+ } else {
16
+ nextEnv[key] = value;
17
+ }
18
+ }
19
+ return nextEnv;
20
+ }
21
+
22
+ function readConfigValueInIsolatedProcess(
23
+ overrides: Record<string, string | undefined>,
24
+ expression: string,
25
+ ): string {
26
+ return execFileSync(
27
+ process.execPath,
28
+ [
29
+ '--import',
30
+ 'tsx',
31
+ '-e',
32
+ `import { config } from ${JSON.stringify(configModuleUrl)}; process.stdout.write(String(${expression}));`,
33
+ ],
34
+ {
35
+ env: buildChildEnv(overrides),
36
+ encoding: 'utf8',
37
+ },
38
+ ).trim();
39
+ }
40
+
41
+ test('isTransientPath: detects fnm_multishells as transient', () => {
42
+ assert.equal(isTransientPath('/tmp/fnm_multishells/12345/bin/codex'), true);
43
+ });
44
+
45
+ test('isTransientPath: returns false for normal paths', () => {
46
+ assert.equal(isTransientPath('/usr/local/bin/codex'), false);
47
+ });
48
+
49
+ test('isTransientPath: returns false for home directory paths', () => {
50
+ assert.equal(isTransientPath('/Users/test/.local/bin/claude'), false);
51
+ });
52
+
53
+ test('isTransientPath: returns false for empty string', () => {
54
+ assert.equal(isTransientPath(''), false);
55
+ });
56
+
57
+ // buildSanitizedEnv tests
58
+
59
+ test('buildSanitizedEnv: returns an object with PATH key', () => {
60
+ const env = buildSanitizedEnv();
61
+ assert.ok('PATH' in env);
62
+ assert.equal(typeof env.PATH, 'string');
63
+ assert.ok(env.PATH.length > 0);
64
+ });
65
+
66
+ test('buildSanitizedEnv: PATH includes extra directories', () => {
67
+ const env = buildSanitizedEnv();
68
+ assert.ok(env.PATH.includes('/usr/local/bin'));
69
+ });
70
+
71
+ test('buildSanitizedEnv: HOME is set', () => {
72
+ const env = buildSanitizedEnv();
73
+ assert.ok('HOME' in env);
74
+ assert.equal(typeof env.HOME, 'string');
75
+ assert.ok(env.HOME.length > 0);
76
+ });
77
+
78
+ test('buildSanitizedEnv: CLAUDE_CODE_ENTRYPOINT is removed', () => {
79
+ process.env.CLAUDE_CODE_ENTRYPOINT = 'test_value';
80
+ const env = buildSanitizedEnv();
81
+ assert.equal(env.CLAUDE_CODE_ENTRYPOINT, undefined);
82
+ delete process.env.CLAUDE_CODE_ENTRYPOINT;
83
+ });
84
+
85
+ test('buildSanitizedEnv: CLAUDECODE is removed', () => {
86
+ process.env.CLAUDECODE = 'test_value';
87
+ const env = buildSanitizedEnv();
88
+ assert.equal(env.CLAUDECODE, undefined);
89
+ delete process.env.CLAUDECODE;
90
+ });
91
+
92
+ test('buildSanitizedEnv: contains process.env values', () => {
93
+ const testKey = 'VIBE_TEST_UNIQUE_KEY_' + Date.now();
94
+ process.env[testKey] = 'test_value_123';
95
+ const env = buildSanitizedEnv();
96
+ assert.equal(env[testKey], 'test_value_123');
97
+ delete process.env[testKey];
98
+ });
99
+
100
+ // execViaLoginShell tests
101
+
102
+ test('execViaLoginShell: returns correct command (a shell path)', () => {
103
+ const result = execViaLoginShell('claude', ['--help']);
104
+ assert.equal(typeof result.command, 'string');
105
+ assert.ok(result.command.startsWith('/'));
106
+ });
107
+
108
+ test('execViaLoginShell: args contain -lc flag', () => {
109
+ const result = execViaLoginShell('claude', ['--help']);
110
+ assert.equal(result.args[0], '-lc');
111
+ });
112
+
113
+ test('execViaLoginShell: includes the passed cmd in the shell command string', () => {
114
+ const result = execViaLoginShell('claude', ['--help', '--verbose']);
115
+ assert.ok(result.args[1].includes('claude'));
116
+ });
117
+
118
+ test('execViaLoginShell: includes the passed args after -- separator', () => {
119
+ const result = execViaLoginShell('claude', ['--help', '--verbose']);
120
+ const dashDashIndex = result.args.indexOf('--');
121
+ assert.ok(dashDashIndex >= 0);
122
+ assert.equal(result.args[dashDashIndex + 1], '--help');
123
+ assert.equal(result.args[dashDashIndex + 2], '--verbose');
124
+ });
125
+
126
+ // config object tests
127
+
128
+ test('config.port: defaults to 9876 when VIBE_PORT not set', () => {
129
+ assert.equal(config.port, 9876);
130
+ });
131
+
132
+ test('config.legacyToken: defaults to an empty string', () => {
133
+ assert.equal(config.legacyToken, '');
134
+ });
135
+
136
+ test('config.legacyToken: respects VIBE_TOKEN env override on fresh import', async () => {
137
+ const original = process.env.VIBE_TOKEN;
138
+ process.env.VIBE_TOKEN = 'legacy-secret';
139
+ try {
140
+ const freshModule = await import(`./config.ts?test=${Date.now()}`);
141
+ assert.equal(freshModule.config.legacyToken, 'legacy-secret');
142
+ } finally {
143
+ if (original == null) {
144
+ delete process.env.VIBE_TOKEN;
145
+ } else {
146
+ process.env.VIBE_TOKEN = original;
147
+ }
148
+ }
149
+ });
150
+
151
+ test('config.idleTimeoutMs: allows disabling idle sweep with 0', () => {
152
+ assert.equal(
153
+ readConfigValueInIsolatedProcess(
154
+ { VIBE_IDLE_TIMEOUT_MS: '0' },
155
+ 'config.idleTimeoutMs',
156
+ ),
157
+ '0',
158
+ );
159
+ });
160
+
161
+ test('config.turnStallTimeoutMs: defaults to 5 minutes', () => {
162
+ assert.equal(config.turnStallTimeoutMs, 5 * 60 * 1000);
163
+ });
164
+
165
+ test('config.turnStallTimeoutMs: allows disabling stall sweep with 0', () => {
166
+ assert.equal(
167
+ readConfigValueInIsolatedProcess(
168
+ { VIBE_TURN_STALL_TIMEOUT_MS: '0' },
169
+ 'config.turnStallTimeoutMs',
170
+ ),
171
+ '0',
172
+ );
173
+ });
174
+
175
+ test('config.turnStallTimeoutMs: respects env override on fresh import', () => {
176
+ assert.equal(
177
+ readConfigValueInIsolatedProcess(
178
+ { VIBE_TURN_STALL_TIMEOUT_MS: '120000' },
179
+ 'config.turnStallTimeoutMs',
180
+ ),
181
+ '120000',
182
+ );
183
+ });
184
+
185
+ test('config.isTransientPath: is the isTransientPath function', () => {
186
+ assert.equal(config.isTransientPath, isTransientPath);
187
+ });
188
+
189
+ test('config.buildSanitizedEnv: is the buildSanitizedEnv function', () => {
190
+ assert.equal(config.buildSanitizedEnv, buildSanitizedEnv);
191
+ });
192
+
193
+ // Binary detection / cached resolver tests
194
+
195
+ test('config.claudePath: returns a path string', () => {
196
+ // This may resolve to a real path or fall back to 'claude'
197
+ const path = config.claudePath;
198
+ assert.equal(typeof path, 'string');
199
+ assert.ok(path.length > 0);
200
+ });
201
+
202
+ test('config.codexPath: returns a path string', () => {
203
+ const path = config.codexPath;
204
+ assert.equal(typeof path, 'string');
205
+ assert.ok(path.length > 0);
206
+ });
207
+
208
+ test('config.claudePath: respects CLAUDE_PATH env override', () => {
209
+ assert.equal(
210
+ readConfigValueInIsolatedProcess(
211
+ { CLAUDE_PATH: '/custom/path/to/claude' },
212
+ 'config.claudePath',
213
+ ),
214
+ '/custom/path/to/claude',
215
+ );
216
+ });
217
+
218
+ test('config.codexPath: respects CODEX_PATH env override', () => {
219
+ assert.equal(
220
+ readConfigValueInIsolatedProcess(
221
+ { CODEX_PATH: '/custom/path/to/codex' },
222
+ 'config.codexPath',
223
+ ),
224
+ '/custom/path/to/codex',
225
+ );
226
+ });
227
+
228
+ test('buildSanitizedEnv: PATH includes all required extra directories', () => {
229
+ const env = buildSanitizedEnv();
230
+ const home = homedir();
231
+ const requiredDirs = [
232
+ '/usr/local/bin',
233
+ '/opt/homebrew/bin',
234
+ join(home, '.npm-global', 'bin'),
235
+ join(home, '.local', 'bin'),
236
+ join(home, '.claude', 'bin'),
237
+ ];
238
+ for (const dir of requiredDirs) {
239
+ assert.ok(env.PATH.includes(dir), `PATH should include ${dir}`);
240
+ }
241
+ });
242
+
243
+ test('buildSanitizedEnv: includes home-relative paths', () => {
244
+ const env = buildSanitizedEnv();
245
+ const home = homedir();
246
+ assert.ok(env.PATH.includes(join(home, '.local', 'bin')) || env.PATH.includes('.local/bin'));
247
+ });
248
+
249
+ test('execViaLoginShell: works with codex command', () => {
250
+ const result = execViaLoginShell('codex', ['app-server', '--listen', 'stdio://']);
251
+ assert.ok(result.args[1].includes('codex'));
252
+ const dashDashIndex = result.args.indexOf('--');
253
+ assert.equal(result.args[dashDashIndex + 1], 'app-server');
254
+ });
package/src/config.ts ADDED
@@ -0,0 +1,324 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join, delimiter } from 'path';
4
+ import { homedir } from 'os';
5
+ import { logger as rootLogger } from './logger.js';
6
+
7
+ const TRANSIENT_PATH_MARKERS = ['fnm_multishells'];
8
+ const CACHE_TTL = 60_000;
9
+ const IS_WIN32 = process.platform === 'win32';
10
+ const log = rootLogger.child({ module: 'config' });
11
+
12
+ export function isTransientPath(p: string): boolean {
13
+ return TRANSIENT_PATH_MARKERS.some(marker => p.includes(marker));
14
+ }
15
+
16
+ function getLoginShellCandidates(): string[] {
17
+ if (IS_WIN32) return []; // No login shell on Windows
18
+ return [process.env.SHELL, '/bin/zsh', '/bin/bash', '/bin/sh']
19
+ .filter((s): s is string => !!s)
20
+ .filter((s, i, a) => a.indexOf(s) === i);
21
+ }
22
+
23
+ // ===== Shell Environment =====
24
+
25
+ let cachedShellEnv: Record<string, string> | null = null;
26
+
27
+ function loadUserShellEnv(): Record<string, string> {
28
+ if (cachedShellEnv && !isTransientPath(cachedShellEnv.PATH ?? '')) return cachedShellEnv;
29
+
30
+ for (const shell of getLoginShellCandidates()) {
31
+ try {
32
+ const result = execFileSync(shell, ['-ilc', 'env'], {
33
+ timeout: 5000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
34
+ });
35
+ const env: Record<string, string> = {};
36
+ for (const line of result.split('\n')) {
37
+ const idx = line.indexOf('=');
38
+ if (idx > 0) env[line.slice(0, idx)] = line.slice(idx + 1);
39
+ }
40
+ if (!isTransientPath(env.PATH ?? '')) {
41
+ cachedShellEnv = env;
42
+ } else {
43
+ cachedShellEnv = null;
44
+ }
45
+ return env;
46
+ } catch { /* next */ }
47
+ }
48
+ cachedShellEnv = {};
49
+ return {};
50
+ }
51
+
52
+ function getExpandedPath(shellEnv: Record<string, string> = loadUserShellEnv()): string {
53
+ const home = homedir();
54
+ const pnpmHome = shellEnv.PNPM_HOME || process.env.PNPM_HOME || (IS_WIN32 ? join(home, 'pnpm') : join(home, 'Library', 'pnpm'));
55
+ const basePath = shellEnv.PATH || process.env.PATH || '';
56
+ const extra = IS_WIN32
57
+ ? [
58
+ join(home, '.npm-global', 'bin'),
59
+ join(home, '.local', 'bin'),
60
+ join(home, '.claude', 'bin'),
61
+ pnpmHome,
62
+ join(home, 'AppData', 'Roaming', 'npm'),
63
+ join(home, 'AppData', 'Local', 'Programs', 'node'),
64
+ ]
65
+ : [
66
+ '/usr/local/bin', '/opt/homebrew/bin', '/usr/bin', '/bin',
67
+ join(home, '.npm-global', 'bin'),
68
+ join(home, '.local', 'bin'),
69
+ join(home, '.claude', 'bin'),
70
+ pnpmHome,
71
+ ];
72
+ const current = basePath.split(delimiter).filter(Boolean);
73
+ const seen = new Set(current);
74
+ for (const p of extra) {
75
+ if (!seen.has(p)) { current.push(p); seen.add(p); }
76
+ }
77
+ return current.join(delimiter);
78
+ }
79
+
80
+ function getPositiveNumberEnv(name: string, fallback: number): number {
81
+ const raw = process.env[name];
82
+ if (!raw) return fallback;
83
+ const parsed = Number(raw);
84
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
85
+ }
86
+
87
+ function getNonNegativeNumberEnv(name: string, fallback: number): number {
88
+ const raw = process.env[name];
89
+ if (raw == null || raw === '') return fallback;
90
+ const parsed = Number(raw);
91
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
92
+ }
93
+
94
+ export function buildSanitizedEnv(): Record<string, string> {
95
+ const shellEnv = loadUserShellEnv();
96
+ const merged: Record<string, string> = {};
97
+ for (const [k, v] of Object.entries(process.env)) {
98
+ if (typeof v === 'string') merged[k] = v;
99
+ }
100
+ for (const [k, v] of Object.entries(shellEnv)) {
101
+ if (typeof v === 'string') merged[k] = v;
102
+ }
103
+ merged.PATH = getExpandedPath(shellEnv);
104
+ if (!merged.HOME) merged.HOME = homedir();
105
+ delete merged.CLAUDE_CODE_ENTRYPOINT;
106
+ delete merged.CLAUDECODE;
107
+ return merged;
108
+ }
109
+
110
+ // ===== Binary Detection =====
111
+
112
+ /**
113
+ * On Windows, resolve a bare command name like "codex" to its real executable
114
+ * path (e.g. codex.exe) using `where.exe`. This avoids the common issue where
115
+ * Node's `spawn('codex')` fails with ENOENT because it doesn't search PATH
116
+ * for `.cmd`/`.ps1`/`.exe` extensions the way PowerShell/cmd do.
117
+ */
118
+ function resolveViaWhere(name: string): string | undefined {
119
+ if (!IS_WIN32) return undefined;
120
+ try {
121
+ const output = execFileSync('where.exe', [name], {
122
+ timeout: 3000, stdio: 'pipe', encoding: 'utf-8',
123
+ }).trim();
124
+ // `where` returns one result per line. Prefer .exe over .cmd/.ps1
125
+ const lines = output.split(/\r?\n/).filter(Boolean);
126
+ const exe = lines.find(l => l.endsWith('.exe'));
127
+ return exe ?? lines[0];
128
+ } catch { return undefined; }
129
+ }
130
+
131
+ function runVersionSync(binaryPath: string, timeout = 3000): string | null {
132
+ try {
133
+ return execFileSync(binaryPath, ['--version'], {
134
+ timeout, stdio: 'pipe', encoding: 'utf-8', env: buildSanitizedEnv(),
135
+ ...(IS_WIN32 ? { shell: true } : {}),
136
+ }).trim() || null;
137
+ } catch { return null; }
138
+ }
139
+
140
+ function runViaLoginShellSync(cmd: string, args: string[], timeout = 5000): string | null {
141
+ for (const shell of getLoginShellCandidates()) {
142
+ try {
143
+ return execFileSync(shell, ['-ilc', `command -v ${cmd} >/dev/null 2>&1 && exec ${cmd} "$@"`, '--', ...args], {
144
+ timeout, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8',
145
+ }).trim() || null;
146
+ } catch { /* next */ }
147
+ }
148
+ return null;
149
+ }
150
+
151
+ function findViaLoginShell(cmd: string): string | undefined {
152
+ for (const shell of getLoginShellCandidates()) {
153
+ try {
154
+ const result = execFileSync(shell, ['-ilc', `command -v ${cmd}`], {
155
+ timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf-8',
156
+ }).trim();
157
+ if (result) return result;
158
+ } catch { /* next */ }
159
+ }
160
+ return undefined;
161
+ }
162
+
163
+ // ===== Claude Detection =====
164
+
165
+ function getClaudeCandidates(): string[] {
166
+ const home = homedir();
167
+ return [
168
+ join(home, '.local', 'bin', 'claude'),
169
+ join(home, '.claude', 'bin', 'claude'),
170
+ '/usr/local/bin/claude',
171
+ '/opt/homebrew/bin/claude',
172
+ join(home, '.npm-global', 'bin', 'claude'),
173
+ ];
174
+ }
175
+
176
+ function findClaudeUncached(): string | undefined {
177
+ // On Windows, use `where.exe` instead of known Unix paths
178
+ if (IS_WIN32) {
179
+ const found = resolveViaWhere('claude');
180
+ if (found && runVersionSync(found)) return found;
181
+ return undefined;
182
+ }
183
+ // Check known paths
184
+ for (const p of getClaudeCandidates()) {
185
+ if (runVersionSync(p)) return p;
186
+ }
187
+ // which (with sanitized env that has expanded PATH)
188
+ try {
189
+ const result = execFileSync('/usr/bin/which', ['claude'], {
190
+ timeout: 3000, stdio: 'pipe', encoding: 'utf-8', env: buildSanitizedEnv(),
191
+ }).trim();
192
+ // Skip cmux wrappers
193
+ if (result && !result.includes('cmux') && runVersionSync(result)) return result;
194
+ } catch { /* not found */ }
195
+ return undefined;
196
+ }
197
+
198
+ // ===== Codex Detection =====
199
+
200
+ function getCodexCandidates(): string[] {
201
+ const home = homedir();
202
+ const pnpmHomes = [process.env.PNPM_HOME, join(home, 'Library', 'pnpm')]
203
+ .filter((p): p is string => !!p);
204
+ return [
205
+ '/usr/local/bin/codex',
206
+ '/opt/homebrew/bin/codex',
207
+ join(home, '.npm-global', 'bin', 'codex'),
208
+ join(home, '.local', 'bin', 'codex'),
209
+ ...pnpmHomes.map(p => join(p, 'codex')),
210
+ ];
211
+ }
212
+
213
+ function findCodexUncached(): string | undefined {
214
+ // On Windows, use `where.exe` to find the real executable
215
+ if (IS_WIN32) {
216
+ const found = resolveViaWhere('codex');
217
+ if (found && runVersionSync(found)) return found;
218
+ return undefined;
219
+ }
220
+ let transientFallback: string | undefined;
221
+ // Check known paths
222
+ for (const p of getCodexCandidates()) {
223
+ if (!runVersionSync(p)) continue;
224
+ if (!isTransientPath(p)) return p;
225
+ transientFallback = p;
226
+ }
227
+ // which with sanitized env
228
+ try {
229
+ const result = execFileSync('/usr/bin/which', ['codex'], {
230
+ timeout: 3000, stdio: 'pipe', encoding: 'utf-8', env: buildSanitizedEnv(),
231
+ }).trim();
232
+ if (result && runVersionSync(result)) {
233
+ if (!isTransientPath(result)) return result;
234
+ transientFallback = transientFallback ?? result;
235
+ }
236
+ } catch { /* not found */ }
237
+ // Login shell fallback
238
+ const found = findViaLoginShell('codex');
239
+ if (found && runVersionSync(found)) {
240
+ if (!isTransientPath(found)) return found;
241
+ transientFallback = transientFallback ?? found;
242
+ }
243
+ return transientFallback;
244
+ }
245
+
246
+ // ===== Cached Resolvers =====
247
+
248
+ function createCachedResolver(finder: () => string | undefined, name: string) {
249
+ let cached: string | null = null;
250
+ let timestamp = 0;
251
+ return (): string => {
252
+ const envKey = name === 'claude' ? 'CLAUDE_PATH' : 'CODEX_PATH';
253
+ if (process.env[envKey]) return process.env[envKey]!;
254
+
255
+ const now = Date.now();
256
+ // Re-resolve if expired, missing, or transient
257
+ if (!cached || now - timestamp > CACHE_TTL || !existsSync(cached) || isTransientPath(cached)) {
258
+ const found = finder();
259
+ cached = found ?? name;
260
+ timestamp = now;
261
+ log.info({ name, path: cached, transient: isTransientPath(cached) }, 'binary resolved');
262
+ }
263
+ return cached;
264
+ };
265
+ }
266
+
267
+ const resolveClaude = createCachedResolver(findClaudeUncached, 'claude');
268
+ const resolveCodex = createCachedResolver(findCodexUncached, 'codex');
269
+
270
+ // ===== CLI Args =====
271
+
272
+ function parseCliArgs(): Record<string, string> {
273
+ const result: Record<string, string> = {};
274
+ const args = process.argv.slice(2);
275
+ for (let i = 0; i < args.length; i++) {
276
+ const arg = args[i];
277
+ if (arg.startsWith('--') && i + 1 < args.length) {
278
+ const key = arg.slice(2);
279
+ result[key] = args[++i];
280
+ }
281
+ }
282
+ return result;
283
+ }
284
+
285
+ const cliArgs = parseCliArgs();
286
+
287
+ // ===== Export =====
288
+
289
+ export function execViaLoginShell(cmd: string, args: string[]): { command: string; args: string[] } {
290
+ if (IS_WIN32) {
291
+ // On Windows, resolve via where.exe and spawn directly
292
+ const resolved = resolveViaWhere(cmd);
293
+ return { command: resolved ?? cmd, args };
294
+ }
295
+ const shell = getLoginShellCandidates()[0] ?? '/bin/zsh';
296
+ return {
297
+ command: shell,
298
+ args: ['-lc', `command -v ${cmd} >/dev/null 2>&1 && exec ${cmd} "$@"`, '--', ...args],
299
+ };
300
+ }
301
+
302
+ export const config = {
303
+ port: Number(process.env.VIBE_PORT) || 9876,
304
+ legacyToken: process.env.VIBE_TOKEN || '',
305
+ canonicalHost: cliArgs['host'] || process.env.VIBELET_CANONICAL_HOST || process.env.VIBELET_HOST || '',
306
+ fallbackHosts: cliArgs['fallback-hosts'] || process.env.VIBELET_FALLBACK_HOSTS || '',
307
+ /** Public relay URL (e.g. https://abc.trycloudflare.com). Overrides host/port in QR pairing payload. */
308
+ relayUrl: process.env.VIBELET_RELAY_URL || '',
309
+ /** Idle timeout in ms before a driver is automatically released. Default: 30 minutes. 0 = disabled. */
310
+ idleTimeoutMs: getNonNegativeNumberEnv('VIBE_IDLE_TIMEOUT_MS', 30 * 60 * 1000),
311
+ /** Maximum inactivity in ms for an in-flight turn before it is failed and the driver is stopped. 0 = disabled. */
312
+ turnStallTimeoutMs: getNonNegativeNumberEnv('VIBE_TURN_STALL_TIMEOUT_MS', 5 * 60 * 1000),
313
+ /** Maximum size for ~/.vibelet/data/audit.jsonl before it is trimmed in place. */
314
+ auditMaxBytes: getPositiveNumberEnv('VIBE_AUDIT_MAX_BYTES', 8 * 1024 * 1024),
315
+ /** Maximum size for each ~/.vibelet/logs/daemon.*.log file before it is trimmed in place. */
316
+ daemonLogMaxBytes: getPositiveNumberEnv('VIBE_DAEMON_LOG_MAX_BYTES', 16 * 1024 * 1024),
317
+ /** How often the daemon trims its own stdout/stderr logs. */
318
+ storageHousekeepingIntervalMs: getPositiveNumberEnv('VIBE_STORAGE_HOUSEKEEPING_INTERVAL_MS', 5 * 60 * 1000),
319
+ get claudePath() { return resolveClaude(); },
320
+ get codexPath() { return resolveCodex(); },
321
+ isTransientPath,
322
+ execViaLoginShell,
323
+ buildSanitizedEnv,
324
+ };