replicas-engine 0.1.229 → 0.1.231

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 (2) hide show
  1. package/dist/src/index.js +281 -88
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -1652,9 +1652,26 @@ function parseGoalCommand(message) {
1652
1652
  // ../shared/src/replicas-config.ts
1653
1653
  import { parse as parseYaml } from "yaml";
1654
1654
 
1655
+ // ../shared/src/hook-config.ts
1656
+ function resolveHookConfig(value) {
1657
+ if (value === void 0) {
1658
+ return null;
1659
+ }
1660
+ const commands = typeof value === "string" ? [value.trim()].filter(Boolean) : value.commands.map((command) => command.trim()).filter(Boolean);
1661
+ if (commands.length === 0) {
1662
+ return null;
1663
+ }
1664
+ return {
1665
+ commands,
1666
+ timeoutMs: typeof value === "string" ? void 0 : value.timeout,
1667
+ separate: typeof value === "string" ? void 0 : value.separate
1668
+ };
1669
+ }
1670
+
1655
1671
  // ../shared/src/warm-hooks.ts
1656
- var DEFAULT_WARM_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
1657
- var MAX_WARM_HOOK_TIMEOUT_MS = 15 * 60 * 1e3;
1672
+ var DEFAULT_WARM_HOOK_TIMEOUT_MS = 60 * 60 * 1e3;
1673
+ var MAX_WARM_HOOK_TIMEOUT_MS = 120 * 60 * 1e3;
1674
+ var DEFAULT_START_HOOK_TIMEOUT_MS = 5 * 60 * 1e3;
1658
1675
  var DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS = 1e5;
1659
1676
  var HOOK_EXEC_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
1660
1677
  function isRecord2(value) {
@@ -1703,16 +1720,7 @@ function resolveWarmHookConfig(value) {
1703
1720
  if (value === void 0) {
1704
1721
  return null;
1705
1722
  }
1706
- const parsed = parseWarmHookConfig(value);
1707
- const commands = typeof parsed === "string" ? [parsed.trim()].filter(Boolean) : parsed.commands.map((command) => command.trim()).filter(Boolean);
1708
- if (commands.length === 0) {
1709
- return null;
1710
- }
1711
- return {
1712
- commands,
1713
- timeoutMs: typeof parsed === "string" ? void 0 : parsed.timeout,
1714
- separate: typeof parsed === "string" ? void 0 : parsed.separate
1715
- };
1723
+ return resolveHookConfig(parseWarmHookConfig(value));
1716
1724
  }
1717
1725
 
1718
1726
  // ../shared/src/replicas-config.ts
@@ -1776,7 +1784,7 @@ function isClaudeAuthErrorText(text) {
1776
1784
  }
1777
1785
 
1778
1786
  // ../shared/src/engine/environment.ts
1779
- var DAYTONA_SNAPSHOT_ID = "28-05-2026-royal-york-v6";
1787
+ var DAYTONA_SNAPSHOT_ID = "29-05-2026-royal-york-v2";
1780
1788
 
1781
1789
  // ../shared/src/engine/types.ts
1782
1790
  var DEFAULT_CHAT_TITLES = {
@@ -2022,7 +2030,9 @@ function loadEngineEnv() {
2022
2030
  AWS_REGION: readEnv("AWS_REGION"),
2023
2031
  REPLICAS_CLAUDE_AUTH_METHOD: parseClaudeAuthMethod(readEnv("REPLICAS_CLAUDE_AUTH_METHOD")),
2024
2032
  REPLICAS_CODEX_AUTH_METHOD: parseCodexAuthMethod(readEnv("REPLICAS_CODEX_AUTH_METHOD")),
2025
- REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT")
2033
+ REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT"),
2034
+ REPLICAS_ENV_START_HOOK: readEnv("REPLICAS_ENV_START_HOOK"),
2035
+ REPLICAS_DISABLE_AUTO_START_HOOKS: readEnv("REPLICAS_DISABLE_AUTO_START_HOOKS")?.toLowerCase() === "true"
2026
2036
  };
2027
2037
  if (!IS_WARMING_MODE && !env.WORKSPACE_ID) {
2028
2038
  console.error("WORKSPACE_ID is not set \u2014 this is required in normal (non-warming) mode");
@@ -3053,8 +3063,7 @@ import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFil
3053
3063
  import { existsSync as existsSync4 } from "fs";
3054
3064
  import { join as join9 } from "path";
3055
3065
  import { homedir as homedir7 } from "os";
3056
- import { exec } from "child_process";
3057
- import { promisify as promisify2 } from "util";
3066
+ import { spawn } from "child_process";
3058
3067
 
3059
3068
  // src/services/environment-details-service.ts
3060
3069
  import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
@@ -3114,6 +3123,7 @@ function createDefaultDetails() {
3114
3123
  engineVersion: DAYTONA_SNAPSHOT_ID,
3115
3124
  globalWarmHookCompleted: { status: "n/a", details: null },
3116
3125
  environmentWarmHookCompleted: { status: "n/a", details: null },
3126
+ environmentStartHookCompleted: { status: "n/a", details: null },
3117
3127
  repositories: [],
3118
3128
  filesUploaded: [],
3119
3129
  envVarsSet: [],
@@ -3199,6 +3209,12 @@ var EnvironmentDetailsService = class {
3199
3209
  current.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
3200
3210
  await writeDetails(current);
3201
3211
  }
3212
+ async setEnvironmentStartHook(status, details) {
3213
+ const current = await readDetails();
3214
+ current.environmentStartHookCompleted = { status, details: details ?? null };
3215
+ current.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
3216
+ await writeDetails(current);
3217
+ }
3202
3218
  async setRepositoryWarmHook(repositoryName, status) {
3203
3219
  const current = await readDetails();
3204
3220
  current.repositories = upsertRepositoryStatus(current.repositories, [{
@@ -3294,7 +3310,6 @@ var StartHookLogsService = class {
3294
3310
  var startHookLogsService = new StartHookLogsService();
3295
3311
 
3296
3312
  // src/services/replicas-config-service.ts
3297
- var execAsync = promisify2(exec);
3298
3313
  var START_HOOKS_LOG = join9(homedir7(), ".replicas", "startHooks.log");
3299
3314
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
3300
3315
  Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
@@ -3326,6 +3341,12 @@ var ReplicasConfigService = class {
3326
3341
  */
3327
3342
  async initialize() {
3328
3343
  await this.loadConfigs();
3344
+ if (IS_WARMING_MODE || ENGINE_ENV.REPLICAS_DISABLE_AUTO_START_HOOKS) {
3345
+ this.hooksRunning = false;
3346
+ this.hooksCompleted = false;
3347
+ this.hooksFailed = false;
3348
+ return;
3349
+ }
3329
3350
  void this.executeStartHooks().catch((error) => {
3330
3351
  const errorMessage = error instanceof Error ? error.message : String(error);
3331
3352
  this.hooksFailed = true;
@@ -3376,71 +3397,156 @@ var ReplicasConfigService = class {
3376
3397
  console.error("Failed to write to start hooks log:", error);
3377
3398
  }
3378
3399
  }
3400
+ async executeEnvironmentStartHook(params) {
3401
+ await this.logToFile(`[environment] --- Running env-level start hook ---`);
3402
+ const result = await this.execStartHookCommand({
3403
+ command: params.content,
3404
+ label: "env start hook",
3405
+ repoName: "environment",
3406
+ cwd: homedir7(),
3407
+ timeout: params.timeoutMs ?? DEFAULT_START_HOOK_TIMEOUT_MS,
3408
+ onOutputChunk: params.onOutputChunk
3409
+ });
3410
+ if (result.exitCode !== 0) {
3411
+ this.hooksFailed = true;
3412
+ await environmentDetailsService.setEnvironmentStartHook("no");
3413
+ } else {
3414
+ await environmentDetailsService.setEnvironmentStartHook("yes");
3415
+ }
3416
+ return result;
3417
+ }
3379
3418
  async execStartHookCommand(params) {
3380
3419
  const output = [];
3381
- try {
3382
- await this.logToFile(`[${params.repoName}] --- Running: ${params.label} ---`);
3383
- const { stdout, stderr } = await execAsync(params.command, {
3420
+ let outputBytes = 0;
3421
+ let bufferExceeded = false;
3422
+ const emit = (chunk) => {
3423
+ output.push(chunk);
3424
+ params.onOutputChunk?.(chunk);
3425
+ };
3426
+ await this.logToFile(`[${params.repoName}] --- Running: ${params.label} ---`);
3427
+ emit(`$ ${params.label}
3428
+ `);
3429
+ return new Promise((resolve3) => {
3430
+ const proc = spawn("bash", ["-lc", params.command], {
3384
3431
  cwd: params.cwd,
3385
- timeout: params.timeout,
3386
- maxBuffer: HOOK_EXEC_MAX_BUFFER_BYTES,
3387
- env: process.env
3432
+ env: process.env,
3433
+ stdio: ["pipe", "pipe", "pipe"]
3388
3434
  });
3389
- if (stdout) {
3390
- output.push(stdout);
3391
- await this.logToFile(`[${params.repoName}] [stdout] ${stdout}`);
3392
- }
3393
- if (stderr) {
3394
- output.push(stderr);
3395
- await this.logToFile(`[${params.repoName}] [stderr] ${stderr}`);
3435
+ let timedOut = false;
3436
+ let settled = false;
3437
+ const finish = (result) => {
3438
+ if (settled) return;
3439
+ settled = true;
3440
+ resolve3(result);
3441
+ };
3442
+ const timer = setTimeout(() => {
3443
+ timedOut = true;
3444
+ proc.kill("SIGTERM");
3445
+ }, params.timeout);
3446
+ const handleData = (data, streamName) => {
3447
+ outputBytes += data.length;
3448
+ if (bufferExceeded) return;
3449
+ if (outputBytes > HOOK_EXEC_MAX_BUFFER_BYTES) {
3450
+ bufferExceeded = true;
3451
+ const message = `
3452
+ [output truncated \u2014 exceeded ${HOOK_EXEC_MAX_BUFFER_BYTES} bytes]
3453
+ `;
3454
+ emit(message);
3455
+ void this.logToFile(`[${params.repoName}] [ERROR] ${message.trim()}`);
3456
+ proc.kill("SIGTERM");
3457
+ return;
3458
+ }
3459
+ const text = data.toString();
3460
+ emit(text);
3461
+ void this.logToFile(`[${params.repoName}] [${streamName}] ${text}`);
3462
+ };
3463
+ proc.stdout.on("data", (data) => handleData(data, "stdout"));
3464
+ proc.stderr.on("data", (data) => handleData(data, "stderr"));
3465
+ proc.on("close", (code) => {
3466
+ clearTimeout(timer);
3467
+ const exitCode = bufferExceeded ? 1 : code ?? 1;
3468
+ if (exitCode !== 0) {
3469
+ const reason = timedOut ? "timed out" : `exited with code ${exitCode}`;
3470
+ const message = `[ERROR] ${params.label} failed: ${reason}
3471
+ `;
3472
+ emit(message);
3473
+ void this.logToFile(`[${params.repoName}] ${message.trim()}`);
3474
+ } else {
3475
+ void this.logToFile(`[${params.repoName}] --- Completed: ${params.label} ---`);
3476
+ }
3477
+ finish({ exitCode, timedOut: timedOut && !bufferExceeded, output });
3478
+ });
3479
+ proc.on("error", (error) => {
3480
+ clearTimeout(timer);
3481
+ const message = `[ERROR] ${params.label} failed: ${error.message}
3482
+ `;
3483
+ emit(message);
3484
+ void this.logToFile(`[${params.repoName}] ${message.trim()}`);
3485
+ finish({ exitCode: 1, timedOut: false, output });
3486
+ });
3487
+ proc.stdin.end();
3488
+ });
3489
+ }
3490
+ async executeStartHooks(params = {}) {
3491
+ const onEvent = params.onEvent ?? (() => {
3492
+ });
3493
+ const envHookContent = (params.environmentStartHook ?? ENGINE_ENV.REPLICAS_ENV_START_HOOK)?.trim() ?? "";
3494
+ const hookEntries = this.configs.filter((entry) => entry.config.startHook && entry.config.startHook.commands.length > 0);
3495
+ const outputBlocks = [];
3496
+ let overallExitCode = 0;
3497
+ let overallTimedOut = false;
3498
+ const recordResult = (result) => {
3499
+ const output = result.output.join("");
3500
+ if (output) {
3501
+ outputBlocks.push(output);
3396
3502
  }
3397
- await this.logToFile(`[${params.repoName}] --- Completed: ${params.label} ---`);
3398
- return { exitCode: 0, timedOut: false, output };
3399
- } catch (error) {
3400
- const execError = error;
3401
- if (execError.stdout) {
3402
- output.push(execError.stdout);
3403
- await this.logToFile(`[${params.repoName}] [stdout] ${execError.stdout}`);
3503
+ if (result.exitCode !== 0 && overallExitCode === 0) {
3504
+ overallExitCode = result.exitCode;
3404
3505
  }
3405
- if (execError.stderr) {
3406
- output.push(execError.stderr);
3407
- await this.logToFile(`[${params.repoName}] [stderr] ${execError.stderr}`);
3506
+ if (result.timedOut) {
3507
+ overallTimedOut = true;
3408
3508
  }
3409
- const rawMessage = execError.message ?? "Unknown error";
3410
- const errorMessage = rawMessage.split("\n", 1)[0];
3411
- output.push(`[ERROR] ${params.label} failed: ${errorMessage}`);
3412
- await this.logToFile(`[${params.repoName}] [ERROR] ${params.label} failed: ${errorMessage}`);
3413
- return { exitCode: execError.code ?? 1, timedOut: execError.killed === true, output };
3414
- }
3415
- }
3416
- /**
3417
- * Execute start hooks from all repositories sequentially.
3418
- */
3419
- async executeStartHooks() {
3420
- const hookEntries = this.configs.filter((entry) => entry.config.startHook && entry.config.startHook.commands.length > 0);
3421
- if (hookEntries.length === 0) {
3509
+ };
3510
+ if (!envHookContent && hookEntries.length === 0) {
3422
3511
  this.hooksRunning = false;
3423
3512
  this.hooksCompleted = true;
3424
3513
  this.hooksFailed = false;
3514
+ await environmentDetailsService.setEnvironmentStartHook("n/a");
3425
3515
  const repos = await gitService.listRepositories();
3426
3516
  for (const repo of repos) {
3427
3517
  await environmentDetailsService.setRepositoryStartHook(repo.name, "n/a");
3428
3518
  }
3429
- return;
3519
+ const result = { exitCode: 0, output: "No start hooks configured.", timedOut: false };
3520
+ onEvent({ type: "output", data: `${result.output}
3521
+ `, label: "start-hooks" });
3522
+ onEvent({ type: "complete", exitCode: result.exitCode, timedOut: result.timedOut });
3523
+ return result;
3430
3524
  }
3431
3525
  this.hooksRunning = true;
3432
3526
  this.hooksCompleted = false;
3527
+ this.hooksFailed = false;
3433
3528
  try {
3434
3529
  await mkdir5(join9(homedir7(), ".replicas"), { recursive: true });
3435
3530
  await writeFile5(
3436
3531
  START_HOOKS_LOG,
3437
3532
  `=== Start Hooks Execution Log ===
3438
3533
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
3534
+ Environment hook: ${envHookContent ? "yes" : "no"}
3439
3535
  Repositories: ${hookEntries.length}
3440
3536
 
3441
3537
  `,
3442
3538
  "utf-8"
3443
3539
  );
3540
+ if (envHookContent) {
3541
+ const envResult = await this.executeEnvironmentStartHook({
3542
+ content: envHookContent,
3543
+ timeoutMs: params.timeoutMs,
3544
+ onOutputChunk: (chunk) => onEvent({ type: "output", data: chunk, label: "environment" })
3545
+ });
3546
+ recordResult(envResult);
3547
+ } else {
3548
+ await environmentDetailsService.setEnvironmentStartHook("n/a");
3549
+ }
3444
3550
  const repos = await gitService.listRepositories();
3445
3551
  const reposWithHooks = new Set(hookEntries.map((entry) => entry.repoName));
3446
3552
  for (const repo of repos) {
@@ -3454,48 +3560,53 @@ Repositories: ${hookEntries.length}
3454
3560
  continue;
3455
3561
  }
3456
3562
  const persistedRepoState = await loadRepoState(entry.repoName);
3457
- if (persistedRepoState?.startHooksCompleted) {
3563
+ if (!params.ignorePersistedState && persistedRepoState?.startHooksCompleted) {
3458
3564
  await this.logToFile(`[${entry.repoName}] Start hooks already completed in this workspace lifecycle, skipping`);
3459
3565
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "yes");
3460
3566
  continue;
3461
3567
  }
3462
- const timeout = startHookConfig.timeout ?? 3e5;
3568
+ const timeout = params.timeoutMs ?? startHookConfig.timeout ?? DEFAULT_START_HOOK_TIMEOUT_MS;
3463
3569
  let repoFailed = false;
3464
3570
  let lastExitCode = 0;
3465
3571
  let repoTimedOut = false;
3466
3572
  const repoOutput = [];
3573
+ const onRepoOutput = (chunk) => onEvent({ type: "output", data: chunk, label: `repo:${entry.repoName}` });
3467
3574
  await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
3468
3575
  if (startHookConfig.separate === false) {
3469
3576
  const combinedScript = `set -e
3470
3577
  ${startHookConfig.commands.join("\n")}`;
3471
- const result = await this.execStartHookCommand({
3578
+ const result2 = await this.execStartHookCommand({
3472
3579
  command: combinedScript,
3473
3580
  label: "combined script",
3474
3581
  repoName: entry.repoName,
3475
3582
  cwd: entry.workingDirectory,
3476
- timeout
3583
+ timeout,
3584
+ onOutputChunk: onRepoOutput
3477
3585
  });
3478
- repoOutput.push(...result.output);
3479
- if (result.exitCode !== 0) {
3480
- lastExitCode = result.exitCode;
3481
- repoTimedOut = result.timedOut;
3586
+ repoOutput.push(...result2.output);
3587
+ recordResult(result2);
3588
+ if (result2.exitCode !== 0) {
3589
+ lastExitCode = result2.exitCode;
3590
+ repoTimedOut = result2.timedOut;
3482
3591
  this.hooksFailed = true;
3483
3592
  repoFailed = true;
3484
3593
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
3485
3594
  }
3486
3595
  } else {
3487
3596
  for (const hook of startHookConfig.commands) {
3488
- const result = await this.execStartHookCommand({
3597
+ const result2 = await this.execStartHookCommand({
3489
3598
  command: hook,
3490
3599
  label: hook,
3491
3600
  repoName: entry.repoName,
3492
3601
  cwd: entry.workingDirectory,
3493
- timeout
3602
+ timeout,
3603
+ onOutputChunk: onRepoOutput
3494
3604
  });
3495
- repoOutput.push(...result.output);
3496
- if (result.exitCode !== 0) {
3497
- lastExitCode = result.exitCode;
3498
- repoTimedOut = result.timedOut;
3605
+ repoOutput.push(...result2.output);
3606
+ recordResult(result2);
3607
+ if (result2.exitCode !== 0) {
3608
+ lastExitCode = result2.exitCode;
3609
+ repoTimedOut = result2.timedOut;
3499
3610
  this.hooksFailed = true;
3500
3611
  repoFailed = true;
3501
3612
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
@@ -3510,20 +3621,30 @@ ${startHookConfig.commands.join("\n")}`;
3510
3621
  timedOut: repoTimedOut,
3511
3622
  executedAt: (/* @__PURE__ */ new Date()).toISOString()
3512
3623
  });
3513
- const fallbackRepoState = persistedRepoState ?? {
3514
- name: entry.repoName,
3515
- path: entry.workingDirectory,
3516
- defaultBranch: entry.defaultBranch,
3517
- currentBranch: entry.defaultBranch,
3518
- prUrls: [],
3519
- gitDiff: null,
3520
- startHooksCompleted: false
3521
- };
3522
- await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
3624
+ if ((params.markRepoCompleted ?? true) && !repoFailed) {
3625
+ const fallbackRepoState = persistedRepoState ?? {
3626
+ name: entry.repoName,
3627
+ path: entry.workingDirectory,
3628
+ defaultBranch: entry.defaultBranch,
3629
+ currentBranch: entry.defaultBranch,
3630
+ prUrls: [],
3631
+ gitDiff: null,
3632
+ startHooksCompleted: false
3633
+ };
3634
+ await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
3635
+ }
3523
3636
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, repoFailed ? "no" : "yes");
3524
3637
  }
3525
3638
  this.hooksCompleted = true;
3639
+ this.hooksFailed = overallExitCode !== 0;
3526
3640
  await this.logToFile(`=== All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
3641
+ const result = {
3642
+ exitCode: overallExitCode,
3643
+ output: outputBlocks.join("\n\n"),
3644
+ timedOut: overallTimedOut
3645
+ };
3646
+ onEvent({ type: "complete", exitCode: result.exitCode, timedOut: result.timedOut });
3647
+ return result;
3527
3648
  } catch (error) {
3528
3649
  const errorMessage = error instanceof Error ? error.message : String(error);
3529
3650
  this.hooksFailed = true;
@@ -3535,6 +3656,16 @@ ${startHookConfig.commands.join("\n")}`;
3535
3656
  this.hooksRunning = false;
3536
3657
  }
3537
3658
  }
3659
+ async runStartHooksStreaming(params) {
3660
+ await this.loadConfigs();
3661
+ return this.executeStartHooks({
3662
+ environmentStartHook: params.environmentStartHook,
3663
+ timeoutMs: params.timeoutMs,
3664
+ ignorePersistedState: true,
3665
+ markRepoCompleted: false,
3666
+ onEvent: params.onEvent
3667
+ });
3668
+ }
3538
3669
  /**
3539
3670
  * Check if start hooks are currently running.
3540
3671
  */
@@ -3547,14 +3678,18 @@ ${startHookConfig.commands.join("\n")}`;
3547
3678
  didHooksFail() {
3548
3679
  return this.hooksFailed;
3549
3680
  }
3550
- /**
3551
- * Get aggregated start hook metadata.
3552
- */
3553
3681
  getStartHookConfig() {
3554
- const commands = this.configs.flatMap((entry) => {
3682
+ const commands = [];
3683
+ const envHookContent = ENGINE_ENV.REPLICAS_ENV_START_HOOK?.trim();
3684
+ if (envHookContent) {
3685
+ commands.push(`[environment] ${envHookContent}`);
3686
+ }
3687
+ for (const entry of this.configs) {
3555
3688
  const repoCommands = entry.config.startHook?.commands ?? [];
3556
- return repoCommands.map((command) => `[${entry.repoName}] ${command}`);
3557
- });
3689
+ for (const command of repoCommands) {
3690
+ commands.push(`[${entry.repoName}] ${command}`);
3691
+ }
3692
+ }
3558
3693
  if (commands.length === 0) {
3559
3694
  return void 0;
3560
3695
  }
@@ -5358,7 +5493,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
5358
5493
  };
5359
5494
 
5360
5495
  // src/managers/codex-asp/app-server-process.ts
5361
- import { spawn } from "child_process";
5496
+ import { spawn as spawn2 } from "child_process";
5362
5497
  import { EventEmitter as EventEmitter2 } from "events";
5363
5498
 
5364
5499
  // src/managers/codex-asp/asp-client.ts
@@ -5580,7 +5715,7 @@ var AppServerProcess = class {
5580
5715
  return { client: this.client };
5581
5716
  }
5582
5717
  this.shuttingDown = false;
5583
- const child = spawn(this.binary, this.args, {
5718
+ const child = spawn2(this.binary, this.args, {
5584
5719
  cwd: this.cwd,
5585
5720
  env: this.env,
5586
5721
  stdio: ["pipe", "pipe", "pipe"]
@@ -9010,7 +9145,7 @@ var PlanService = class {
9010
9145
  var planService = new PlanService();
9011
9146
 
9012
9147
  // src/services/warm-hooks-service.ts
9013
- import { spawn as spawn2 } from "child_process";
9148
+ import { spawn as spawn3 } from "child_process";
9014
9149
  import { readFile as readFile12 } from "fs/promises";
9015
9150
  import { existsSync as existsSync8 } from "fs";
9016
9151
  import { join as join19 } from "path";
@@ -9199,7 +9334,7 @@ async function executeHookScriptStreaming(params) {
9199
9334
  params.onChunk(`$ ${params.label}
9200
9335
  `);
9201
9336
  return new Promise((resolve3) => {
9202
- const proc = spawn2("bash", ["-lc", params.content], {
9337
+ const proc = spawn3("bash", ["-lc", params.content], {
9203
9338
  cwd: params.cwd,
9204
9339
  env: process.env,
9205
9340
  stdio: ["pipe", "pipe", "pipe"]
@@ -9854,6 +9989,64 @@ function createV1Routes(deps) {
9854
9989
  );
9855
9990
  }
9856
9991
  });
9992
+ app2.post("/start-hooks/run/stream", async (c) => {
9993
+ try {
9994
+ const body = await c.req.json();
9995
+ const encoder = new TextEncoder();
9996
+ const stream = new ReadableStream({
9997
+ start: (controller) => {
9998
+ let closed = false;
9999
+ const safeEnqueue = (chunk) => {
10000
+ if (closed) return false;
10001
+ try {
10002
+ controller.enqueue(encoder.encode(chunk));
10003
+ return true;
10004
+ } catch {
10005
+ closed = true;
10006
+ return false;
10007
+ }
10008
+ };
10009
+ const heartbeat = setInterval(() => {
10010
+ if (!safeEnqueue(": ping\n\n")) clearInterval(heartbeat);
10011
+ }, 15e3);
10012
+ safeEnqueue(": connected\n\n");
10013
+ replicasConfigService.runStartHooksStreaming({
10014
+ environmentStartHook: body.environmentStartHook,
10015
+ timeoutMs: body.timeoutMs,
10016
+ onEvent: (event) => {
10017
+ safeEnqueue(`data: ${JSON.stringify(event)}
10018
+
10019
+ `);
10020
+ }
10021
+ }).then(() => {
10022
+ clearInterval(heartbeat);
10023
+ if (!closed) {
10024
+ closed = true;
10025
+ controller.close();
10026
+ }
10027
+ }).catch((err) => {
10028
+ const message = err instanceof Error ? err.message : "Unknown error";
10029
+ safeEnqueue(`data: ${JSON.stringify({ type: "error", data: message })}
10030
+
10031
+ `);
10032
+ clearInterval(heartbeat);
10033
+ if (!closed) {
10034
+ closed = true;
10035
+ controller.close();
10036
+ }
10037
+ });
10038
+ }
10039
+ });
10040
+ return new Response(stream, {
10041
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }
10042
+ });
10043
+ } catch (error) {
10044
+ return c.json(
10045
+ jsonError("Failed to run start hooks", error instanceof Error ? error.message : "Unknown error"),
10046
+ 500
10047
+ );
10048
+ }
10049
+ });
9857
10050
  app2.get("/warm-hooks/logs", async (c) => {
9858
10051
  try {
9859
10052
  const logs = await warmHookLogsService.getAllLogs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.229",
3
+ "version": "0.1.231",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",