@cueloop/daemon 0.1.0-alpha.0 → 0.1.0-alpha.100

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 (80) hide show
  1. package/README.md +7 -0
  2. package/SDK.md +102 -0
  3. package/package.json +43 -16
  4. package/src/agent-tools.ts +98 -0
  5. package/src/agent-turn.test.ts +24 -0
  6. package/src/agent-turn.ts +34 -0
  7. package/src/api.test.ts +1081 -65
  8. package/src/api.ts +1425 -89
  9. package/src/capabilities.test.ts +57 -0
  10. package/src/capabilities.ts +95 -0
  11. package/src/client-errors.ts +24 -0
  12. package/src/client-reliability.test.ts +189 -0
  13. package/src/client.test.ts +37 -0
  14. package/src/client.ts +820 -94
  15. package/src/curate.ts +109 -0
  16. package/src/daemon-sdk-effect.ts +113 -0
  17. package/src/daemon-sdk.test.ts +306 -0
  18. package/src/daemon-sdk.ts +397 -0
  19. package/src/daemon-version.test.ts +142 -0
  20. package/src/diff-hot-reload.test.ts +419 -0
  21. package/src/diff-watcher.test.ts +117 -0
  22. package/src/diff-watcher.ts +326 -0
  23. package/src/examples/daemon-sdk.ts +55 -0
  24. package/src/gh.test.ts +59 -0
  25. package/src/gh.ts +74 -0
  26. package/src/ghostty-thread-surface-store.test.ts +49 -0
  27. package/src/ghostty-thread-surface-store.ts +101 -0
  28. package/src/ghostty-thread-surface.test.ts +398 -0
  29. package/src/ghostty-thread-surface.ts +267 -0
  30. package/src/harness-delivery.test.ts +269 -0
  31. package/src/harness-diagnostics.test.ts +26 -0
  32. package/src/harness-diagnostics.ts +30 -0
  33. package/src/harness-state-store.test.ts +93 -0
  34. package/src/harness-state-store.ts +213 -0
  35. package/src/herdr-split.test.ts +106 -0
  36. package/src/herdr-split.ts +85 -0
  37. package/src/herdr-thread-surface-store.test.ts +81 -0
  38. package/src/herdr-thread-surface-store.ts +66 -0
  39. package/src/herdr-thread-surface.test.ts +465 -0
  40. package/src/herdr-thread-surface.ts +378 -0
  41. package/src/index.ts +8 -8
  42. package/src/interruptible-wait.test.ts +127 -0
  43. package/src/interruptible-wait.ts +66 -0
  44. package/src/jj-working-tree.ts +131 -0
  45. package/src/main.ts +4 -1
  46. package/src/operation-receipts.test.ts +111 -0
  47. package/src/operation-receipts.ts +91 -0
  48. package/src/paths.ts +49 -0
  49. package/src/pr-poller.test.ts +102 -0
  50. package/src/pr-poller.ts +73 -0
  51. package/src/project-files.test.ts +134 -0
  52. package/src/project-files.ts +68 -0
  53. package/src/protocol.ts +78 -7
  54. package/src/retention.test.ts +191 -0
  55. package/src/retention.ts +123 -0
  56. package/src/sdk-identities.ts +35 -0
  57. package/src/server.test.ts +465 -40
  58. package/src/server.ts +601 -88
  59. package/src/share-blob.test.ts +119 -0
  60. package/src/share-blob.ts +57 -0
  61. package/src/single-instance.test.ts +162 -13
  62. package/src/store.conformance.test.ts +41 -0
  63. package/src/store.jsonl.test.ts +173 -0
  64. package/src/store.ts +267 -34
  65. package/src/testing/store-conformance.ts +192 -0
  66. package/src/thread-agent-validation.ts +119 -0
  67. package/src/thread-agent.test.ts +412 -0
  68. package/src/thread-agent.ts +713 -0
  69. package/src/thread-review.test.ts +483 -0
  70. package/src/thread-review.ts +382 -0
  71. package/src/thread-subscription.test.ts +174 -0
  72. package/src/thread-subscription.ts +129 -0
  73. package/src/thread-surface-config.test.ts +63 -0
  74. package/src/thread-surface-config.ts +61 -0
  75. package/src/validate.test.ts +366 -21
  76. package/src/validate.ts +458 -47
  77. package/src/vcs-source.test.ts +180 -0
  78. package/src/vcs-source.ts +229 -0
  79. package/src/version.ts +6 -0
  80. package/src/working-tree.ts +150 -0
