replicas-engine 0.1.230 → 0.1.232
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.
- package/dist/src/index.js +380 -131
- 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;
|
|
@@ -1667,17 +1683,25 @@ function clampWarmHookTimeoutMs(timeoutMs) {
|
|
|
1667
1683
|
}
|
|
1668
1684
|
return Math.min(timeoutMs, MAX_WARM_HOOK_TIMEOUT_MS);
|
|
1669
1685
|
}
|
|
1670
|
-
function
|
|
1686
|
+
function buildOutputPreview(text, {
|
|
1687
|
+
maxChars = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS,
|
|
1688
|
+
truncateFrom = "head",
|
|
1689
|
+
marker = "\n...[truncated \u2014 download the full log to see the rest]"
|
|
1690
|
+
} = {}) {
|
|
1671
1691
|
if (text.length <= maxChars) {
|
|
1672
1692
|
return { output: text, outputTruncated: false, outputTotalChars: text.length };
|
|
1673
1693
|
}
|
|
1694
|
+
const previewChars = Math.max(0, maxChars - marker.length);
|
|
1695
|
+
const output = previewChars === 0 ? marker.slice(0, maxChars) : truncateFrom === "tail" ? `${marker}${text.slice(-previewChars)}` : `${text.slice(0, previewChars)}${marker}`;
|
|
1674
1696
|
return {
|
|
1675
|
-
output
|
|
1676
|
-
...[truncated \u2014 download the full log to see the rest]`,
|
|
1697
|
+
output,
|
|
1677
1698
|
outputTruncated: true,
|
|
1678
1699
|
outputTotalChars: text.length
|
|
1679
1700
|
};
|
|
1680
1701
|
}
|
|
1702
|
+
function buildHookOutputPreview(text, maxChars = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS) {
|
|
1703
|
+
return buildOutputPreview(text, { maxChars });
|
|
1704
|
+
}
|
|
1681
1705
|
function parseWarmHookConfig(value, filename = "replicas.json") {
|
|
1682
1706
|
if (typeof value === "string") {
|
|
1683
1707
|
return value;
|
|
@@ -1704,16 +1728,7 @@ function resolveWarmHookConfig(value) {
|
|
|
1704
1728
|
if (value === void 0) {
|
|
1705
1729
|
return null;
|
|
1706
1730
|
}
|
|
1707
|
-
|
|
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
|
-
};
|
|
1731
|
+
return resolveHookConfig(parseWarmHookConfig(value));
|
|
1717
1732
|
}
|
|
1718
1733
|
|
|
1719
1734
|
// ../shared/src/replicas-config.ts
|
|
@@ -1777,7 +1792,7 @@ function isClaudeAuthErrorText(text) {
|
|
|
1777
1792
|
}
|
|
1778
1793
|
|
|
1779
1794
|
// ../shared/src/engine/environment.ts
|
|
1780
|
-
var DAYTONA_SNAPSHOT_ID = "29-05-2026-royal-york-
|
|
1795
|
+
var DAYTONA_SNAPSHOT_ID = "29-05-2026-royal-york-v3";
|
|
1781
1796
|
|
|
1782
1797
|
// ../shared/src/engine/types.ts
|
|
1783
1798
|
var DEFAULT_CHAT_TITLES = {
|
|
@@ -2023,7 +2038,9 @@ function loadEngineEnv() {
|
|
|
2023
2038
|
AWS_REGION: readEnv("AWS_REGION"),
|
|
2024
2039
|
REPLICAS_CLAUDE_AUTH_METHOD: parseClaudeAuthMethod(readEnv("REPLICAS_CLAUDE_AUTH_METHOD")),
|
|
2025
2040
|
REPLICAS_CODEX_AUTH_METHOD: parseCodexAuthMethod(readEnv("REPLICAS_CODEX_AUTH_METHOD")),
|
|
2026
|
-
REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT")
|
|
2041
|
+
REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT"),
|
|
2042
|
+
REPLICAS_ENV_START_HOOK: readEnv("REPLICAS_ENV_START_HOOK"),
|
|
2043
|
+
REPLICAS_DISABLE_AUTO_START_HOOKS: readEnv("REPLICAS_DISABLE_AUTO_START_HOOKS")?.toLowerCase() === "true"
|
|
2027
2044
|
};
|
|
2028
2045
|
if (!IS_WARMING_MODE && !env.WORKSPACE_ID) {
|
|
2029
2046
|
console.error("WORKSPACE_ID is not set \u2014 this is required in normal (non-warming) mode");
|
|
@@ -2426,11 +2443,24 @@ import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
|
2426
2443
|
import { join as join5 } from "path";
|
|
2427
2444
|
|
|
2428
2445
|
// src/utils/state.ts
|
|
2429
|
-
import { readFile,
|
|
2446
|
+
import { readFile, mkdir } from "fs/promises";
|
|
2430
2447
|
import { existsSync } from "fs";
|
|
2431
2448
|
import { join as join3 } from "path";
|
|
2432
2449
|
import { homedir as homedir3 } from "os";
|
|
2433
2450
|
|
|
2451
|
+
// src/utils/file.ts
|
|
2452
|
+
import { rename, unlink, writeFile } from "fs/promises";
|
|
2453
|
+
async function atomicWriteFile(path4, data) {
|
|
2454
|
+
const tmpFile = `${path4}.${process.pid}.${Date.now()}.tmp`;
|
|
2455
|
+
try {
|
|
2456
|
+
await writeFile(tmpFile, data, "utf-8");
|
|
2457
|
+
await rename(tmpFile, path4);
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
await unlink(tmpFile).catch(() => void 0);
|
|
2460
|
+
throw error;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2434
2464
|
// src/utils/type-guards.ts
|
|
2435
2465
|
function isRecord4(value) {
|
|
2436
2466
|
return typeof value === "object" && value !== null;
|
|
@@ -2453,14 +2483,7 @@ async function updateEngineState(updater) {
|
|
|
2453
2483
|
await mkdir(STATE_DIR, { recursive: true });
|
|
2454
2484
|
const currentState = await loadEngineState();
|
|
2455
2485
|
const nextState = updater(currentState);
|
|
2456
|
-
|
|
2457
|
-
try {
|
|
2458
|
-
await writeFile(tmpFile, JSON.stringify(nextState, null, 2), "utf-8");
|
|
2459
|
-
await rename(tmpFile, STATE_FILE);
|
|
2460
|
-
} catch (err) {
|
|
2461
|
-
await unlink(tmpFile).catch(() => void 0);
|
|
2462
|
-
throw err;
|
|
2463
|
-
}
|
|
2486
|
+
await atomicWriteFile(STATE_FILE, JSON.stringify(nextState, null, 2));
|
|
2464
2487
|
});
|
|
2465
2488
|
}
|
|
2466
2489
|
function isEngineRepoDiff(value) {
|
|
@@ -3050,15 +3073,14 @@ var EngineLogger = class {
|
|
|
3050
3073
|
var engineLogger = new EngineLogger();
|
|
3051
3074
|
|
|
3052
3075
|
// src/services/replicas-config-service.ts
|
|
3053
|
-
import { readFile as readFile4, appendFile as appendFile2, writeFile as
|
|
3076
|
+
import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
|
|
3054
3077
|
import { existsSync as existsSync4 } from "fs";
|
|
3055
3078
|
import { join as join9 } from "path";
|
|
3056
3079
|
import { homedir as homedir7 } from "os";
|
|
3057
|
-
import {
|
|
3058
|
-
import { promisify as promisify2 } from "util";
|
|
3080
|
+
import { spawn } from "child_process";
|
|
3059
3081
|
|
|
3060
3082
|
// src/services/environment-details-service.ts
|
|
3061
|
-
import { mkdir as mkdir3, readFile as readFile2
|
|
3083
|
+
import { mkdir as mkdir3, readFile as readFile2 } from "fs/promises";
|
|
3062
3084
|
import { existsSync as existsSync3 } from "fs";
|
|
3063
3085
|
import { homedir as homedir5 } from "os";
|
|
3064
3086
|
import { join as join7 } from "path";
|
|
@@ -3115,6 +3137,7 @@ function createDefaultDetails() {
|
|
|
3115
3137
|
engineVersion: DAYTONA_SNAPSHOT_ID,
|
|
3116
3138
|
globalWarmHookCompleted: { status: "n/a", details: null },
|
|
3117
3139
|
environmentWarmHookCompleted: { status: "n/a", details: null },
|
|
3140
|
+
environmentStartHookCompleted: { status: "n/a", details: null },
|
|
3118
3141
|
repositories: [],
|
|
3119
3142
|
filesUploaded: [],
|
|
3120
3143
|
envVarsSet: [],
|
|
@@ -3145,8 +3168,8 @@ async function readDetails() {
|
|
|
3145
3168
|
}
|
|
3146
3169
|
async function writeDetails(details) {
|
|
3147
3170
|
await mkdir3(REPLICAS_DIR, { recursive: true });
|
|
3148
|
-
await
|
|
3149
|
-
|
|
3171
|
+
await atomicWriteFile(DETAILS_FILE, `${JSON.stringify(details, null, 2)}
|
|
3172
|
+
`);
|
|
3150
3173
|
}
|
|
3151
3174
|
var EnvironmentDetailsService = class {
|
|
3152
3175
|
async getDetails() {
|
|
@@ -3200,6 +3223,12 @@ var EnvironmentDetailsService = class {
|
|
|
3200
3223
|
current.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3201
3224
|
await writeDetails(current);
|
|
3202
3225
|
}
|
|
3226
|
+
async setEnvironmentStartHook(status, details) {
|
|
3227
|
+
const current = await readDetails();
|
|
3228
|
+
current.environmentStartHookCompleted = { status, details: details ?? null };
|
|
3229
|
+
current.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3230
|
+
await writeDetails(current);
|
|
3231
|
+
}
|
|
3203
3232
|
async setRepositoryWarmHook(repositoryName, status) {
|
|
3204
3233
|
const current = await readDetails();
|
|
3205
3234
|
current.repositories = upsertRepositoryStatus(current.repositories, [{
|
|
@@ -3225,7 +3254,7 @@ var environmentDetailsService = new EnvironmentDetailsService();
|
|
|
3225
3254
|
|
|
3226
3255
|
// src/services/start-hook-logs-service.ts
|
|
3227
3256
|
import { createHash } from "crypto";
|
|
3228
|
-
import { mkdir as mkdir4, readFile as readFile3, writeFile as
|
|
3257
|
+
import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile3, readdir as readdir2 } from "fs/promises";
|
|
3229
3258
|
import { homedir as homedir6 } from "os";
|
|
3230
3259
|
import { join as join8 } from "path";
|
|
3231
3260
|
var LOGS_DIR = join8(homedir6(), ".replicas", "start-hook-logs");
|
|
@@ -3248,7 +3277,7 @@ var StartHookLogsService = class {
|
|
|
3248
3277
|
async saveRepoLog(repoName, entry) {
|
|
3249
3278
|
await this.ensureDir();
|
|
3250
3279
|
const log = { repoName, ...entry };
|
|
3251
|
-
await
|
|
3280
|
+
await writeFile3(join8(LOGS_DIR, repoFilename(repoName)), `${JSON.stringify(log, null, 2)}
|
|
3252
3281
|
`, "utf-8");
|
|
3253
3282
|
}
|
|
3254
3283
|
async getAllLogs() {
|
|
@@ -3295,7 +3324,6 @@ var StartHookLogsService = class {
|
|
|
3295
3324
|
var startHookLogsService = new StartHookLogsService();
|
|
3296
3325
|
|
|
3297
3326
|
// src/services/replicas-config-service.ts
|
|
3298
|
-
var execAsync = promisify2(exec);
|
|
3299
3327
|
var START_HOOKS_LOG = join9(homedir7(), ".replicas", "startHooks.log");
|
|
3300
3328
|
var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
|
|
3301
3329
|
Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
|
|
@@ -3327,6 +3355,12 @@ var ReplicasConfigService = class {
|
|
|
3327
3355
|
*/
|
|
3328
3356
|
async initialize() {
|
|
3329
3357
|
await this.loadConfigs();
|
|
3358
|
+
if (IS_WARMING_MODE || ENGINE_ENV.REPLICAS_DISABLE_AUTO_START_HOOKS) {
|
|
3359
|
+
this.hooksRunning = false;
|
|
3360
|
+
this.hooksCompleted = false;
|
|
3361
|
+
this.hooksFailed = false;
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3330
3364
|
void this.executeStartHooks().catch((error) => {
|
|
3331
3365
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3332
3366
|
this.hooksFailed = true;
|
|
@@ -3377,71 +3411,156 @@ var ReplicasConfigService = class {
|
|
|
3377
3411
|
console.error("Failed to write to start hooks log:", error);
|
|
3378
3412
|
}
|
|
3379
3413
|
}
|
|
3414
|
+
async executeEnvironmentStartHook(params) {
|
|
3415
|
+
await this.logToFile(`[environment] --- Running env-level start hook ---`);
|
|
3416
|
+
const result = await this.execStartHookCommand({
|
|
3417
|
+
command: params.content,
|
|
3418
|
+
label: "env start hook",
|
|
3419
|
+
repoName: "environment",
|
|
3420
|
+
cwd: homedir7(),
|
|
3421
|
+
timeout: params.timeoutMs ?? DEFAULT_START_HOOK_TIMEOUT_MS,
|
|
3422
|
+
onOutputChunk: params.onOutputChunk
|
|
3423
|
+
});
|
|
3424
|
+
if (result.exitCode !== 0) {
|
|
3425
|
+
this.hooksFailed = true;
|
|
3426
|
+
await environmentDetailsService.setEnvironmentStartHook("no");
|
|
3427
|
+
} else {
|
|
3428
|
+
await environmentDetailsService.setEnvironmentStartHook("yes");
|
|
3429
|
+
}
|
|
3430
|
+
return result;
|
|
3431
|
+
}
|
|
3380
3432
|
async execStartHookCommand(params) {
|
|
3381
3433
|
const output = [];
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3434
|
+
let outputBytes = 0;
|
|
3435
|
+
let bufferExceeded = false;
|
|
3436
|
+
const emit = (chunk) => {
|
|
3437
|
+
output.push(chunk);
|
|
3438
|
+
params.onOutputChunk?.(chunk);
|
|
3439
|
+
};
|
|
3440
|
+
await this.logToFile(`[${params.repoName}] --- Running: ${params.label} ---`);
|
|
3441
|
+
emit(`$ ${params.label}
|
|
3442
|
+
`);
|
|
3443
|
+
return new Promise((resolve3) => {
|
|
3444
|
+
const proc = spawn("bash", ["-lc", params.command], {
|
|
3385
3445
|
cwd: params.cwd,
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
env: process.env
|
|
3446
|
+
env: process.env,
|
|
3447
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3389
3448
|
});
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3449
|
+
let timedOut = false;
|
|
3450
|
+
let settled = false;
|
|
3451
|
+
const finish = (result) => {
|
|
3452
|
+
if (settled) return;
|
|
3453
|
+
settled = true;
|
|
3454
|
+
resolve3(result);
|
|
3455
|
+
};
|
|
3456
|
+
const timer = setTimeout(() => {
|
|
3457
|
+
timedOut = true;
|
|
3458
|
+
proc.kill("SIGTERM");
|
|
3459
|
+
}, params.timeout);
|
|
3460
|
+
const handleData = (data, streamName) => {
|
|
3461
|
+
outputBytes += data.length;
|
|
3462
|
+
if (bufferExceeded) return;
|
|
3463
|
+
if (outputBytes > HOOK_EXEC_MAX_BUFFER_BYTES) {
|
|
3464
|
+
bufferExceeded = true;
|
|
3465
|
+
const message = `
|
|
3466
|
+
[output truncated \u2014 exceeded ${HOOK_EXEC_MAX_BUFFER_BYTES} bytes]
|
|
3467
|
+
`;
|
|
3468
|
+
emit(message);
|
|
3469
|
+
void this.logToFile(`[${params.repoName}] [ERROR] ${message.trim()}`);
|
|
3470
|
+
proc.kill("SIGTERM");
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
const text = data.toString();
|
|
3474
|
+
emit(text);
|
|
3475
|
+
void this.logToFile(`[${params.repoName}] [${streamName}] ${text}`);
|
|
3476
|
+
};
|
|
3477
|
+
proc.stdout.on("data", (data) => handleData(data, "stdout"));
|
|
3478
|
+
proc.stderr.on("data", (data) => handleData(data, "stderr"));
|
|
3479
|
+
proc.on("close", (code) => {
|
|
3480
|
+
clearTimeout(timer);
|
|
3481
|
+
const exitCode = bufferExceeded ? 1 : code ?? 1;
|
|
3482
|
+
if (exitCode !== 0) {
|
|
3483
|
+
const reason = timedOut ? "timed out" : `exited with code ${exitCode}`;
|
|
3484
|
+
const message = `[ERROR] ${params.label} failed: ${reason}
|
|
3485
|
+
`;
|
|
3486
|
+
emit(message);
|
|
3487
|
+
void this.logToFile(`[${params.repoName}] ${message.trim()}`);
|
|
3488
|
+
} else {
|
|
3489
|
+
void this.logToFile(`[${params.repoName}] --- Completed: ${params.label} ---`);
|
|
3490
|
+
}
|
|
3491
|
+
finish({ exitCode, timedOut: timedOut && !bufferExceeded, output });
|
|
3492
|
+
});
|
|
3493
|
+
proc.on("error", (error) => {
|
|
3494
|
+
clearTimeout(timer);
|
|
3495
|
+
const message = `[ERROR] ${params.label} failed: ${error.message}
|
|
3496
|
+
`;
|
|
3497
|
+
emit(message);
|
|
3498
|
+
void this.logToFile(`[${params.repoName}] ${message.trim()}`);
|
|
3499
|
+
finish({ exitCode: 1, timedOut: false, output });
|
|
3500
|
+
});
|
|
3501
|
+
proc.stdin.end();
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
async executeStartHooks(params = {}) {
|
|
3505
|
+
const onEvent = params.onEvent ?? (() => {
|
|
3506
|
+
});
|
|
3507
|
+
const envHookContent = (params.environmentStartHook ?? ENGINE_ENV.REPLICAS_ENV_START_HOOK)?.trim() ?? "";
|
|
3508
|
+
const hookEntries = this.configs.filter((entry) => entry.config.startHook && entry.config.startHook.commands.length > 0);
|
|
3509
|
+
const outputBlocks = [];
|
|
3510
|
+
let overallExitCode = 0;
|
|
3511
|
+
let overallTimedOut = false;
|
|
3512
|
+
const recordResult = (result) => {
|
|
3513
|
+
const output = result.output.join("");
|
|
3514
|
+
if (output) {
|
|
3515
|
+
outputBlocks.push(output);
|
|
3397
3516
|
}
|
|
3398
|
-
|
|
3399
|
-
|
|
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}`);
|
|
3517
|
+
if (result.exitCode !== 0 && overallExitCode === 0) {
|
|
3518
|
+
overallExitCode = result.exitCode;
|
|
3405
3519
|
}
|
|
3406
|
-
if (
|
|
3407
|
-
|
|
3408
|
-
await this.logToFile(`[${params.repoName}] [stderr] ${execError.stderr}`);
|
|
3520
|
+
if (result.timedOut) {
|
|
3521
|
+
overallTimedOut = true;
|
|
3409
3522
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
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) {
|
|
3523
|
+
};
|
|
3524
|
+
if (!envHookContent && hookEntries.length === 0) {
|
|
3423
3525
|
this.hooksRunning = false;
|
|
3424
3526
|
this.hooksCompleted = true;
|
|
3425
3527
|
this.hooksFailed = false;
|
|
3528
|
+
await environmentDetailsService.setEnvironmentStartHook("n/a");
|
|
3426
3529
|
const repos = await gitService.listRepositories();
|
|
3427
3530
|
for (const repo of repos) {
|
|
3428
3531
|
await environmentDetailsService.setRepositoryStartHook(repo.name, "n/a");
|
|
3429
3532
|
}
|
|
3430
|
-
|
|
3533
|
+
const result = { exitCode: 0, output: "No start hooks configured.", timedOut: false };
|
|
3534
|
+
onEvent({ type: "output", data: `${result.output}
|
|
3535
|
+
`, label: "start-hooks" });
|
|
3536
|
+
onEvent({ type: "complete", exitCode: result.exitCode, timedOut: result.timedOut });
|
|
3537
|
+
return result;
|
|
3431
3538
|
}
|
|
3432
3539
|
this.hooksRunning = true;
|
|
3433
3540
|
this.hooksCompleted = false;
|
|
3541
|
+
this.hooksFailed = false;
|
|
3434
3542
|
try {
|
|
3435
3543
|
await mkdir5(join9(homedir7(), ".replicas"), { recursive: true });
|
|
3436
|
-
await
|
|
3544
|
+
await writeFile4(
|
|
3437
3545
|
START_HOOKS_LOG,
|
|
3438
3546
|
`=== Start Hooks Execution Log ===
|
|
3439
3547
|
Started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3548
|
+
Environment hook: ${envHookContent ? "yes" : "no"}
|
|
3440
3549
|
Repositories: ${hookEntries.length}
|
|
3441
3550
|
|
|
3442
3551
|
`,
|
|
3443
3552
|
"utf-8"
|
|
3444
3553
|
);
|
|
3554
|
+
if (envHookContent) {
|
|
3555
|
+
const envResult = await this.executeEnvironmentStartHook({
|
|
3556
|
+
content: envHookContent,
|
|
3557
|
+
timeoutMs: params.timeoutMs,
|
|
3558
|
+
onOutputChunk: (chunk) => onEvent({ type: "output", data: chunk, label: "environment" })
|
|
3559
|
+
});
|
|
3560
|
+
recordResult(envResult);
|
|
3561
|
+
} else {
|
|
3562
|
+
await environmentDetailsService.setEnvironmentStartHook("n/a");
|
|
3563
|
+
}
|
|
3445
3564
|
const repos = await gitService.listRepositories();
|
|
3446
3565
|
const reposWithHooks = new Set(hookEntries.map((entry) => entry.repoName));
|
|
3447
3566
|
for (const repo of repos) {
|
|
@@ -3455,48 +3574,53 @@ Repositories: ${hookEntries.length}
|
|
|
3455
3574
|
continue;
|
|
3456
3575
|
}
|
|
3457
3576
|
const persistedRepoState = await loadRepoState(entry.repoName);
|
|
3458
|
-
if (persistedRepoState?.startHooksCompleted) {
|
|
3577
|
+
if (!params.ignorePersistedState && persistedRepoState?.startHooksCompleted) {
|
|
3459
3578
|
await this.logToFile(`[${entry.repoName}] Start hooks already completed in this workspace lifecycle, skipping`);
|
|
3460
3579
|
await environmentDetailsService.setRepositoryStartHook(entry.repoName, "yes");
|
|
3461
3580
|
continue;
|
|
3462
3581
|
}
|
|
3463
|
-
const timeout = startHookConfig.timeout ?? DEFAULT_START_HOOK_TIMEOUT_MS;
|
|
3582
|
+
const timeout = params.timeoutMs ?? startHookConfig.timeout ?? DEFAULT_START_HOOK_TIMEOUT_MS;
|
|
3464
3583
|
let repoFailed = false;
|
|
3465
3584
|
let lastExitCode = 0;
|
|
3466
3585
|
let repoTimedOut = false;
|
|
3467
3586
|
const repoOutput = [];
|
|
3587
|
+
const onRepoOutput = (chunk) => onEvent({ type: "output", data: chunk, label: `repo:${entry.repoName}` });
|
|
3468
3588
|
await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
|
|
3469
3589
|
if (startHookConfig.separate === false) {
|
|
3470
3590
|
const combinedScript = `set -e
|
|
3471
3591
|
${startHookConfig.commands.join("\n")}`;
|
|
3472
|
-
const
|
|
3592
|
+
const result2 = await this.execStartHookCommand({
|
|
3473
3593
|
command: combinedScript,
|
|
3474
3594
|
label: "combined script",
|
|
3475
3595
|
repoName: entry.repoName,
|
|
3476
3596
|
cwd: entry.workingDirectory,
|
|
3477
|
-
timeout
|
|
3597
|
+
timeout,
|
|
3598
|
+
onOutputChunk: onRepoOutput
|
|
3478
3599
|
});
|
|
3479
|
-
repoOutput.push(...
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3600
|
+
repoOutput.push(...result2.output);
|
|
3601
|
+
recordResult(result2);
|
|
3602
|
+
if (result2.exitCode !== 0) {
|
|
3603
|
+
lastExitCode = result2.exitCode;
|
|
3604
|
+
repoTimedOut = result2.timedOut;
|
|
3483
3605
|
this.hooksFailed = true;
|
|
3484
3606
|
repoFailed = true;
|
|
3485
3607
|
await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
|
|
3486
3608
|
}
|
|
3487
3609
|
} else {
|
|
3488
3610
|
for (const hook of startHookConfig.commands) {
|
|
3489
|
-
const
|
|
3611
|
+
const result2 = await this.execStartHookCommand({
|
|
3490
3612
|
command: hook,
|
|
3491
3613
|
label: hook,
|
|
3492
3614
|
repoName: entry.repoName,
|
|
3493
3615
|
cwd: entry.workingDirectory,
|
|
3494
|
-
timeout
|
|
3616
|
+
timeout,
|
|
3617
|
+
onOutputChunk: onRepoOutput
|
|
3495
3618
|
});
|
|
3496
|
-
repoOutput.push(...
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3619
|
+
repoOutput.push(...result2.output);
|
|
3620
|
+
recordResult(result2);
|
|
3621
|
+
if (result2.exitCode !== 0) {
|
|
3622
|
+
lastExitCode = result2.exitCode;
|
|
3623
|
+
repoTimedOut = result2.timedOut;
|
|
3500
3624
|
this.hooksFailed = true;
|
|
3501
3625
|
repoFailed = true;
|
|
3502
3626
|
await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
|
|
@@ -3511,20 +3635,30 @@ ${startHookConfig.commands.join("\n")}`;
|
|
|
3511
3635
|
timedOut: repoTimedOut,
|
|
3512
3636
|
executedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3513
3637
|
});
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3638
|
+
if ((params.markRepoCompleted ?? true) && !repoFailed) {
|
|
3639
|
+
const fallbackRepoState = persistedRepoState ?? {
|
|
3640
|
+
name: entry.repoName,
|
|
3641
|
+
path: entry.workingDirectory,
|
|
3642
|
+
defaultBranch: entry.defaultBranch,
|
|
3643
|
+
currentBranch: entry.defaultBranch,
|
|
3644
|
+
prUrls: [],
|
|
3645
|
+
gitDiff: null,
|
|
3646
|
+
startHooksCompleted: false
|
|
3647
|
+
};
|
|
3648
|
+
await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
|
|
3649
|
+
}
|
|
3524
3650
|
await environmentDetailsService.setRepositoryStartHook(entry.repoName, repoFailed ? "no" : "yes");
|
|
3525
3651
|
}
|
|
3526
3652
|
this.hooksCompleted = true;
|
|
3653
|
+
this.hooksFailed = overallExitCode !== 0;
|
|
3527
3654
|
await this.logToFile(`=== All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
|
|
3655
|
+
const result = {
|
|
3656
|
+
exitCode: overallExitCode,
|
|
3657
|
+
output: outputBlocks.join("\n\n"),
|
|
3658
|
+
timedOut: overallTimedOut
|
|
3659
|
+
};
|
|
3660
|
+
onEvent({ type: "complete", exitCode: result.exitCode, timedOut: result.timedOut });
|
|
3661
|
+
return result;
|
|
3528
3662
|
} catch (error) {
|
|
3529
3663
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3530
3664
|
this.hooksFailed = true;
|
|
@@ -3536,6 +3670,16 @@ ${startHookConfig.commands.join("\n")}`;
|
|
|
3536
3670
|
this.hooksRunning = false;
|
|
3537
3671
|
}
|
|
3538
3672
|
}
|
|
3673
|
+
async runStartHooksStreaming(params) {
|
|
3674
|
+
await this.loadConfigs();
|
|
3675
|
+
return this.executeStartHooks({
|
|
3676
|
+
environmentStartHook: params.environmentStartHook,
|
|
3677
|
+
timeoutMs: params.timeoutMs,
|
|
3678
|
+
ignorePersistedState: true,
|
|
3679
|
+
markRepoCompleted: false,
|
|
3680
|
+
onEvent: params.onEvent
|
|
3681
|
+
});
|
|
3682
|
+
}
|
|
3539
3683
|
/**
|
|
3540
3684
|
* Check if start hooks are currently running.
|
|
3541
3685
|
*/
|
|
@@ -3548,14 +3692,18 @@ ${startHookConfig.commands.join("\n")}`;
|
|
|
3548
3692
|
didHooksFail() {
|
|
3549
3693
|
return this.hooksFailed;
|
|
3550
3694
|
}
|
|
3551
|
-
/**
|
|
3552
|
-
* Get aggregated start hook metadata.
|
|
3553
|
-
*/
|
|
3554
3695
|
getStartHookConfig() {
|
|
3555
|
-
const commands =
|
|
3696
|
+
const commands = [];
|
|
3697
|
+
const envHookContent = ENGINE_ENV.REPLICAS_ENV_START_HOOK?.trim();
|
|
3698
|
+
if (envHookContent) {
|
|
3699
|
+
commands.push(`[environment] ${envHookContent}`);
|
|
3700
|
+
}
|
|
3701
|
+
for (const entry of this.configs) {
|
|
3556
3702
|
const repoCommands = entry.config.startHook?.commands ?? [];
|
|
3557
|
-
|
|
3558
|
-
|
|
3703
|
+
for (const command of repoCommands) {
|
|
3704
|
+
commands.push(`[${entry.repoName}] ${command}`);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3559
3707
|
if (commands.length === 0) {
|
|
3560
3708
|
return void 0;
|
|
3561
3709
|
}
|
|
@@ -3619,7 +3767,7 @@ var EventService = class {
|
|
|
3619
3767
|
var eventService = new EventService();
|
|
3620
3768
|
|
|
3621
3769
|
// src/services/preview-service.ts
|
|
3622
|
-
import { mkdir as mkdir7, readFile as readFile5
|
|
3770
|
+
import { mkdir as mkdir7, readFile as readFile5 } from "fs/promises";
|
|
3623
3771
|
import { existsSync as existsSync5 } from "fs";
|
|
3624
3772
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3625
3773
|
import { homedir as homedir9 } from "os";
|
|
@@ -3639,8 +3787,8 @@ async function readPreviewsFile() {
|
|
|
3639
3787
|
async function writePreviewsFile(data) {
|
|
3640
3788
|
const dir = dirname(PREVIEW_PORTS_FILE);
|
|
3641
3789
|
await mkdir7(dir, { recursive: true });
|
|
3642
|
-
await
|
|
3643
|
-
|
|
3790
|
+
await atomicWriteFile(PREVIEW_PORTS_FILE, `${JSON.stringify(data, null, 2)}
|
|
3791
|
+
`);
|
|
3644
3792
|
}
|
|
3645
3793
|
var PreviewService = class {
|
|
3646
3794
|
async initialize() {
|
|
@@ -3692,7 +3840,7 @@ var previewService = new PreviewService();
|
|
|
3692
3840
|
|
|
3693
3841
|
// src/services/chat/chat-service.ts
|
|
3694
3842
|
import { existsSync as existsSync7 } from "fs";
|
|
3695
|
-
import { appendFile as appendFile5, mkdir as mkdir11, readFile as readFile8,
|
|
3843
|
+
import { appendFile as appendFile5, copyFile, mkdir as mkdir11, readFile as readFile8, rename as rename2, rm } from "fs/promises";
|
|
3696
3844
|
import { homedir as homedir13 } from "os";
|
|
3697
3845
|
import { join as join15 } from "path";
|
|
3698
3846
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
@@ -4245,7 +4393,7 @@ function extractPlanFromCodexAspNotification(notification) {
|
|
|
4245
4393
|
|
|
4246
4394
|
// src/utils/image-utils.ts
|
|
4247
4395
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
4248
|
-
import { mkdir as mkdir8, unlink as unlink2, writeFile as
|
|
4396
|
+
import { mkdir as mkdir8, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
4249
4397
|
import { homedir as homedir10 } from "os";
|
|
4250
4398
|
import { join as join12 } from "path";
|
|
4251
4399
|
function isImageMediaType(value) {
|
|
@@ -4333,7 +4481,7 @@ async function saveNormalizedImagesToTempFiles(images, tempImageDir = join12(hom
|
|
|
4333
4481
|
const ext = image.source.media_type.split("/")[1] || "png";
|
|
4334
4482
|
const filename = `img_${randomUUID3()}.${ext}`;
|
|
4335
4483
|
const filepath = join12(tempImageDir, filename);
|
|
4336
|
-
await
|
|
4484
|
+
await writeFile5(filepath, Buffer.from(image.source.data, "base64"));
|
|
4337
4485
|
tempPaths.push(filepath);
|
|
4338
4486
|
}
|
|
4339
4487
|
} catch (error) {
|
|
@@ -5359,7 +5507,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
|
|
|
5359
5507
|
};
|
|
5360
5508
|
|
|
5361
5509
|
// src/managers/codex-asp/app-server-process.ts
|
|
5362
|
-
import { spawn } from "child_process";
|
|
5510
|
+
import { spawn as spawn2 } from "child_process";
|
|
5363
5511
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
5364
5512
|
|
|
5365
5513
|
// src/managers/codex-asp/asp-client.ts
|
|
@@ -5581,7 +5729,7 @@ var AppServerProcess = class {
|
|
|
5581
5729
|
return { client: this.client };
|
|
5582
5730
|
}
|
|
5583
5731
|
this.shuttingDown = false;
|
|
5584
|
-
const child =
|
|
5732
|
+
const child = spawn2(this.binary, this.args, {
|
|
5585
5733
|
cwd: this.cwd,
|
|
5586
5734
|
env: this.env,
|
|
5587
5735
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -5831,6 +5979,7 @@ var THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear";
|
|
|
5831
5979
|
var TURN_START_METHOD = "turn/start";
|
|
5832
5980
|
var TURN_INTERRUPT_METHOD = "turn/interrupt";
|
|
5833
5981
|
var ACCOUNT_RATE_LIMITS_READ_METHOD = "account/rateLimits/read";
|
|
5982
|
+
var MAX_CODEX_ASP_TRANSCRIPT_OUTPUT_CHARS = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS;
|
|
5834
5983
|
function toReasoningEffort(thinkingLevel) {
|
|
5835
5984
|
return codexReasoningEffortForThinkingLevel(thinkingLevel);
|
|
5836
5985
|
}
|
|
@@ -5852,7 +6001,7 @@ function timestampFromMilliseconds(value) {
|
|
|
5852
6001
|
}
|
|
5853
6002
|
function stringifyToolOutput(value) {
|
|
5854
6003
|
if (value === null || value === void 0) return void 0;
|
|
5855
|
-
if (typeof value === "string") return value;
|
|
6004
|
+
if (typeof value === "string") return truncateCodexAspTranscriptOutput(value);
|
|
5856
6005
|
if (typeof value === "object" && "content" in value && Array.isArray(value.content)) {
|
|
5857
6006
|
const text = value.content.map((item) => {
|
|
5858
6007
|
if (typeof item === "string") return item;
|
|
@@ -5861,14 +6010,24 @@ function stringifyToolOutput(value) {
|
|
|
5861
6010
|
}
|
|
5862
6011
|
return "";
|
|
5863
6012
|
}).filter(Boolean).join("\n");
|
|
5864
|
-
if (text) return text;
|
|
6013
|
+
if (text) return truncateCodexAspTranscriptOutput(text);
|
|
5865
6014
|
}
|
|
5866
6015
|
try {
|
|
5867
|
-
return JSON.stringify(value);
|
|
6016
|
+
return truncateCodexAspTranscriptOutput(JSON.stringify(value));
|
|
5868
6017
|
} catch {
|
|
5869
|
-
return String(value);
|
|
6018
|
+
return truncateCodexAspTranscriptOutput(String(value));
|
|
5870
6019
|
}
|
|
5871
6020
|
}
|
|
6021
|
+
function truncateCodexAspTranscriptOutput(output, maxChars = MAX_CODEX_ASP_TRANSCRIPT_OUTPUT_CHARS) {
|
|
6022
|
+
return buildOutputPreview(output, {
|
|
6023
|
+
maxChars,
|
|
6024
|
+
truncateFrom: "tail",
|
|
6025
|
+
marker: "\n[output truncated: showing tail of output]\n"
|
|
6026
|
+
}).output;
|
|
6027
|
+
}
|
|
6028
|
+
function appendCodexAspTranscriptOutput(current, delta, maxChars = MAX_CODEX_ASP_TRANSCRIPT_OUTPUT_CHARS) {
|
|
6029
|
+
return truncateCodexAspTranscriptOutput(`${current ?? ""}${delta}`, maxChars);
|
|
6030
|
+
}
|
|
5872
6031
|
function transcriptPatchOperation(change) {
|
|
5873
6032
|
return {
|
|
5874
6033
|
action: change.kind.type,
|
|
@@ -5915,7 +6074,7 @@ function itemToTranscriptItem(item, timestamp, status) {
|
|
|
5915
6074
|
type: "commandExecution",
|
|
5916
6075
|
id: item.id,
|
|
5917
6076
|
command: item.command,
|
|
5918
|
-
...item.aggregatedOutput ? { output: item.aggregatedOutput } : {},
|
|
6077
|
+
...item.aggregatedOutput ? { output: truncateCodexAspTranscriptOutput(item.aggregatedOutput) } : {},
|
|
5919
6078
|
exitCode,
|
|
5920
6079
|
timestamp,
|
|
5921
6080
|
status: normalizeCodexAspTranscriptStatus(item.status, typeof exitCode === "number" && exitCode !== 0)
|
|
@@ -5939,7 +6098,7 @@ function itemToTranscriptItem(item, timestamp, status) {
|
|
|
5939
6098
|
server: item.server,
|
|
5940
6099
|
tool: item.tool,
|
|
5941
6100
|
input: item.arguments,
|
|
5942
|
-
output: item.error?.message
|
|
6101
|
+
output: item.error?.message ? truncateCodexAspTranscriptOutput(item.error.message) : stringifyToolOutput(item.result),
|
|
5943
6102
|
timestamp,
|
|
5944
6103
|
status: normalizeCodexAspTranscriptStatus(item.status, item.status === "failed")
|
|
5945
6104
|
};
|
|
@@ -6953,7 +7112,7 @@ var CodexAspManager = class extends CodingAgentManager {
|
|
|
6953
7112
|
if (item.type !== "commandExecution" && item.type !== "fileChange") return;
|
|
6954
7113
|
turn.items[itemIndex] = {
|
|
6955
7114
|
...item,
|
|
6956
|
-
output:
|
|
7115
|
+
output: appendCodexAspTranscriptOutput(item.output, delta),
|
|
6957
7116
|
status: "in_progress"
|
|
6958
7117
|
};
|
|
6959
7118
|
this.codexAspTranscript.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -7055,7 +7214,7 @@ var CodexAspManager = class extends CodingAgentManager {
|
|
|
7055
7214
|
|
|
7056
7215
|
// src/managers/codex-manager.ts
|
|
7057
7216
|
import { Codex } from "@openai/codex-sdk";
|
|
7058
|
-
import { readdir as readdir3, stat as stat2, writeFile as
|
|
7217
|
+
import { readdir as readdir3, stat as stat2, writeFile as writeFile6, mkdir as mkdir10, readFile as readFile7 } from "fs/promises";
|
|
7059
7218
|
import { existsSync as existsSync6 } from "fs";
|
|
7060
7219
|
import { join as join14 } from "path";
|
|
7061
7220
|
import { homedir as homedir12 } from "os";
|
|
@@ -7159,7 +7318,7 @@ var CodexManager = class extends CodingAgentManager {
|
|
|
7159
7318
|
delete config.developer_instructions;
|
|
7160
7319
|
}
|
|
7161
7320
|
const tomlContent = stringifyToml(config);
|
|
7162
|
-
await
|
|
7321
|
+
await writeFile6(CODEX_CONFIG_PATH, tomlContent, "utf-8");
|
|
7163
7322
|
console.log("[CodexManager] Updated config.toml with developer_instructions");
|
|
7164
7323
|
} catch (error) {
|
|
7165
7324
|
console.error("[CodexManager] Failed to update config.toml:", error);
|
|
@@ -8070,6 +8229,7 @@ var CLAUDE_HISTORY_DIR = join15(ENGINE_DIR2, "claude-histories");
|
|
|
8070
8229
|
var RELAY_HISTORY_DIR = join15(ENGINE_DIR2, "relay-histories");
|
|
8071
8230
|
var CHAT_SENDERS_DIR = join15(ENGINE_DIR2, "chat-senders");
|
|
8072
8231
|
var CODEX_AUTH_PATH2 = join15(homedir13(), ".codex", "auth.json");
|
|
8232
|
+
var CHATS_BACKUP_FILE = `${CHATS_FILE}.bak`;
|
|
8073
8233
|
function isChatMessageSender(value) {
|
|
8074
8234
|
if (!isRecord4(value)) return false;
|
|
8075
8235
|
return typeof value.senderUserId === "string" && typeof value.senderEmail === "string" && typeof value.recordedAt === "string";
|
|
@@ -8124,6 +8284,16 @@ function normalizePersistedChat(chat) {
|
|
|
8124
8284
|
...chat.provider === "codex" ? { codexBackend: codexBackendForChat(chat) } : {}
|
|
8125
8285
|
};
|
|
8126
8286
|
}
|
|
8287
|
+
function parsePersistedChatsContent(content) {
|
|
8288
|
+
const parsed = JSON.parse(content);
|
|
8289
|
+
if (!Array.isArray(parsed)) {
|
|
8290
|
+
return [];
|
|
8291
|
+
}
|
|
8292
|
+
return parsed.filter((entry) => isPersistedChat(entry)).map((entry) => normalizePersistedChat(entry));
|
|
8293
|
+
}
|
|
8294
|
+
function corruptChatsFilePath() {
|
|
8295
|
+
return `${CHATS_FILE}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
|
|
8296
|
+
}
|
|
8127
8297
|
function createUserMessageEvent(message, messageId) {
|
|
8128
8298
|
return {
|
|
8129
8299
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -8615,23 +8785,44 @@ var ChatService = class {
|
|
|
8615
8785
|
async loadChats() {
|
|
8616
8786
|
try {
|
|
8617
8787
|
const content = await readFile8(CHATS_FILE, "utf-8");
|
|
8618
|
-
|
|
8619
|
-
if (!Array.isArray(parsed)) {
|
|
8620
|
-
return [];
|
|
8621
|
-
}
|
|
8622
|
-
return parsed.filter((entry) => isPersistedChat(entry)).map((entry) => normalizePersistedChat(entry));
|
|
8788
|
+
return parsePersistedChatsContent(content);
|
|
8623
8789
|
} catch (error) {
|
|
8624
8790
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
8625
8791
|
return [];
|
|
8626
8792
|
}
|
|
8627
|
-
|
|
8793
|
+
const quarantinePath = corruptChatsFilePath();
|
|
8794
|
+
console.error(`[ChatService] Failed to load ${CHATS_FILE}; quarantining and trying backup:`, error);
|
|
8795
|
+
try {
|
|
8796
|
+
await rename2(CHATS_FILE, quarantinePath);
|
|
8797
|
+
console.error(`[ChatService] Quarantined corrupt chats file at ${quarantinePath}`);
|
|
8798
|
+
} catch (renameError) {
|
|
8799
|
+
console.error("[ChatService] Failed to quarantine corrupt chats file:", renameError);
|
|
8800
|
+
}
|
|
8801
|
+
try {
|
|
8802
|
+
const backupContent = await readFile8(CHATS_BACKUP_FILE, "utf-8");
|
|
8803
|
+
return parsePersistedChatsContent(backupContent);
|
|
8804
|
+
} catch (backupError) {
|
|
8805
|
+
if (backupError && typeof backupError === "object" && "code" in backupError && backupError.code === "ENOENT") {
|
|
8806
|
+
return [];
|
|
8807
|
+
}
|
|
8808
|
+
console.error(`[ChatService] Failed to load backup ${CHATS_BACKUP_FILE}; starting with defaults:`, backupError);
|
|
8809
|
+
return [];
|
|
8810
|
+
}
|
|
8628
8811
|
}
|
|
8629
8812
|
}
|
|
8630
8813
|
async persistAllChats() {
|
|
8631
8814
|
this.writeChain = this.writeChain.catch(() => {
|
|
8632
8815
|
}).then(async () => {
|
|
8633
8816
|
const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
|
|
8634
|
-
|
|
8817
|
+
try {
|
|
8818
|
+
await copyFile(CHATS_FILE, CHATS_BACKUP_FILE);
|
|
8819
|
+
} catch (error) {
|
|
8820
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
|
|
8821
|
+
console.error("[ChatService] Failed to update chats backup:", error);
|
|
8822
|
+
}
|
|
8823
|
+
}
|
|
8824
|
+
await atomicWriteFile(CHATS_FILE, `${JSON.stringify(payload, null, 2)}
|
|
8825
|
+
`);
|
|
8635
8826
|
});
|
|
8636
8827
|
await this.writeChain;
|
|
8637
8828
|
}
|
|
@@ -9011,14 +9202,14 @@ var PlanService = class {
|
|
|
9011
9202
|
var planService = new PlanService();
|
|
9012
9203
|
|
|
9013
9204
|
// src/services/warm-hooks-service.ts
|
|
9014
|
-
import { spawn as
|
|
9205
|
+
import { spawn as spawn3 } from "child_process";
|
|
9015
9206
|
import { readFile as readFile12 } from "fs/promises";
|
|
9016
9207
|
import { existsSync as existsSync8 } from "fs";
|
|
9017
9208
|
import { join as join19 } from "path";
|
|
9018
9209
|
|
|
9019
9210
|
// src/services/warm-hook-logs-service.ts
|
|
9020
9211
|
import { createHash as createHash2 } from "crypto";
|
|
9021
|
-
import { mkdir as mkdir12, readFile as readFile11, writeFile as
|
|
9212
|
+
import { mkdir as mkdir12, readFile as readFile11, writeFile as writeFile7, readdir as readdir5, appendFile as appendFile6, unlink as unlink3 } from "fs/promises";
|
|
9022
9213
|
import { homedir as homedir15 } from "os";
|
|
9023
9214
|
import { join as join18 } from "path";
|
|
9024
9215
|
var LOGS_DIR2 = join18(homedir15(), ".replicas", "warm-hook-logs");
|
|
@@ -9052,7 +9243,7 @@ var WarmHookLogsService = class {
|
|
|
9052
9243
|
hookName: "organization",
|
|
9053
9244
|
...entry
|
|
9054
9245
|
};
|
|
9055
|
-
await
|
|
9246
|
+
await writeFile7(join18(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
|
|
9056
9247
|
`, "utf-8");
|
|
9057
9248
|
}
|
|
9058
9249
|
async saveEnvironmentHookLog(entry) {
|
|
@@ -9062,7 +9253,7 @@ var WarmHookLogsService = class {
|
|
|
9062
9253
|
hookName: "environment",
|
|
9063
9254
|
...entry
|
|
9064
9255
|
};
|
|
9065
|
-
await
|
|
9256
|
+
await writeFile7(join18(LOGS_DIR2, environmentFilename()), `${JSON.stringify(log, null, 2)}
|
|
9066
9257
|
`, "utf-8");
|
|
9067
9258
|
}
|
|
9068
9259
|
async saveRepoHookLog(repoName, entry) {
|
|
@@ -9072,7 +9263,7 @@ var WarmHookLogsService = class {
|
|
|
9072
9263
|
hookName: repoName,
|
|
9073
9264
|
...entry
|
|
9074
9265
|
};
|
|
9075
|
-
await
|
|
9266
|
+
await writeFile7(join18(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
|
|
9076
9267
|
`, "utf-8");
|
|
9077
9268
|
}
|
|
9078
9269
|
async getAllLogs() {
|
|
@@ -9200,7 +9391,7 @@ async function executeHookScriptStreaming(params) {
|
|
|
9200
9391
|
params.onChunk(`$ ${params.label}
|
|
9201
9392
|
`);
|
|
9202
9393
|
return new Promise((resolve3) => {
|
|
9203
|
-
const proc =
|
|
9394
|
+
const proc = spawn3("bash", ["-lc", params.content], {
|
|
9204
9395
|
cwd: params.cwd,
|
|
9205
9396
|
env: process.env,
|
|
9206
9397
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -9855,6 +10046,64 @@ function createV1Routes(deps) {
|
|
|
9855
10046
|
);
|
|
9856
10047
|
}
|
|
9857
10048
|
});
|
|
10049
|
+
app2.post("/start-hooks/run/stream", async (c) => {
|
|
10050
|
+
try {
|
|
10051
|
+
const body = await c.req.json();
|
|
10052
|
+
const encoder = new TextEncoder();
|
|
10053
|
+
const stream = new ReadableStream({
|
|
10054
|
+
start: (controller) => {
|
|
10055
|
+
let closed = false;
|
|
10056
|
+
const safeEnqueue = (chunk) => {
|
|
10057
|
+
if (closed) return false;
|
|
10058
|
+
try {
|
|
10059
|
+
controller.enqueue(encoder.encode(chunk));
|
|
10060
|
+
return true;
|
|
10061
|
+
} catch {
|
|
10062
|
+
closed = true;
|
|
10063
|
+
return false;
|
|
10064
|
+
}
|
|
10065
|
+
};
|
|
10066
|
+
const heartbeat = setInterval(() => {
|
|
10067
|
+
if (!safeEnqueue(": ping\n\n")) clearInterval(heartbeat);
|
|
10068
|
+
}, 15e3);
|
|
10069
|
+
safeEnqueue(": connected\n\n");
|
|
10070
|
+
replicasConfigService.runStartHooksStreaming({
|
|
10071
|
+
environmentStartHook: body.environmentStartHook,
|
|
10072
|
+
timeoutMs: body.timeoutMs,
|
|
10073
|
+
onEvent: (event) => {
|
|
10074
|
+
safeEnqueue(`data: ${JSON.stringify(event)}
|
|
10075
|
+
|
|
10076
|
+
`);
|
|
10077
|
+
}
|
|
10078
|
+
}).then(() => {
|
|
10079
|
+
clearInterval(heartbeat);
|
|
10080
|
+
if (!closed) {
|
|
10081
|
+
closed = true;
|
|
10082
|
+
controller.close();
|
|
10083
|
+
}
|
|
10084
|
+
}).catch((err) => {
|
|
10085
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
10086
|
+
safeEnqueue(`data: ${JSON.stringify({ type: "error", data: message })}
|
|
10087
|
+
|
|
10088
|
+
`);
|
|
10089
|
+
clearInterval(heartbeat);
|
|
10090
|
+
if (!closed) {
|
|
10091
|
+
closed = true;
|
|
10092
|
+
controller.close();
|
|
10093
|
+
}
|
|
10094
|
+
});
|
|
10095
|
+
}
|
|
10096
|
+
});
|
|
10097
|
+
return new Response(stream, {
|
|
10098
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }
|
|
10099
|
+
});
|
|
10100
|
+
} catch (error) {
|
|
10101
|
+
return c.json(
|
|
10102
|
+
jsonError("Failed to run start hooks", error instanceof Error ? error.message : "Unknown error"),
|
|
10103
|
+
500
|
|
10104
|
+
);
|
|
10105
|
+
}
|
|
10106
|
+
});
|
|
9858
10107
|
app2.get("/warm-hooks/logs", async (c) => {
|
|
9859
10108
|
try {
|
|
9860
10109
|
const logs = await warmHookLogsService.getAllLogs();
|