libretto 0.6.10 → 0.6.12
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/README.md +4 -0
- package/README.template.md +4 -0
- package/dist/cli/cli.js +4 -3
- package/dist/cli/commands/ai.js +3 -2
- package/dist/cli/commands/browser.js +17 -17
- package/dist/cli/commands/execution.js +254 -234
- package/dist/cli/commands/experiments.js +100 -0
- package/dist/cli/commands/setup.js +20 -34
- package/dist/cli/commands/shared.js +10 -0
- package/dist/cli/commands/snapshot.js +81 -9
- package/dist/cli/commands/status.js +5 -4
- package/dist/cli/core/ai-model.js +6 -3
- package/dist/cli/core/browser.js +300 -121
- package/dist/cli/core/config.js +4 -2
- package/dist/cli/core/context.js +4 -0
- package/dist/cli/core/daemon/config.js +0 -6
- package/dist/cli/core/daemon/daemon.js +535 -89
- package/dist/cli/core/daemon/ipc.js +170 -129
- package/dist/cli/core/daemon/snapshot.js +72 -6
- package/dist/cli/core/experiments.js +66 -0
- package/dist/cli/core/session.js +5 -4
- package/dist/cli/core/skill-version.js +2 -1
- package/dist/cli/core/snapshot-analyzer.js +4 -3
- package/dist/cli/core/workflow-runner/runner.js +147 -0
- package/dist/cli/core/workflow-runtime.js +60 -0
- package/dist/cli/router.js +4 -1
- package/dist/shared/debug/pause-handler.d.ts +9 -0
- package/dist/shared/debug/pause-handler.js +15 -0
- package/dist/shared/debug/pause.d.ts +1 -2
- package/dist/shared/debug/pause.js +13 -36
- package/dist/shared/ipc/child-process-transport.d.ts +7 -0
- package/dist/shared/ipc/child-process-transport.js +60 -0
- package/dist/shared/ipc/child-process-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/child-process-transport.spec.js +68 -0
- package/dist/shared/ipc/ipc.d.ts +46 -0
- package/dist/shared/ipc/ipc.js +165 -0
- package/dist/shared/ipc/ipc.spec.d.ts +2 -0
- package/dist/shared/ipc/ipc.spec.js +114 -0
- package/dist/shared/ipc/socket-transport.d.ts +9 -0
- package/dist/shared/ipc/socket-transport.js +143 -0
- package/dist/shared/ipc/socket-transport.spec.d.ts +2 -0
- package/dist/shared/ipc/socket-transport.spec.js +117 -0
- package/dist/shared/package-manager.d.ts +7 -0
- package/dist/shared/package-manager.js +60 -0
- package/dist/shared/paths/paths.d.ts +1 -8
- package/dist/shared/paths/paths.js +1 -49
- package/dist/shared/snapshot/capture-snapshot.d.ts +9 -0
- package/dist/shared/snapshot/capture-snapshot.js +463 -0
- package/dist/shared/snapshot/diff-snapshots.d.ts +72 -0
- package/dist/shared/snapshot/diff-snapshots.js +358 -0
- package/dist/shared/snapshot/render-snapshot.d.ts +39 -0
- package/dist/shared/snapshot/render-snapshot.js +651 -0
- package/dist/shared/snapshot/snapshot.spec.d.ts +2 -0
- package/dist/shared/snapshot/snapshot.spec.js +333 -0
- package/dist/shared/snapshot/types.d.ts +40 -0
- package/dist/shared/snapshot/types.js +0 -0
- package/dist/shared/snapshot/wait-for-page-stable.d.ts +17 -0
- package/dist/shared/snapshot/wait-for-page-stable.js +281 -0
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +1 -0
- package/docs/experiments.md +67 -0
- package/package.json +4 -2
- package/skills/libretto/SKILL.md +3 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/AGENTS.md +7 -0
- package/src/cli/cli.ts +4 -3
- package/src/cli/commands/ai.ts +3 -2
- package/src/cli/commands/browser.ts +13 -11
- package/src/cli/commands/execution.ts +303 -271
- package/src/cli/commands/experiments.ts +120 -0
- package/src/cli/commands/setup.ts +18 -36
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/snapshot.ts +99 -11
- package/src/cli/commands/status.ts +5 -4
- package/src/cli/core/ai-model.ts +6 -3
- package/src/cli/core/browser.ts +369 -147
- package/src/cli/core/config.ts +3 -1
- package/src/cli/core/context.ts +4 -0
- package/src/cli/core/daemon/config.ts +35 -19
- package/src/cli/core/daemon/daemon.ts +686 -106
- package/src/cli/core/daemon/ipc.ts +330 -214
- package/src/cli/core/daemon/snapshot.ts +106 -8
- package/src/cli/core/experiments.ts +85 -0
- package/src/cli/core/session.ts +5 -4
- package/src/cli/core/skill-version.ts +2 -1
- package/src/cli/core/snapshot-analyzer.ts +4 -3
- package/src/cli/core/workflow-runner/runner.ts +237 -0
- package/src/cli/core/workflow-runtime.ts +85 -0
- package/src/cli/router.ts +4 -1
- package/src/shared/debug/pause-handler.ts +20 -0
- package/src/shared/debug/pause.ts +14 -48
- package/src/shared/ipc/AGENTS.md +24 -0
- package/src/shared/ipc/child-process-transport.spec.ts +86 -0
- package/src/shared/ipc/child-process-transport.ts +96 -0
- package/src/shared/ipc/ipc.spec.ts +161 -0
- package/src/shared/ipc/ipc.ts +288 -0
- package/src/shared/ipc/socket-transport.spec.ts +141 -0
- package/src/shared/ipc/socket-transport.ts +189 -0
- package/src/shared/package-manager.ts +76 -0
- package/src/shared/paths/paths.ts +0 -72
- package/src/shared/snapshot/capture-snapshot.ts +615 -0
- package/src/shared/snapshot/diff-snapshots.ts +579 -0
- package/src/shared/snapshot/render-snapshot.ts +962 -0
- package/src/shared/snapshot/snapshot.spec.ts +388 -0
- package/src/shared/snapshot/types.ts +43 -0
- package/src/shared/snapshot/wait-for-page-stable.ts +425 -0
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/daemon/index.js +0 -16
- package/dist/cli/core/daemon/spawn.js +0 -90
- package/dist/cli/core/pause-signals.js +0 -29
- package/dist/cli/workers/run-integration-runtime.js +0 -235
- package/dist/cli/workers/run-integration-worker-protocol.js +0 -17
- package/dist/cli/workers/run-integration-worker.js +0 -64
- package/src/cli/core/daemon/index.ts +0 -24
- package/src/cli/core/daemon/spawn.ts +0 -171
- package/src/cli/core/pause-signals.ts +0 -35
- package/src/cli/workers/run-integration-runtime.ts +0 -326
- package/src/cli/workers/run-integration-worker-protocol.ts +0 -19
- package/src/cli/workers/run-integration-worker.ts +0 -72
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { writeFile } from "node:fs/promises";
|
|
3
|
-
import { cwd } from "node:process";
|
|
4
|
-
import { isAbsolute, resolve } from "node:path";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
getDefaultWorkflowFromModuleExports,
|
|
8
|
-
getWorkflowsFromModuleExports,
|
|
9
|
-
instrumentContext,
|
|
10
|
-
launchBrowser
|
|
11
|
-
} from "../../index.js";
|
|
12
|
-
import { parseSessionStateContent } from "../../shared/state/index.js";
|
|
13
|
-
import {
|
|
14
|
-
getProfilePath,
|
|
15
|
-
normalizeDomain,
|
|
16
|
-
normalizeUrl
|
|
17
|
-
} from "../core/browser.js";
|
|
18
|
-
import {
|
|
19
|
-
getSessionActionsLogPath,
|
|
20
|
-
getSessionNetworkLogPath,
|
|
21
|
-
getSessionStatePath
|
|
22
|
-
} from "../core/context.js";
|
|
23
|
-
import {
|
|
24
|
-
getPauseSignalPaths,
|
|
25
|
-
removeSignalIfExists
|
|
26
|
-
} from "../core/pause-signals.js";
|
|
27
|
-
import { installSessionTelemetry } from "../core/session-telemetry.js";
|
|
28
|
-
const FAILURE_HOLD_POLL_INTERVAL_MS = 250;
|
|
29
|
-
const TSCONFIG_HINT = "TypeScript compilation failed. Pass --tsconfig <path> to run against a specific tsconfig.";
|
|
30
|
-
function isTsxCompileError(error) {
|
|
31
|
-
return error instanceof Error && (error.name === "TransformError" || error.message.startsWith("Cannot resolve tsconfig at path:"));
|
|
32
|
-
}
|
|
33
|
-
function mirrorStdoutToFile(filePath) {
|
|
34
|
-
const stdout = process.stdout;
|
|
35
|
-
const originalWrite = stdout.write.bind(stdout);
|
|
36
|
-
stdout.write = ((chunk, ...args) => {
|
|
37
|
-
try {
|
|
38
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
39
|
-
appendFileSync(filePath, buffer);
|
|
40
|
-
} catch {
|
|
41
|
-
}
|
|
42
|
-
return originalWrite(chunk, ...args);
|
|
43
|
-
});
|
|
44
|
-
return () => {
|
|
45
|
-
stdout.write = originalWrite;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
function readSessionStatePid(session) {
|
|
49
|
-
const statePath = getSessionStatePath(session);
|
|
50
|
-
if (!existsSync(statePath)) return null;
|
|
51
|
-
try {
|
|
52
|
-
return parseSessionStateContent(readFileSync(statePath, "utf8"), statePath).pid ?? null;
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
async function waitForFailureSessionRelease(args) {
|
|
58
|
-
const { session, expectedPid, logger } = args;
|
|
59
|
-
logger.info("run-failure-session-hold", { session, expectedPid });
|
|
60
|
-
while (true) {
|
|
61
|
-
const currentPid = readSessionStatePid(session);
|
|
62
|
-
if (currentPid !== expectedPid) {
|
|
63
|
-
logger.info("run-failure-session-released", {
|
|
64
|
-
session,
|
|
65
|
-
expectedPid,
|
|
66
|
-
currentPid
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
await new Promise(
|
|
71
|
-
(resolveWait) => setTimeout(resolveWait, FAILURE_HOLD_POLL_INTERVAL_MS)
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function getMissingLocalAuthProfileError(args) {
|
|
76
|
-
return [
|
|
77
|
-
`Local auth profile not found for domain "${args.normalizedDomain}".`,
|
|
78
|
-
`Expected profile file: ${args.profilePath}`,
|
|
79
|
-
"To create it:",
|
|
80
|
-
` 1. libretto open https://${args.normalizedDomain} --headed --session ${args.session}`,
|
|
81
|
-
" 2. Log in manually in the browser window.",
|
|
82
|
-
` 3. libretto save ${args.normalizedDomain} --session ${args.session}`
|
|
83
|
-
].join("\n");
|
|
84
|
-
}
|
|
85
|
-
function getAbsoluteIntegrationPath(integrationPath) {
|
|
86
|
-
const absolutePath = isAbsolute(integrationPath) ? integrationPath : resolve(cwd(), integrationPath);
|
|
87
|
-
if (!existsSync(absolutePath)) {
|
|
88
|
-
throw new Error(`Integration file does not exist: ${absolutePath}`);
|
|
89
|
-
}
|
|
90
|
-
return absolutePath;
|
|
91
|
-
}
|
|
92
|
-
async function loadDefaultWorkflow(absolutePath) {
|
|
93
|
-
let loadedModule;
|
|
94
|
-
try {
|
|
95
|
-
loadedModule = await import(pathToFileURL(absolutePath).href);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
-
const compileHint = isTsxCompileError(error) ? `
|
|
99
|
-
${TSCONFIG_HINT}` : "";
|
|
100
|
-
throw new Error(
|
|
101
|
-
`Failed to import integration module at ${absolutePath}: ${message}${compileHint}`
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
const defaultWorkflow = getDefaultWorkflowFromModuleExports(loadedModule);
|
|
105
|
-
if (defaultWorkflow) {
|
|
106
|
-
return defaultWorkflow;
|
|
107
|
-
}
|
|
108
|
-
const availableWorkflowNames = getWorkflowsFromModuleExports(loadedModule).map(
|
|
109
|
-
(candidate) => candidate.name
|
|
110
|
-
);
|
|
111
|
-
if (availableWorkflowNames.length === 0) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
`No default-exported workflow found in ${absolutePath}. Export the workflow with \`export default workflow("name", handler)\`.`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
throw new Error(
|
|
117
|
-
`No default-exported workflow found in ${absolutePath}. libretto run only uses the file's default export. Available named workflows: ${availableWorkflowNames.join(", ")}`
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
async function installHeadedWorkflowVisualization(args) {
|
|
121
|
-
await (args.instrument ?? instrumentContext)(args.context, {
|
|
122
|
-
visualize: true,
|
|
123
|
-
logger: args.logger
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
async function runIntegrationInternal(args, options) {
|
|
127
|
-
const { logger } = options;
|
|
128
|
-
const absolutePath = getAbsoluteIntegrationPath(args.integrationPath);
|
|
129
|
-
const workflow = await loadDefaultWorkflow(absolutePath);
|
|
130
|
-
const signalPaths = getPauseSignalPaths(args.session);
|
|
131
|
-
await removeSignalIfExists(signalPaths.pausedSignalPath);
|
|
132
|
-
await removeSignalIfExists(signalPaths.resumeSignalPath);
|
|
133
|
-
await removeSignalIfExists(signalPaths.completedSignalPath);
|
|
134
|
-
await removeSignalIfExists(signalPaths.failedSignalPath);
|
|
135
|
-
const restoreStdout = mirrorStdoutToFile(signalPaths.outputSignalPath);
|
|
136
|
-
console.log(
|
|
137
|
-
`Running workflow "${workflow.name}" from ${absolutePath} (${args.headless ? "headless" : "headed"})...`
|
|
138
|
-
);
|
|
139
|
-
const integrationLogger = logger.withScope("integration-run", {
|
|
140
|
-
integrationPath: absolutePath,
|
|
141
|
-
workflowName: workflow.name,
|
|
142
|
-
session: args.session
|
|
143
|
-
});
|
|
144
|
-
const authProfileDomain = args.authProfileDomain;
|
|
145
|
-
const normalizedAuthProfileDomain = authProfileDomain ? normalizeDomain(normalizeUrl(authProfileDomain)) : void 0;
|
|
146
|
-
const storageStatePath = normalizedAuthProfileDomain ? getProfilePath(normalizedAuthProfileDomain) : void 0;
|
|
147
|
-
if (normalizedAuthProfileDomain && storageStatePath && !existsSync(storageStatePath)) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
getMissingLocalAuthProfileError({
|
|
150
|
-
normalizedDomain: normalizedAuthProfileDomain,
|
|
151
|
-
profilePath: storageStatePath,
|
|
152
|
-
session: args.session
|
|
153
|
-
})
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
const browserSession = await launchBrowser({
|
|
157
|
-
sessionName: args.session,
|
|
158
|
-
headless: args.headless,
|
|
159
|
-
storageStatePath,
|
|
160
|
-
viewport: args.viewport,
|
|
161
|
-
accessMode: args.accessMode,
|
|
162
|
-
cdpEndpoint: args.cdpEndpoint,
|
|
163
|
-
provider: args.provider
|
|
164
|
-
});
|
|
165
|
-
if (!args.headless && args.visualize !== false) {
|
|
166
|
-
await installHeadedWorkflowVisualization({
|
|
167
|
-
context: browserSession.context,
|
|
168
|
-
logger: integrationLogger
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
const actionsLogPath = getSessionActionsLogPath(args.session);
|
|
172
|
-
const networkLogPath = getSessionNetworkLogPath(args.session);
|
|
173
|
-
await installSessionTelemetry({
|
|
174
|
-
context: browserSession.context,
|
|
175
|
-
initialPage: browserSession.page,
|
|
176
|
-
includeUserDomActions: true,
|
|
177
|
-
logAction: (entry) => {
|
|
178
|
-
appendFileSync(actionsLogPath, JSON.stringify(entry) + "\n");
|
|
179
|
-
},
|
|
180
|
-
logNetwork: (entry) => {
|
|
181
|
-
appendFileSync(networkLogPath, JSON.stringify(entry) + "\n");
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
await browserSession.context.addInitScript(() => {
|
|
185
|
-
globalThis.__name = (target, value) => Object.defineProperty(target, "name", { value, configurable: true });
|
|
186
|
-
});
|
|
187
|
-
const workflowContext = {
|
|
188
|
-
session: args.session,
|
|
189
|
-
page: browserSession.page
|
|
190
|
-
};
|
|
191
|
-
try {
|
|
192
|
-
try {
|
|
193
|
-
await workflow.run(workflowContext, args.params ?? {});
|
|
194
|
-
} catch (error) {
|
|
195
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
196
|
-
await writeFile(
|
|
197
|
-
signalPaths.failedSignalPath,
|
|
198
|
-
JSON.stringify(
|
|
199
|
-
{
|
|
200
|
-
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
201
|
-
message: errorMessage,
|
|
202
|
-
phase: "workflow"
|
|
203
|
-
},
|
|
204
|
-
null,
|
|
205
|
-
2
|
|
206
|
-
),
|
|
207
|
-
"utf8"
|
|
208
|
-
);
|
|
209
|
-
await waitForFailureSessionRelease({
|
|
210
|
-
session: args.session,
|
|
211
|
-
expectedPid: process.pid,
|
|
212
|
-
logger
|
|
213
|
-
});
|
|
214
|
-
return { status: "failed-held" };
|
|
215
|
-
}
|
|
216
|
-
await writeFile(
|
|
217
|
-
signalPaths.completedSignalPath,
|
|
218
|
-
JSON.stringify({ completedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
219
|
-
"utf8"
|
|
220
|
-
);
|
|
221
|
-
return { status: "completed" };
|
|
222
|
-
} finally {
|
|
223
|
-
restoreStdout();
|
|
224
|
-
await browserSession.close();
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
async function runIntegrationFromFileInWorker(args, logger) {
|
|
228
|
-
return await runIntegrationInternal(args, {
|
|
229
|
-
logger
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
export {
|
|
233
|
-
installHeadedWorkflowVisualization,
|
|
234
|
-
runIntegrationFromFileInWorker
|
|
235
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { SessionAccessModeSchema } from "../../shared/state/index.js";
|
|
3
|
-
const RunIntegrationWorkerRequestSchema = z.object({
|
|
4
|
-
integrationPath: z.string().min(1),
|
|
5
|
-
session: z.string().min(1),
|
|
6
|
-
params: z.unknown(),
|
|
7
|
-
headless: z.boolean(),
|
|
8
|
-
visualize: z.boolean().default(true),
|
|
9
|
-
authProfileDomain: z.string().optional(),
|
|
10
|
-
viewport: z.object({ width: z.number(), height: z.number() }).optional(),
|
|
11
|
-
accessMode: SessionAccessModeSchema.default("write-access"),
|
|
12
|
-
cdpEndpoint: z.string().optional(),
|
|
13
|
-
provider: z.object({ name: z.string(), sessionId: z.string() }).optional()
|
|
14
|
-
});
|
|
15
|
-
export {
|
|
16
|
-
RunIntegrationWorkerRequestSchema
|
|
17
|
-
};
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
2
|
-
import { ZodError } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
RunIntegrationWorkerRequestSchema
|
|
5
|
-
} from "./run-integration-worker-protocol.js";
|
|
6
|
-
import { runIntegrationFromFileInWorker } from "./run-integration-runtime.js";
|
|
7
|
-
import { ensureLibrettoSetup, withSessionLogger } from "../core/context.js";
|
|
8
|
-
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
9
|
-
function parseWorkerRequest(argv) {
|
|
10
|
-
const rawPayload = argv[2];
|
|
11
|
-
if (!rawPayload) {
|
|
12
|
-
throw new Error("Missing worker payload argument.");
|
|
13
|
-
}
|
|
14
|
-
let parsed;
|
|
15
|
-
try {
|
|
16
|
-
parsed = JSON.parse(rawPayload);
|
|
17
|
-
} catch (error) {
|
|
18
|
-
throw new Error(
|
|
19
|
-
`Invalid worker payload JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
try {
|
|
23
|
-
return RunIntegrationWorkerRequestSchema.parse(parsed);
|
|
24
|
-
} catch (error) {
|
|
25
|
-
if (error instanceof ZodError) {
|
|
26
|
-
const details = error.issues.map((issue) => `${issue.path.join(".") || "root"}: ${issue.message}`).join("; ");
|
|
27
|
-
throw new Error(`Worker payload is invalid: ${details}`);
|
|
28
|
-
}
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async function main() {
|
|
33
|
-
let request = null;
|
|
34
|
-
let exitCode = 0;
|
|
35
|
-
try {
|
|
36
|
-
request = parseWorkerRequest(process.argv);
|
|
37
|
-
const workerRequest = request;
|
|
38
|
-
ensureLibrettoSetup();
|
|
39
|
-
await withSessionLogger(workerRequest.session, async (logger) => {
|
|
40
|
-
await runIntegrationFromFileInWorker(workerRequest, logger);
|
|
41
|
-
});
|
|
42
|
-
} catch (error) {
|
|
43
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
-
if (request) {
|
|
45
|
-
const { failedSignalPath } = getPauseSignalPaths(request.session);
|
|
46
|
-
await writeFile(
|
|
47
|
-
failedSignalPath,
|
|
48
|
-
JSON.stringify(
|
|
49
|
-
{
|
|
50
|
-
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
51
|
-
message,
|
|
52
|
-
phase: "setup"
|
|
53
|
-
},
|
|
54
|
-
null,
|
|
55
|
-
2
|
|
56
|
-
),
|
|
57
|
-
"utf8"
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
exitCode = 1;
|
|
61
|
-
}
|
|
62
|
-
process.exit(exitCode);
|
|
63
|
-
}
|
|
64
|
-
void main();
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
DaemonServer,
|
|
3
|
-
DaemonClient,
|
|
4
|
-
DaemonClientError,
|
|
5
|
-
getDaemonSocketPath,
|
|
6
|
-
type DaemonCommandResult,
|
|
7
|
-
type DaemonExecOutput,
|
|
8
|
-
type DaemonRequest,
|
|
9
|
-
type DaemonResponse,
|
|
10
|
-
type DaemonResultMap,
|
|
11
|
-
type RequestHandler,
|
|
12
|
-
} from "./ipc.js";
|
|
13
|
-
|
|
14
|
-
export {
|
|
15
|
-
type DaemonLaunchConfig,
|
|
16
|
-
type DaemonConnectConfig,
|
|
17
|
-
type DaemonConfig,
|
|
18
|
-
} from "./config.js";
|
|
19
|
-
|
|
20
|
-
export {
|
|
21
|
-
spawnSessionDaemon,
|
|
22
|
-
type SpawnSessionDaemonOptions,
|
|
23
|
-
type SpawnSessionDaemonResult,
|
|
24
|
-
} from "./spawn.js";
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spawn and wait for a browser daemon process.
|
|
3
|
-
*
|
|
4
|
-
* Shared by `runOpen`, `runConnect`, and `runOpenWithProvider` in
|
|
5
|
-
* `browser.ts`. Encapsulates the child-process lifecycle and IPC
|
|
6
|
-
* readiness polling so callers only need to provide config and
|
|
7
|
-
* handle session-state persistence.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { openSync, closeSync } from "node:fs";
|
|
11
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
-
import { createRequire } from "node:module";
|
|
13
|
-
import { spawn } from "node:child_process";
|
|
14
|
-
import type { LoggerApi } from "../../../shared/logger/index.js";
|
|
15
|
-
import { getDaemonSocketPath } from "./ipc.js";
|
|
16
|
-
import { DaemonClient } from "./ipc.js";
|
|
17
|
-
import type { DaemonConfig } from "./config.js";
|
|
18
|
-
|
|
19
|
-
// ── Public types ─────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
export type SpawnSessionDaemonOptions = {
|
|
22
|
-
/** Daemon config — serialized as JSON and passed to the child process. */
|
|
23
|
-
config: DaemonConfig;
|
|
24
|
-
session: string;
|
|
25
|
-
logger: LoggerApi;
|
|
26
|
-
/** Path for the child's stderr log file. */
|
|
27
|
-
logPath: string;
|
|
28
|
-
/** How long to wait for the daemon's IPC server (default: 10 000 ms). */
|
|
29
|
-
ipcTimeoutMs?: number;
|
|
30
|
-
/**
|
|
31
|
-
* Called before throwing when the daemon fails to start (spawn error,
|
|
32
|
-
* early exit, or IPC timeout). Use for cleanup — e.g. closing a cloud
|
|
33
|
-
* provider session. Return value is ignored.
|
|
34
|
-
*/
|
|
35
|
-
onFailure?: () => Promise<unknown>;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type SpawnSessionDaemonResult = {
|
|
39
|
-
/** PID of the detached daemon child process. */
|
|
40
|
-
pid: number;
|
|
41
|
-
/** Unix domain socket path for daemon IPC. */
|
|
42
|
-
socketPath: string;
|
|
43
|
-
/** Ready-to-use IPC client (already confirmed reachable via ping). */
|
|
44
|
-
client: DaemonClient;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// ── Implementation ───────────────────────────────────────────────────
|
|
48
|
-
|
|
49
|
-
const DEFAULT_IPC_TIMEOUT_MS = 10_000;
|
|
50
|
-
const IPC_POLL_INTERVAL_MS = 250;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Spawn a daemon child process with the given config and wait for its
|
|
54
|
-
* IPC server to become reachable.
|
|
55
|
-
*
|
|
56
|
-
* The daemon entry point is resolved relative to this module so the
|
|
57
|
-
* caller doesn't need to know where the daemon script lives.
|
|
58
|
-
*/
|
|
59
|
-
export async function spawnSessionDaemon(
|
|
60
|
-
options: SpawnSessionDaemonOptions,
|
|
61
|
-
): Promise<SpawnSessionDaemonResult> {
|
|
62
|
-
const {
|
|
63
|
-
config,
|
|
64
|
-
session,
|
|
65
|
-
logger,
|
|
66
|
-
logPath,
|
|
67
|
-
ipcTimeoutMs = DEFAULT_IPC_TIMEOUT_MS,
|
|
68
|
-
onFailure,
|
|
69
|
-
} = options;
|
|
70
|
-
|
|
71
|
-
// Resolve paths for the daemon entry point and tsx loader.
|
|
72
|
-
const daemonEntryPath = fileURLToPath(
|
|
73
|
-
new URL("./daemon.js", import.meta.url),
|
|
74
|
-
);
|
|
75
|
-
const require = createRequire(import.meta.url);
|
|
76
|
-
const tsxImportPath = pathToFileURL(require.resolve("tsx/esm")).href;
|
|
77
|
-
|
|
78
|
-
// Spawn detached child process with stderr going to the log file.
|
|
79
|
-
const childStderrFd = openSync(logPath, "a");
|
|
80
|
-
const child = spawn(
|
|
81
|
-
process.execPath,
|
|
82
|
-
["--import", tsxImportPath, daemonEntryPath, JSON.stringify(config)],
|
|
83
|
-
{
|
|
84
|
-
detached: true,
|
|
85
|
-
stdio: ["ignore", "ignore", childStderrFd],
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
child.unref();
|
|
89
|
-
closeSync(childStderrFd);
|
|
90
|
-
|
|
91
|
-
const pid = child.pid!;
|
|
92
|
-
logger.info("daemon-spawned", { pid, session });
|
|
93
|
-
|
|
94
|
-
// Track spawn errors and early exits so the polling loop can fail fast.
|
|
95
|
-
let childSpawnError: Error | null = null;
|
|
96
|
-
let childEarlyExit: {
|
|
97
|
-
code: number | null;
|
|
98
|
-
signal: NodeJS.Signals | null;
|
|
99
|
-
} | null = null;
|
|
100
|
-
|
|
101
|
-
child.on("error", (err) => {
|
|
102
|
-
childSpawnError = err;
|
|
103
|
-
logger.error("daemon-spawn-error", { error: err, session });
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
child.on("exit", (code, signal) => {
|
|
107
|
-
childEarlyExit = { code, signal };
|
|
108
|
-
logger.warn("daemon-early-exit", { code, signal, session, pid });
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Poll the daemon's IPC server until it responds to a ping.
|
|
112
|
-
const socketPath = getDaemonSocketPath(session);
|
|
113
|
-
const client = new DaemonClient(socketPath);
|
|
114
|
-
const maxAttempts = Math.ceil(ipcTimeoutMs / IPC_POLL_INTERVAL_MS);
|
|
115
|
-
let ipcReady = false;
|
|
116
|
-
|
|
117
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
118
|
-
// Fail fast on spawn errors. The cast is needed because TypeScript
|
|
119
|
-
// doesn't track that the variable is mutated asynchronously by the
|
|
120
|
-
// child's "error" event handler.
|
|
121
|
-
const spawnError = childSpawnError as Error | null;
|
|
122
|
-
if (spawnError !== null) {
|
|
123
|
-
await onFailure?.();
|
|
124
|
-
const errWithCode = spawnError as Error & { code?: string };
|
|
125
|
-
const hint =
|
|
126
|
-
errWithCode.code === "ENOENT"
|
|
127
|
-
? " Ensure Node.js is available in PATH for child processes."
|
|
128
|
-
: "";
|
|
129
|
-
throw new Error(
|
|
130
|
-
`Failed to spawn daemon: ${spawnError.message}.${hint} Check logs: ${logPath}`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Fail fast on early exit.
|
|
135
|
-
const earlyExit = childEarlyExit as {
|
|
136
|
-
code: number | null;
|
|
137
|
-
signal: NodeJS.Signals | null;
|
|
138
|
-
} | null;
|
|
139
|
-
if (earlyExit !== null) {
|
|
140
|
-
await onFailure?.();
|
|
141
|
-
const status = earlyExit.code ?? earlyExit.signal ?? "unknown";
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Daemon exited before startup (status: ${status}). Check logs: ${logPath}`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
await new Promise((r) => setTimeout(r, IPC_POLL_INTERVAL_MS));
|
|
148
|
-
ipcReady = await client.ping();
|
|
149
|
-
if (ipcReady) break;
|
|
150
|
-
|
|
151
|
-
if (i > 0 && i % 10 === 0) {
|
|
152
|
-
logger.info("daemon-waiting-for-ipc", { attempt: i, session });
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!ipcReady) {
|
|
157
|
-
// Kill the orphaned daemon process before reporting failure.
|
|
158
|
-
try {
|
|
159
|
-
process.kill(pid, "SIGTERM");
|
|
160
|
-
} catch {
|
|
161
|
-
// Process may have already exited.
|
|
162
|
-
}
|
|
163
|
-
await onFailure?.();
|
|
164
|
-
throw new Error(
|
|
165
|
-
`Daemon failed to start within ${Math.ceil(ipcTimeoutMs / 1000)}s. Check logs: ${logPath}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
logger.info("daemon-ipc-ready", { session, socketPath });
|
|
170
|
-
return { pid, socketPath, client };
|
|
171
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { unlink } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { getSessionDir } from "./context.js";
|
|
5
|
-
|
|
6
|
-
export type PauseSignalPaths = {
|
|
7
|
-
pausedSignalPath: string;
|
|
8
|
-
resumeSignalPath: string;
|
|
9
|
-
completedSignalPath: string;
|
|
10
|
-
failedSignalPath: string;
|
|
11
|
-
outputSignalPath: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function getPauseSignalPaths(session: string): PauseSignalPaths {
|
|
15
|
-
const sessionDir = getSessionDir(session);
|
|
16
|
-
return {
|
|
17
|
-
pausedSignalPath: join(sessionDir, `${session}.paused`),
|
|
18
|
-
resumeSignalPath: join(sessionDir, `${session}.resume`),
|
|
19
|
-
completedSignalPath: join(sessionDir, `${session}.completed`),
|
|
20
|
-
failedSignalPath: join(sessionDir, `${session}.failed`),
|
|
21
|
-
outputSignalPath: join(sessionDir, `${session}.output`),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function removeSignalIfExists(path: string): Promise<void> {
|
|
26
|
-
if (!existsSync(path)) return;
|
|
27
|
-
try {
|
|
28
|
-
await unlink(path);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
31
|
-
if (code !== "ENOENT") {
|
|
32
|
-
throw error;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|