tmux-ide 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +14 -9
  2. package/bin/cli.js +1024 -519
  3. package/bin/cli.ts +63 -6
  4. package/bunfig.toml +4 -0
  5. package/package.json +18 -7
  6. package/packages/contracts/package.json +22 -0
  7. package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
  8. package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
  9. package/packages/contracts/src/actions-contract.ts +310 -0
  10. package/packages/contracts/src/actions-errors.ts +41 -0
  11. package/packages/contracts/src/domain.ts +36 -0
  12. package/packages/contracts/src/ide-config.ts +170 -0
  13. package/packages/contracts/src/index.ts +24 -0
  14. package/packages/contracts/src/lib-internal/auth.ts +13 -0
  15. package/packages/contracts/src/lib-internal/hq.ts +38 -0
  16. package/packages/contracts/src/terminals.ts +116 -0
  17. package/packages/contracts/src/tmux.ts +60 -0
  18. package/packages/contracts/src/workspace.ts +67 -0
  19. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  20. package/packages/daemon/dist/agent-explain.js +19 -3
  21. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  22. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  23. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  24. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  25. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  26. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  27. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  28. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  29. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  30. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  31. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  32. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  33. package/packages/daemon/src/agent-explain.ts +298 -0
  34. package/packages/daemon/src/attach.ts +20 -0
  35. package/packages/daemon/src/bin.ts +4 -0
  36. package/packages/daemon/src/canonical.ts +7 -0
  37. package/packages/daemon/src/cli.ts +499 -0
  38. package/packages/daemon/src/command-center/actions/contract.ts +2 -0
  39. package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
  40. package/packages/daemon/src/command-center/actions/errors.ts +105 -0
  41. package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
  42. package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
  43. package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
  44. package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
  45. package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
  46. package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
  47. package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
  48. package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
  49. package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
  50. package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
  51. package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
  52. package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
  53. package/packages/daemon/src/command-center/actions/registry.ts +149 -0
  54. package/packages/daemon/src/command-center/discovery.ts +96 -0
  55. package/packages/daemon/src/command-center/index.ts +31 -0
  56. package/packages/daemon/src/command-center/schemas.ts +85 -0
  57. package/packages/daemon/src/command-center/server.ts +1260 -0
  58. package/packages/daemon/src/command-center/ws-events.ts +316 -0
  59. package/packages/daemon/src/config.ts +549 -0
  60. package/packages/daemon/src/detect.ts +248 -0
  61. package/packages/daemon/src/doctor.ts +242 -0
  62. package/packages/daemon/src/embed.ts +5 -0
  63. package/packages/daemon/src/index.ts +12 -0
  64. package/packages/daemon/src/init.ts +211 -0
  65. package/packages/daemon/src/inspect.ts +178 -0
  66. package/packages/daemon/src/js-yaml.d.ts +10 -0
  67. package/packages/daemon/src/launch.ts +349 -0
  68. package/packages/daemon/src/lib/active-projects.ts +49 -0
  69. package/packages/daemon/src/lib/agent-discovery.ts +121 -0
  70. package/packages/daemon/src/lib/app-config.ts +427 -0
  71. package/packages/daemon/src/lib/app-settings.ts +53 -0
  72. package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
  73. package/packages/daemon/src/lib/auth/middleware.ts +56 -0
  74. package/packages/daemon/src/lib/auth/types.ts +2 -0
  75. package/packages/daemon/src/lib/auth-token.ts +5 -0
  76. package/packages/daemon/src/lib/authorship.ts +280 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
  78. package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
  79. package/packages/daemon/src/lib/daemon-embed.ts +782 -0
  80. package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
  81. package/packages/daemon/src/lib/daemon.ts +79 -0
  82. package/packages/daemon/src/lib/dot-path.ts +17 -0
  83. package/packages/daemon/src/lib/errors.ts +67 -0
  84. package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
  85. package/packages/daemon/src/lib/launch-plan.ts +90 -0
  86. package/packages/daemon/src/lib/log.ts +134 -0
  87. package/packages/daemon/src/lib/output.ts +76 -0
  88. package/packages/daemon/src/lib/project-init-runner.ts +150 -0
  89. package/packages/daemon/src/lib/project-inspect.ts +80 -0
  90. package/packages/daemon/src/lib/project-onboard.ts +149 -0
  91. package/packages/daemon/src/lib/project-probe.ts +92 -0
  92. package/packages/daemon/src/lib/project-registry.ts +296 -0
  93. package/packages/daemon/src/lib/session-monitor.ts +122 -0
  94. package/packages/daemon/src/lib/session-options.ts +100 -0
  95. package/packages/daemon/src/lib/shell.ts +8 -0
  96. package/packages/daemon/src/lib/sizes.ts +36 -0
  97. package/packages/daemon/src/lib/skill-sync.ts +155 -0
  98. package/packages/daemon/src/lib/slugify.ts +10 -0
  99. package/packages/daemon/src/lib/terminals-store.ts +125 -0
  100. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  101. package/packages/daemon/src/lib/update-check.ts +298 -0
  102. package/packages/daemon/src/lib/update.ts +158 -0
  103. package/packages/daemon/src/lib/workspace-registry.ts +229 -0
  104. package/packages/daemon/src/lib/worktree.ts +289 -0
  105. package/packages/daemon/src/lib/yaml-io.ts +27 -0
  106. package/packages/daemon/src/ls.ts +40 -0
  107. package/packages/daemon/src/restart.ts +24 -0
  108. package/packages/daemon/src/restore.ts +514 -0
  109. package/packages/daemon/src/schemas/domain.ts +2 -0
  110. package/packages/daemon/src/schemas/filesystem.ts +34 -0
  111. package/packages/daemon/src/schemas/ide-config.ts +2 -0
  112. package/packages/daemon/src/schemas/index.ts +59 -0
  113. package/packages/daemon/src/schemas/inspect.ts +67 -0
  114. package/packages/daemon/src/schemas/registry.ts +55 -0
  115. package/packages/daemon/src/schemas/ws-events.ts +135 -0
  116. package/packages/daemon/src/send.ts +171 -0
  117. package/packages/daemon/src/server/README.md +15 -0
  118. package/packages/daemon/src/server/index.ts +74 -0
  119. package/packages/daemon/src/server/pty-bridge.ts +532 -0
  120. package/packages/daemon/src/server/standalone.ts +18 -0
  121. package/packages/daemon/src/server/ws-route.ts +483 -0
  122. package/packages/daemon/src/status.ts +59 -0
  123. package/packages/daemon/src/stop.ts +28 -0
  124. package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
  125. package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
  126. package/packages/daemon/src/terminal/README.md +92 -0
  127. package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
  128. package/packages/daemon/src/tui/chrome/chip.ts +30 -0
  129. package/packages/daemon/src/tui/chrome/events.ts +119 -0
  130. package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
  131. package/packages/daemon/src/tui/chrome/menu.ts +289 -0
  132. package/packages/daemon/src/tui/chrome/notify.ts +382 -0
  133. package/packages/daemon/src/tui/chrome/panels.ts +111 -0
  134. package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
  135. package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
  136. package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
  137. package/packages/daemon/src/tui/chrome/updater.ts +510 -0
  138. package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
  139. package/packages/daemon/src/tui/compiled.ts +121 -0
  140. package/packages/daemon/src/tui/detect/classify.ts +208 -0
  141. package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
  142. package/packages/daemon/src/tui/detect/manifest.ts +199 -0
  143. package/packages/daemon/src/tui/detect/manifests.ts +354 -0
  144. package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
  145. package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
  146. package/packages/daemon/src/tui/integrations/claude.ts +176 -0
  147. package/packages/daemon/src/tui/integrations/offer.ts +145 -0
  148. package/packages/daemon/src/tui/main.ts +82 -0
  149. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  150. package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
  151. package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
  152. package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
  153. package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
  154. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  155. package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
  156. package/packages/daemon/src/tui/mirror/control.ts +97 -0
  157. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  158. package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
  159. package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
  160. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  161. package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
  162. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  163. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  164. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  165. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  166. package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
  167. package/packages/daemon/src/tui/mirror/palette.ts +274 -0
  168. package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
  169. package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
  170. package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
  171. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  172. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  173. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  174. package/packages/daemon/src/tui/mirror/selection.ts +262 -0
  175. package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
  176. package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
  177. package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
  178. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  179. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  180. package/packages/daemon/src/tui/team/CONTROL.md +50 -0
  181. package/packages/daemon/src/tui/team/entry.ts +38 -0
  182. package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
  183. package/packages/daemon/src/tui/team/home.ts +170 -0
  184. package/packages/daemon/src/tui/team/index.tsx +1521 -0
  185. package/packages/daemon/src/tui/team/input.ts +34 -0
  186. package/packages/daemon/src/tui/team/keymap.ts +127 -0
  187. package/packages/daemon/src/tui/team/mouse.ts +29 -0
  188. package/packages/daemon/src/tui/team/nav.ts +31 -0
  189. package/packages/daemon/src/tui/team/preview.ts +34 -0
  190. package/packages/daemon/src/tui/team/projects.ts +191 -0
  191. package/packages/daemon/src/tui/team/report.ts +83 -0
  192. package/packages/daemon/src/tui/team/sessions.ts +433 -0
  193. package/packages/daemon/src/tui/team/tree.ts +62 -0
  194. package/packages/daemon/src/types.ts +13 -0
  195. package/packages/daemon/src/ui/index.ts +32 -0
  196. package/packages/daemon/src/ui/terminal/index.ts +9 -0
  197. package/packages/daemon/src/ui/types.ts +91 -0
  198. package/packages/daemon/src/ui/web/base.css +80 -0
  199. package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
  200. package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
  201. package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
  202. package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
  203. package/packages/daemon/src/ui/web/hooks.ts +106 -0
  204. package/packages/daemon/src/ui/web/index.ts +27 -0
  205. package/packages/daemon/src/ui/web/render.ts +77 -0
  206. package/packages/daemon/src/ui/web/utils/color.ts +27 -0
  207. package/packages/daemon/src/validate.ts +217 -0
  208. package/packages/daemon/src/widgets/changes/README.md +3 -0
  209. package/packages/daemon/src/widgets/changes/index.tsx +691 -0
  210. package/packages/daemon/src/widgets/config/README.md +3 -0
  211. package/packages/daemon/src/widgets/config/index.tsx +481 -0
  212. package/packages/daemon/src/widgets/explorer/README.md +3 -0
  213. package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
  214. package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
  215. package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
  216. package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
  217. package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
  218. package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
  219. package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
  220. package/packages/daemon/src/widgets/lib/files.ts +88 -0
  221. package/packages/daemon/src/widgets/lib/git.ts +88 -0
  222. package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
  223. package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
  224. package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
  225. package/packages/daemon/src/widgets/lib/theme.ts +194 -0
  226. package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
  227. package/packages/daemon/src/widgets/preview/README.md +3 -0
  228. package/packages/daemon/src/widgets/preview/index.tsx +416 -0
  229. package/packages/daemon/src/widgets/resolve.ts +121 -0
  230. package/packages/daemon/src/widgets/setup/README.md +3 -0
  231. package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
  232. package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
  233. package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
  234. package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
  235. package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
  236. package/packages/daemon/src/widgets/setup/index.tsx +341 -0
  237. package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
  238. package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
  239. package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
  240. package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
  241. package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
  242. package/packages/tmux-bridge/package.json +22 -0
  243. package/packages/tmux-bridge/src/errors.ts +28 -0
  244. package/packages/tmux-bridge/src/index.ts +31 -0
  245. package/packages/tmux-bridge/src/monitor.ts +77 -0
  246. package/packages/tmux-bridge/src/panes.ts +136 -0
  247. package/packages/tmux-bridge/src/runner.test.ts +501 -0
  248. package/packages/tmux-bridge/src/runner.ts +91 -0
  249. package/packages/tmux-bridge/src/sessions.ts +126 -0
  250. package/packages/tmux-bridge/src/targeting.test.ts +107 -0
  251. package/packages/tmux-bridge/src/targeting.ts +90 -0
  252. package/scripts/build-tui.mjs +11 -4
  253. package/scripts/perf-mirror.mjs +313 -0
  254. package/scripts/postinstall.js +26 -2
  255. package/skill/SKILL.md +22 -0
  256. package/templates/AGENTS.md +14 -7
  257. package/templates/agent-team-monorepo.yml +8 -0
  258. package/templates/agent-team-nextjs.yml +8 -0
  259. package/templates/agent-team.yml +10 -0
  260. package/templates/convex.yml +2 -0
  261. package/templates/default.yml +11 -5
  262. package/templates/go.yml +4 -0
  263. package/templates/missions.yml +6 -0
  264. package/templates/nextjs.yml +4 -0
  265. package/templates/python.yml +4 -0
  266. package/templates/skills/backend.md +5 -12
  267. package/templates/skills/frontend.md +5 -12
  268. package/templates/skills/general-worker.md +5 -12
  269. package/templates/skills/researcher.md +7 -12
  270. package/templates/skills/reviewer.md +7 -16
  271. package/templates/vite.yml +4 -0
