replicas-engine 0.1.230 → 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 +278 -86
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -1652,6 +1652,22 @@ 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
1672
  var DEFAULT_WARM_HOOK_TIMEOUT_MS = 60 * 60 * 1e3;
1657
1673
  var MAX_WARM_HOOK_TIMEOUT_MS = 120 * 60 * 1e3;
@@ -1704,16 +1720,7 @@ function resolveWarmHookConfig(value) {
1704
1720
  if (value === void 0) {
1705
1721
  return null;
1706
1722
  }
1707
- const parsed = parseWarmHookConfig(value);
1708
- const commands = typeof parsed === "string" ? [parsed.trim()].filter(Boolean) : parsed.commands.map((command) => command.trim()).filter(Boolean);
1709
- if (commands.length === 0) {
1710
- return null;
1711
- }
1712
- return {
1713
- commands,
1714
- timeoutMs: typeof parsed === "string" ? void 0 : parsed.timeout,
1715
- separate: typeof parsed === "string" ? void 0 : parsed.separate
1716
- };
1723
+ return resolveHookConfig(parseWarmHookConfig(value));
1717
1724
  }
1718
1725
 
1719
1726
  // ../shared/src/replicas-config.ts
@@ -1777,7 +1784,7 @@ function isClaudeAuthErrorText(text) {
1777
1784
  }
1778
1785
 
1779
1786
  // ../shared/src/engine/environment.ts
1780
- var DAYTONA_SNAPSHOT_ID = "29-05-2026-royal-york-v1";
1787
+ var DAYTONA_SNAPSHOT_ID = "29-05-2026-royal-york-v2";
1781
1788
 
1782
1789
  // ../shared/src/engine/types.ts
