@otto-code/cli 0.5.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 (200) hide show
  1. package/bin/otto +3 -0
  2. package/dist/classify.d.ts +19 -0
  3. package/dist/classify.js +49 -0
  4. package/dist/cli.d.ts +3 -0
  5. package/dist/cli.js +113 -0
  6. package/dist/commands/agent/archive.d.ts +18 -0
  7. package/dist/commands/agent/archive.js +102 -0
  8. package/dist/commands/agent/attach.d.ts +11 -0
  9. package/dist/commands/agent/attach.js +159 -0
  10. package/dist/commands/agent/delete.d.ts +15 -0
  11. package/dist/commands/agent/delete.js +107 -0
  12. package/dist/commands/agent/import.d.ts +14 -0
  13. package/dist/commands/agent/import.js +122 -0
  14. package/dist/commands/agent/index.d.ts +3 -0
  15. package/dist/commands/agent/index.js +48 -0
  16. package/dist/commands/agent/inspect.d.ts +15 -0
  17. package/dist/commands/agent/inspect.js +209 -0
  18. package/dist/commands/agent/logs.d.ts +19 -0
  19. package/dist/commands/agent/logs.js +180 -0
  20. package/dist/commands/agent/ls.d.ts +44 -0
  21. package/dist/commands/agent/ls.js +204 -0
  22. package/dist/commands/agent/mode.d.ts +18 -0
  23. package/dist/commands/agent/mode.js +100 -0
  24. package/dist/commands/agent/reload.d.ts +15 -0
  25. package/dist/commands/agent/reload.js +75 -0
  26. package/dist/commands/agent/run.d.ts +44 -0
  27. package/dist/commands/agent/run.js +464 -0
  28. package/dist/commands/agent/send.d.ts +19 -0
  29. package/dist/commands/agent/send.js +187 -0
  30. package/dist/commands/agent/stop.d.ts +17 -0
  31. package/dist/commands/agent/stop.js +115 -0
  32. package/dist/commands/agent/update.d.ts +18 -0
  33. package/dist/commands/agent/update.js +139 -0
  34. package/dist/commands/agent/wait.d.ts +17 -0
  35. package/dist/commands/agent/wait.js +158 -0
  36. package/dist/commands/chat/create.d.ts +9 -0
  37. package/dist/commands/chat/create.js +23 -0
  38. package/dist/commands/chat/delete.d.ts +6 -0
  39. package/dist/commands/chat/delete.js +20 -0
  40. package/dist/commands/chat/index.d.ts +3 -0
  41. package/dist/commands/chat/index.js +47 -0
  42. package/dist/commands/chat/inspect.d.ts +6 -0
  43. package/dist/commands/chat/inspect.js +20 -0
  44. package/dist/commands/chat/ls.d.ts +6 -0
  45. package/dist/commands/chat/ls.js +20 -0
  46. package/dist/commands/chat/post.d.ts +9 -0
  47. package/dist/commands/chat/post.js +28 -0
  48. package/dist/commands/chat/read.d.ts +11 -0
  49. package/dist/commands/chat/read.js +40 -0
  50. package/dist/commands/chat/schema.d.ts +36 -0
  51. package/dist/commands/chat/schema.js +81 -0
  52. package/dist/commands/chat/shared.d.ts +19 -0
  53. package/dist/commands/chat/shared.js +118 -0
  54. package/dist/commands/chat/wait.d.ts +9 -0
  55. package/dist/commands/chat/wait.js +45 -0
  56. package/dist/commands/daemon/index.d.ts +3 -0
  57. package/dist/commands/daemon/index.js +57 -0
  58. package/dist/commands/daemon/local-daemon.d.ts +78 -0
  59. package/dist/commands/daemon/local-daemon.js +544 -0
  60. package/dist/commands/daemon/pair.d.ts +9 -0
  61. package/dist/commands/daemon/pair.js +76 -0
  62. package/dist/commands/daemon/restart.d.ts +12 -0
  63. package/dist/commands/daemon/restart.js +99 -0
  64. package/dist/commands/daemon/runtime-toolchain.d.ts +6 -0
  65. package/dist/commands/daemon/runtime-toolchain.js +87 -0
  66. package/dist/commands/daemon/set-password.d.ts +17 -0
  67. package/dist/commands/daemon/set-password.js +82 -0
  68. package/dist/commands/daemon/start.d.ts +6 -0
  69. package/dist/commands/daemon/start.js +56 -0
  70. package/dist/commands/daemon/status.d.ts +10 -0
  71. package/dist/commands/daemon/status.js +338 -0
  72. package/dist/commands/daemon/stop.d.ts +15 -0
  73. package/dist/commands/daemon/stop.js +60 -0
  74. package/dist/commands/hooks.d.ts +19 -0
  75. package/dist/commands/hooks.js +89 -0
  76. package/dist/commands/loop/index.d.ts +3 -0
  77. package/dist/commands/loop/index.js +18 -0
  78. package/dist/commands/loop/inspect.d.ts +13 -0
  79. package/dist/commands/loop/inspect.js +100 -0
  80. package/dist/commands/loop/logs.d.ts +8 -0
  81. package/dist/commands/loop/logs.js +69 -0
  82. package/dist/commands/loop/ls.d.ts +18 -0
  83. package/dist/commands/loop/ls.js +62 -0
  84. package/dist/commands/loop/run.d.ts +30 -0
  85. package/dist/commands/loop/run.js +162 -0
  86. package/dist/commands/loop/stop.d.ts +15 -0
  87. package/dist/commands/loop/stop.js +56 -0
  88. package/dist/commands/loop/types.d.ts +138 -0
  89. package/dist/commands/loop/types.js +2 -0
  90. package/dist/commands/onboard.d.ts +10 -0
  91. package/dist/commands/onboard.js +416 -0
  92. package/dist/commands/open.d.ts +2 -0
  93. package/dist/commands/open.js +81 -0
  94. package/dist/commands/permit/allow.d.ts +20 -0
  95. package/dist/commands/permit/allow.js +133 -0
  96. package/dist/commands/permit/deny.d.ts +12 -0
  97. package/dist/commands/permit/deny.js +105 -0
  98. package/dist/commands/permit/index.d.ts +3 -0
  99. package/dist/commands/permit/index.js +27 -0
  100. package/dist/commands/permit/ls.d.ts +18 -0
  101. package/dist/commands/permit/ls.js +66 -0
  102. package/dist/commands/provider/index.d.ts +3 -0
  103. package/dist/commands/provider/index.js +16 -0
  104. package/dist/commands/provider/ls.d.ts +19 -0
  105. package/dist/commands/provider/ls.js +73 -0
  106. package/dist/commands/provider/models.d.ts +20 -0
  107. package/dist/commands/provider/models.js +53 -0
  108. package/dist/commands/schedule/create.d.ts +18 -0
  109. package/dist/commands/schedule/create.js +40 -0
  110. package/dist/commands/schedule/delete.d.ts +10 -0
  111. package/dist/commands/schedule/delete.js +32 -0
  112. package/dist/commands/schedule/index.d.ts +3 -0
  113. package/dist/commands/schedule/index.js +66 -0
  114. package/dist/commands/schedule/inspect.d.ts +6 -0
  115. package/dist/commands/schedule/inspect.js +24 -0
  116. package/dist/commands/schedule/logs.d.ts +6 -0
  117. package/dist/commands/schedule/logs.js +23 -0
  118. package/dist/commands/schedule/ls.d.ts +5 -0
  119. package/dist/commands/schedule/ls.js +23 -0
  120. package/dist/commands/schedule/pause.d.ts +5 -0
  121. package/dist/commands/schedule/pause.js +23 -0
  122. package/dist/commands/schedule/resume.d.ts +5 -0
  123. package/dist/commands/schedule/resume.js +23 -0
  124. package/dist/commands/schedule/run-once.d.ts +5 -0
  125. package/dist/commands/schedule/run-once.js +23 -0
  126. package/dist/commands/schedule/schema.d.ts +21 -0
  127. package/dist/commands/schedule/schema.js +67 -0
  128. package/dist/commands/schedule/shared.d.ts +56 -0
  129. package/dist/commands/schedule/shared.js +346 -0
  130. package/dist/commands/schedule/types.d.ts +168 -0
  131. package/dist/commands/schedule/types.js +2 -0
  132. package/dist/commands/schedule/update.d.ts +21 -0
  133. package/dist/commands/schedule/update.js +39 -0
  134. package/dist/commands/speech/index.d.ts +3 -0
  135. package/dist/commands/speech/index.js +5 -0
  136. package/dist/commands/terminal/capture.d.ts +10 -0
  137. package/dist/commands/terminal/capture.js +68 -0
  138. package/dist/commands/terminal/create.d.ts +10 -0
  139. package/dist/commands/terminal/create.js +38 -0
  140. package/dist/commands/terminal/index.d.ts +3 -0
  141. package/dist/commands/terminal/index.js +43 -0
  142. package/dist/commands/terminal/kill.d.ts +6 -0
  143. package/dist/commands/terminal/kill.js +36 -0
  144. package/dist/commands/terminal/ls.d.ts +10 -0
  145. package/dist/commands/terminal/ls.js +21 -0
  146. package/dist/commands/terminal/schema.d.ts +18 -0
  147. package/dist/commands/terminal/schema.js +23 -0
  148. package/dist/commands/terminal/send-keys.d.ts +7 -0
  149. package/dist/commands/terminal/send-keys.js +76 -0
  150. package/dist/commands/terminal/shared.d.ts +12 -0
  151. package/dist/commands/terminal/shared.js +61 -0
  152. package/dist/commands/worktree/archive.d.ts +20 -0
  153. package/dist/commands/worktree/archive.js +104 -0
  154. package/dist/commands/worktree/create-input.d.ts +15 -0
  155. package/dist/commands/worktree/create-input.js +56 -0
  156. package/dist/commands/worktree/create.d.ts +11 -0
  157. package/dist/commands/worktree/create.js +58 -0
  158. package/dist/commands/worktree/index.d.ts +3 -0
  159. package/dist/commands/worktree/index.js +25 -0
  160. package/dist/commands/worktree/ls.d.ts +19 -0
  161. package/dist/commands/worktree/ls.js +97 -0
  162. package/dist/index.d.ts +2 -0
  163. package/dist/index.js +6 -0
  164. package/dist/output/index.d.ts +45 -0
  165. package/dist/output/index.js +47 -0
  166. package/dist/output/json.d.ts +11 -0
  167. package/dist/output/json.js +37 -0
  168. package/dist/output/quiet.d.ts +9 -0
  169. package/dist/output/quiet.js +22 -0
  170. package/dist/output/render.d.ts +15 -0
  171. package/dist/output/render.js +83 -0
  172. package/dist/output/table.d.ts +13 -0
  173. package/dist/output/table.js +135 -0
  174. package/dist/output/types.d.ts +73 -0
  175. package/dist/output/types.js +8 -0
  176. package/dist/output/with-output.d.ts +41 -0
  177. package/dist/output/with-output.js +87 -0
  178. package/dist/output/yaml.d.ts +11 -0
  179. package/dist/output/yaml.js +38 -0
  180. package/dist/run.d.ts +14 -0
  181. package/dist/run.js +33 -0
  182. package/dist/utils/client-id.d.ts +2 -0
  183. package/dist/utils/client-id.js +37 -0
  184. package/dist/utils/client.d.ts +44 -0
  185. package/dist/utils/client.js +330 -0
  186. package/dist/utils/command-options.d.ts +6 -0
  187. package/dist/utils/command-options.js +17 -0
  188. package/dist/utils/duration.d.ts +7 -0
  189. package/dist/utils/duration.js +38 -0
  190. package/dist/utils/errors.d.ts +5 -0
  191. package/dist/utils/errors.js +10 -0
  192. package/dist/utils/paths.d.ts +12 -0
  193. package/dist/utils/paths.js +22 -0
  194. package/dist/utils/provider-model.d.ts +11 -0
  195. package/dist/utils/provider-model.js +49 -0
  196. package/dist/utils/timeline.d.ts +11 -0
  197. package/dist/utils/timeline.js +11 -0
  198. package/dist/version.d.ts +2 -0
  199. package/dist/version.js +10 -0
  200. package/package.json +48 -0
