libretto 0.6.11 → 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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { expect, test as base } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
createIpcPeer
|
|
5
|
+
} from "./ipc.js";
|
|
6
|
+
const test = base.extend({
|
|
7
|
+
peers: async ({}, use) => {
|
|
8
|
+
const channel = new EventEmitter();
|
|
9
|
+
const a = createIpcPeer({
|
|
10
|
+
send(message) {
|
|
11
|
+
channel.emit("b", message);
|
|
12
|
+
},
|
|
13
|
+
listen(callback) {
|
|
14
|
+
channel.on("a", callback);
|
|
15
|
+
return () => channel.off("a", callback);
|
|
16
|
+
}
|
|
17
|
+
}, {
|
|
18
|
+
greet(name) {
|
|
19
|
+
return `hello ${name}`;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const b = createIpcPeer({
|
|
23
|
+
send(message) {
|
|
24
|
+
channel.emit("a", message);
|
|
25
|
+
},
|
|
26
|
+
listen(callback) {
|
|
27
|
+
channel.on("b", callback);
|
|
28
|
+
return () => channel.off("b", callback);
|
|
29
|
+
}
|
|
30
|
+
}, {
|
|
31
|
+
async add(left, right) {
|
|
32
|
+
return left + right;
|
|
33
|
+
},
|
|
34
|
+
async fail() {
|
|
35
|
+
throw new Error("expected failure");
|
|
36
|
+
},
|
|
37
|
+
async failWithCause() {
|
|
38
|
+
throw new Error("outer failure", {
|
|
39
|
+
cause: new Error("inner failure", {
|
|
40
|
+
cause: new TypeError("root failure")
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
async failWithCode() {
|
|
45
|
+
const error = new Error("coded failure");
|
|
46
|
+
error.code = "ERR_EXPECTED";
|
|
47
|
+
throw error;
|
|
48
|
+
},
|
|
49
|
+
async failWithNonError() {
|
|
50
|
+
throw "plain failure";
|
|
51
|
+
},
|
|
52
|
+
async wait() {
|
|
53
|
+
return new Promise(() => {
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
await use({ a, b });
|
|
58
|
+
a.destroy();
|
|
59
|
+
b.destroy();
|
|
60
|
+
},
|
|
61
|
+
a: async ({ peers }, use) => {
|
|
62
|
+
await use(peers.a);
|
|
63
|
+
},
|
|
64
|
+
b: async ({ peers }, use) => {
|
|
65
|
+
await use(peers.b);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
test("calls handlers on the remote peer", async ({ a, b }) => {
|
|
69
|
+
await expect(a.call.add(2, 3)).resolves.toBe(5);
|
|
70
|
+
await expect(b.call.greet("Ada")).resolves.toBe("hello Ada");
|
|
71
|
+
});
|
|
72
|
+
test("rejects with the remote handler error", async ({ a }) => {
|
|
73
|
+
await expect(a.call.fail()).rejects.toThrow("fail > expected failure");
|
|
74
|
+
});
|
|
75
|
+
test("rejects with the remote handler error cause chain", async ({ a }) => {
|
|
76
|
+
const error = await getRejectedError(a.call.failWithCause());
|
|
77
|
+
expect(error.message).toBe("failWithCause > outer failure");
|
|
78
|
+
expect(error.stack).toContain("outer failure");
|
|
79
|
+
const cause = getErrorCause(error);
|
|
80
|
+
expect(cause.message).toBe("inner failure");
|
|
81
|
+
const rootCause = getErrorCause(cause);
|
|
82
|
+
expect(rootCause.name).toBe("TypeError");
|
|
83
|
+
expect(rootCause.message).toBe("root failure");
|
|
84
|
+
});
|
|
85
|
+
test("rejects with the remote handler error code", async ({ a }) => {
|
|
86
|
+
const error = await getRejectedError(a.call.failWithCode());
|
|
87
|
+
expect(error.message).toBe("failWithCode > coded failure");
|
|
88
|
+
expect(error.code).toBe("ERR_EXPECTED");
|
|
89
|
+
expect(error.stack).toContain("coded failure");
|
|
90
|
+
});
|
|
91
|
+
test("rejects with a serialized non-error thrown value", async ({ a }) => {
|
|
92
|
+
const error = await getRejectedError(a.call.failWithNonError());
|
|
93
|
+
expect(error.name).toBe("NonError");
|
|
94
|
+
expect(error.message).toBe("failWithNonError > plain failure");
|
|
95
|
+
expect(error.stack).toContain("failWithNonError");
|
|
96
|
+
});
|
|
97
|
+
test("rejects pending calls when destroyed", async ({ a }) => {
|
|
98
|
+
const result = a.call.wait();
|
|
99
|
+
a.destroy();
|
|
100
|
+
await expect(result).rejects.toThrow("IPC peer destroyed");
|
|
101
|
+
});
|
|
102
|
+
async function getRejectedError(promise) {
|
|
103
|
+
try {
|
|
104
|
+
await promise;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
expect(error).toBeInstanceOf(Error);
|
|
107
|
+
return error;
|
|
108
|
+
}
|
|
109
|
+
throw new Error("Expected promise to reject");
|
|
110
|
+
}
|
|
111
|
+
function getErrorCause(error) {
|
|
112
|
+
expect(error.cause).toBeInstanceOf(Error);
|
|
113
|
+
return error.cause;
|
|
114
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Server } from 'node:net';
|
|
2
|
+
import { IpcTransport, IpcProtocolMessage } from './ipc.js';
|
|
3
|
+
|
|
4
|
+
declare function connectToIpcSocket(socketPath: string): Promise<IpcTransport<IpcProtocolMessage>>;
|
|
5
|
+
declare function createIpcSocketServer(onConnection: (transport: IpcTransport<IpcProtocolMessage>) => void): Server;
|
|
6
|
+
declare function listenForIpcConnections(socketPath: string, onConnection: (transport: IpcTransport<IpcProtocolMessage>) => void): Promise<Server>;
|
|
7
|
+
declare function listenOnIpcSocket(server: Server, socketPath: string): Promise<void>;
|
|
8
|
+
|
|
9
|
+
export { connectToIpcSocket, createIpcSocketServer, listenForIpcConnections, listenOnIpcSocket };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
createServer,
|
|
4
|
+
createConnection
|
|
5
|
+
} from "node:net";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
import { mkdir } from "node:fs/promises";
|
|
8
|
+
function createJsonSocketTransport(socket) {
|
|
9
|
+
socket.setEncoding("utf8");
|
|
10
|
+
return {
|
|
11
|
+
send(message) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const line = `${JSON.stringify(message)}
|
|
14
|
+
`;
|
|
15
|
+
socket.write(line, (error) => {
|
|
16
|
+
if (error) reject(error);
|
|
17
|
+
else resolve();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
listen(callback) {
|
|
22
|
+
let buffer = "";
|
|
23
|
+
const onData = (chunk) => {
|
|
24
|
+
buffer += chunk;
|
|
25
|
+
while (true) {
|
|
26
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
27
|
+
if (newlineIndex === -1) break;
|
|
28
|
+
const line = buffer.slice(0, newlineIndex);
|
|
29
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
30
|
+
if (line.length === 0) continue;
|
|
31
|
+
let message;
|
|
32
|
+
try {
|
|
33
|
+
message = JSON.parse(line);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
socket.destroy(
|
|
36
|
+
new Error(
|
|
37
|
+
`Invalid IPC socket message: ${error instanceof Error ? error.message : String(error)}`
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!isIpcProtocolMessage(message)) {
|
|
43
|
+
socket.destroy(new Error("Invalid IPC socket protocol message."));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
callback(message);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
socket.on("data", onData);
|
|
50
|
+
return () => socket.off("data", onData);
|
|
51
|
+
},
|
|
52
|
+
onClose(callback) {
|
|
53
|
+
let closeError;
|
|
54
|
+
const onError = (error) => {
|
|
55
|
+
closeError = error;
|
|
56
|
+
};
|
|
57
|
+
const onClose = () => callback(closeError);
|
|
58
|
+
socket.on("error", onError);
|
|
59
|
+
socket.on("close", onClose);
|
|
60
|
+
return () => {
|
|
61
|
+
socket.off("error", onError);
|
|
62
|
+
socket.off("close", onClose);
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
close() {
|
|
66
|
+
socket.destroy();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
async function connectToIpcSocket(socketPath) {
|
|
71
|
+
const socket = await connectSocket(socketPath);
|
|
72
|
+
return createJsonSocketTransport(socket);
|
|
73
|
+
}
|
|
74
|
+
function createIpcSocketServer(onConnection) {
|
|
75
|
+
return createServer((socket) => {
|
|
76
|
+
onConnection(createJsonSocketTransport(socket));
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
async function listenForIpcConnections(socketPath, onConnection) {
|
|
80
|
+
const server = createIpcSocketServer(onConnection);
|
|
81
|
+
await listenOnIpcSocket(server, socketPath);
|
|
82
|
+
return server;
|
|
83
|
+
}
|
|
84
|
+
async function listenOnIpcSocket(server, socketPath) {
|
|
85
|
+
await mkdir(dirname(socketPath), { recursive: true });
|
|
86
|
+
await rm(socketPath, { force: true });
|
|
87
|
+
const originalClose = server.close.bind(server);
|
|
88
|
+
server.close = ((callback) => {
|
|
89
|
+
return originalClose((error) => {
|
|
90
|
+
void rm(socketPath, { force: true }).finally(() => callback?.(error));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
const onError = (error) => {
|
|
95
|
+
server.off("listening", onListening);
|
|
96
|
+
reject(error);
|
|
97
|
+
};
|
|
98
|
+
const onListening = () => {
|
|
99
|
+
server.off("error", onError);
|
|
100
|
+
resolve();
|
|
101
|
+
};
|
|
102
|
+
server.once("error", onError);
|
|
103
|
+
server.once("listening", onListening);
|
|
104
|
+
server.listen(socketPath);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async function connectSocket(socketPath) {
|
|
108
|
+
const socket = createConnection(socketPath);
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const onError = (error) => {
|
|
111
|
+
socket.off("connect", onConnect);
|
|
112
|
+
reject(error);
|
|
113
|
+
};
|
|
114
|
+
const onConnect = () => {
|
|
115
|
+
socket.off("error", onError);
|
|
116
|
+
resolve(socket);
|
|
117
|
+
};
|
|
118
|
+
socket.once("error", onError);
|
|
119
|
+
socket.once("connect", onConnect);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function isIpcProtocolMessage(message) {
|
|
123
|
+
if (!isRecord(message)) return false;
|
|
124
|
+
if (message.type === "ipc-request") {
|
|
125
|
+
return typeof message.id === "string" && typeof message.method === "string" && Array.isArray(message.args);
|
|
126
|
+
}
|
|
127
|
+
if (message.type === "ipc-response") {
|
|
128
|
+
return typeof message.id === "string" && typeof message.method === "string" && (message.error === void 0 || isSerializedError(message.error));
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
function isSerializedError(value) {
|
|
133
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.message === "string" && (value.stack === void 0 || typeof value.stack === "string");
|
|
134
|
+
}
|
|
135
|
+
function isRecord(value) {
|
|
136
|
+
return typeof value === "object" && value !== null;
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
connectToIpcSocket,
|
|
140
|
+
createIpcSocketServer,
|
|
141
|
+
listenForIpcConnections,
|
|
142
|
+
listenOnIpcSocket
|
|
143
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { mkdtemp, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { expect, test as base } from "vitest";
|
|
6
|
+
import { createIpcPeer } from "./ipc.js";
|
|
7
|
+
import {
|
|
8
|
+
connectToIpcSocket,
|
|
9
|
+
listenForIpcConnections
|
|
10
|
+
} from "./socket-transport.js";
|
|
11
|
+
const test = base.extend({
|
|
12
|
+
socketPath: async ({}, use) => {
|
|
13
|
+
const directory = await mkdtemp(join(tmpdir(), "libretto-ipc-"));
|
|
14
|
+
await use(join(directory, "daemon.sock"));
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
test("sends concurrent calls over one socket", async ({ socketPath }) => {
|
|
18
|
+
await writeFile(socketPath, "stale");
|
|
19
|
+
const serverPeers = [];
|
|
20
|
+
const server = await listenForIpcConnections(socketPath, (transport) => {
|
|
21
|
+
serverPeers.push(
|
|
22
|
+
createIpcPeer(transport, {
|
|
23
|
+
async double(value) {
|
|
24
|
+
return value * 2;
|
|
25
|
+
},
|
|
26
|
+
async wait() {
|
|
27
|
+
return "done";
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
const client = createIpcPeer(
|
|
33
|
+
await connectToIpcSocket(socketPath),
|
|
34
|
+
{
|
|
35
|
+
ping() {
|
|
36
|
+
return "pong";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
await expect(
|
|
41
|
+
Promise.all([client.call.double(2), client.call.double(21)])
|
|
42
|
+
).resolves.toEqual([4, 42]);
|
|
43
|
+
client.destroy();
|
|
44
|
+
for (const peer of serverPeers) peer.destroy();
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
47
|
+
});
|
|
48
|
+
await expect(stat(socketPath)).rejects.toThrow();
|
|
49
|
+
});
|
|
50
|
+
test("rejects pending calls when the socket closes", async ({ socketPath }) => {
|
|
51
|
+
const serverPeers = [];
|
|
52
|
+
const server = await listenForIpcConnections(socketPath, (transport) => {
|
|
53
|
+
serverPeers.push(
|
|
54
|
+
createIpcPeer(transport, {
|
|
55
|
+
async double(value) {
|
|
56
|
+
return value * 2;
|
|
57
|
+
},
|
|
58
|
+
async wait() {
|
|
59
|
+
return new Promise(() => {
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
const client = createIpcPeer(
|
|
66
|
+
await connectToIpcSocket(socketPath),
|
|
67
|
+
{
|
|
68
|
+
ping() {
|
|
69
|
+
return "pong";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
const pending = client.call.wait();
|
|
74
|
+
for (const peer of serverPeers) peer.destroy();
|
|
75
|
+
await expect(pending).rejects.toThrow(/IPC transport closed|ECONNRESET/);
|
|
76
|
+
client.destroy();
|
|
77
|
+
await new Promise((resolve, reject) => {
|
|
78
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
test("closes malformed socket messages without stopping the server", async ({
|
|
82
|
+
socketPath
|
|
83
|
+
}) => {
|
|
84
|
+
const server = await listenForIpcConnections(socketPath, (transport) => {
|
|
85
|
+
createIpcPeer(transport, {
|
|
86
|
+
async double(value) {
|
|
87
|
+
return value * 2;
|
|
88
|
+
},
|
|
89
|
+
async wait() {
|
|
90
|
+
return "done";
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
const malformedClient = createConnection(socketPath);
|
|
95
|
+
await new Promise((resolve, reject) => {
|
|
96
|
+
malformedClient.once("connect", resolve);
|
|
97
|
+
malformedClient.once("error", reject);
|
|
98
|
+
});
|
|
99
|
+
const malformedClosed = new Promise((resolve) => {
|
|
100
|
+
malformedClient.once("close", () => resolve());
|
|
101
|
+
});
|
|
102
|
+
malformedClient.write("not-json\n");
|
|
103
|
+
await malformedClosed;
|
|
104
|
+
const validClient = createIpcPeer(
|
|
105
|
+
await connectToIpcSocket(socketPath),
|
|
106
|
+
{
|
|
107
|
+
ping() {
|
|
108
|
+
return "pong";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
await expect(validClient.call.double(4)).resolves.toBe(8);
|
|
113
|
+
validClient.destroy();
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
2
|
+
declare function detectPackageManager(root?: string, env?: NodeJS.ProcessEnv): PackageManager;
|
|
3
|
+
declare function detectProjectPackageManager(root?: string): PackageManager;
|
|
4
|
+
declare function installCommand(packageManager?: PackageManager): string;
|
|
5
|
+
declare function librettoCommand(args?: string, packageManager?: PackageManager): string;
|
|
6
|
+
|
|
7
|
+
export { type PackageManager, detectPackageManager, detectProjectPackageManager, installCommand, librettoCommand };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveLibrettoRepoRoot } from "./paths/repo-root.js";
|
|
4
|
+
function packageManagerFromUserAgent(env = process.env) {
|
|
5
|
+
const userAgent = env.npm_config_user_agent ?? "";
|
|
6
|
+
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
7
|
+
if (userAgent.startsWith("yarn")) return "yarn";
|
|
8
|
+
if (userAgent.startsWith("bun")) return "bun";
|
|
9
|
+
if (userAgent.startsWith("npm")) return "npm";
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function packageManagerFromLockfile(root) {
|
|
13
|
+
if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
14
|
+
if (existsSync(join(root, "yarn.lock"))) return "yarn";
|
|
15
|
+
if (existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bun.lock")))
|
|
16
|
+
return "bun";
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function detectPackageManager(root = resolveLibrettoRepoRoot(), env = process.env) {
|
|
20
|
+
const fromUserAgent = packageManagerFromUserAgent(env);
|
|
21
|
+
if (fromUserAgent) return fromUserAgent;
|
|
22
|
+
return packageManagerFromLockfile(root) ?? "npm";
|
|
23
|
+
}
|
|
24
|
+
function detectProjectPackageManager(root = resolveLibrettoRepoRoot()) {
|
|
25
|
+
return packageManagerFromLockfile(root) ?? "npm";
|
|
26
|
+
}
|
|
27
|
+
function installCommand(packageManager = detectProjectPackageManager()) {
|
|
28
|
+
switch (packageManager) {
|
|
29
|
+
case "yarn":
|
|
30
|
+
return "yarn add";
|
|
31
|
+
case "bun":
|
|
32
|
+
return "bun add";
|
|
33
|
+
case "pnpm":
|
|
34
|
+
return "pnpm add";
|
|
35
|
+
default:
|
|
36
|
+
return "npm install";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function librettoRunner(packageManager = detectPackageManager()) {
|
|
40
|
+
switch (packageManager) {
|
|
41
|
+
case "pnpm":
|
|
42
|
+
return "pnpm exec";
|
|
43
|
+
case "yarn":
|
|
44
|
+
return "yarn";
|
|
45
|
+
case "bun":
|
|
46
|
+
return "bunx";
|
|
47
|
+
default:
|
|
48
|
+
return "npx";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function librettoCommand(args = "", packageManager = detectPackageManager()) {
|
|
52
|
+
const suffix = args.trim();
|
|
53
|
+
return `${librettoRunner(packageManager)} libretto${suffix ? ` ${suffix}` : ""}`;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
detectPackageManager,
|
|
57
|
+
detectProjectPackageManager,
|
|
58
|
+
installCommand,
|
|
59
|
+
librettoCommand
|
|
60
|
+
};
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
declare function getLibrettoPauseSignalDir(sessionName: string, cwd?: string): string;
|
|
2
|
-
declare function getRunnerLogPathForDir(logDir: string): string;
|
|
3
|
-
declare function getPauseSignalPathForDir(signalDir: string, sessionName: string, signal: "paused" | "resume"): string;
|
|
4
|
-
declare function getLibrettoPausedSignalPath(sessionName: string, cwd?: string): string;
|
|
5
|
-
declare function getLibrettoResumeSignalPath(sessionName: string, cwd?: string): string;
|
|
6
1
|
declare function ensureLibrettoSessionStatePath(sessionName: string, cwd?: string): string;
|
|
7
|
-
declare function ensureLibrettoPauseSignalDir(sessionName: string, cwd?: string): string;
|
|
8
|
-
declare function ensureLibrettoRunnerLogDir(sessionName: string, cwd?: string): string;
|
|
9
2
|
|
|
10
|
-
export {
|
|
3
|
+
export { ensureLibrettoSessionStatePath };
|
|
@@ -4,10 +4,6 @@ import { resolveLibrettoRepoRoot } from "./repo-root.js";
|
|
|
4
4
|
const LIBRETTO_DIRNAME = ".libretto";
|
|
5
5
|
const LIBRETTO_SESSIONS_DIRNAME = "sessions";
|
|
6
6
|
const SESSION_STATE_FILENAME = "state.json";
|
|
7
|
-
const RUNNER_LOG_DIRNAME = "logs";
|
|
8
|
-
const RUNNER_LOG_FILENAME = "logs.jsonl";
|
|
9
|
-
const PAUSED_SIGNAL_SUFFIX = "paused";
|
|
10
|
-
const RESUME_SIGNAL_SUFFIX = "resume";
|
|
11
7
|
function getLibrettoRoot(cwd = process.cwd()) {
|
|
12
8
|
return join(resolveLibrettoRepoRoot(cwd), LIBRETTO_DIRNAME);
|
|
13
9
|
}
|
|
@@ -20,55 +16,11 @@ function getLibrettoSessionDir(sessionName, cwd = process.cwd()) {
|
|
|
20
16
|
function getLibrettoSessionStatePath(sessionName, cwd = process.cwd()) {
|
|
21
17
|
return join(getLibrettoSessionDir(sessionName, cwd), SESSION_STATE_FILENAME);
|
|
22
18
|
}
|
|
23
|
-
function getLibrettoPauseSignalDir(sessionName, cwd = process.cwd()) {
|
|
24
|
-
return getLibrettoSessionDir(sessionName, cwd);
|
|
25
|
-
}
|
|
26
|
-
function getLibrettoRunnerLogDir(sessionName, cwd = process.cwd()) {
|
|
27
|
-
return join(getLibrettoSessionDir(sessionName, cwd), RUNNER_LOG_DIRNAME);
|
|
28
|
-
}
|
|
29
|
-
function getRunnerLogPathForDir(logDir) {
|
|
30
|
-
return join(logDir, RUNNER_LOG_FILENAME);
|
|
31
|
-
}
|
|
32
|
-
function getPauseSignalPathForDir(signalDir, sessionName, signal) {
|
|
33
|
-
const suffix = signal === "paused" ? PAUSED_SIGNAL_SUFFIX : RESUME_SIGNAL_SUFFIX;
|
|
34
|
-
return join(signalDir, `${sessionName}.${suffix}`);
|
|
35
|
-
}
|
|
36
|
-
function getLibrettoPausedSignalPath(sessionName, cwd = process.cwd()) {
|
|
37
|
-
return getPauseSignalPathForDir(
|
|
38
|
-
getLibrettoPauseSignalDir(sessionName, cwd),
|
|
39
|
-
sessionName,
|
|
40
|
-
"paused"
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
function getLibrettoResumeSignalPath(sessionName, cwd = process.cwd()) {
|
|
44
|
-
return getPauseSignalPathForDir(
|
|
45
|
-
getLibrettoPauseSignalDir(sessionName, cwd),
|
|
46
|
-
sessionName,
|
|
47
|
-
"resume"
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
19
|
function ensureLibrettoSessionStatePath(sessionName, cwd = process.cwd()) {
|
|
51
20
|
const filePath = getLibrettoSessionStatePath(sessionName, cwd);
|
|
52
21
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
53
22
|
return filePath;
|
|
54
23
|
}
|
|
55
|
-
function ensureLibrettoPauseSignalDir(sessionName, cwd = process.cwd()) {
|
|
56
|
-
const dir = getLibrettoPauseSignalDir(sessionName, cwd);
|
|
57
|
-
mkdirSync(dir, { recursive: true });
|
|
58
|
-
return dir;
|
|
59
|
-
}
|
|
60
|
-
function ensureLibrettoRunnerLogDir(sessionName, cwd = process.cwd()) {
|
|
61
|
-
const dir = getLibrettoRunnerLogDir(sessionName, cwd);
|
|
62
|
-
mkdirSync(dir, { recursive: true });
|
|
63
|
-
return dir;
|
|
64
|
-
}
|
|
65
24
|
export {
|
|
66
|
-
|
|
67
|
-
ensureLibrettoRunnerLogDir,
|
|
68
|
-
ensureLibrettoSessionStatePath,
|
|
69
|
-
getLibrettoPauseSignalDir,
|
|
70
|
-
getLibrettoPausedSignalPath,
|
|
71
|
-
getLibrettoResumeSignalPath,
|
|
72
|
-
getPauseSignalPathForDir,
|
|
73
|
-
getRunnerLogPathForDir
|
|
25
|
+
ensureLibrettoSessionStatePath
|
|
74
26
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Page } from 'playwright';
|
|
2
|
+
import { Snapshot, SnapshotNode } from './types.js';
|
|
3
|
+
export { SnapshotAvailableFrame, SnapshotFrame, SnapshotPrimitive, SnapshotUnavailableFrame } from './types.js';
|
|
4
|
+
|
|
5
|
+
declare function snapshot(page: Page): Promise<Snapshot>;
|
|
6
|
+
declare function findSnapshotNodeByRef(snapshotTree: Snapshot, ref: string): SnapshotNode;
|
|
7
|
+
declare function scopeSnapshotToRef(snapshotTree: Snapshot, ref: string): Snapshot;
|
|
8
|
+
|
|
9
|
+
export { Snapshot, SnapshotNode, findSnapshotNodeByRef, scopeSnapshotToRef, snapshot };
|