@@ -0,0 +1,782 @@
1
+ /**
2
+ * Programmatic tmux-ide daemon entrypoint.
3
+ *
4
+ * This is the shared runtime used by the standalone daemon process and by
5
+ * embedders such as Electron's main process.
6
+ */
7
+
8
+ import { execFileSync } from "node:child_process";
9
+ import { randomBytes } from "node:crypto";
10
+ import { createServer, type Server } from "node:http";
11
+ import { createRequire } from "node:module";
12
+ import type { Socket } from "node:net";
13
+ import { WebSocket, WebSocketServer } from "ws";
14
+ import { computeAgentStates, computePortPanes } from "./session-monitor.ts";
15
+ import { DaemonShutdownError, DaemonStartupError } from "./errors.ts";
16
+ import { handlePtyWebSocket, shutdownPtyBridges } from "../server/ws-route.ts";
17
+ import { handleWsEventsConnection } from "../command-center/ws-events.ts";
18
+ import { setRemoteAccessRestartBackend } from "../command-center/actions/handlers/app-set-remote-access.ts";
19
+ import { setDaemonShutdownBackend } from "../command-center/actions/handlers/daemon-shutdown.ts";
20
+ import { readAppSettings } from "./app-settings.ts";
21
+ import { getDefaultWorkspaceRegistry } from "./workspace-registry.ts";
22
+ import { setActivationBackend, type ProjectActivationOptions } from "./active-projects.ts";
23
+ import {
24
+ clearCanonicalDaemonInfo,
25
+ isCanonicalDaemonAlive,
26
+ readCanonicalDaemonInfo,
27
+ writeCanonicalDaemonInfo,
28
+ } from "./canonical-daemon.ts";
29
+
30
+ const requireFromHere = createRequire(import.meta.url);
31
+ const DEFAULT_HOSTNAME = "127.0.0.1";
32
+ const DEFAULT_GRACEFUL_MS = 2000;
33
+ const MONITOR_INTERVAL_MS = 1000;
34
+ const EMBEDDED_SESSION_NAME = "__embedded__";
35
+
36
+ export interface EmbeddedDaemonOptions {
37
+ sessionName?: string;
38
+ port?: number;
39
+ bindHostname?: string;
40
+ authToken?: string | null;
41
+ localBypassToken?: string | null;
42
+ takeoverIfRunning?: boolean;
43
+ silent?: boolean;
44
+ /** @deprecated Use bindHostname. */
45
+ hostname?: string;
46
+ }
47
+
48
+ export interface EmbeddedDaemonHandle {
49
+ readonly port: number;
50
+ readonly apiBaseUrl: string;
51
+ readonly wsUrl: string;
52
+ readonly localBypassToken: string | null;
53
+ activateProject(
54
+ projectName: string,
55
+ options?: ProjectActivationOptions,
56
+ ): Promise<{ stop: () => Promise<void> }>;
57
+ stop(opts?: { gracefulMs?: number }): Promise<void>;
58
+ }
59
+
60
+ interface MonitorPane {
61
+ id: string;
62
+ pid: string;
63
+ cmd?: string;
64
+ title?: string;
65
+ role?: string;
66
+ type?: string;
67
+ name?: string;
68
+ }
69
+
70
+ function tmux(...args: string[]): string {
71
+ return execFileSync("tmux", args, {
72
+ encoding: "utf-8",
73
+ // Pipe stdio explicitly. Inheriting (the default) inherits the parent's
74
+ // file descriptors; when the daemon is launched detached (nohup, disown,
75
+ // launchd, etc.) the controlling terminal's fds can be invalid, and the
76
+ // child spawn fails with EBADF. The visible symptom is sessionExists()
77
+ // returning false → stopSelf → ghost daemon.
78
+ stdio: ["ignore", "pipe", "pipe"],
79
+ }).trim();
80
+ }
81
+
82
+ function tmuxSilent(...args: string[]): string {
83
+ try {
84
+ return tmux(...args);
85
+ } catch {
86
+ return "";
87
+ }
88
+ }
89
+
90
+ function assertTmuxSession(sessionName: string): void {
91
+ try {
92
+ tmux("has-session", "-t", sessionName);
93
+ } catch (err) {
94
+ throw new DaemonStartupError(
95
+ `tmux session "${sessionName}" does not exist`,
96
+ "tmux_session_missing",
97
+ { cause: err as Error },
98
+ );
99
+ }
100
+ }
101
+
102
+ function validatePort(port: number): void {
103
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
104
+ throw new DaemonStartupError(`Invalid daemon port: ${port}`, "port_invalid");
105
+ }
106
+ }
107
+
108
+ async function pickFreePort(hostname: string): Promise<number> {
109
+ const probe = createServer();
110
+ return await new Promise<number>((resolve, reject) => {
111
+ probe.once("error", reject);
112
+ probe.listen(0, hostname, () => {
113
+ const address = probe.address();
114
+ const port = typeof address === "object" && address ? address.port : null;
115
+ probe.close(() => {
116
+ if (port) resolve(port);
117
+ else reject(new DaemonStartupError("Could not allocate daemon port", "bind_failed"));
118
+ });
119
+ });
120
+ });
121
+ }
122
+
123
+ function sessionExists(sessionName: string): "yes" | "no" | "unknown" {
124
+ try {
125
+ tmux("has-session", "-t", sessionName);
126
+ return "yes";
127
+ } catch (err) {
128
+ const msg = (err as NodeJS.ErrnoException).message ?? "";
129
+ const code = (err as NodeJS.ErrnoException).code;
130
+ // Spawn-level failures (EBADF, EAGAIN, etc.) are transient OS issues,
131
+ // not "session is gone" signals. Same for non-zero exits without stderr
132
+ // — `has-session` returns 1 only when the session is genuinely missing,
133
+ // and tmux always writes to stderr in that case.
134
+ if (
135
+ code === "EBADF" ||
136
+ code === "EAGAIN" ||
137
+ code === "EMFILE" ||
138
+ code === "ENFILE" ||
139
+ msg.includes("EBADF") ||
140
+ msg.includes("EAGAIN")
141
+ ) {
142
+ console.error("[daemon] sessionExists transient spawn error:", msg);
143
+ return "unknown";
144
+ }
145
+ return "no";
146
+ }
147
+ }
148
+
149
+ function hasClients(): boolean {
150
+ return tmuxSilent("list-clients").length > 0;
151
+ }
152
+
153
+ function listPanes(sessionName: string): MonitorPane[] {
154
+ const raw = tmuxSilent(
155
+ "list-panes",
156
+ "-t",
157
+ sessionName,
158
+ "-F",
159
+ "#{pane_id}\t#{pane_pid}\t#{pane_current_command}\t#{pane_title}\t#{@ide_role}\t#{@ide_type}\t#{@ide_name}",
160
+ );
161
+ if (!raw) return [];
162
+ return raw.split("\n").map((line) => {
163
+ const [id, pid, cmd, title, role, type, name] = line.split("\t");
164
+ return {
165
+ id: id!,
166
+ pid: pid!,
167
+ cmd,
168
+ title,
169
+ role: role || undefined,
170
+ type: type || undefined,
171
+ name: name || undefined,
172
+ };
173
+ });
174
+ }
175
+
176
+ function bearerToken(authHeader: string | string[] | undefined): string | null {
177
+ const value = Array.isArray(authHeader) ? authHeader[0] : authHeader;
178
+ if (!value?.startsWith("Bearer ")) return null;
179
+ return value.slice("Bearer ".length);
180
+ }
181
+
182
+ function requestToken(req: import("node:http").IncomingMessage): string | null {
183
+ const headerToken = bearerToken(req.headers.authorization);
184
+ if (headerToken) return headerToken;
185
+ try {
186
+ const url = new URL(req.url ?? "/", "http://localhost");
187
+ return url.searchParams.get("token");
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ function isLoopbackRequest(req: import("node:http").IncomingMessage): boolean {
194
+ const remote = req.socket.remoteAddress ?? "";
195
+ // Cover both IPv4 (127.0.0.1) and IPv6 (::1 / ::ffff:127.0.0.1) loopback.
196
+ return (
197
+ remote === "127.0.0.1" ||
198
+ remote === "::1" ||
199
+ remote === "::ffff:127.0.0.1" ||
200
+ remote.startsWith("127.")
201
+ );
202
+ }
203
+
204
+ function isLoopbackBind(bindHostname: string | null | undefined): boolean {
205
+ return bindHostname === "127.0.0.1" || bindHostname === "::1" || bindHostname === "localhost";
206
+ }
207
+
208
+ function isUpgradeAuthorized(
209
+ req: import("node:http").IncomingMessage,
210
+ token: string | null | undefined,
211
+ localBypassToken: string | null | undefined,
212
+ bindHostname?: string | null,
213
+ ): boolean {
214
+ if (!token) return true;
215
+ // T085: when the daemon binds to a loopback interface, the only way to
216
+ // reach `/ws/events` + `/ws/pty/:id` is from the same machine — match the
217
+ // REST `requireAuth` loopback bypass so the localhost dashboard doesn't
218
+ // have to juggle the remote-access token through every URL. Remote-bound
219
+ // daemons (0.0.0.0 / explicit hostnames) still enforce the token even on
220
+ // loopback because the operator has explicitly opted into remote access.
221
+ if (isLoopbackBind(bindHostname) && isLoopbackRequest(req)) return true;
222
+ const supplied = requestToken(req);
223
+ return supplied === token || (localBypassToken != null && supplied === localBypassToken);
224
+ }
225
+
226
+ function rejectUpgradeWithPolicy(
227
+ wss: WebSocketServer,
228
+ req: import("node:http").IncomingMessage,
229
+ socket: Socket,
230
+ head: Buffer,
231
+ ): void {
232
+ wss.handleUpgrade(req, socket, head, (ws) => {
233
+ ws.close(1008, "Remote access token required");
234
+ });
235
+ }
236
+
237
+ function attachWebSockets(
238
+ server: Server,
239
+ opts: {
240
+ authToken?: string | null;
241
+ localBypassToken?: string | null;
242
+ bindHostname?: string | null;
243
+ } = {},
244
+ ): {
245
+ closeClients: () => void;
246
+ closeServers: () => Promise<void>;
247
+ } {
248
+ const eventsWss = new WebSocketServer({ noServer: true });
249
+ const ptyWss = new WebSocketServer({ noServer: true });
250
+ const clients = new Set<WebSocket>();
251
+
252
+ const track = (ws: WebSocket): void => {
253
+ clients.add(ws);
254
+ ws.on("close", () => clients.delete(ws));
255
+ ws.on("error", () => clients.delete(ws));
256
+ };
257
+
258
+ const upgradeListener = (
259
+ req: import("node:http").IncomingMessage,
260
+ socket: import("node:net").Socket,
261
+ head: Buffer,
262
+ ): void => {
263
+ const pathname = (req.url ?? "/").split("?")[0] ?? "/";
264
+ if (
265
+ (pathname === "/ws/events" || pathname.startsWith("/ws/pty/")) &&
266
+ !isUpgradeAuthorized(req, opts.authToken, opts.localBypassToken, opts.bindHostname)
267
+ ) {
268
+ rejectUpgradeWithPolicy(pathname === "/ws/events" ? eventsWss : ptyWss, req, socket, head);
269
+ return;
270
+ }
271
+
272
+ if (pathname === "/ws/events") {
273
+ eventsWss.handleUpgrade(req, socket, head, (ws) => {
274
+ track(ws);
275
+ handleWsEventsConnection(ws);
276
+ });
277
+ return;
278
+ }
279
+
280
+ const ptyMatch = pathname.match(/^\/ws\/pty\/([^/]+)$/);
281
+ if (ptyMatch) {
282
+ const id = decodeURIComponent(ptyMatch[1]!);
283
+ ptyWss.handleUpgrade(req, socket, head, (ws) => {
284
+ track(ws);
285
+ handlePtyWebSocket(ws, id);
286
+ });
287
+ return;
288
+ }
289
+ };
290
+
291
+ server.on("upgrade", upgradeListener);
292
+
293
+ return {
294
+ closeClients: () => {
295
+ for (const ws of clients) {
296
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
297
+ closeWsGoingAway(ws);
298
+ }
299
+ }
300
+ },
301
+ closeServers: async () => {
302
+ server.off("upgrade", upgradeListener);
303
+ for (const ws of clients) {
304
+ if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CLOSING) {
305
+ ws.terminate();
306
+ }
307
+ }
308
+ const closeWss = (wss: WebSocketServer) =>
309
+ Promise.race([new Promise<void>((resolve) => wss.close(() => resolve())), delay(100)]);
310
+ await Promise.all([closeWss(eventsWss), closeWss(ptyWss)]);
311
+ },
312
+ };
313
+ }
314
+
315
+ function waitForServerClose(server: Server): Promise<void> {
316
+ return new Promise((resolve, reject) => {
317
+ server.close((err) => {
318
+ if (err) reject(err);
319
+ else resolve();
320
+ });
321
+ });
322
+ }
323
+
324
+ function delay(ms: number): Promise<void> {
325
+ return new Promise((resolve) => setTimeout(resolve, ms));
326
+ }
327
+
328
+ function generateLocalBypassToken(): string {
329
+ return randomBytes(32).toString("base64url");
330
+ }
331
+
332
+ function probeHostname(bindHostname: string): string {
333
+ return bindHostname === "0.0.0.0" ? "127.0.0.1" : bindHostname;
334
+ }
335
+
336
+ function timeoutSignal(ms: number): AbortSignal {
337
+ const controller = new AbortController();
338
+ setTimeout(() => controller.abort(), ms).unref?.();
339
+ return controller.signal;
340
+ }
341
+
342
+ async function healthGone(port: number, bindHostname: string): Promise<boolean> {
343
+ try {
344
+ const res = await fetch(`http://${probeHostname(bindHostname)}:${port}/health`, {
345
+ signal: timeoutSignal(500),
346
+ });
347
+ return !res.ok;
348
+ } catch {
349
+ return true;
350
+ }
351
+ }
352
+
353
+ async function requestDaemonShutdown(port: number, bindHostname: string): Promise<void> {
354
+ try {
355
+ await fetch(`http://${probeHostname(bindHostname)}:${port}/api/v2/action/daemon.shutdown`, {
356
+ method: "POST",
357
+ headers: { "Content-Type": "application/json" },
358
+ body: JSON.stringify({ reason: "takeover" }),
359
+ signal: timeoutSignal(1_000),
360
+ });
361
+ } catch {
362
+ // The existing daemon may predate the action or be mid-shutdown. Polling
363
+ // and the final PID kill below handle both cases.
364
+ }
365
+ }
366
+
367
+ async function takeoverCanonicalDaemon(info: {
368
+ pid: number;
369
+ port: number;
370
+ bindHostname: string;
371
+ }): Promise<void> {
372
+ await requestDaemonShutdown(info.port, info.bindHostname);
373
+ const deadline = Date.now() + 10_000;
374
+ while (Date.now() < deadline) {
375
+ const current = readCanonicalDaemonInfo();
376
+ const fileGone = !current || current.pid !== info.pid || current.port !== info.port;
377
+ const serverGone = await healthGone(info.port, info.bindHostname);
378
+ if (fileGone && serverGone) return;
379
+ await delay(150);
380
+ }
381
+
382
+ try {
383
+ process.kill(info.pid, "SIGTERM");
384
+ } catch {
385
+ // Already gone or not ours.
386
+ }
387
+ await delay(500);
388
+ try {
389
+ process.kill(info.pid, "SIGKILL");
390
+ } catch {
391
+ // Already gone or not ours.
392
+ }
393
+ clearCanonicalDaemonInfo();
394
+ }
395
+
396
+ function closeWsGoingAway(ws: WebSocket): void {
397
+ const reason = Buffer.from("going away");
398
+ const payload = Buffer.allocUnsafe(2 + reason.length);
399
+ payload.writeUInt16BE(1001, 0);
400
+ reason.copy(payload, 2);
401
+ const frame = Buffer.concat([Buffer.from([0x88, payload.length]), payload]);
402
+ const socket = (ws as unknown as { _socket?: Socket })._socket;
403
+ if (socket && !socket.destroyed && socket.writable) {
404
+ socket.end(frame);
405
+ return;
406
+ }
407
+ ws.close(1001, reason);
408
+ }
409
+
410
+ async function startHttpServer({
411
+ sessionName,
412
+ requestedPort,
413
+ bindHostname,
414
+ dir,
415
+ authToken,
416
+ localBypassToken,
417
+ silent,
418
+ }: {
419
+ sessionName: string;
420
+ requestedPort: number;
421
+ bindHostname: string;
422
+ dir: string;
423
+ authToken?: string | null;
424
+ localBypassToken?: string | null;
425
+ silent?: boolean;
426
+ }): Promise<{
427
+ server: Server;
428
+ sockets: Set<Socket>;
429
+ closeClients: () => void;
430
+ closeWsServers: () => Promise<void>;
431
+ }> {
432
+ const { createApp } = await import("../command-center/server.ts");
433
+ const { getRequestListener } = await import(requireFromHere.resolve("@hono/node-server"));
434
+ const { AuthService } = await import("./auth/auth-service.ts");
435
+ const { AuthConfigSchema } = await import("./auth/types.ts");
436
+
437
+ let authConfig = AuthConfigSchema.parse({});
438
+ try {
439
+ const { readConfig } = await import("./yaml-io.ts");
440
+ const { config } = readConfig(dir);
441
+ if (config.auth) authConfig = AuthConfigSchema.parse(config.auth);
442
+ } catch {
443
+ // Config not readable — use defaults.
444
+ }
445
+
446
+ const authService = new AuthService(authConfig.secret);
447
+
448
+ const app = createApp({
449
+ authService,
450
+ authConfig,
451
+ remoteAccess: {
452
+ bindHostname,
453
+ token: authToken ?? null,
454
+ localBypassToken: localBypassToken ?? null,
455
+ },
456
+ });
457
+ app.get("/api/daemon/health", (c: { json: (body: unknown, status?: number) => Response }) => {
458
+ return c.json({ ok: true, session: sessionName });
459
+ });
460
+
461
+ const server = createServer(getRequestListener(app.fetch));
462
+ const sockets = new Set<Socket>();
463
+ server.on("connection", (socket) => {
464
+ sockets.add(socket);
465
+ socket.on("close", () => sockets.delete(socket));
466
+ });
467
+
468
+ const { closeClients, closeServers: closeWsServers } = attachWebSockets(server, {
469
+ authToken,
470
+ localBypassToken,
471
+ bindHostname,
472
+ });
473
+
474
+ await new Promise<void>((resolve, reject) => {
475
+ const onError = (err: NodeJS.ErrnoException) => {
476
+ server.off("listening", onListening);
477
+ if (err.code === "EADDRINUSE") {
478
+ reject(
479
+ new DaemonStartupError(`Port ${requestedPort} is already in use`, "port_in_use", {
480
+ cause: err,
481
+ }),
482
+ );
483
+ } else {
484
+ reject(
485
+ new DaemonStartupError(`Failed to bind daemon on port ${requestedPort}`, "bind_failed", {
486
+ cause: err,
487
+ }),
488
+ );
489
+ }
490
+ };
491
+ const onListening = () => {
492
+ server.off("error", onError);
493
+ if (!silent) {
494
+ console.log(
495
+ `[daemon] Command Center on http://${bindHostname}:${requestedPort} (session: ${sessionName})`,
496
+ );
497
+ }
498
+ resolve();
499
+ };
500
+ server.once("error", onError);
501
+ server.once("listening", onListening);
502
+ server.listen(requestedPort, bindHostname);
503
+ });
504
+
505
+ return {
506
+ server,
507
+ sockets,
508
+ closeClients,
509
+ closeWsServers,
510
+ };
511
+ }
512
+
513
+ export async function startEmbeddedDaemon(
514
+ opts: EmbeddedDaemonOptions,
515
+ ): Promise<EmbeddedDaemonHandle> {
516
+ const sessionName = opts.sessionName ?? EMBEDDED_SESSION_NAME;
517
+ const sessionless = opts.sessionName == null;
518
+ const appSettings = readAppSettings();
519
+ const persistedRemoteAccess =
520
+ appSettings.remoteAccess.enabled && appSettings.remoteAccess.token
521
+ ? appSettings.remoteAccess
522
+ : null;
523
+ const bindHostname =
524
+ opts.bindHostname ?? opts.hostname ?? (persistedRemoteAccess ? "0.0.0.0" : DEFAULT_HOSTNAME);
525
+ const authToken = opts.authToken ?? persistedRemoteAccess?.token ?? null;
526
+ const localBypassToken = opts.localBypassToken ?? generateLocalBypassToken();
527
+ const existingCanonical = readCanonicalDaemonInfo();
528
+ if (existingCanonical) {
529
+ if (await isCanonicalDaemonAlive(existingCanonical)) {
530
+ if (opts.takeoverIfRunning) {
531
+ await takeoverCanonicalDaemon(existingCanonical);
532
+ } else {
533
+ throw new DaemonStartupError(
534
+ `Canonical daemon is already running on port ${existingCanonical.port}`,
535
+ "canonical_already_running",
536
+ );
537
+ }
538
+ } else {
539
+ clearCanonicalDaemonInfo();
540
+ }
541
+ }
542
+ if (!sessionless) assertTmuxSession(sessionName);
543
+ const port = opts.port ?? (await pickFreePort(bindHostname));
544
+ validatePort(port);
545
+ const dir = process.cwd();
546
+
547
+ // Workspace registry: load + reconcile against live tmux sessions on
548
+ // startup. Backwards-compat: if TMUX_IDE_SESSION is set, auto-add it as
549
+ // a workspace so legacy single-session callers (pre-T068) keep working.
550
+ // The registry is the source of truth for /api/project/:name lookups.
551
+ const workspaceRegistry = getDefaultWorkspaceRegistry();
552
+ await workspaceRegistry.load();
553
+ const legacySession = process.env.TMUX_IDE_SESSION;
554
+ if (legacySession && !workspaceRegistry.has(legacySession)) {
555
+ try {
556
+ workspaceRegistry.add({
557
+ name: legacySession,
558
+ sessionName: legacySession,
559
+ projectDir: dir,
560
+ });
561
+ } catch {
562
+ // Already added or persistence failed; non-fatal.
563
+ }
564
+ }
565
+ // Also register the explicit sessionName when the daemon is launched
566
+ // against a specific tmux session (e.g. `node daemon.ts new-name 6060`).
567
+ // Without this the operator has to POST /api/workspaces by hand before
568
+ // /api/sessions / /api/project/:name return anything — which was the
569
+ // dashboard's "terminal not connecting" symptom before this fix.
570
+ if (
571
+ !sessionless &&
572
+ sessionName !== EMBEDDED_SESSION_NAME &&
573
+ !workspaceRegistry.has(sessionName)
574
+ ) {
575
+ try {
576
+ workspaceRegistry.add({
577
+ name: sessionName,
578
+ sessionName,
579
+ projectDir: dir,
580
+ });
581
+ } catch {
582
+ // Already added or persistence failed; non-fatal.
583
+ }
584
+ }
585
+ const { server, sockets, closeClients, closeWsServers } = await startHttpServer({
586
+ sessionName,
587
+ requestedPort: port,
588
+ bindHostname,
589
+ dir,
590
+ authToken,
591
+ localBypassToken,
592
+ silent: opts.silent,
593
+ });
594
+ const pkg = requireFromHere("../../package.json") as { version?: string };
595
+ writeCanonicalDaemonInfo({
596
+ pid: process.pid,
597
+ port,
598
+ version: pkg.version ?? "0.0.0",
599
+ startedAt: new Date().toISOString(),
600
+ bindHostname,
601
+ authToken,
602
+ });
603
+
604
+ let lastState = "";
605
+ let stopped = false;
606
+ let stopping: Promise<void> | null = null;
607
+ let stopSelf: (() => void) | null = null;
608
+ const activeProjectStops = new Map<string, { stop: () => void }>();
609
+
610
+ // Track active projects so the session-control surface can open/close
611
+ // them. Orchestration moved out of tmux-ide (agent coordination lives in
612
+ // sfora.ai), so activation is now bookkeeping only.
613
+ const activateProjectOnDaemon = async (
614
+ projectName: string,
615
+ _options: ProjectActivationOptions = {},
616
+ ): Promise<{ stop: () => Promise<void> }> => {
617
+ if (!activeProjectStops.has(projectName)) {
618
+ activeProjectStops.set(projectName, { stop: () => undefined });
619
+ }
620
+ return {
621
+ stop: async () => {
622
+ const current = activeProjectStops.get(projectName);
623
+ if (!current) return;
624
+ activeProjectStops.delete(projectName);
625
+ current.stop();
626
+ },
627
+ };
628
+ };
629
+
630
+ setActivationBackend({
631
+ activateProject: async (name, options) => {
632
+ await activateProjectOnDaemon(name, options);
633
+ },
634
+ deactivateProject: async (name) => {
635
+ const stop = activeProjectStops.get(name);
636
+ if (!stop) return;
637
+ activeProjectStops.delete(name);
638
+ stop.stop();
639
+ },
640
+ });
641
+
642
+ const tick = (): void => {
643
+ if (sessionless) return;
644
+ const session = sessionExists(sessionName);
645
+ if (session === "no") {
646
+ stopSelf?.();
647
+ return;
648
+ }
649
+ if (session === "unknown") {
650
+ // Transient spawn failure — skip this tick rather than self-destruct.
651
+ return;
652
+ }
653
+ if (!hasClients()) return;
654
+
655
+ const panes = listPanes(sessionName);
656
+ if (panes.length === 0) return;
657
+
658
+ const portPanes = computePortPanes(panes);
659
+ const agentStates = computeAgentStates(panes);
660
+ const stateKey = panes
661
+ .map((pane) => {
662
+ const portState = portPanes.has(pane.id) ? "1" : "0";
663
+ const agent = agentStates.get(pane.id) ?? "-";
664
+ const titleDrift = pane.name && pane.title !== pane.name ? "d" : "ok";
665
+ return `${pane.id}:${portState}:${agent}:${titleDrift}`;
666
+ })
667
+ .join("|");
668
+
669
+ if (stateKey === lastState) return;
670
+
671
+ for (const pane of panes) {
672
+ const hasPort = portPanes.has(pane.id) ? "1" : "0";
673
+ const agent = agentStates.get(pane.id);
674
+ tmuxSilent("set-option", "-pqt", pane.id, "@has_port", hasPort);
675
+ tmuxSilent("set-option", "-pqt", pane.id, "@agent_busy", agent === "busy" ? "1" : "0");
676
+ tmuxSilent("set-option", "-pqt", pane.id, "@agent_idle", agent === "idle" ? "1" : "0");
677
+ if (pane.name && pane.title !== pane.name) {
678
+ tmuxSilent("select-pane", "-t", pane.id, "-T", pane.name);
679
+ }
680
+ }
681
+
682
+ tmuxSilent("refresh-client", "-S");
683
+ lastState = stateKey;
684
+ };
685
+
686
+ const monitorInterval = setInterval(tick, MONITOR_INTERVAL_MS);
687
+
688
+ const apiBaseUrl = `http://${bindHostname}:${port}`;
689
+ const wsUrl = `ws://${bindHostname}:${port}/ws/events`;
690
+
691
+ const handle: EmbeddedDaemonHandle = {
692
+ port,
693
+ apiBaseUrl,
694
+ wsUrl,
695
+ localBypassToken,
696
+ stop: async ({ gracefulMs = DEFAULT_GRACEFUL_MS } = {}) => {
697
+ if (stopped) return;
698
+ if (stopping) return stopping;
699
+ stopping = (async () => {
700
+ try {
701
+ stopped = true;
702
+ setActivationBackend(null);
703
+ clearInterval(monitorInterval);
704
+
705
+ const closePromise = waitForServerClose(server);
706
+ closeClients();
707
+ for (const stop of activeProjectStops.values()) {
708
+ stop.stop();
709
+ }
710
+ activeProjectStops.clear();
711
+ shutdownPtyBridges();
712
+ await Promise.race([closePromise, delay(gracefulMs)]);
713
+ for (const socket of sockets) socket.destroy();
714
+ await Promise.race([closePromise.catch(() => undefined), delay(100)]);
715
+ await closeWsServers();
716
+ setRemoteAccessRestartBackend(null);
717
+ setDaemonShutdownBackend(null);
718
+ } catch (err) {
719
+ throw new DaemonShutdownError("Daemon shutdown failed", { cause: err as Error });
720
+ } finally {
721
+ clearCanonicalDaemonInfo();
722
+ }
723
+ })();
724
+ return stopping;
725
+ },
726
+ activateProject: activateProjectOnDaemon,
727
+ };
728
+ setDaemonShutdownBackend(async () => {
729
+ await handle.stop({ gracefulMs: 500 });
730
+ });
731
+ setRemoteAccessRestartBackend((request) => {
732
+ setTimeout(() => {
733
+ void (async () => {
734
+ const restartPort = request.port ?? port;
735
+ try {
736
+ await handle.stop({ gracefulMs: 500 });
737
+ } catch (err) {
738
+ console.error("[daemon] Remote access stop before restart failed:", err);
739
+ } finally {
740
+ clearCanonicalDaemonInfo();
741
+ }
742
+
743
+ for (let attempt = 0; attempt < 2; attempt++) {
744
+ try {
745
+ const nextHandle = await startEmbeddedDaemon({
746
+ sessionName: sessionless ? undefined : sessionName,
747
+ port: restartPort,
748
+ bindHostname: request.bindHostname,
749
+ authToken: request.token,
750
+ localBypassToken,
751
+ });
752
+ const mutableHandle = handle as {
753
+ stop: EmbeddedDaemonHandle["stop"];
754
+ activateProject: EmbeddedDaemonHandle["activateProject"];
755
+ };
756
+ mutableHandle.stop = nextHandle.stop;
757
+ mutableHandle.activateProject = nextHandle.activateProject;
758
+ return;
759
+ } catch (err) {
760
+ if (
761
+ err instanceof DaemonStartupError &&
762
+ err.reason === "port_in_use" &&
763
+ attempt === 0
764
+ ) {
765
+ await delay(150);
766
+ continue;
767
+ }
768
+ throw err;
769
+ }
770
+ }
771
+ })().catch((err) => {
772
+ console.error("[daemon] Remote access restart failed:", err);
773
+ clearCanonicalDaemonInfo();
774
+ });
775
+ }, 50).unref?.();
776
+ return { port };
777
+ });
778
+ stopSelf = () => void handle.stop();
779
+ tick();
780
+
781
+ return handle;
782
+ }