@@ -0,0 +1,544 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+ import { loadConfig, resolveOttoHome, spawnProcess } from "@otto-code/server";
6
+ import treeKill from "tree-kill";
7
+ import { tryConnectToDaemon } from "../../utils/client.js";
8
+ const DETACHED_STARTUP_GRACE_MS = 1200;
9
+ const PID_POLL_INTERVAL_MS = 100;
10
+ const DAEMON_LOG_FILENAME = "daemon.log";
11
+ const DAEMON_PID_FILENAME = "otto.pid";
12
+ export const DEFAULT_STOP_TIMEOUT_MS = 15000;
13
+ export const DEFAULT_KILL_TIMEOUT_MS = 3000;
14
+ const require = createRequire(import.meta.url);
15
+ const defaultDaemonLaunchRuntime = {
16
+ resolveRunnerEntry: resolveDaemonRunnerEntry,
17
+ resolveHome: resolveOttoHome,
18
+ spawnDetached: spawnProcess,
19
+ spawnForeground: spawnSync,
20
+ };
21
+ const startupReady = () => ({ exitedEarly: false });
22
+ const startupExited = (details) => ({
23
+ exitedEarly: true,
24
+ ...details,
25
+ });
26
+ function envWithHome(home) {
27
+ if (!home) {
28
+ return process.env;
29
+ }
30
+ return { ...process.env, OTTO_HOME: home };
31
+ }
32
+ function buildRunnerArgs(options) {
33
+ const args = [];
34
+ if (options.relay === false) {
35
+ args.push("--no-relay");
36
+ }
37
+ if (options.relayUseTls === true) {
38
+ args.push("--relay-use-tls");
39
+ }
40
+ if (options.mcp === false) {
41
+ args.push("--no-mcp");
42
+ }
43
+ if (options.injectMcp === false) {
44
+ args.push("--no-inject-mcp");
45
+ }
46
+ if (options.webUi === true) {
47
+ args.push("--web-ui");
48
+ }
49
+ if (options.webUi === false) {
50
+ args.push("--no-web-ui");
51
+ }
52
+ return args;
53
+ }
54
+ function buildChildEnv(options) {
55
+ const childEnv = { ...process.env };
56
+ if (options.home) {
57
+ childEnv.OTTO_HOME = options.home;
58
+ }
59
+ if (options.listen) {
60
+ childEnv.OTTO_LISTEN = options.listen;
61
+ }
62
+ else if (options.port) {
63
+ childEnv.OTTO_LISTEN = `127.0.0.1:${options.port}`;
64
+ }
65
+ if (options.hostnames) {
66
+ childEnv.OTTO_HOSTNAMES = options.hostnames;
67
+ }
68
+ if (options.relayUseTls === true) {
69
+ childEnv.OTTO_RELAY_USE_TLS = "true";
70
+ }
71
+ if (options.webUi === true) {
72
+ childEnv.OTTO_WEB_UI_ENABLED = "true";
73
+ }
74
+ if (options.webUi === false) {
75
+ childEnv.OTTO_WEB_UI_ENABLED = "false";
76
+ }
77
+ return childEnv;
78
+ }
79
+ function resolveServerRunnerFromDir(currentDir) {
80
+ const packageJsonPath = path.join(currentDir, "package.json");
81
+ if (!existsSync(packageJsonPath))
82
+ return null;
83
+ try {
84
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
85
+ if (packageJson.name !== "@otto-code/server")
86
+ return null;
87
+ const distRunner = path.join(currentDir, "dist", "scripts", "supervisor-entrypoint.js");
88
+ if (existsSync(distRunner)) {
89
+ return distRunner;
90
+ }
91
+ return path.join(currentDir, "scripts", "supervisor-entrypoint.ts");
92
+ }
93
+ catch {
94
+ return null;
95
+ }
96
+ }
97
+ function resolveDaemonRunnerEntry() {
98
+ const serverExportPath = require.resolve("@otto-code/server");
99
+ let currentDir = path.dirname(serverExportPath);
100
+ while (true) {
101
+ const entry = resolveServerRunnerFromDir(currentDir);
102
+ if (entry) {
103
+ return entry;
104
+ }
105
+ const parentDir = path.dirname(currentDir);
106
+ if (parentDir === currentDir) {
107
+ break;
108
+ }
109
+ currentDir = parentDir;
110
+ }
111
+ throw new Error("Unable to resolve @otto-code/server package root for daemon runner");
112
+ }
113
+ function pidFilePath(ottoHome) {
114
+ return path.join(ottoHome, DAEMON_PID_FILENAME);
115
+ }
116
+ function resolveListenField(listen, sockPath) {
117
+ if (typeof listen === "string")
118
+ return listen;
119
+ if (typeof sockPath === "string")
120
+ return sockPath;
121
+ return undefined;
122
+ }
123
+ function resolveStopMessage(forced, lifecycleRequested, fallbackMessage) {
124
+ if (forced)
125
+ return "Daemon owner process was force-stopped";
126
+ if (lifecycleRequested)
127
+ return "Daemon stopped gracefully";
128
+ return fallbackMessage ?? "Daemon stopped via owner PID signal";
129
+ }
130
+ function resolveStopReason(forced, lifecycleRequested) {
131
+ if (forced)
132
+ return "owner_pid_sigkill";
133
+ if (lifecycleRequested)
134
+ return "lifecycle_shutdown_rpc";
135
+ return "owner_pid_signal";
136
+ }
137
+ function readPidFile(pidPath) {
138
+ try {
139
+ const parsed = JSON.parse(readFileSync(pidPath, "utf-8"));
140
+ const pidValue = parsed.pid;
141
+ if (typeof pidValue !== "number" || !Number.isInteger(pidValue) || pidValue <= 0) {
142
+ return null;
143
+ }
144
+ return {
145
+ pid: pidValue,
146
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : undefined,
147
+ hostname: typeof parsed.hostname === "string" ? parsed.hostname : undefined,
148
+ uid: typeof parsed.uid === "number" ? parsed.uid : undefined,
149
+ listen: resolveListenField(parsed.listen, parsed.sockPath),
150
+ desktopManaged: parsed.desktopManaged === true ? true : undefined,
151
+ };
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ }
157
+ function tailFile(filePath, lines = 30) {
158
+ try {
159
+ const content = readFileSync(filePath, "utf-8");
160
+ return content.split("\n").filter(Boolean).slice(-lines).join("\n");
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
166
+ function sleep(ms) {
167
+ return new Promise((resolve) => {
168
+ setTimeout(resolve, ms);
169
+ });
170
+ }
171
+ function readNodeErrnoCode(error) {
172
+ if (typeof error !== "object" || error === null || !("code" in error)) {
173
+ return undefined;
174
+ }
175
+ return typeof error.code === "string" ? error.code : undefined;
176
+ }
177
+ function isProcessRunning(pid) {
178
+ try {
179
+ process.kill(pid, 0);
180
+ return true;
181
+ }
182
+ catch (err) {
183
+ const code = readNodeErrnoCode(err);
184
+ if (code === "EPERM") {
185
+ return true;
186
+ }
187
+ return false;
188
+ }
189
+ }
190
+ function signalProcess(pid, signal) {
191
+ try {
192
+ process.kill(pid, signal);
193
+ return true;
194
+ }
195
+ catch (err) {
196
+ const code = readNodeErrnoCode(err);
197
+ if (code === "ESRCH") {
198
+ return false;
199
+ }
200
+ throw err;
201
+ }
202
+ }
203
+ function signalProcessSafely(pid, signal) {
204
+ if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) {
205
+ return false;
206
+ }
207
+ try {
208
+ return signalProcess(pid, signal);
209
+ }
210
+ catch (err) {
211
+ const code = readNodeErrnoCode(err);
212
+ if (code === "EPERM") {
213
+ return true;
214
+ }
215
+ throw err;
216
+ }
217
+ }
218
+ async function signalProcessTreeSafely(pid, signal) {
219
+ if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) {
220
+ return false;
221
+ }
222
+ return new Promise((resolve, reject) => {
223
+ treeKill(pid, signal, (err) => {
224
+ if (!err) {
225
+ resolve(true);
226
+ return;
227
+ }
228
+ const code = readNodeErrnoCode(err);
229
+ if (code === "ESRCH") {
230
+ resolve(false);
231
+ return;
232
+ }
233
+ if (code === "EPERM") {
234
+ resolve(true);
235
+ return;
236
+ }
237
+ reject(err);
238
+ });
239
+ });
240
+ }
241
+ async function signalProcessTreeOrOwnerSafely(pid, signal) {
242
+ try {
243
+ return await signalProcessTreeSafely(pid, signal);
244
+ }
245
+ catch {
246
+ return signalProcessSafely(pid, signal);
247
+ }
248
+ }
249
+ async function waitForPidExit(pid, timeoutMs) {
250
+ const deadline = Date.now() + timeoutMs;
251
+ async function poll() {
252
+ if (!isProcessRunning(pid))
253
+ return true;
254
+ if (Date.now() >= deadline)
255
+ return !isProcessRunning(pid);
256
+ await sleep(PID_POLL_INTERVAL_MS);
257
+ return poll();
258
+ }
259
+ return poll();
260
+ }
261
+ async function waitForDaemonUnreachable(state, timeoutMs) {
262
+ const host = resolveTcpHostFromListen(state.listen);
263
+ if (!host) {
264
+ return true;
265
+ }
266
+ const reachableHost = host;
267
+ const deadline = Date.now() + timeoutMs;
268
+ async function poll() {
269
+ const client = await tryConnectToDaemon({ host: reachableHost, timeout: 500 });
270
+ if (!client) {
271
+ return true;
272
+ }
273
+ await client.close().catch(() => undefined);
274
+ if (Date.now() >= deadline) {
275
+ const finalClient = await tryConnectToDaemon({
276
+ host: reachableHost,
277
+ timeout: PID_POLL_INTERVAL_MS,
278
+ });
279
+ if (!finalClient) {
280
+ return true;
281
+ }
282
+ await finalClient.close().catch(() => undefined);
283
+ return false;
284
+ }
285
+ await sleep(PID_POLL_INTERVAL_MS);
286
+ return poll();
287
+ }
288
+ return poll();
289
+ }
290
+ function removeStalePidFile(state) {
291
+ if (!state.stalePidFile) {
292
+ return;
293
+ }
294
+ try {
295
+ unlinkSync(state.pidPath);
296
+ }
297
+ catch {
298
+ // Best-effort cleanup only. The successful lifecycle stop is authoritative.
299
+ }
300
+ }
301
+ function createNotRunningStopResult(state, pid, message) {
302
+ return {
303
+ action: "not_running",
304
+ home: state.home,
305
+ pid,
306
+ forced: false,
307
+ usedLifecycleRpc: false,
308
+ reason: "not_running",
309
+ message,
310
+ };
311
+ }
312
+ function createStopTimeoutError(state, pid, timeoutMs) {
313
+ if (!state.running) {
314
+ const host = resolveTcpHostFromListen(state.listen);
315
+ return new Error(`Timed out waiting for daemon${host ? ` at ${host}` : ""} to stop after ${Math.ceil(timeoutMs / 1000)}s`);
316
+ }
317
+ return new Error(`Timed out waiting for daemon PID ${pid} to stop after ${Math.ceil(timeoutMs / 1000)}s`);
318
+ }
319
+ async function signalDaemonOwnerForStop(state, pid) {
320
+ if (pid === null) {
321
+ return createNotRunningStopResult(state, null, "Daemon is not running");
322
+ }
323
+ const signaled = await signalProcessTreeOrOwnerSafely(pid, "SIGTERM");
324
+ if (signaled) {
325
+ return null;
326
+ }
327
+ return createNotRunningStopResult(state, pid, "Daemon process was already stopped");
328
+ }
329
+ async function waitForStopAfterRequest(args) {
330
+ const { state, pid, timeoutMs, killTimeoutMs, force } = args;
331
+ let stopped = state.running && pid !== null
332
+ ? await waitForPidExit(pid, timeoutMs)
333
+ : await waitForDaemonUnreachable(state, timeoutMs);
334
+ if (!stopped && force && state.running && pid !== null) {
335
+ await signalProcessTreeOrOwnerSafely(pid, "SIGKILL");
336
+ stopped = await waitForPidExit(pid, killTimeoutMs);
337
+ return { stopped, forced: true };
338
+ }
339
+ return { stopped, forced: false };
340
+ }
341
+ function getErrorMessage(error) {
342
+ return error instanceof Error ? error.message : String(error);
343
+ }
344
+ export function resolveLocalOttoHome(home) {
345
+ return resolveOttoHome(envWithHome(home));
346
+ }
347
+ export function resolveTcpHostFromListen(listen) {
348
+ const normalized = listen.trim();
349
+ if (!normalized) {
350
+ return null;
351
+ }
352
+ if (normalized.startsWith("/") ||
353
+ normalized.startsWith("unix://") ||
354
+ normalized.startsWith("pipe://") ||
355
+ normalized.startsWith("\\\\.\\pipe\\") ||
356
+ /^[A-Za-z]:[/\\]/.test(normalized)) {
357
+ return null;
358
+ }
359
+ if (/^\d+$/.test(normalized)) {
360
+ return `127.0.0.1:${normalized}`;
361
+ }
362
+ if (normalized.includes(":")) {
363
+ return normalized;
364
+ }
365
+ return null;
366
+ }
367
+ export function resolveLocalDaemonState(options = {}) {
368
+ const env = {
369
+ ...envWithHome(options.home),
370
+ // Status should reflect local persisted config + pid file, not inherited daemon env overrides.
371
+ // This is CLI-side defensive scrubbing; the daemon RPC is authoritative when available.
372
+ OTTO_LISTEN: undefined,
373
+ OTTO_HOSTNAMES: undefined,
374
+ OTTO_ALLOWED_HOSTS: undefined,
375
+ OTTO_RELAY_ENABLED: undefined,
376
+ OTTO_RELAY_ENDPOINT: undefined,
377
+ OTTO_RELAY_PUBLIC_ENDPOINT: undefined,
378
+ OTTO_RELAY_USE_TLS: undefined,
379
+ OTTO_RELAY_PUBLIC_USE_TLS: undefined,
380
+ };
381
+ const home = resolveOttoHome(env);
382
+ const config = loadConfig(home, { env });
383
+ const pidPath = pidFilePath(home);
384
+ const logPath = path.join(home, DAEMON_LOG_FILENAME);
385
+ const pidInfo = existsSync(pidPath) ? readPidFile(pidPath) : null;
386
+ const running = pidInfo ? isProcessRunning(pidInfo.pid) : false;
387
+ const listen = pidInfo?.listen ?? config.listen;
388
+ return {
389
+ home,
390
+ listen,
391
+ relayEnabled: config.relayEnabled ?? true,
392
+ relayEndpoint: config.relayPublicEndpoint ?? config.relayEndpoint ?? "relay.otto-code.me:443",
393
+ relayUseTls: config.relayUseTls ?? false,
394
+ relayPublicUseTls: config.relayPublicUseTls ?? config.relayUseTls ?? false,
395
+ logPath,
396
+ pidPath,
397
+ pidInfo,
398
+ running,
399
+ stalePidFile: Boolean(pidInfo) && !running,
400
+ };
401
+ }
402
+ export function tailDaemonLog(home, lines = 30) {
403
+ const logPath = path.join(resolveLocalOttoHome(home), DAEMON_LOG_FILENAME);
404
+ return tailFile(logPath, lines);
405
+ }
406
+ export async function startLocalDaemonDetached(options, runtime = defaultDaemonLaunchRuntime) {
407
+ if (options.listen && options.port) {
408
+ throw new Error("Cannot use --listen and --port together");
409
+ }
410
+ const daemonRunnerEntry = runtime.resolveRunnerEntry();
411
+ const childEnv = buildChildEnv(options);
412
+ const ottoHome = runtime.resolveHome(childEnv);
413
+ const logPath = path.join(ottoHome, DAEMON_LOG_FILENAME);
414
+ const child = runtime.spawnDetached(process.execPath, [...process.execArgv, daemonRunnerEntry, ...buildRunnerArgs(options)], {
415
+ detached: true,
416
+ envMode: "internal",
417
+ env: childEnv,
418
+ stdio: ["ignore", "ignore", "ignore"],
419
+ });
420
+ child.unref();
421
+ const startup = await new Promise((resolve) => {
422
+ let settled = false;
423
+ const finish = (value) => {
424
+ if (settled)
425
+ return;
426
+ settled = true;
427
+ resolve(value);
428
+ };
429
+ const timer = setTimeout(() => finish(startupReady()), DETACHED_STARTUP_GRACE_MS);
430
+ child.once("error", (error) => {
431
+ clearTimeout(timer);
432
+ finish(startupExited({ code: null, signal: null, error }));
433
+ });
434
+ child.once("exit", (code, signal) => {
435
+ clearTimeout(timer);
436
+ finish(startupExited({ code, signal }));
437
+ });
438
+ });
439
+ if (startup.exitedEarly) {
440
+ const reason = startup.error
441
+ ? startup.error.message
442
+ : `exit code ${startup.code ?? "unknown"}${startup.signal ? ` (${startup.signal})` : ""}`;
443
+ const recentLogs = tailFile(logPath);
444
+ throw new Error([
445
+ `Daemon failed to start in background (${reason}).`,
446
+ recentLogs ? `Recent daemon logs:\n${recentLogs}` : null,
447
+ ]
448
+ .filter(Boolean)
449
+ .join("\n\n"));
450
+ }
451
+ return {
452
+ pid: child.pid ?? null,
453
+ logPath,
454
+ };
455
+ }
456
+ export function startLocalDaemonForeground(options, runtime = defaultDaemonLaunchRuntime) {
457
+ if (options.listen && options.port) {
458
+ throw new Error("Cannot use --listen and --port together");
459
+ }
460
+ const daemonRunnerEntry = runtime.resolveRunnerEntry();
461
+ const childEnv = buildChildEnv(options);
462
+ const result = runtime.spawnForeground(process.execPath, [...process.execArgv, daemonRunnerEntry, ...buildRunnerArgs(options)], {
463
+ env: childEnv,
464
+ stdio: "inherit",
465
+ });
466
+ if (result.error) {
467
+ throw result.error;
468
+ }
469
+ return result.status ?? 1;
470
+ }
471
+ async function requestLifecycleShutdown(state, timeoutMs) {
472
+ const host = resolveTcpHostFromListen(state.listen);
473
+ if (!host) {
474
+ return {
475
+ requested: false,
476
+ reason: "daemon listen target is not TCP, falling back to owner PID signal",
477
+ };
478
+ }
479
+ const deadline = Date.now() + timeoutMs;
480
+ const remainingTimeoutMs = () => Math.max(1, deadline - Date.now());
481
+ const client = await tryConnectToDaemon({ host, timeout: Math.min(remainingTimeoutMs(), 5000) });
482
+ if (!client) {
483
+ return {
484
+ requested: false,
485
+ reason: `daemon websocket at ${host} is not reachable, falling back to owner PID signal`,
486
+ };
487
+ }
488
+ try {
489
+ await client.shutdownServer({ timeout: Math.min(remainingTimeoutMs(), 5000) });
490
+ return { requested: true };
491
+ }
492
+ catch (error) {
493
+ return {
494
+ requested: false,
495
+ reason: `daemon lifecycle shutdown request failed (${getErrorMessage(error)}), falling back to owner PID signal`,
496
+ };
497
+ }
498
+ finally {
499
+ await client.close().catch(() => undefined);
500
+ }
501
+ }
502
+ export async function stopLocalDaemon(options = {}) {
503
+ const timeoutMs = options.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
504
+ const killTimeoutMs = options.killTimeoutMs ?? DEFAULT_KILL_TIMEOUT_MS;
505
+ const state = resolveLocalDaemonState({ home: options.home });
506
+ const deadline = Date.now() + timeoutMs;
507
+ const remainingTimeoutMs = () => Math.max(1, deadline - Date.now());
508
+ const shutdownAttempt = await requestLifecycleShutdown(state, remainingTimeoutMs());
509
+ const lifecycleRequested = shutdownAttempt.requested;
510
+ if (!state.pidInfo || (!state.running && !lifecycleRequested)) {
511
+ const staleSuffix = state.stalePidFile && state.pidInfo ? ` (stale PID file for ${state.pidInfo.pid})` : "";
512
+ return createNotRunningStopResult(state, state.pidInfo?.pid ?? null, `Daemon is not running${staleSuffix}`);
513
+ }
514
+ const pid = state.pidInfo?.pid ?? null;
515
+ const fallbackMessage = shutdownAttempt.requested ? null : shutdownAttempt.reason;
516
+ if (!lifecycleRequested) {
517
+ const notRunningResult = await signalDaemonOwnerForStop(state, pid);
518
+ if (notRunningResult)
519
+ return notRunningResult;
520
+ }
521
+ const { stopped, forced } = await waitForStopAfterRequest({
522
+ state,
523
+ pid,
524
+ timeoutMs: remainingTimeoutMs(),
525
+ killTimeoutMs,
526
+ force: options.force,
527
+ });
528
+ if (!stopped) {
529
+ throw createStopTimeoutError(state, pid, timeoutMs);
530
+ }
531
+ if (lifecycleRequested) {
532
+ removeStalePidFile(state);
533
+ }
534
+ return {
535
+ action: "stopped",
536
+ home: state.home,
537
+ pid,
538
+ forced,
539
+ usedLifecycleRpc: lifecycleRequested,
540
+ reason: resolveStopReason(forced, lifecycleRequested),
541
+ message: resolveStopMessage(forced, lifecycleRequested, fallbackMessage),
542
+ };
543
+ }
544
+ //# sourceMappingURL=local-daemon.js.map
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+ interface PairOptions {
3
+ home?: string;
4
+ json?: boolean;
5
+ }
6
+ export declare function pairCommand(): Command;
7
+ export declare function runPairCommand(options: PairOptions): Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=pair.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { Command } from "commander";
2
+ import chalk from "chalk";
3
+ import { generateLocalPairingOffer, loadConfig, resolveOttoHome } from "@otto-code/server";
4
+ import { tryConnectToDaemon } from "../../utils/client.js";
5
+ import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
6
+ import { addJsonOption } from "../../utils/command-options.js";
7
+ const PAIRING_DAEMON_RPC_TIMEOUT_MS = 1500;
8
+ export function pairCommand() {
9
+ return addJsonOption(new Command("pair").description("Print the daemon pairing QR code and link"))
10
+ .option("--home <path>", "Otto home directory (default: ~/.otto)")
11
+ .action(async (_options, command) => {
12
+ await runPairCommand(command.optsWithGlobals());
13
+ });
14
+ }
15
+ export async function runPairCommand(options) {
16
+ if (options.home) {
17
+ process.env.OTTO_HOME = options.home;
18
+ }
19
+ const ottoHome = resolveOttoHome();
20
+ const state = resolveLocalDaemonState({ home: ottoHome });
21
+ const host = resolveTcpHostFromListen(state.listen);
22
+ // Try to get the pairing offer from the running daemon first.
23
+ if (host) {
24
+ const client = await tryConnectToDaemon({ host, timeout: 1500 });
25
+ if (client) {
26
+ const supportsDaemonStatusRpc = client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
27
+ if (supportsDaemonStatusRpc) {
28
+ try {
29
+ const offer = await client.getDaemonPairingOffer({
30
+ timeout: PAIRING_DAEMON_RPC_TIMEOUT_MS,
31
+ });
32
+ await client.close().catch(() => { });
33
+ outputPairingResult({ relayEnabled: offer.relayEnabled, url: offer.url, qr: offer.qr ?? null }, options);
34
+ return;
35
+ }
36
+ catch {
37
+ // COMPAT(daemon-rpc-rollout): fall back to CLI-side pairing generation while
38
+ // old daemons lack daemonStatusRpc. Remove once the daemon floor is past
39
+ // v0.1.76; pairing should come from daemon.get_pairing_offer.
40
+ }
41
+ }
42
+ await client.close().catch(() => { });
43
+ }
44
+ }
45
+ // Fall back to local pairing offer generation.
46
+ const config = loadConfig(ottoHome);
47
+ const pairing = await generateLocalPairingOffer({
48
+ ottoHome,
49
+ relayEnabled: config.relayEnabled,
50
+ relayEndpoint: config.relayEndpoint,
51
+ relayPublicEndpoint: config.relayPublicEndpoint,
52
+ relayUseTls: config.relayUseTls,
53
+ relayPublicUseTls: config.relayPublicUseTls,
54
+ appBaseUrl: config.appBaseUrl,
55
+ includeQr: true,
56
+ });
57
+ outputPairingResult(pairing, options);
58
+ }
59
+ function outputPairingResult(pairing, options) {
60
+ if (!pairing.relayEnabled || !pairing.url) {
61
+ console.error(chalk.red("Relay pairing is disabled for this daemon config."));
62
+ console.error(chalk.yellow("Enable relay and run this command again."));
63
+ process.exit(1);
64
+ }
65
+ if (options.json) {
66
+ process.stdout.write(`${JSON.stringify({
67
+ relayEnabled: pairing.relayEnabled,
68
+ url: pairing.url,
69
+ qr: pairing.qr,
70
+ }, null, 2)}\n`);
71
+ return;
72
+ }
73
+ const qrBlock = pairing.qr ? `${pairing.qr}\n` : "";
74
+ process.stdout.write(`\nScan to pair:\n${qrBlock}${pairing.url}\n`);
75
+ }
76
+ //# sourceMappingURL=pair.js.map
@@ -0,0 +1,12 @@
1
+ import type { Command } from "commander";
2
+ import type { CommandOptions, SingleResult } from "../../output/index.js";
3
+ interface RestartResult {
4
+ action: "restarted";
5
+ home: string;
6
+ pid: string;
7
+ message: string;
8
+ }
9
+ export type RestartCommandResult = SingleResult<RestartResult>;
10
+ export declare function runRestartCommand(options: CommandOptions, _command: Command): Promise<RestartCommandResult>;
11
+ export {};
12
+ //# sourceMappingURL=restart.d.ts.map