@vibelet/cli 0.1.35 → 0.1.37

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 -24
  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 -1035
  323. package/dist/index.cjs +0 -125
@@ -0,0 +1,537 @@
1
+ import { execFileSync, spawn } from 'node:child_process';
2
+ import { mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { Socket } from 'node:net';
4
+ import { constants as osConstants, homedir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createRequire } from 'node:module';
8
+ import { extractQuickTunnelUrl } from '../../../bin/cloudflared-quick-tunnel.mjs';
9
+ import { formatCloudflaredFailureMessage, resolveCloudflaredLaunchSpec } from '../../../bin/cloudflared-resolver.mjs';
10
+
11
+ const require = createRequire(import.meta.url);
12
+ const currentScriptPath = fileURLToPath(import.meta.url);
13
+ const scriptDir = path.dirname(currentScriptPath);
14
+ const daemonDir = path.resolve(scriptDir, '..');
15
+ const rootPackageJson = JSON.parse(readFileSync(path.resolve(daemonDir, '..', '..', 'package.json'), 'utf8'));
16
+ const tsxPackageJsonPath = require.resolve('tsx/package.json');
17
+ const tsxCliPath = path.resolve(path.dirname(tsxPackageJsonPath), 'dist/cli.mjs');
18
+ const supportsDetachedGroupKill = process.platform !== 'win32';
19
+ const vibeletDir = path.join(homedir(), '.vibelet');
20
+ const relayConfigPath = path.join(vibeletDir, 'relay.json');
21
+
22
+ let shuttingDown = false;
23
+ let forceKillTimer;
24
+
25
+ function fail(message) {
26
+ console.error(`[dev] ${message}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ function parseNamedArg(name) {
31
+ const inlinePrefix = `--${name}=`;
32
+ const inlineArg = process.argv.find((arg) => arg.startsWith(inlinePrefix));
33
+ if (inlineArg) {
34
+ return inlineArg.slice(inlinePrefix.length);
35
+ }
36
+
37
+ const idx = process.argv.indexOf(`--${name}`);
38
+ if (idx === -1) return null;
39
+ const value = process.argv[idx + 1];
40
+ if (!value || value.startsWith('-')) {
41
+ return null;
42
+ }
43
+ return value;
44
+ }
45
+
46
+ function parseCommandArg(argv) {
47
+ const positionalArgs = [];
48
+ const namedArgsWithValues = new Set(['--relay', '--host', '--fallback-hosts']);
49
+
50
+ for (let index = 2; index < argv.length; index += 1) {
51
+ const arg = argv[index];
52
+ if (!arg || arg === '--') continue;
53
+
54
+ if (namedArgsWithValues.has(arg)) {
55
+ index += 1;
56
+ continue;
57
+ }
58
+
59
+ if (
60
+ arg.startsWith('--relay=')
61
+ || arg.startsWith('--host=')
62
+ || arg.startsWith('--fallback-hosts=')
63
+ || arg.startsWith('--')
64
+ ) {
65
+ continue;
66
+ }
67
+
68
+ positionalArgs.push(arg);
69
+ }
70
+
71
+ if (positionalArgs.length === 0) {
72
+ return 'start';
73
+ }
74
+
75
+ if (positionalArgs.length > 1) {
76
+ fail(`Too many positional arguments: ${positionalArgs.join(' ')}`);
77
+ }
78
+
79
+ return positionalArgs[0];
80
+ }
81
+
82
+ function readNpmConfigArg(name) {
83
+ const envKey = `npm_config_${name.replace(/-/g, '_')}`;
84
+ const value = process.env[envKey];
85
+ return typeof value === 'string' ? value : null;
86
+ }
87
+
88
+ function loadRelayConfig() {
89
+ try {
90
+ const data = JSON.parse(readFileSync(relayConfigPath, 'utf8'));
91
+ return typeof data.relayUrl === 'string' ? data.relayUrl : '';
92
+ } catch {
93
+ return '';
94
+ }
95
+ }
96
+
97
+ function saveRelayConfig(relayUrl) {
98
+ mkdirSync(vibeletDir, { recursive: true });
99
+ writeFileSync(relayConfigPath, JSON.stringify({ relayUrl }, null, 2) + '\n', 'utf8');
100
+ }
101
+
102
+ function clearRelayConfig() {
103
+ rmSync(relayConfigPath, { force: true });
104
+ }
105
+
106
+ // ─── Tunnel management (shared logic with vibelet.mjs) ──────────────────────
107
+
108
+ const port = Number(process.env.VIBE_PORT) || 9876;
109
+ const tunnelStatePath = path.join(vibeletDir, 'tunnel.json');
110
+ const devStatePath = path.join(vibeletDir, `dev-daemon-${port}.json`);
111
+ const logDir = path.join(vibeletDir, 'logs');
112
+ const command = parseCommandArg(process.argv);
113
+
114
+ if (command !== 'start' && command !== 'restart') {
115
+ fail(`Unknown command: ${command}. Use \`start\` or \`restart\`.`);
116
+ }
117
+
118
+ function consumeFlag(name) {
119
+ const idx = process.argv.indexOf(`--${name}`);
120
+ if (idx === -1) return false;
121
+ process.argv.splice(idx, 1);
122
+ return true;
123
+ }
124
+
125
+ function isProcessAlive(pid) {
126
+ try { process.kill(pid, 0); return true; } catch { return false; }
127
+ }
128
+
129
+ function loadTunnelState() {
130
+ try { return JSON.parse(readFileSync(tunnelStatePath, 'utf8')); } catch { return null; }
131
+ }
132
+
133
+ function saveTunnelState(pid, url) {
134
+ mkdirSync(vibeletDir, { recursive: true });
135
+ writeFileSync(tunnelStatePath, JSON.stringify({ pid, url }, null, 2) + '\n', 'utf8');
136
+ }
137
+
138
+ function clearTunnelState() { rmSync(tunnelStatePath, { force: true }); }
139
+
140
+ function readDevState() {
141
+ try {
142
+ const parsed = JSON.parse(readFileSync(devStatePath, 'utf8'));
143
+ const pid = Number(parsed?.pid);
144
+ return Number.isFinite(pid) && pid > 0 ? { pid } : null;
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ function writeDevState(pid = process.pid) {
151
+ mkdirSync(vibeletDir, { recursive: true });
152
+ writeFileSync(devStatePath, JSON.stringify({ pid, port }, null, 2) + '\n', 'utf8');
153
+ }
154
+
155
+ function clearDevState(expectedPid = process.pid) {
156
+ const state = readDevState();
157
+ if (state && state.pid !== expectedPid) {
158
+ return;
159
+ }
160
+ rmSync(devStatePath, { force: true });
161
+ }
162
+
163
+ function stopTunnel() {
164
+ const state = loadTunnelState();
165
+ if (state?.pid && isProcessAlive(state.pid)) {
166
+ try { process.kill(state.pid, 'SIGTERM'); } catch { /* */ }
167
+ }
168
+ clearTunnelState();
169
+ }
170
+
171
+ function getAliveTunnel() {
172
+ const state = loadTunnelState();
173
+ return (state?.pid && state?.url && isProcessAlive(state.pid)) ? state : null;
174
+ }
175
+
176
+ function readCommandForPid(pid) {
177
+ try {
178
+ return execFileSync('ps', ['-p', String(pid), '-o', 'command='], {
179
+ encoding: 'utf8',
180
+ stdio: ['ignore', 'pipe', 'ignore'],
181
+ }).trim();
182
+ } catch {
183
+ return '';
184
+ }
185
+ }
186
+
187
+ function getTrackedWrapperPid() {
188
+ const state = readDevState();
189
+ if (!state || state.pid === process.pid) {
190
+ return null;
191
+ }
192
+ if (!isProcessAlive(state.pid)) {
193
+ clearDevState(state.pid);
194
+ return null;
195
+ }
196
+ const commandLine = readCommandForPid(state.pid);
197
+ if (!commandLine.includes(currentScriptPath)) {
198
+ return null;
199
+ }
200
+ return state.pid;
201
+ }
202
+
203
+ function delay(ms) {
204
+ return new Promise((resolve) => setTimeout(resolve, ms));
205
+ }
206
+
207
+ function isPortListening(targetPort) {
208
+ return new Promise((resolve) => {
209
+ const socket = new Socket();
210
+
211
+ const finish = (result) => {
212
+ socket.removeAllListeners();
213
+ socket.destroy();
214
+ resolve(result);
215
+ };
216
+
217
+ socket.setTimeout(200);
218
+ socket.once('connect', () => finish(true));
219
+ socket.once('timeout', () => finish(false));
220
+ socket.once('error', () => finish(false));
221
+ socket.connect(targetPort, '127.0.0.1');
222
+ });
223
+ }
224
+
225
+ async function waitForPortState(expectedListening, timeoutMs) {
226
+ const deadline = Date.now() + timeoutMs;
227
+ while (Date.now() < deadline) {
228
+ if ((await isPortListening(port)) === expectedListening) {
229
+ return true;
230
+ }
231
+ await delay(100);
232
+ }
233
+ return (await isPortListening(port)) === expectedListening;
234
+ }
235
+
236
+ async function waitForProcessExit(pid, timeoutMs) {
237
+ const deadline = Date.now() + timeoutMs;
238
+ while (Date.now() < deadline) {
239
+ if (!isProcessAlive(pid)) {
240
+ return true;
241
+ }
242
+ await delay(100);
243
+ }
244
+ return !isProcessAlive(pid);
245
+ }
246
+
247
+ function readCloudflaredLog(logPath) {
248
+ try {
249
+ return readFileSync(logPath, 'utf8');
250
+ } catch {
251
+ return '';
252
+ }
253
+ }
254
+
255
+ function startTunnel() {
256
+ return new Promise((resolve, reject) => {
257
+ const logPath = path.join(logDir, 'tunnel.stderr.log');
258
+ mkdirSync(logDir, { recursive: true });
259
+
260
+ const launchSpec = resolveCloudflaredLaunchSpec();
261
+
262
+ writeFileSync(logPath, '', 'utf8');
263
+ const logFd = openSync(logPath, 'a');
264
+ const child = spawn(launchSpec.command, [
265
+ ...launchSpec.args,
266
+ 'tunnel',
267
+ '--protocol',
268
+ 'http2',
269
+ '--url',
270
+ `http://localhost:${port}`,
271
+ ], {
272
+ detached: true,
273
+ stdio: ['ignore', logFd, logFd],
274
+ });
275
+ child.unref();
276
+
277
+ const pid = child.pid;
278
+ let url = null;
279
+
280
+ const timeout = setTimeout(() => {
281
+ if (!url) {
282
+ try { process.kill(pid, 'SIGTERM'); } catch { /* */ }
283
+ reject(new Error(formatCloudflaredFailureMessage({
284
+ launchSpec,
285
+ logContent: readCloudflaredLog(logPath),
286
+ logPath,
287
+ phase: 'timeout',
288
+ })));
289
+ }
290
+ }, launchSpec.urlTimeoutMs);
291
+
292
+ const poll = setInterval(() => {
293
+ try {
294
+ const content = readFileSync(logPath, 'utf8');
295
+ const tunnelUrl = extractQuickTunnelUrl(content);
296
+ if (tunnelUrl) {
297
+ url = tunnelUrl;
298
+ clearInterval(poll);
299
+ clearTimeout(timeout);
300
+ saveTunnelState(pid, url);
301
+ resolve({ pid, url });
302
+ return;
303
+ }
304
+ if (!isProcessAlive(pid)) {
305
+ clearInterval(poll);
306
+ clearTimeout(timeout);
307
+ reject(new Error(formatCloudflaredFailureMessage({
308
+ launchSpec,
309
+ logContent: content,
310
+ logPath,
311
+ phase: 'exit',
312
+ })));
313
+ }
314
+ } catch { /* file not ready yet */ }
315
+ }, 300);
316
+
317
+ child.on('error', (err) => {
318
+ clearInterval(poll);
319
+ clearTimeout(timeout);
320
+ reject(new Error(formatCloudflaredFailureMessage({
321
+ launchSpec,
322
+ logContent: readCloudflaredLog(logPath),
323
+ logPath,
324
+ err,
325
+ phase: 'spawn',
326
+ })));
327
+ });
328
+ });
329
+ }
330
+
331
+ async function restartExistingDevDaemon() {
332
+ const trackedWrapperPid = getTrackedWrapperPid();
333
+ if (trackedWrapperPid) {
334
+ console.log(`[dev] Stopping previous dev wrapper (pid ${trackedWrapperPid})...`);
335
+ try {
336
+ process.kill(trackedWrapperPid, 'SIGTERM');
337
+ } catch {}
338
+ }
339
+
340
+ await requestDaemonShutdown();
341
+
342
+ const [portClosed, wrapperExited] = await Promise.all([
343
+ waitForPortState(false, 5_000),
344
+ trackedWrapperPid ? waitForProcessExit(trackedWrapperPid, 5_000) : Promise.resolve(true),
345
+ ]);
346
+
347
+ if (!wrapperExited) {
348
+ fail(`Timed out waiting for previous dev wrapper (pid ${trackedWrapperPid}) to exit.`);
349
+ }
350
+
351
+ if (!portClosed) {
352
+ fail(`Timed out waiting for port ${port} to be released.`);
353
+ }
354
+
355
+ if (trackedWrapperPid) {
356
+ clearDevState(trackedWrapperPid);
357
+ }
358
+ }
359
+
360
+ // ─── Parse flags ────────────────────────────────────────────────────────────
361
+
362
+ function readNpmConfigFlag(name) {
363
+ const value = process.env[`npm_config_${name.replace(/-/g, '_')}`];
364
+ return value === '' || value === 'true';
365
+ }
366
+
367
+ consumeFlag('remote') || consumeFlag('tunnel') || readNpmConfigFlag('remote') || readNpmConfigFlag('tunnel');
368
+ const localFlag = consumeFlag('local') || readNpmConfigFlag('local');
369
+ const forceFlag = consumeFlag('force') || readNpmConfigFlag('force');
370
+ const relayArg = parseNamedArg('relay') ?? readNpmConfigArg('relay');
371
+ const hostArg = parseNamedArg('host') ?? readNpmConfigArg('host');
372
+ const fallbackHostsArg = parseNamedArg('fallback-hosts') ?? readNpmConfigArg('fallback-hosts');
373
+ const envRelayUrl = process.env.VIBELET_RELAY_URL || '';
374
+ const envCanonicalHost = process.env.VIBELET_CANONICAL_HOST || null;
375
+ const envFallbackHosts = process.env.VIBELET_FALLBACK_HOSTS || null;
376
+ const canonicalHost = hostArg ?? envCanonicalHost;
377
+ const fallbackHosts = fallbackHostsArg ?? envFallbackHosts;
378
+ const shouldManageTunnel = !localFlag
379
+ && relayArg === null
380
+ && !envRelayUrl
381
+ && !hostArg
382
+ && !fallbackHostsArg
383
+ && !envCanonicalHost
384
+ && !envFallbackHosts;
385
+
386
+ if (command === 'restart') {
387
+ await restartExistingDevDaemon();
388
+ }
389
+
390
+ // Managed remote access: auto-start or reuse a Cloudflare Tunnel by default.
391
+ if (shouldManageTunnel) {
392
+ const existing = forceFlag ? null : getAliveTunnel();
393
+ if (existing) {
394
+ console.log(`[dev] Reusing tunnel: ${existing.url} (pid ${existing.pid})`);
395
+ saveRelayConfig(existing.url);
396
+ } else {
397
+ if (forceFlag) stopTunnel();
398
+ console.log('[dev] Starting Cloudflare Tunnel...');
399
+ try {
400
+ const tunnel = await startTunnel();
401
+ console.log(`[dev] Tunnel ready: ${tunnel.url}`);
402
+ saveRelayConfig(tunnel.url);
403
+ } catch (err) {
404
+ console.error(`[dev] Tunnel failed: ${err.message}`);
405
+ process.exit(1);
406
+ }
407
+ }
408
+ }
409
+
410
+ if (relayArg !== null) {
411
+ if (relayArg) {
412
+ saveRelayConfig(relayArg);
413
+ } else {
414
+ clearRelayConfig();
415
+ }
416
+ }
417
+
418
+ const shouldIgnoreSavedRelay = relayArg === null && (localFlag || Boolean(hostArg) || Boolean(fallbackHostsArg) || Boolean(envCanonicalHost) || Boolean(envFallbackHosts));
419
+ const relayUrl = relayArg !== null
420
+ ? relayArg
421
+ : (localFlag || Boolean(hostArg) || Boolean(fallbackHostsArg) ? '' : (envRelayUrl || (shouldIgnoreSavedRelay ? '' : loadRelayConfig())));
422
+
423
+ function shouldDetachChild() {
424
+ if (!supportsDetachedGroupKill) {
425
+ return false;
426
+ }
427
+
428
+ const override = process.env.VIBE_DEV_DETACHED?.toLowerCase();
429
+ if (override === '1' || override === 'true') {
430
+ return true;
431
+ }
432
+ if (override === '0' || override === 'false') {
433
+ return false;
434
+ }
435
+
436
+ // In interactive shells, keep tsx watch in the foreground process group so
437
+ // terminal Ctrl+C reaches both the watcher and its child daemon directly.
438
+ return !process.stdin.isTTY;
439
+ }
440
+
441
+ const useDetachedGroupKill = shouldDetachChild();
442
+
443
+ if (!getTrackedWrapperPid()) {
444
+ writeDevState();
445
+ }
446
+
447
+ // In non-interactive environments, run tsx watch in its own process group so
448
+ // wrapper shutdown can tear down the watcher and the daemon child in one kill.
449
+ const child = spawn(process.execPath, [tsxCliPath, 'watch', 'src/index.ts'], {
450
+ cwd: daemonDir,
451
+ env: {
452
+ ...process.env,
453
+ VIBE_DEV: process.env.VIBE_DEV ?? '1',
454
+ VIBELET_CLI_VERSION: process.env.VIBELET_CLI_VERSION ?? rootPackageJson.version,
455
+ ...(relayUrl ? { VIBELET_RELAY_URL: relayUrl } : {}),
456
+ ...(canonicalHost ? { VIBELET_CANONICAL_HOST: canonicalHost } : {}),
457
+ ...(fallbackHosts ? { VIBELET_FALLBACK_HOSTS: fallbackHosts } : {}),
458
+ },
459
+ stdio: 'inherit',
460
+ detached: useDetachedGroupKill,
461
+ });
462
+
463
+ child.once('error', (error) => {
464
+ console.error('[daemon:dev] failed to start tsx watch', error);
465
+ process.exit(1);
466
+ });
467
+
468
+ function getSignalExitCode(signal) {
469
+ const signalNumber = osConstants.signals?.[signal];
470
+ return typeof signalNumber === 'number' ? 128 + signalNumber : 1;
471
+ }
472
+
473
+ function requestDaemonShutdown() {
474
+ return fetch(`http://127.0.0.1:${port}/shutdown`, {
475
+ method: 'POST',
476
+ }).catch(() => undefined);
477
+ }
478
+
479
+ function killChildTree(signal = 'SIGTERM') {
480
+ if (child.exitCode !== null || child.signalCode !== null) {
481
+ return;
482
+ }
483
+
484
+ try {
485
+ if (useDetachedGroupKill && typeof child.pid === 'number') {
486
+ process.kill(-child.pid, signal);
487
+ } else {
488
+ child.kill(signal);
489
+ }
490
+ } catch (error) {
491
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH') {
492
+ return;
493
+ }
494
+ throw error;
495
+ }
496
+ }
497
+
498
+ function shutdown(signal) {
499
+ if (shuttingDown) {
500
+ return;
501
+ }
502
+ shuttingDown = true;
503
+
504
+ if (!useDetachedGroupKill) {
505
+ void requestDaemonShutdown();
506
+ }
507
+ killChildTree(signal === 'SIGHUP' ? 'SIGTERM' : signal);
508
+ forceKillTimer = setTimeout(() => {
509
+ try {
510
+ killChildTree('SIGKILL');
511
+ } catch {}
512
+ }, 3000);
513
+ forceKillTimer.unref();
514
+ }
515
+
516
+ process.once('SIGINT', () => shutdown('SIGINT'));
517
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
518
+ process.once('SIGHUP', () => shutdown('SIGHUP'));
519
+ process.once('exit', () => {
520
+ clearDevState(process.pid);
521
+ try {
522
+ killChildTree('SIGTERM');
523
+ } catch {}
524
+ });
525
+
526
+ child.once('exit', (code, signal) => {
527
+ if (forceKillTimer) {
528
+ clearTimeout(forceKillTimer);
529
+ }
530
+
531
+ if (signal) {
532
+ process.exit(getSignalExitCode(signal));
533
+ return;
534
+ }
535
+
536
+ process.exit(code ?? 0);
537
+ });
@@ -0,0 +1,125 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import type { NetworkInterfaceInfo } from 'os';
4
+ import {
5
+ computeAdvertisedConnectionTarget,
6
+ computeAdvertisedHosts,
7
+ parseTailscaleStatus,
8
+ readConfiguredFallbackHosts,
9
+ readTailscaleHosts,
10
+ } from './advertised-hosts.js';
11
+
12
+ function ipv4(address: string): NetworkInterfaceInfo {
13
+ return {
14
+ address,
15
+ netmask: '255.255.255.0',
16
+ family: 'IPv4',
17
+ mac: '00:00:00:00:00:00',
18
+ internal: false,
19
+ cidr: `${address}/24`,
20
+ };
21
+ }
22
+
23
+ test('computeAdvertisedHosts prefers configured canonical host and prioritizes Tailscale IPs', () => {
24
+ const advertised = computeAdvertisedHosts({
25
+ canonicalHost: 'desk-mac.local',
26
+ configuredCanonicalHost: 'desk.tailnet.ts.net',
27
+ configuredFallbackHosts: ['100.88.1.9'],
28
+ tailscaleCanonicalHost: '100.77.2.3',
29
+ tailscaleFallbackHosts: ['desk.ts.net'],
30
+ interfaces: {
31
+ en0: [ipv4('192.168.31.20')],
32
+ utun8: [ipv4('100.77.2.3')],
33
+ },
34
+ });
35
+
36
+ assert.equal(advertised.canonicalHost, 'desk.tailnet.ts.net');
37
+ assert.deepStrictEqual(advertised.fallbackHosts, ['desk-mac.local', '100.88.1.9', 'desk.ts.net', '100.77.2.3', '192.168.31.20']);
38
+ });
39
+
40
+ test('computeAdvertisedHosts removes duplicates and excludes the canonical host', () => {
41
+ const advertised = computeAdvertisedHosts({
42
+ canonicalHost: '100.77.2.3',
43
+ configuredFallbackHosts: ['100.77.2.3', '192.168.31.20', '192.168.31.20'],
44
+ interfaces: {
45
+ utun8: [ipv4('100.77.2.3')],
46
+ en0: [ipv4('192.168.31.20')],
47
+ },
48
+ });
49
+
50
+ assert.equal(advertised.canonicalHost, '100.77.2.3');
51
+ assert.deepStrictEqual(advertised.fallbackHosts, ['192.168.31.20']);
52
+ });
53
+
54
+ test('computeAdvertisedHosts filters out link-local and benchmark addresses', () => {
55
+ const advertised = computeAdvertisedHosts({
56
+ canonicalHost: 'desk-mac.local',
57
+ interfaces: {
58
+ en0: [ipv4('192.168.31.20'), ipv4('169.254.12.2')],
59
+ utun4: [ipv4('198.19.0.1')],
60
+ },
61
+ });
62
+
63
+ assert.equal(advertised.canonicalHost, 'desk-mac.local');
64
+ assert.deepStrictEqual(advertised.fallbackHosts, ['192.168.31.20']);
65
+ });
66
+
67
+ test('parseTailscaleStatus prefers the first Tailscale IP and keeps MagicDNS as fallback', () => {
68
+ const advertised = parseTailscaleStatus(JSON.stringify({
69
+ Self: {
70
+ DNSName: 'desk.tailnet.ts.net.',
71
+ TailscaleIPs: ['100.88.1.9', '100.99.2.4'],
72
+ },
73
+ }));
74
+
75
+ assert.deepStrictEqual(advertised, {
76
+ canonicalHost: '100.88.1.9',
77
+ fallbackHosts: ['desk.tailnet.ts.net', '100.99.2.4'],
78
+ });
79
+ });
80
+
81
+ test('readTailscaleHosts returns empty data when tailscale is unavailable', () => {
82
+ const advertised = readTailscaleHosts(() => {
83
+ throw new Error('tailscale unavailable');
84
+ });
85
+
86
+ assert.deepStrictEqual(advertised, { fallbackHosts: [] });
87
+ });
88
+
89
+ test('readConfiguredFallbackHosts parses comma separated env values', () => {
90
+ assert.deepStrictEqual(
91
+ readConfiguredFallbackHosts(' 100.77.2.3, desk.tailnet.ts.net , 192.168.31.20 '),
92
+ ['100.77.2.3', 'desk.tailnet.ts.net', '192.168.31.20'],
93
+ );
94
+ });
95
+
96
+ test('computeAdvertisedConnectionTarget keeps the base host and port without a relay', () => {
97
+ assert.deepStrictEqual(
98
+ computeAdvertisedConnectionTarget({
99
+ canonicalHost: 'desk-mac.local',
100
+ fallbackHosts: ['100.88.1.9', '192.168.31.20'],
101
+ port: 9876,
102
+ }),
103
+ {
104
+ canonicalHost: 'desk-mac.local',
105
+ fallbackHosts: ['100.88.1.9', '192.168.31.20'],
106
+ port: 9876,
107
+ },
108
+ );
109
+ });
110
+
111
+ test('computeAdvertisedConnectionTarget prefers the relay host and preserves local fallbacks', () => {
112
+ assert.deepStrictEqual(
113
+ computeAdvertisedConnectionTarget({
114
+ canonicalHost: 'desk-mac.local',
115
+ fallbackHosts: ['desk-mac.local', '100.88.1.9', '100.88.1.9', '192.168.31.20'],
116
+ port: 9876,
117
+ relayUrl: 'https://relay.example.com',
118
+ }),
119
+ {
120
+ canonicalHost: 'relay.example.com',
121
+ fallbackHosts: ['desk-mac.local', '100.88.1.9', '192.168.31.20'],
122
+ port: 443,
123
+ },
124
+ );
125
+ });