@rallycry/conveyor-agent 10.13.71 → 11.0.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.
@@ -0,0 +1,697 @@
1
+ import {
2
+ awaitGitReady,
3
+ buildSessionPreviewPorts,
4
+ loadForwardPorts,
5
+ readWorkspaceBytes,
6
+ statWorkspacePath,
7
+ workspacePathExists
8
+ } from "./chunk-N4WSUTGV.js";
9
+ import {
10
+ reportBootMilestone
11
+ } from "./chunk-GL2DIQEQ.js";
12
+ import {
13
+ getWorkbenchClient
14
+ } from "./chunk-EXQ6AHOY.js";
15
+ import {
16
+ workbenchEnabled
17
+ } from "./chunk-KMB3BU4S.js";
18
+ import {
19
+ runSetupCommand,
20
+ runStartCommand,
21
+ terminateProcessGroup
22
+ } from "./chunk-W4LZ7R6Z.js";
23
+
24
+ // src/setup/db-schema-sync.ts
25
+ import { basename, dirname, join } from "path";
26
+ var PRISMA_SCHEMA_CANDIDATES = [
27
+ "packages/db/prisma/schema.prisma",
28
+ "prisma/schema.prisma",
29
+ "apps/api/prisma/schema.prisma",
30
+ "packages/db/schema.prisma"
31
+ ];
32
+ var PRISMA_DB_PUSH = "bunx prisma db push";
33
+ var SCHEMA_SYNC_TIMEOUT_MS = 18e4;
34
+ function depsInclude(rawDeps, dep) {
35
+ return (rawDeps ?? "").split(",").map((entry) => entry.trim()).includes(dep);
36
+ }
37
+ function isDisabled(env) {
38
+ const flag = env.CONVEYOR_DB_SCHEMA_SYNC?.trim().toLowerCase();
39
+ return flag === "0" || flag === "false";
40
+ }
41
+ function packageRootForSchema(schemaPath) {
42
+ const dir = dirname(schemaPath);
43
+ return basename(dir) === "prisma" ? dirname(dir) : dir;
44
+ }
45
+ async function resolveSchemaSyncPlan(options) {
46
+ const { workspaceDir, env } = options;
47
+ const pathExists = options.pathExists ?? workspacePathExists;
48
+ if (isDisabled(env)) return null;
49
+ const override = env.CONVEYOR_DB_SCHEMA_SYNC_COMMAND?.trim();
50
+ if (override) return { command: override, cwd: workspaceDir, source: "override" };
51
+ if (!depsInclude(env.CONVEYOR_DEPS, "postgresql")) return null;
52
+ if (!env.DATABASE_URL?.trim()) return null;
53
+ for (const relative of PRISMA_SCHEMA_CANDIDATES) {
54
+ const schemaPath = join(workspaceDir, relative);
55
+ if (await pathExists(schemaPath)) {
56
+ return { command: PRISMA_DB_PUSH, cwd: packageRootForSchema(schemaPath), source: "prisma" };
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ async function runDbSchemaSync(options) {
62
+ let plan = null;
63
+ try {
64
+ plan = await resolveSchemaSyncPlan(options);
65
+ } catch {
66
+ return "skipped";
67
+ }
68
+ if (!plan) return "skipped";
69
+ const { onOutput, runCommand } = options;
70
+ onOutput("stdout", `[db] syncing schema (${plan.source}): ${plan.command}
71
+ `);
72
+ const controller = new AbortController();
73
+ const abort = () => controller.abort();
74
+ options.signal?.addEventListener("abort", abort, { once: true });
75
+ const timer = setTimeout(abort, options.timeoutMs ?? SCHEMA_SYNC_TIMEOUT_MS);
76
+ timer.unref();
77
+ try {
78
+ await runCommand(plan.command, plan.cwd, onOutput, controller.signal);
79
+ onOutput("stdout", "[db] schema in sync\n");
80
+ return "synced";
81
+ } catch (error) {
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ onOutput("stderr", `[db] schema sync failed: ${message}
84
+ `);
85
+ return "failed";
86
+ } finally {
87
+ clearTimeout(timer);
88
+ options.signal?.removeEventListener("abort", abort);
89
+ }
90
+ }
91
+
92
+ // src/setup/deps-sync.ts
93
+ import { join as join2 } from "path";
94
+ var INSTALLER_SCRIPT_CANDIDATES = ["scripts/install-deps.sh"];
95
+ var BUN_INSTALL = "bun install --frozen-lockfile";
96
+ var DEPS_SYNC_TIMEOUT_MS = 9e5;
97
+ var REGISTRY_KEY_ENV = "NPM_CACHE_READER_KEY";
98
+ function isDisabled2(env) {
99
+ const flag = env.CONVEYOR_DEPS_SYNC?.trim().toLowerCase();
100
+ return flag === "0" || flag === "false";
101
+ }
102
+ var CKSUM_POLYNOMIAL = 79764919;
103
+ var CKSUM_TABLE = (() => {
104
+ const table = new Uint32Array(256);
105
+ for (let index = 0; index < 256; index++) {
106
+ let value = index << 24;
107
+ for (let bit = 0; bit < 8; bit++) {
108
+ value = value & 2147483648 ? (value << 1 ^ CKSUM_POLYNOMIAL) >>> 0 : value << 1 >>> 0;
109
+ }
110
+ table[index] = value >>> 0;
111
+ }
112
+ return table;
113
+ })();
114
+ function posixCksum(bytes) {
115
+ let crc = 0;
116
+ for (const byte of bytes) {
117
+ crc = (crc << 8 ^ CKSUM_TABLE[(crc >>> 24 ^ byte) & 255]) >>> 0;
118
+ }
119
+ for (let length = bytes.length; length > 0; length >>>= 8) {
120
+ crc = (crc << 8 ^ CKSUM_TABLE[(crc >>> 24 ^ length & 255) & 255]) >>> 0;
121
+ }
122
+ return ~crc >>> 0;
123
+ }
124
+ function bunLockFingerprint(bytes) {
125
+ return `${posixCksum(bytes)}:${bytes.length}`;
126
+ }
127
+ async function defaultDirExists(path) {
128
+ return (await statWorkspacePath(path)).isDirectory;
129
+ }
130
+ async function readLockDrift(options) {
131
+ const { workspaceDir, readBytes } = options;
132
+ let baked;
133
+ try {
134
+ const raw = await readBytes(join2(workspaceDir, ".conveyor/prebake-cache.json"));
135
+ const parsed = JSON.parse(Buffer.from(raw).toString("utf8"));
136
+ const value = parsed?.bunLockCrc;
137
+ if (typeof value !== "string" || !value) return null;
138
+ baked = value;
139
+ } catch {
140
+ return null;
141
+ }
142
+ try {
143
+ const actual = bunLockFingerprint(await readBytes(join2(workspaceDir, "bun.lock")));
144
+ return actual === baked ? null : { baked, actual };
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+ async function resolveDepsSyncPlan(options) {
150
+ const { workspaceDir, env } = options;
151
+ const pathExists = options.pathExists ?? workspacePathExists;
152
+ const dirExists = options.dirExists ?? defaultDirExists;
153
+ const readBytes = options.readBytes ?? readWorkspaceBytes;
154
+ if (isDisabled2(env)) return null;
155
+ if (!await pathExists(join2(workspaceDir, "package.json"))) return null;
156
+ const override = env.CONVEYOR_DEPS_SYNC_COMMAND?.trim();
157
+ const resolveCommand = async () => {
158
+ if (override) return override;
159
+ for (const relative of INSTALLER_SCRIPT_CANDIDATES) {
160
+ if (await pathExists(join2(workspaceDir, relative))) return `bash ${relative}`;
161
+ }
162
+ return BUN_INSTALL;
163
+ };
164
+ if (!await dirExists(join2(workspaceDir, "node_modules"))) {
165
+ return {
166
+ command: await resolveCommand(),
167
+ cwd: workspaceDir,
168
+ reason: "missing_node_modules",
169
+ detail: "the pod booted with no node_modules \u2014 the baked dependency tree is absent"
170
+ };
171
+ }
172
+ const drift = await readLockDrift({ workspaceDir, readBytes });
173
+ if (drift) {
174
+ return {
175
+ command: await resolveCommand(),
176
+ cwd: workspaceDir,
177
+ reason: "lockfile_drift",
178
+ detail: `bun.lock changed since the image bake (baked ${drift.baked}, checked out ${drift.actual})`
179
+ };
180
+ }
181
+ return null;
182
+ }
183
+ function describeFailure(plan, error, env) {
184
+ const cause = error instanceof Error ? error.message : String(error);
185
+ const parts = [
186
+ `Dependency install failed (${plan.reason}): ${plan.detail}.`,
187
+ `Ran \`${plan.command}\` in ${plan.cwd} \u2014 ${cause}.`
188
+ ];
189
+ if (!env[REGISTRY_KEY_ENV]?.trim()) {
190
+ parts.push(
191
+ `${REGISTRY_KEY_ENV} is not set on this pod, so packages on the private registry cache cannot be fetched \u2014 set it as a project secret.`
192
+ );
193
+ }
194
+ return parts.join(" ");
195
+ }
196
+ async function runDepsSync(options) {
197
+ let plan = null;
198
+ try {
199
+ plan = await resolveDepsSyncPlan(options);
200
+ } catch {
201
+ return { outcome: "skipped" };
202
+ }
203
+ if (!plan) return { outcome: "skipped" };
204
+ const { onOutput, runCommand, env } = options;
205
+ onOutput("stdout", `[deps] ${plan.detail}
206
+ `);
207
+ onOutput("stdout", `[deps] installing (${plan.reason}): ${plan.command}
208
+ `);
209
+ const controller = new AbortController();
210
+ const abort = () => controller.abort();
211
+ options.signal?.addEventListener("abort", abort, { once: true });
212
+ const timer = setTimeout(abort, options.timeoutMs ?? DEPS_SYNC_TIMEOUT_MS);
213
+ timer.unref();
214
+ try {
215
+ await runCommand(plan.command, plan.cwd, onOutput, controller.signal);
216
+ onOutput("stdout", "[deps] dependencies installed\n");
217
+ return { outcome: "installed" };
218
+ } catch (error) {
219
+ const message = describeFailure(plan, error, env);
220
+ onOutput("stderr", `[deps] ${message}
221
+ `);
222
+ return { outcome: "failed", message };
223
+ } finally {
224
+ clearTimeout(timer);
225
+ options.signal?.removeEventListener("abort", abort);
226
+ }
227
+ }
228
+
229
+ // src/setup/sidecars.ts
230
+ import net from "net";
231
+ var POSTGRES_TIMEOUT_MS = 12e4;
232
+ var FIREBASE_TIMEOUT_MS = 6e4;
233
+ var FALLBACK_TIMEOUT_MS = 3e4;
234
+ var DEFAULT_SIDECAR_POLL_INTERVAL_MS = 1e3;
235
+ var DEFAULT_PROBE_TIMEOUT_MS = 2e3;
236
+ var POSTGRES_DEFAULT_PORT = 5432;
237
+ var FIREBASE_DEFAULT_PORT = 9099;
238
+ function parseHostPort(value, defaultPort) {
239
+ const trimmed = value.trim().replace(/^[a-z]+:\/\//i, "");
240
+ if (!trimmed) return null;
241
+ const idx = trimmed.lastIndexOf(":");
242
+ if (idx === -1) {
243
+ return { host: trimmed, port: defaultPort };
244
+ }
245
+ const host = trimmed.slice(0, idx) || "localhost";
246
+ const port = Number(trimmed.slice(idx + 1));
247
+ return { host, port: Number.isFinite(port) ? port : defaultPort };
248
+ }
249
+ function resolveSidecarTargets(env = process.env) {
250
+ const targets = [];
251
+ const databaseUrl = env.DATABASE_URL;
252
+ if (databaseUrl) {
253
+ try {
254
+ const url = new URL(databaseUrl);
255
+ const host = url.hostname || "localhost";
256
+ const port = url.port ? Number(url.port) : POSTGRES_DEFAULT_PORT;
257
+ if (Number.isFinite(port)) {
258
+ targets.push({ name: "postgres", host, port, timeoutMs: POSTGRES_TIMEOUT_MS });
259
+ }
260
+ } catch {
261
+ }
262
+ }
263
+ const firebaseHost = env.FIREBASE_AUTH_EMULATOR_HOST;
264
+ if (firebaseHost) {
265
+ const parsed = parseHostPort(firebaseHost, FIREBASE_DEFAULT_PORT);
266
+ if (parsed) {
267
+ targets.push({
268
+ name: "firebase auth emulator",
269
+ host: parsed.host,
270
+ port: parsed.port,
271
+ timeoutMs: FIREBASE_TIMEOUT_MS
272
+ });
273
+ }
274
+ }
275
+ return targets;
276
+ }
277
+ function abortError() {
278
+ const error = new Error("Operation aborted");
279
+ error.name = "AbortError";
280
+ return error;
281
+ }
282
+ function throwIfAborted(signal) {
283
+ if (signal?.aborted) throw abortError();
284
+ }
285
+ function defaultProbe(target, signal) {
286
+ throwIfAborted(signal);
287
+ return new Promise((resolve) => {
288
+ let settled = false;
289
+ const socket = net.createConnection({ host: target.host, port: target.port });
290
+ const done = (ok) => {
291
+ if (settled) return;
292
+ settled = true;
293
+ socket.destroy();
294
+ signal?.removeEventListener("abort", onAbort);
295
+ resolve(ok);
296
+ };
297
+ const onAbort = () => done(false);
298
+ socket.once("connect", () => done(true));
299
+ socket.once("error", () => done(false));
300
+ socket.setTimeout(DEFAULT_PROBE_TIMEOUT_MS, () => done(false));
301
+ signal?.addEventListener("abort", onAbort, { once: true });
302
+ });
303
+ }
304
+ var delay = (ms, signal) => {
305
+ throwIfAborted(signal);
306
+ return new Promise((resolve, reject) => {
307
+ const timer = setTimeout(() => {
308
+ signal?.removeEventListener("abort", onAbort);
309
+ resolve();
310
+ }, ms);
311
+ const onAbort = () => {
312
+ clearTimeout(timer);
313
+ reject(abortError());
314
+ };
315
+ signal?.addEventListener("abort", onAbort, { once: true });
316
+ });
317
+ };
318
+ function raceWithAbort(promise, signal) {
319
+ if (!signal) return promise;
320
+ throwIfAborted(signal);
321
+ return new Promise((resolve, reject) => {
322
+ const onAbort = () => reject(abortError());
323
+ signal.addEventListener("abort", onAbort, { once: true });
324
+ void promise.then(
325
+ (value) => {
326
+ signal.removeEventListener("abort", onAbort);
327
+ resolve(value);
328
+ },
329
+ (error) => {
330
+ signal.removeEventListener("abort", onAbort);
331
+ reject(error);
332
+ }
333
+ );
334
+ });
335
+ }
336
+ async function waitForTarget(target, opts) {
337
+ const { onLog, pollIntervalMs, probe, signal } = opts;
338
+ const timeoutMs = opts.timeoutMs ?? target.timeoutMs ?? FALLBACK_TIMEOUT_MS;
339
+ const deadline = Date.now() + timeoutMs;
340
+ onLog(`Waiting for ${target.name} on ${target.host}:${target.port}...`);
341
+ while (true) {
342
+ throwIfAborted(signal);
343
+ if (await raceWithAbort(probe(target, signal), signal)) {
344
+ onLog(`${target.name} is ready`);
345
+ return;
346
+ }
347
+ if (Date.now() >= deadline) {
348
+ onLog(
349
+ `WARNING: ${target.name} not ready after ${Math.round(timeoutMs / 1e3)}s, continuing anyway`
350
+ );
351
+ return;
352
+ }
353
+ await delay(pollIntervalMs, signal);
354
+ }
355
+ }
356
+ async function waitForSidecars(opts = {}) {
357
+ const {
358
+ env = process.env,
359
+ onLog = () => {
360
+ },
361
+ timeoutMs,
362
+ pollIntervalMs = DEFAULT_SIDECAR_POLL_INTERVAL_MS,
363
+ probe = defaultProbe,
364
+ signal
365
+ } = opts;
366
+ throwIfAborted(signal);
367
+ const targets = resolveSidecarTargets(env);
368
+ if (targets.length === 0) return;
369
+ await Promise.all(
370
+ targets.map(
371
+ (target) => waitForTarget(target, { onLog, timeoutMs, pollIntervalMs, probe, signal })
372
+ )
373
+ );
374
+ }
375
+
376
+ // src/setup/workspace-command-supervisor.ts
377
+ function defaultCommandExecutors() {
378
+ if (workbenchEnabled()) {
379
+ const client = getWorkbenchClient();
380
+ return {
381
+ runStartCommand: (cmd, cwd, onOutput) => client.runStartCommand(cmd, cwd, onOutput),
382
+ runShellCommand: (cmd, cwd, onOutput, signal) => client.runSetupCommand(cmd, cwd, onOutput, signal)
383
+ };
384
+ }
385
+ return { runStartCommand, runShellCommand: runSetupCommand };
386
+ }
387
+ var defaultWriteOutput = (stream, data) => {
388
+ (stream === "stderr" ? process.stderr : process.stdout).write(data);
389
+ };
390
+ function stopWorkspaceCommands(supervisor) {
391
+ return supervisor?.stop() ?? Promise.resolve();
392
+ }
393
+ async function startWorkspaceCommandsAfterConnect(options) {
394
+ const connected = await options.connect();
395
+ if (!connected || options.isShuttingDown()) return null;
396
+ const supervisor = options.create();
397
+ supervisor.start();
398
+ return supervisor;
399
+ }
400
+ var WorkspaceCommandSupervisor = class {
401
+ abortController = new AbortController();
402
+ expectedStartCommandStops = /* @__PURE__ */ new WeakSet();
403
+ config;
404
+ workspaceDir;
405
+ connection;
406
+ env;
407
+ awaitGitReadyFn;
408
+ waitForSidecarsFn;
409
+ runStartCommandFn;
410
+ syncDbSchemaFn;
411
+ syncDepsFn;
412
+ loadForwardPortsFn;
413
+ writeOutput;
414
+ terminateStartCommand;
415
+ reportBootMilestoneFn;
416
+ startCommandChild = null;
417
+ liveStartCommandChildren = /* @__PURE__ */ new Set();
418
+ startCommandTerminations = /* @__PURE__ */ new WeakMap();
419
+ startCommandEndPromises = /* @__PURE__ */ new WeakMap();
420
+ resolveStartCommandEnd = /* @__PURE__ */ new WeakMap();
421
+ backgroundTasks = /* @__PURE__ */ new Set();
422
+ startCommandQueue = Promise.resolve();
423
+ shutdownPromise = null;
424
+ startCommandLaunchRequested = false;
425
+ started = false;
426
+ stopped = false;
427
+ /** Resolved by notifyLoopReady() once the PTY is live — the runner fires it
428
+ * on the first harness event of the initial query (or when a prefill
429
+ * parks), NOT after the first turn completes, so the app's start command
430
+ * isn't held behind a long working turn. Also resolved by stop() if
431
+ * shutdown arrives first — either way unblocks the awaiter in
432
+ * runSetupAndStart so a supervisor stopped before the loop ever signals
433
+ * doesn't hang its background task (and therefore stop()) forever. */
434
+ resolveLoopReady;
435
+ loopReady = new Promise((resolve) => {
436
+ this.resolveLoopReady = resolve;
437
+ });
438
+ constructor(options) {
439
+ this.config = options.config;
440
+ this.workspaceDir = options.workspaceDir;
441
+ this.connection = options.connection;
442
+ this.env = options.env ?? process.env;
443
+ this.awaitGitReadyFn = options.awaitGitReady ?? ((opts) => awaitGitReady({
444
+ onLog: opts.onLog,
445
+ signal: opts.signal
446
+ }));
447
+ this.waitForSidecarsFn = options.waitForSidecars ?? ((opts) => waitForSidecars({ onLog: opts.onLog, signal: opts.signal }));
448
+ const executors = defaultCommandExecutors();
449
+ this.runStartCommandFn = options.runStartCommand ?? executors.runStartCommand;
450
+ this.syncDbSchemaFn = options.syncDbSchema ?? ((opts) => runDbSchemaSync({
451
+ workspaceDir: this.workspaceDir,
452
+ env: this.env,
453
+ runCommand: executors.runShellCommand,
454
+ onOutput: opts.onOutput,
455
+ signal: opts.signal
456
+ }));
457
+ this.syncDepsFn = options.syncDeps ?? ((opts) => runDepsSync({
458
+ workspaceDir: this.workspaceDir,
459
+ env: this.env,
460
+ runCommand: executors.runShellCommand,
461
+ onOutput: opts.onOutput,
462
+ signal: opts.signal
463
+ }));
464
+ this.loadForwardPortsFn = options.loadForwardPorts ?? loadForwardPorts;
465
+ this.writeOutput = options.writeOutput ?? defaultWriteOutput;
466
+ this.terminateStartCommand = options.terminateStartCommand ?? terminateProcessGroup;
467
+ this.reportBootMilestoneFn = options.reportBootMilestone ?? ((key) => {
468
+ void reportBootMilestone({ key, env: this.env });
469
+ });
470
+ }
471
+ start() {
472
+ if (this.started || this.stopped) return;
473
+ this.started = true;
474
+ this.connection.onRunStartCommand(() => this.restartStartCommand());
475
+ if (this.config) {
476
+ this.trackBackgroundTask(
477
+ this.runSetupAndStart().catch((error) => this.reportUnexpectedError(error))
478
+ );
479
+ }
480
+ }
481
+ /** Release the start-command launch — called once the runner's core loop
482
+ * (the PTY) is live. Idempotent; resolving an already-settled promise is a
483
+ * no-op, so a late/duplicate call (or one after stop() already resolved
484
+ * it) is harmless. */
485
+ notifyLoopReady() {
486
+ this.resolveLoopReady();
487
+ }
488
+ stop() {
489
+ if (this.shutdownPromise) return this.shutdownPromise;
490
+ this.stopped = true;
491
+ this.resolveLoopReady();
492
+ this.abortController.abort();
493
+ const backgroundTasks = [...this.backgroundTasks];
494
+ const termination = this.terminateAllStartCommands();
495
+ this.shutdownPromise = (async () => {
496
+ await Promise.allSettled([termination, this.startCommandQueue, ...backgroundTasks]);
497
+ await this.terminateAllStartCommands();
498
+ })();
499
+ return this.shutdownPromise;
500
+ }
501
+ async runSetupAndStart() {
502
+ const gitState = await this.awaitGitReadyFn({
503
+ onLog: (message) => this.forwardSetupOutput("stdout", `[git] ${message}
504
+ `),
505
+ signal: this.abortController.signal
506
+ });
507
+ if (this.stopped) return;
508
+ if (gitState === "failed" || gitState === "timeout") {
509
+ this.connection.sendEvent({
510
+ type: "setup_error",
511
+ message: "Workspace not ready \u2014 skipping setup/start"
512
+ });
513
+ return;
514
+ }
515
+ await this.waitForSidecarsFn({
516
+ onLog: (message) => this.forwardSetupOutput("stdout", `[sidecars] ${message}
517
+ `),
518
+ signal: this.abortController.signal
519
+ });
520
+ if (this.stopped) return;
521
+ this.reportBootMilestoneFn("sidecars_ready");
522
+ await this.loopReady;
523
+ if (this.stopped) return;
524
+ await this.syncDeps();
525
+ if (this.stopped) return;
526
+ await this.syncDbSchema();
527
+ if (this.stopped) return;
528
+ const startCommandRunning = this.config?.startCommand ? await this.ensureStartCommandLaunched(this.config.startCommand) : false;
529
+ if (this.stopped) return;
530
+ this.reportBootMilestoneFn("start_command_launched");
531
+ const forwardPorts = await this.loadForwardPortsFn(this.workspaceDir);
532
+ if (this.stopped) return;
533
+ const previewPorts = buildSessionPreviewPorts(forwardPorts);
534
+ this.connection.sendEvent({
535
+ type: "setup_complete",
536
+ startCommandRunning,
537
+ // Reported separately so the server can tell "nothing to run" apart from
538
+ // "the dev server failed to launch" — both report startCommandRunning:false.
539
+ startCommandConfigured: Boolean(this.config?.startCommand),
540
+ ...previewPorts.length > 0 ? { previewPorts } : {}
541
+ });
542
+ }
543
+ /** Run the dependency sync, forwarding its output as setup output so it lands
544
+ * in the card's setup log. A failure is announced with its own cause rather
545
+ * than thrown: the start command still launches, but the card now carries
546
+ * the reason instead of a generic `start command exited with code 1`. */
547
+ async syncDeps() {
548
+ const result = await this.syncDepsFn({
549
+ onOutput: (stream, data) => this.forwardSetupOutput(stream, data),
550
+ signal: this.abortController.signal
551
+ });
552
+ if (result.outcome !== "failed" || this.stopped) return;
553
+ this.connection.sendEvent({
554
+ type: "setup_error",
555
+ message: result.message ?? "Dependency install failed \u2014 the start command will fail until dependencies are installed (see pod logs)"
556
+ });
557
+ }
558
+ /** Run the schema sync, forwarding its output as setup output so it lands in
559
+ * the card's setup log. A failure is announced rather than thrown: it is the
560
+ * exact condition that used to render as a silent "Displaying 0 results". */
561
+ async syncDbSchema() {
562
+ const outcome = await this.syncDbSchemaFn({
563
+ onOutput: (stream, data) => this.forwardSetupOutput(stream, data),
564
+ signal: this.abortController.signal
565
+ });
566
+ if (outcome !== "failed" || this.stopped) return;
567
+ this.connection.sendEvent({
568
+ type: "setup_error",
569
+ message: "Database schema sync failed \u2014 the app may error on queries touching recently added columns (see pod logs)"
570
+ });
571
+ }
572
+ restartStartCommand() {
573
+ if (this.stopped || !this.config?.startCommand) return;
574
+ this.connection.sendEvent({
575
+ type: "start_command_output",
576
+ stream: "stdout",
577
+ data: "[conveyor-agent] Restarting start command...\n"
578
+ });
579
+ void this.enqueueStartCommandReplacement(this.config.startCommand);
580
+ }
581
+ async ensureStartCommandLaunched(command) {
582
+ if (!this.startCommandLaunchRequested) {
583
+ return this.enqueueStartCommandReplacement(command);
584
+ }
585
+ await this.startCommandQueue;
586
+ return !this.stopped && this.liveStartCommandChildren.size > 0;
587
+ }
588
+ enqueueStartCommandReplacement(command) {
589
+ this.startCommandLaunchRequested = true;
590
+ const operation = this.startCommandQueue.then(async () => {
591
+ if (this.stopped) return false;
592
+ await this.terminateAllStartCommands();
593
+ if (this.stopped) return false;
594
+ return this.launchStartCommand(command);
595
+ });
596
+ this.startCommandQueue = operation.then(
597
+ () => void 0,
598
+ () => void 0
599
+ );
600
+ return operation;
601
+ }
602
+ launchStartCommand(command) {
603
+ if (this.stopped) return false;
604
+ this.connection.sendEvent({ type: "start_command_started" });
605
+ try {
606
+ const child = this.runStartCommandFn(command, this.workspaceDir, (stream, data) => {
607
+ if (this.stopped) return;
608
+ this.connection.sendEvent({ type: "start_command_output", stream, data });
609
+ this.writeOutput(stream, data);
610
+ });
611
+ this.liveStartCommandChildren.add(child);
612
+ const ended = new Promise((resolve) => {
613
+ this.resolveStartCommandEnd.set(child, resolve);
614
+ });
615
+ this.startCommandEndPromises.set(child, ended);
616
+ if (this.stopped) {
617
+ void this.terminateStartCommandChild(child);
618
+ return false;
619
+ }
620
+ this.startCommandChild = child;
621
+ child.on("exit", (code, signal) => {
622
+ if (this.startCommandChild === child) this.startCommandChild = null;
623
+ this.liveStartCommandChildren.delete(child);
624
+ this.settleStartCommandEnd(child);
625
+ if (this.stopped || this.expectedStartCommandStops.has(child)) return;
626
+ const message = `start command exited${code === null ? "" : ` with code ${code}`}${signal ? ` (signal: ${signal})` : ""}`;
627
+ this.connection.sendEvent({ type: "start_command_exited", code, signal, message });
628
+ if (code !== null && code !== 0) {
629
+ this.connection.sendEvent({
630
+ type: "start_command_error",
631
+ message: `start command exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`
632
+ });
633
+ }
634
+ });
635
+ child.on("error", (error) => {
636
+ if (child.pid === void 0) {
637
+ if (this.startCommandChild === child) this.startCommandChild = null;
638
+ this.liveStartCommandChildren.delete(child);
639
+ this.settleStartCommandEnd(child);
640
+ this.startCommandTerminations.delete(child);
641
+ }
642
+ if (this.stopped) return;
643
+ this.connection.sendEvent({ type: "start_command_error", message: error.message });
644
+ });
645
+ return true;
646
+ } catch (error) {
647
+ if (!this.stopped) {
648
+ this.connection.sendEvent({
649
+ type: "start_command_error",
650
+ message: error instanceof Error ? error.message : String(error)
651
+ });
652
+ }
653
+ return false;
654
+ }
655
+ }
656
+ terminateStartCommandChild(child) {
657
+ const existing = this.startCommandTerminations.get(child);
658
+ if (existing) return existing;
659
+ this.expectedStartCommandStops.add(child);
660
+ const processTermination = Promise.resolve(this.terminateStartCommand(child));
661
+ const ended = this.startCommandEndPromises.get(child);
662
+ const termination = (ended ? Promise.race([processTermination, ended]) : processTermination).then(() => void 0).finally(() => this.startCommandTerminations.delete(child));
663
+ this.startCommandTerminations.set(child, termination);
664
+ return termination;
665
+ }
666
+ async terminateAllStartCommands() {
667
+ const live = [...this.liveStartCommandChildren].filter((child) => child.exitCode === null);
668
+ await Promise.all(live.map((child) => this.terminateStartCommandChild(child)));
669
+ }
670
+ settleStartCommandEnd(child) {
671
+ this.resolveStartCommandEnd.get(child)?.();
672
+ this.resolveStartCommandEnd.delete(child);
673
+ this.startCommandEndPromises.delete(child);
674
+ }
675
+ trackBackgroundTask(task) {
676
+ this.backgroundTasks.add(task);
677
+ void task.finally(() => this.backgroundTasks.delete(task));
678
+ }
679
+ forwardSetupOutput(stream, data) {
680
+ if (this.stopped) return;
681
+ this.connection.sendEvent({ type: "setup_output", stream, data });
682
+ this.writeOutput(stream, data);
683
+ }
684
+ reportUnexpectedError(error) {
685
+ if (this.stopped) return;
686
+ this.connection.sendEvent({
687
+ type: "setup_error",
688
+ message: error instanceof Error ? error.message : String(error)
689
+ });
690
+ }
691
+ };
692
+
693
+ export {
694
+ stopWorkspaceCommands,
695
+ startWorkspaceCommandsAfterConnect,
696
+ WorkspaceCommandSupervisor
697
+ };