1783
1790
  var DEFAULT_CHAT_TITLES = {
@@ -2023,7 +2030,9 @@ function loadEngineEnv() {
2023
2030
  AWS_REGION: readEnv("AWS_REGION"),
2024
2031
  REPLICAS_CLAUDE_AUTH_METHOD: parseClaudeAuthMethod(readEnv("REPLICAS_CLAUDE_AUTH_METHOD")),
2025
2032
  REPLICAS_CODEX_AUTH_METHOD: parseCodexAuthMethod(readEnv("REPLICAS_CODEX_AUTH_METHOD")),
2026
- 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"
2027
2036
  };
2028
2037
  if (!IS_WARMING_MODE && !env.WORKSPACE_ID) {
2029
2038
  console.error("WORKSPACE_ID is not set \u2014 this is required in normal (non-warming) mode");
@@ -3054,8 +3063,7 @@ import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFil
3054
3063
  import { existsSync as existsSync4 } from "fs";
3055
3064
  import { join as join9 } from "path";
3056
3065
  import { homedir as homedir7 } from "os";
3057
- import { exec } from "child_process";
3058
- import { promisify as promisify2 } from "util";
3066
+ import { spawn } from "child_process";
3059
3067
 
3060
3068
  // src/services/environment-details-service.ts
3061
3069
  import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
@@ -3115,6 +3123,7 @@ function createDefaultDetails() {
3115
3123
  engineVersion: DAYTONA_SNAPSHOT_ID,
3116
3124
  globalWarmHookCompleted: { status: "n/a", details: null },
3117
3125
  environmentWarmHookCompleted: { status: "n/a", details: null },
3126
+ environmentStartHookCompleted: { status: "n/a", details: null },
3118
3127
  repositories: [],
3119
3128
  filesUploaded: [],
3120
3129
  envVarsSet: [],
@@ -3200,6 +3209,12 @@ var EnvironmentDetailsService = class {
3200
3209
  current.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
3201
3210
  await writeDetails(current);
3202
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
+ }
3203
3218
  async setRepositoryWarmHook(repositoryName, status) {
3204
3219
  const current = await readDetails();
3205
3220
  current.repositories = upsertRepositoryStatus(current.repositories, [{
@@ -3295,7 +3310,6 @@ var StartHookLogsService = class {
3295
3310
  var startHookLogsService = new StartHookLogsService();
3296
3311
 
3297
3312
  // src/services/replicas-config-service.ts
3298
- var execAsync = promisify2(exec);
3299
3313
  var START_HOOKS_LOG = join9(homedir7(), ".replicas", "startHooks.log");
3300
3314
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
3301
3315
  Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
@@ -3327,6 +3341,12 @@ var ReplicasConfigService = class {
3327
3341
  */
3328
3342
  async initialize() {
3329
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
+ }
3330
3350
  void this.executeStartHooks().catch((error) => {
3331
3351
  const errorMessage = error instanceof Error ? error.message : String(error);
3332
3352
  this.hooksFailed = true;
@@ -3377,71 +3397,156 @@ var ReplicasConfigService = class {
3377
3397
  console.error("Failed to write to start hooks log:", error);
3378
3398
  }
3379
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
+ }
3380
3418
  async execStartHookCommand(params) {
3381
3419
  const output = [];
3382
- try {
3383
- await this.logToFile(`[${params.repoName}] --- Running: ${params.label} ---`);
3384
- 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], {
3385
3431
  cwd: params.cwd,
3386
- timeout: params.timeout,
3387
- maxBuffer: HOOK_EXEC_MAX_BUFFER_BYTES,
3388
- env: process.env
3432
+ env: process.env,
3433
+ stdio: ["pipe", "pipe", "pipe"]
3389
3434
  });
3390
- if (stdout) {
3391
- output.push(stdout);
3392
- await this.logToFile(`[${params.repoName}] [stdout] ${stdout}`);
3393
- }
3394
- if (stderr) {
3395
- output.push(stderr);
3396
- 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);
3397
3502
  }
3398
- await this.logToFile(`[${params.repoName}] --- Completed: ${params.label} ---`);
3399
- return { exitCode: 0, timedOut: false, output };
3400
- } catch (error) {
3401
- const execError = error;
3402
- if (execError.stdout) {
3403
- output.push(execError.stdout);
3404
- await this.logToFile(`[${params.repoName}] [stdout] ${execError.stdout}`);
3503
+ if (result.exitCode !== 0 && overallExitCode === 0) {
3504
+ overallExitCode = result.exitCode;
3405
3505
  }
3406
- if (execError.stderr) {
3407
- output.push(execError.stderr);
3408
- await this.logToFile(`[${params.repoName}] [stderr] ${execError.stderr}`);
3506
+ if (result.timedOut) {
3507
+ overallTimedOut = true;
3409
3508
  }
3410
- const rawMessage = execError.message ?? "Unknown error";
3411
- const errorMessage = rawMessage.split("\n", 1)[0];
3412
- output.push(`[ERROR] ${params.label} failed: ${errorMessage}`);
3413
- await this.logToFile(`[${params.repoName}] [ERROR] ${params.label} failed: ${errorMessage}`);
3414
- return { exitCode: execError.code ?? 1, timedOut: execError.killed === true, output };
3415
- }
3416
- }
3417
- /**
3418
- * Execute start hooks from all repositories sequentially.
3419
- */
3420
- async executeStartHooks() {
3421
- const hookEntries = this.configs.filter((entry) => entry.config.startHook && entry.config.startHook.commands.length > 0);
3422
- if (hookEntries.length === 0) {
3509
+ };
3510
+ if (!envHookContent && hookEntries.length === 0) {
3423
3511
  this.hooksRunning = false;
3424
3512
  this.hooksCompleted = true;
3425
3513
  this.hooksFailed = false;
3514
+ await environmentDetailsService.setEnvironmentStartHook("n/a");
3426
3515
  const repos = await gitService.listRepositories();
3427
3516
  for (const repo of repos) {
3428
3517
  await environmentDetailsService.setRepositoryStartHook(repo.name, "n/a");
3429
3518
  }
3430
- 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;
3431
3524
  }
3432
3525
  this.hooksRunning = true;
3433
3526
  this.hooksCompleted = false;
3527
+ this.hooksFailed = false;
3434
3528
  try {
3435
3529
  await mkdir5(join9(homedir7(), ".replicas"), { recursive: true });
3436
3530
  await writeFile5(
3437
3531
  START_HOOKS_LOG,
3438
3532
  `=== Start Hooks Execution Log ===
3439
3533
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
3534
+ Environment hook: ${envHookContent ? "yes" : "no"}
3440
3535
  Repositories: ${hookEntries.length}
3441
3536
 
3442
3537
  `,
3443
3538
  "utf-8"
3444
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
+ }
3445
3550
  const repos = await gitService.listRepositories();
3446
3551
  const reposWithHooks = new Set(hookEntries.map((entry) => entry.repoName));
3447
3552
  for (const repo of repos) {
@@ -3455,48 +3560,53 @@ Repositories: ${hookEntries.length}
3455
3560
  continue;
3456
3561
  }
3457
3562
  const persistedRepoState = await loadRepoState(entry.repoName);
3458
- if (persistedRepoState?.startHooksCompleted) {
3563
+ if (!params.ignorePersistedState && persistedRepoState?.startHooksCompleted) {
3459
3564
  await this.logToFile(`[${entry.repoName}] Start hooks already completed in this workspace lifecycle, skipping`);
3460
3565
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "yes");
3461
3566
  continue;
3462
3567
  }
3463
- const timeout = startHookConfig.timeout ?? DEFAULT_START_HOOK_TIMEOUT_MS;
3568
+ const timeout = params.timeoutMs ?? startHookConfig.timeout ?? DEFAULT_START_HOOK_TIMEOUT_MS;
3464
3569
  let repoFailed = false;
3465
3570
  let lastExitCode = 0;
3466
3571
  let repoTimedOut = false;
3467
3572
  const repoOutput = [];
3573
+ const onRepoOutput = (chunk) => onEvent({ type: "output", data: chunk, label: `repo:${entry.repoName}` });
3468
3574
  await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
3469
3575
  if (startHookConfig.separate === false) {
3470
3576
  const combinedScript = `set -e
3471
3577
  ${startHookConfig.commands.join("\n")}`;
3472
- const result = await this.execStartHookCommand({
3578
+ const result2 = await this.execStartHookCommand({
3473
3579
  command: combinedScript,
3474
3580
  label: "combined script",
3475
3581
  repoName: entry.repoName,
3476
3582
  cwd: entry.workingDirectory,
3477
- timeout
3583
+ timeout,
3584
+ onOutputChunk: onRepoOutput
3478
3585
  });
3479
- repoOutput.push(...result.output);
3480
- if (result.exitCode !== 0) {
3481
- lastExitCode = result.exitCode;
3482
- repoTimedOut = result.timedOut;
3586
+ repoOutput.push(...result2.output);
3587
+ recordResult(result2);
3588
+ if (result2.exitCode !== 0) {
3589
+ lastExitCode = result2.exitCode;
3590
+ repoTimedOut = result2.timedOut;
3483
3591
  this.hooksFailed = true;
3484
3592
  repoFailed = true;
3485
3593
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
3486
3594
  }
3487
3595
  } else {
3488
3596
  for (const hook of startHookConfig.commands) {
3489
- const result = await this.execStartHookCommand({
3597
+ const result2 = await this.execStartHookCommand({
3490
3598
  command: hook,
3491
3599
  label: hook,
3492
3600
  repoName: entry.repoName,
3493
3601
  cwd: entry.workingDirectory,
3494
- timeout
3602
+ timeout,
3603
+ onOutputChunk: onRepoOutput
3495
3604
  });
3496
- repoOutput.push(...result.output);
3497
- if (result.exitCode !== 0) {
3498
- lastExitCode = result.exitCode;
3499
- repoTimedOut = result.timedOut;
3605
+ repoOutput.push(...result2.output);
3606
+ recordResult(result2);
3607
+ if (result2.exitCode !== 0) {
3608
+ lastExitCode = result2.exitCode;
3609
+ repoTimedOut = result2.timedOut;
3500
3610
  this.hooksFailed = true;
3501
3611
  repoFailed = true;
3502
3612
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
@@ -3511,20 +3621,30 @@ ${startHookConfig.commands.join("\n")}`;
3511
3621
  timedOut: repoTimedOut,
3512
3622
  executedAt: (/* @__PURE__ */ new Date()).toISOString()
3513
3623
  });
3514
- const fallbackRepoState = persistedRepoState ?? {
3515
- name: entry.repoName,
3516
- path: entry.workingDirectory,
3517
- defaultBranch: entry.defaultBranch,
3518
- currentBranch: entry.defaultBranch,
3519
- prUrls: [],
3520
- gitDiff: null,
3521
- startHooksCompleted: false
3522
- };
3523
- 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
+ }
3524
3636
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, repoFailed ? "no" : "yes");
3525
3637
  }
3526
3638
  this.hooksCompleted = true;
3639
+ this.hooksFailed = overallExitCode !== 0;
3527
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;
3528
3648
  } catch (error) {
3529
3649
  const errorMessage = error instanceof Error ? error.message : String(error);
3530
3650
  this.hooksFailed = true;
@@ -3536,6 +3656,16 @@ ${startHookConfig.commands.join("\n")}`;
3536
3656
  this.hooksRunning = false;
3537
3657
  }
3538
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
+ }
3539
3669
  /**
3540
3670
  * Check if start hooks are currently running.
3541
3671
  */
@@ -3548,14 +3678,18 @@ ${startHookConfig.commands.join("\n")}`;
3548
3678
  didHooksFail() {
3549
3679
  return this.hooksFailed;
3550
3680
  }
3551
- /**
3552
- * Get aggregated start hook metadata.
3553
- */
3554
3681
  getStartHookConfig() {
3555
- 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) {
3556
3688
  const repoCommands = entry.config.startHook?.commands ?? [];
3557
- return repoCommands.map((command) => `[${entry.repoName}] ${command}`);
3558
- });
3689
+ for (const command of repoCommands) {
3690
+ commands.push(`[${entry.repoName}] ${command}`);
3691
+ }
3692
+ }
3559
3693
  if (commands.length === 0) {
3560
3694
  return void 0;
3561
3695
  }
@@ -5359,7 +5493,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
5359
5493
  };
5360
5494
 
5361
5495
  // src/managers/codex-asp/app-server-process.ts
5362
- import { spawn } from "child_process";
5496
+ import { spawn as spawn2 } from "child_process";
5363
5497
  import { EventEmitter as EventEmitter2 } from "events";
5364
5498
 
5365
5499
  // src/managers/codex-asp/asp-client.ts
@@ -5581,7 +5715,7 @@ var AppServerProcess = class {
5581
5715
  return { client: this.client };
5582
5716
  }
5583
5717
  this.shuttingDown = false;
5584
- const child = spawn(this.binary, this.args, {
5718
+ const child = spawn2(this.binary, this.args, {
5585
5719
  cwd: this.cwd,
5586
5720
  env: this.env,
5587
5721
  stdio: ["pipe", "pipe", "pipe"]
@@ -9011,7 +9145,7 @@ var PlanService = class {
9011
9145
  var planService = new PlanService();
9012
9146
 
9013
9147
  // src/services/warm-hooks-service.ts
9014
- import { spawn as spawn2 } from "child_process";
9148
+ import { spawn as spawn3 } from "child_process";
9015
9149
  import { readFile as readFile12 } from "fs/promises";
9016
9150
  import { existsSync as existsSync8 } from "fs";
9017
9151
  import { join as join19 } from "path";
@@ -9200,7 +9334,7 @@ async function executeHookScriptStreaming(params) {
9200
9334
  params.onChunk(`$ ${params.label}
9201
9335
  `);
9202
9336
  return new Promise((resolve3) => {
9203
- const proc = spawn2("bash", ["-lc", params.content], {
9337
+ const proc = spawn3("bash", ["-lc", params.content], {
9204
9338
  cwd: params.cwd,
9205
9339
  env: process.env,
9206
9340
  stdio: ["pipe", "pipe", "pipe"]
@@ -9855,6 +9989,64 @@ function createV1Routes(deps) {
9855
9989
  );
9856
9990
  }
9857
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
+ });
9858
10050
  app2.get("/warm-hooks/logs", async (c) => {
9859
10051
  try {
9860
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.230",
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",