@@ -0,0 +1,397 @@
1
+ import * as v from "valibot";
2
+ import {
3
+ newAnnotationId,
4
+ type Annotation,
5
+ type Artifact,
6
+ type Message,
7
+ type MessageOutcome,
8
+ type Thread,
9
+ type ThreadAgentState,
10
+ type WorkspaceKey,
11
+ } from "@cueloop/schema";
12
+ import { DaemonClient, DaemonClientError, type ConnectOptions } from "./client";
13
+ import { DaemonTransportError, type DaemonRequestOptions } from "./client-errors";
14
+ import {
15
+ createSdkThreadId,
16
+ createSdkOperationId,
17
+ createSdkSubmissionId,
18
+ type SdkThreadId,
19
+ type SdkOperationId,
20
+ type SdkSubmissionId,
21
+ } from "./sdk-identities";
22
+ export {
23
+ createSdkThreadId,
24
+ createSdkOperationId,
25
+ createSdkSubmissionId,
26
+ type SdkThreadId,
27
+ type SdkOperationId,
28
+ type SdkSubmissionId,
29
+ } from "./sdk-identities";
30
+ import { ThreadRecordSchema } from "./validate";
31
+ import { ThreadAgentSchema } from "./thread-agent-validation";
32
+ import { connectThreadObserver, subscribeThreadState } from "./thread-subscription";
33
+
34
+ /** Validated SDK Thread reads carry a distinct identity without changing the wire model. */
35
+ export type SdkThread = Thread & { id: SdkThreadId };
36
+ const SdkThreadRecordSchema = v.pipe(
37
+ ThreadRecordSchema,
38
+ v.transform((thread): SdkThread => ({ ...thread, id: createSdkThreadId(thread.id) })),
39
+ );
40
+
41
+ /** These errors preserve daemon rejection codes and uncertainty after a lost response. */
42
+ export type DaemonSdkError = DaemonClientError | DaemonTransportError;
43
+
44
+ /** Acceptance identifies the exact batch; idle Thread state alone does not prove completion. */
45
+ export interface AgentAcceptance {
46
+ threadId: SdkThreadId;
47
+ operationId: SdkOperationId;
48
+ submissionIds: readonly SdkSubmissionId[];
49
+ }
50
+
51
+ /** Explicit cancellation and failed execution remain distinct terminal outcomes. */
52
+ export interface AgentCompletion extends AgentAcceptance {
53
+ outcome: "completed" | "failed" | "cancelled";
54
+ state: ThreadAgentState;
55
+ }
56
+
57
+ /** A review capability cannot create Threads, submit agents, or resolve reviews. */
58
+ export class ReviewDaemonSdk {
59
+ readonly ids = {
60
+ thread: createSdkThreadId,
61
+ operation: createSdkOperationId,
62
+ submission: createSdkSubmissionId,
63
+ };
64
+
65
+ protected closed = false;
66
+ private readonly observers = new Set<() => void>();
67
+
68
+ constructor(
69
+ protected readonly client: DaemonClient,
70
+ protected readonly connectionOptions: ConnectOptions = {},
71
+ ) {}
72
+
73
+ /** Snapshot observation owns a separate connection and never replays mutations. */
74
+ observeThread(input: {
75
+ threadId: SdkThreadId;
76
+ onValue: (thread: SdkThread) => void;
77
+ onError: (error: DaemonSdkError) => void;
78
+ }): () => void {
79
+ if (this.closed) {
80
+ input.onError(new DaemonTransportError("connection", "SDK connection is closed", "not_sent"));
81
+
82
+ return () => {};
83
+ }
84
+ const dispose = subscribeThreadState({
85
+ connect: connectThreadObserver(this.connectionOptions),
86
+ matches: (event) => event.sessionId === input.threadId,
87
+ read: (client, signal) =>
88
+ client.request("session.get", { id: input.threadId }, SdkThreadRecordSchema, { signal }),
89
+ onValue: input.onValue,
90
+ onError: (error) => input.onError(normalizeSdkError(error)),
91
+ });
92
+ const stop = () => {
93
+ dispose();
94
+ this.observers.delete(stop);
95
+ };
96
+
97
+ this.observers.add(stop);
98
+
99
+ return stop;
100
+ }
101
+
102
+ get threads() {
103
+ return {
104
+ get: (threadId: SdkThreadId, options?: DaemonRequestOptions): Promise<SdkThread> =>
105
+ this.client.request("session.get", { id: threadId }, SdkThreadRecordSchema, options),
106
+ };
107
+ }
108
+
109
+ readonly comments = {
110
+ add: (
111
+ input: { threadId: SdkThreadId; annotation: Parameters<DaemonClient["sessionComment"]>[1] },
112
+ options?: DaemonRequestOptions,
113
+ ): Promise<SdkThread> =>
114
+ this.client.request(
115
+ "session.comment",
116
+ { id: input.threadId, annotation: input.annotation },
117
+ SdkThreadRecordSchema,
118
+ options,
119
+ ),
120
+ list: async (threadId: SdkThreadId, options?: DaemonRequestOptions): Promise<Annotation[]> =>
121
+ (await this.threads.get(threadId, options)).annotations,
122
+ reply: async (
123
+ input: { threadId: SdkThreadId; commentId: string; body: string; replyId?: string },
124
+ options?: DaemonRequestOptions,
125
+ ): Promise<SdkThread> => {
126
+ const thread = await this.threads.get(input.threadId, options);
127
+ const comment = thread.annotations.find((entry) => entry.id === input.commentId);
128
+
129
+ if (!comment)
130
+ throw new DaemonClientError("not_found", "SDK comment reply target does not exist");
131
+ const rootId = comment.replyTo ?? comment.id;
132
+ const root = thread.annotations.find((entry) => entry.id === rootId);
133
+
134
+ if (!root) throw new DaemonClientError("not_found", "SDK comment reply root does not exist");
135
+
136
+ return this.client.request(
137
+ "session.comment",
138
+ {
139
+ id: input.threadId,
140
+ annotation: {
141
+ id: input.replyId ?? newAnnotationId(),
142
+ kind: "comment",
143
+ anchor: root.anchor,
144
+ target: root.target,
145
+ replyTo: root.id,
146
+ body: input.body,
147
+ },
148
+ },
149
+ SdkThreadRecordSchema,
150
+ options,
151
+ );
152
+ },
153
+ };
154
+
155
+ close(): void {
156
+ this.closed = true;
157
+ for (const stop of this.observers) stop();
158
+ this.client.close();
159
+ }
160
+ }
161
+
162
+ /** Owner methods still require daemon token authorization and the experimental agent flag. */
163
+ export class OwnerDaemonSdk extends ReviewDaemonSdk {
164
+ get threads() {
165
+ return {
166
+ ...super.threads,
167
+ create: (
168
+ input: { workspace: WorkspaceKey; artifact: Artifact },
169
+ options?: DaemonRequestOptions,
170
+ ): Promise<SdkThread> =>
171
+ this.client.request("session.create", input, SdkThreadRecordSchema, options),
172
+ };
173
+ }
174
+
175
+ readonly sessions = {
176
+ sendMessage: async (
177
+ input: {
178
+ threadId: SdkThreadId;
179
+ operationId: SdkOperationId;
180
+ outcome: MessageOutcome;
181
+ summary: string;
182
+ actionBodies?: Record<string, string>;
183
+ },
184
+ options?: DaemonRequestOptions,
185
+ ): Promise<Message> => {
186
+ const thread = await this.client.sessionSendMessage(
187
+ input.threadId,
188
+ input.outcome,
189
+ input.summary,
190
+ input.actionBodies,
191
+ input.operationId,
192
+ options,
193
+ );
194
+
195
+ if (!thread.message)
196
+ throw new DaemonTransportError(
197
+ "protocol",
198
+ "SDK accepted review message is missing",
199
+ "unknown",
200
+ "session.sendMessage",
201
+ );
202
+
203
+ return thread.message;
204
+ },
205
+ };
206
+
207
+ readonly agents = {
208
+ get: (threadId: SdkThreadId, options?: DaemonRequestOptions): Promise<ThreadAgentState> =>
209
+ this.client.request("agent.get", { id: threadId }, ThreadAgentSchema, options),
210
+ prompt: async (
211
+ input: {
212
+ threadId: SdkThreadId;
213
+ operationId: SdkOperationId;
214
+ text: string;
215
+ context?: string;
216
+ retry?: SdkSubmissionId;
217
+ },
218
+ options?: DaemonRequestOptions,
219
+ ): Promise<AgentAcceptance> => {
220
+ const state = await this.client.agentPrompt(
221
+ {
222
+ id: input.threadId,
223
+ operationId: input.operationId,
224
+ text: input.text,
225
+ context: input.context,
226
+ retry: input.retry,
227
+ },
228
+ options,
229
+ );
230
+ const receipt = state.promptOperations?.find(
231
+ (entry) => entry.operationId === input.operationId,
232
+ );
233
+
234
+ if (!receipt)
235
+ throw new DaemonTransportError(
236
+ "protocol",
237
+ "SDK accepted agent operation receipt is missing",
238
+ "unknown",
239
+ "agent.prompt",
240
+ );
241
+
242
+ return {
243
+ threadId: input.threadId,
244
+ operationId: input.operationId,
245
+ submissionIds: receipt.result.map(createSdkSubmissionId),
246
+ };
247
+ },
248
+ replyToComment: (
249
+ input: { threadId: SdkThreadId; commentId: string; body: string },
250
+ options?: DaemonRequestOptions,
251
+ ): Promise<ThreadAgentState> =>
252
+ this.client.request(
253
+ "agent.reply",
254
+ { id: input.threadId, commentId: input.commentId, body: input.body },
255
+ ThreadAgentSchema,
256
+ options,
257
+ ),
258
+ wait: (accepted: AgentAcceptance, options?: DaemonRequestOptions): Promise<AgentCompletion> =>
259
+ this.waitAgentCompletion(accepted, options),
260
+ cancel: (threadId: SdkThreadId, options?: DaemonRequestOptions): Promise<ThreadAgentState> =>
261
+ this.client.request("agent.cancel", { id: threadId }, ThreadAgentSchema, options),
262
+ };
263
+
264
+ private readonly waits = new Set<AbortController>();
265
+
266
+ override close(): void {
267
+ this.closed = true;
268
+ for (const wait of this.waits) wait.abort();
269
+ super.close();
270
+ }
271
+
272
+ private waitAgentCompletion(
273
+ accepted: AgentAcceptance,
274
+ options: DaemonRequestOptions = {},
275
+ ): Promise<AgentCompletion> {
276
+ if (this.closed)
277
+ return Promise.reject(
278
+ new DaemonTransportError("connection", "SDK connection is closed", "not_sent"),
279
+ );
280
+ if (!accepted.submissionIds.length)
281
+ return Promise.reject(
282
+ new DaemonClientError("invalid_params", "SDK wait requires accepted submissions"),
283
+ );
284
+
285
+ const controller = new AbortController();
286
+
287
+ this.waits.add(controller);
288
+ const signal = options.signal
289
+ ? AbortSignal.any([controller.signal, options.signal])
290
+ : controller.signal;
291
+
292
+ return new Promise((resolve, reject) => {
293
+ let done = false;
294
+ let detach: (() => void) | undefined;
295
+ const finish = (result?: AgentCompletion, error?: DaemonSdkError) => {
296
+ if (done) return;
297
+ done = true;
298
+ clearTimeout(timer);
299
+ signal.removeEventListener("abort", abort);
300
+ this.waits.delete(controller);
301
+ detach?.();
302
+ if (result) resolve(result);
303
+ else reject(error);
304
+ };
305
+ const abort = () =>
306
+ finish(
307
+ undefined,
308
+ new DaemonTransportError("cancelled", "SDK agent wait cancelled locally", "not_sent"),
309
+ );
310
+ const timer = setTimeout(
311
+ () =>
312
+ finish(
313
+ undefined,
314
+ new DaemonTransportError(
315
+ "timeout",
316
+ "SDK agent completion deadline elapsed",
317
+ "not_sent",
318
+ ),
319
+ ),
320
+ options.timeoutMs ?? 30_000,
321
+ );
322
+
323
+ signal.addEventListener("abort", abort, { once: true });
324
+ if (signal.aborted) {
325
+ abort();
326
+
327
+ return;
328
+ }
329
+ detach = subscribeThreadState({
330
+ connect: connectThreadObserver(this.connectionOptions),
331
+ matches: (event) =>
332
+ event.event === "agent.updated" && event.sessionId === accepted.threadId,
333
+ read: (client, readSignal) =>
334
+ client.request("agent.get", { id: accepted.threadId }, ThreadAgentSchema, {
335
+ signal: readSignal,
336
+ }),
337
+ onValue: (state) => {
338
+ const receipt = state.promptOperations?.find(
339
+ (entry) => entry.operationId === accepted.operationId,
340
+ );
341
+
342
+ if (!receipt || receipt.result.join("\n") !== accepted.submissionIds.join("\n")) {
343
+ finish(
344
+ undefined,
345
+ new DaemonClientError("not_found", "SDK accepted agent operation no longer exists"),
346
+ );
347
+
348
+ return;
349
+ }
350
+ if (receipt.outcome) finish({ ...accepted, outcome: receipt.outcome, state });
351
+ },
352
+ onError: (error) => {
353
+ if (
354
+ error instanceof DaemonClientError ||
355
+ (error instanceof DaemonTransportError && error.kind === "protocol")
356
+ )
357
+ finish(undefined, normalizeSdkError(error));
358
+ },
359
+ });
360
+ });
361
+ }
362
+
363
+ constructor(client: DaemonClient, connectionOptions: ConnectOptions) {
364
+ super(client, connectionOptions);
365
+ }
366
+ }
367
+
368
+ /** Choose capabilities at connection time; the daemon remains the authority. */
369
+ export async function connectOwnerSdk(
370
+ options: Omit<ConnectOptions, "role"> = {},
371
+ ): Promise<OwnerDaemonSdk> {
372
+ return new OwnerDaemonSdk(await DaemonClient.connect({ ...options, role: "owner" }), {
373
+ ...options,
374
+ role: "owner",
375
+ });
376
+ }
377
+
378
+ /** Collaborators and review agents expose only shared review operations. */
379
+ export async function connectReviewSdk(
380
+ options: ConnectOptions & { role: "collaborator" | "agent" },
381
+ ): Promise<ReviewDaemonSdk> {
382
+ return new ReviewDaemonSdk(await DaemonClient.connect(options), options);
383
+ }
384
+
385
+ /** Preserve typed failures; unexpected transport exceptions carry their original cause. */
386
+ // eslint-disable-next-line type-evidence/no-unknown-parameters -- Promise rejection is an untyped transport boundary.
387
+ export function normalizeSdkError(error: unknown): DaemonSdkError {
388
+ if (error instanceof DaemonClientError || error instanceof DaemonTransportError) return error;
389
+
390
+ return new DaemonTransportError(
391
+ "connection",
392
+ "SDK daemon operation failed",
393
+ "unknown",
394
+ undefined,
395
+ { cause: error },
396
+ );
397
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Version handshake: a client must never keep talking to a daemon from an
3
+ * earlier build (the stale-daemon-after-upgrade bug). The ping carries the
4
+ * daemon's version; a mismatch without autostart is a clear error, and a
5
+ * matching version connects normally.
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { DaemonServer } from "./server";
13
+ import { DaemonClient, DaemonClientError, daemonSpawnCommand } from "./client";
14
+ import { DAEMON_VERSION } from "./version";
15
+ import { pidPath } from "./paths";
16
+ import type { Artifact, WorkspaceKey } from "@cueloop/schema";
17
+
18
+ const WS: WorkspaceKey = { repoRoot: "/repo", branch: "main" };
19
+ const PLAN: Artifact = { type: "plan", content: "# P\n\nBody.\n", meta: {} };
20
+
21
+ let home: string;
22
+ const servers: DaemonServer[] = [];
23
+
24
+ beforeEach(() => {
25
+ home = mkdtempSync(join(tmpdir(), "cueloop-version-"));
26
+ });
27
+ afterEach(() => {
28
+ for (const runningServer of servers.splice(0)) runningServer.stop();
29
+ rmSync(home, { recursive: true, force: true });
30
+ });
31
+
32
+ function server(version?: string): DaemonServer {
33
+ const daemonServer = new DaemonServer({
34
+ home,
35
+ idleExitMs: 0,
36
+ version,
37
+ onIdleExit: () => {},
38
+ });
39
+
40
+ servers.push(daemonServer);
41
+
42
+ return daemonServer;
43
+ }
44
+
45
+ describe("daemon version handshake", () => {
46
+ test("a stale daemon is rejected when there is nothing to replace it", async () => {
47
+ // Arrange - a daemon posing as an earlier build
48
+ server("0.0.0-stale").start();
49
+
50
+ // Act + Assert - without autostart the client cannot restart it, so it says why
51
+ await expect(DaemonClient.connect({ home, autostart: false })).rejects.toMatchObject({
52
+ code: "version_mismatch",
53
+ });
54
+ await expect(DaemonClient.connect({ home, autostart: false })).rejects.toBeInstanceOf(
55
+ DaemonClientError,
56
+ );
57
+ });
58
+
59
+ test("autostart never replaces a live daemon from another version", async () => {
60
+ const liveServer = server("0.1.0-alpha.81");
61
+
62
+ liveServer.start();
63
+ const session = liveServer.core.sessionCreate({ workspace: WS, artifact: PLAN });
64
+ let replacement: DaemonClient | undefined;
65
+ let connectionError: unknown;
66
+
67
+ try {
68
+ replacement = await DaemonClient.connect({ home, autostart: true });
69
+ } catch (error) {
70
+ connectionError = error;
71
+ }
72
+
73
+ replacement?.close();
74
+ expect(connectionError).toBeInstanceOf(DaemonClientError);
75
+ expect(connectionError).toMatchObject({ code: "version_mismatch" });
76
+ expect(readFileSync(pidPath(home), "utf8")).toBe(String(process.pid));
77
+ expect(liveServer.core.sessionGet(session.id).id).toBe(session.id);
78
+ });
79
+
80
+ test("a rejected client releases its connection so the daemon can become idle", async () => {
81
+ let idleExited = false;
82
+ const liveServer = new DaemonServer({
83
+ home,
84
+ idleExitMs: 50,
85
+ version: "0.1.0-alpha.81",
86
+ onIdleExit: () => {
87
+ idleExited = true;
88
+ },
89
+ });
90
+
91
+ servers.push(liveServer);
92
+ liveServer.start();
93
+
94
+ await expect(DaemonClient.connect({ home, autostart: true })).rejects.toMatchObject({
95
+ code: "version_mismatch",
96
+ });
97
+ await Bun.sleep(100);
98
+
99
+ expect(idleExited).toBe(true);
100
+ });
101
+
102
+ test("a current daemon connects and serves normally", async () => {
103
+ // Arrange - a daemon on this build
104
+ server(DAEMON_VERSION).start();
105
+ const client = await DaemonClient.connect({ home, autostart: false });
106
+
107
+ // Act - a round-trip proves the connection is live, not replaced
108
+ const created = await client.sessionCreate(WS, PLAN);
109
+ const listed = await client.sessionList();
110
+
111
+ // Assert
112
+ expect(listed.map((session) => session.id)).toContain(created.id);
113
+ client.close();
114
+ });
115
+ });
116
+
117
+ describe("daemon spawn command", () => {
118
+ const SOURCE_URL = "file:///repo/packages/daemon/src/client.ts";
119
+
120
+ test("from source it bun-runs main.ts", () => {
121
+ expect(daemonSpawnCommand("/bin/bun", SOURCE_URL, false)).toEqual([
122
+ "/bin/bun",
123
+ "run",
124
+ "/repo/packages/daemon/src/main.ts",
125
+ ]);
126
+ });
127
+
128
+ test("dev-watch reloads the daemon on source edits", () => {
129
+ expect(daemonSpawnCommand("/bin/bun", SOURCE_URL, true)).toEqual([
130
+ "/bin/bun",
131
+ "--watch",
132
+ "run",
133
+ "/repo/packages/daemon/src/main.ts",
134
+ ]);
135
+ });
136
+
137
+ test("a compiled binary re-execs its own daemon command, never --watch", () => {
138
+ expect(
139
+ daemonSpawnCommand("/usr/local/bin/cueloop", "file:///$bunfs/root/cueloop", true),
140
+ ).toEqual(["/usr/local/bin/cueloop", "daemon", "--autostart"]);
141
+ });
142
+ });