experimental-a2 0.2.0 → 0.4.0
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/CHANGELOG.md +169 -0
- package/dist/ai-B4YhEnfw.d.ts +333 -0
- package/dist/ai-server.d.ts +48 -9
- package/dist/ai-server.js +121 -49
- package/dist/ai.d.ts +2 -303
- package/dist/ai.js +231 -86
- package/dist/cli-B3VuxoDe.js +597 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +5 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +2 -0
- package/dist/client-BrfDXQ8A.d.ts +155 -0
- package/dist/client-Bt4tAKi9.js +798 -0
- package/dist/client.d.ts +2 -90
- package/dist/client.js +1 -409
- package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
- package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
- package/dist/devtools-J_jZ2vQf.d.ts +151 -0
- package/dist/devtools-kJJaORn-.js +338 -0
- package/dist/devtools-server.browser.js +1 -1
- package/dist/devtools-server.d.ts +2 -2
- package/dist/devtools-server.js +224 -43
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +2 -0
- package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
- package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
- package/dist/http.d.ts +71 -13
- package/dist/http.js +302 -41
- package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
- package/dist/internal-aEotMzu_.js +209 -0
- package/dist/otel.d.ts +1 -1
- package/dist/platform-B4TnJtWu.js +32 -0
- package/dist/react.d.ts +37 -14
- package/dist/react.js +26 -15
- package/dist/scheduler-qstash.d.ts +78 -0
- package/dist/scheduler-qstash.js +499 -0
- package/dist/scheduler-task-BpzhPnRS.js +54 -0
- package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
- package/dist/scheduler-vercel.js +226 -0
- package/dist/server-CcNnFnoW.js +1405 -0
- package/dist/server-YtPq7hjw.d.ts +260 -0
- package/dist/server.d.ts +4 -155
- package/dist/server.js +2 -2
- package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
- package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
- package/dist/store-codec-DTG0Ftek.js +8 -0
- package/dist/store-memory.d.ts +11 -0
- package/dist/{log-memory.js → store-memory.js} +127 -24
- package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
- package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
- package/dist/{log-postgres.js → store-postgres.js} +158 -24
- package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
- package/dist/store-redis-http.d.ts +21 -0
- package/dist/store-redis-http.js +70 -0
- package/dist/store-redis.d.ts +37 -0
- package/dist/store-redis.js +298 -0
- package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
- package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
- package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
- package/dist/testing.browser.d.ts +1 -0
- package/dist/testing.browser.js +4 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +101 -0
- package/dist/wire-DCUZBUlT.js +222 -0
- package/docs/01-quickstart.mdx +4 -5
- package/docs/concepts/01-contracts.mdx +21 -17
- package/docs/concepts/02-handlers.mdx +7 -7
- package/docs/concepts/03-durability.mdx +26 -29
- package/docs/concepts/04-state.mdx +18 -21
- package/docs/guides/01-timers.mdx +154 -54
- package/docs/guides/02-cancellation.mdx +30 -4
- package/docs/guides/03-react.mdx +20 -21
- package/docs/guides/04-local-first.mdx +1 -1
- package/docs/guides/05-production.mdx +321 -60
- package/docs/guides/06-ai-agents.mdx +249 -49
- package/docs/guides/07-devtools.mdx +137 -12
- package/docs/guides/08-application-data.mdx +1 -1
- package/docs/guides/09-presence.mdx +284 -0
- package/docs/guides/10-transports.mdx +131 -0
- package/docs/index.mdx +22 -46
- package/docs/reference/01-api.mdx +751 -102
- package/docs/reference/02-errors.mdx +10 -5
- package/package.json +23 -6
- package/dist/internal-D6wNxTck.js +0 -36
- package/dist/log-memory.d.ts +0 -11
- package/dist/log-redis.d.ts +0 -31
- package/dist/recovery-vercel.js +0 -119
- package/dist/server-DJgD2YWP.js +0 -877
- package/dist/wire-BVsgR8o9.js +0 -62
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { l as parseDevtoolsCapture, s as devtoolsSessionRevision, u as verifyDevtoolsCapture } from "./devtools-kJJaORn-.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { open, readFile } from "node:fs/promises";
|
|
4
|
+
//#region src/cli.ts
|
|
5
|
+
const DEVTOOLS_CLI_EXIT_ERROR = 1;
|
|
6
|
+
const DEVTOOLS_CLI_EXIT_CHECK_FAILED = 2;
|
|
7
|
+
var CliFailure = class extends Error {
|
|
8
|
+
exitCode;
|
|
9
|
+
constructor(message, exitCode = 1) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "CliFailure";
|
|
12
|
+
this.exitCode = exitCode;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var IntegrityFailure = class extends CliFailure {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message, 2);
|
|
18
|
+
this.name = "IntegrityFailure";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const USAGE = `Usage: a2 devtools <command> [options]
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
contracts List mounted contracts
|
|
25
|
+
sessions --contract NAME List a contract's sessions
|
|
26
|
+
show --contract NAME --session ID
|
|
27
|
+
Show one durable session
|
|
28
|
+
export --contract NAME --session ID [--output FILE]
|
|
29
|
+
Save an integrity-protected .a2log capture
|
|
30
|
+
check --contract NAME --session ID [assertions]
|
|
31
|
+
Verify a live session capture
|
|
32
|
+
verify FILE Verify a saved .a2log capture
|
|
33
|
+
|
|
34
|
+
Connection:
|
|
35
|
+
--url URL Mounted devtools URL (or A2_DEVTOOLS_URL)
|
|
36
|
+
--bearer-env ENV Read a bearer token from ENV
|
|
37
|
+
--header-env HEADER=ENV Read a request header from ENV; repeatable
|
|
38
|
+
|
|
39
|
+
Output:
|
|
40
|
+
--format human|json|ndjson Human output is the default
|
|
41
|
+
--output FILE, -o FILE Export destination
|
|
42
|
+
--force Allow export to replace FILE
|
|
43
|
+
|
|
44
|
+
Check assertions:
|
|
45
|
+
--settled Require no pending events
|
|
46
|
+
--no-dead-letters Require no dead-lettered events
|
|
47
|
+
--no-caught-failures Require no caught handler failures
|
|
48
|
+
--max-redispatches N Limit dispatches after each event's first claim
|
|
49
|
+
`;
|
|
50
|
+
const commandNames = /* @__PURE__ */ new Set([
|
|
51
|
+
"contracts",
|
|
52
|
+
"sessions",
|
|
53
|
+
"show",
|
|
54
|
+
"export",
|
|
55
|
+
"check",
|
|
56
|
+
"verify"
|
|
57
|
+
]);
|
|
58
|
+
const fail = (message) => {
|
|
59
|
+
throw new CliFailure(message);
|
|
60
|
+
};
|
|
61
|
+
const nextValue = (argv, index, option) => {
|
|
62
|
+
const token = argv[index];
|
|
63
|
+
const equals = token.indexOf("=");
|
|
64
|
+
if (equals !== -1) {
|
|
65
|
+
const value = token.slice(equals + 1);
|
|
66
|
+
if (value.length === 0) fail(`${option} requires a value`);
|
|
67
|
+
return {
|
|
68
|
+
value,
|
|
69
|
+
next: index
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const value = argv[index + 1];
|
|
73
|
+
if (value === void 0 || value.startsWith("-")) fail(`${option} requires a value`);
|
|
74
|
+
return {
|
|
75
|
+
value,
|
|
76
|
+
next: index + 1
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
const nonNegativeInteger = (value, option) => {
|
|
80
|
+
const parsed = Number(value);
|
|
81
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) fail(`${option} must be a non-negative integer`);
|
|
82
|
+
return parsed;
|
|
83
|
+
};
|
|
84
|
+
const positiveInteger = (value, option) => {
|
|
85
|
+
const parsed = nonNegativeInteger(value, option);
|
|
86
|
+
if (parsed === 0) fail(`${option} must be a positive integer`);
|
|
87
|
+
return parsed;
|
|
88
|
+
};
|
|
89
|
+
const parseArguments = (argv) => {
|
|
90
|
+
if (argv[0] !== "devtools") {
|
|
91
|
+
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") return {
|
|
92
|
+
command: null,
|
|
93
|
+
help: true,
|
|
94
|
+
url: void 0,
|
|
95
|
+
format: "human",
|
|
96
|
+
contract: void 0,
|
|
97
|
+
session: void 0,
|
|
98
|
+
cursor: void 0,
|
|
99
|
+
limit: void 0,
|
|
100
|
+
output: void 0,
|
|
101
|
+
force: false,
|
|
102
|
+
bearerEnv: void 0,
|
|
103
|
+
headerEnvs: [],
|
|
104
|
+
settled: false,
|
|
105
|
+
noDeadLetters: false,
|
|
106
|
+
noCaughtFailures: false,
|
|
107
|
+
maxRedispatches: void 0,
|
|
108
|
+
positionals: []
|
|
109
|
+
};
|
|
110
|
+
fail("expected the 'devtools' command namespace");
|
|
111
|
+
}
|
|
112
|
+
const parsed = {
|
|
113
|
+
command: null,
|
|
114
|
+
help: false,
|
|
115
|
+
url: void 0,
|
|
116
|
+
format: "human",
|
|
117
|
+
contract: void 0,
|
|
118
|
+
session: void 0,
|
|
119
|
+
cursor: void 0,
|
|
120
|
+
limit: void 0,
|
|
121
|
+
output: void 0,
|
|
122
|
+
force: false,
|
|
123
|
+
bearerEnv: void 0,
|
|
124
|
+
headerEnvs: [],
|
|
125
|
+
settled: false,
|
|
126
|
+
noDeadLetters: false,
|
|
127
|
+
noCaughtFailures: false,
|
|
128
|
+
maxRedispatches: void 0,
|
|
129
|
+
positionals: []
|
|
130
|
+
};
|
|
131
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
132
|
+
const token = argv[index];
|
|
133
|
+
if (token === "--help" || token === "-h" || token === "help") {
|
|
134
|
+
parsed.help = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (token === "--token" || token.startsWith("--token=") || token === "--bearer" || token.startsWith("--bearer=") || token === "--header" || token.startsWith("--header=")) fail("literal credential flags are not supported; use A2_DEVTOOLS_TOKEN, --bearer-env, or --header-env");
|
|
138
|
+
if (token === "--force") {
|
|
139
|
+
parsed.force = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (token === "--settled") {
|
|
143
|
+
parsed.settled = true;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (token === "--no-dead-letters") {
|
|
147
|
+
parsed.noDeadLetters = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (token === "--no-caught-failures") {
|
|
151
|
+
parsed.noCaughtFailures = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const option = token.split("=", 1)[0];
|
|
155
|
+
if (option === "--url" || option === "--format" || option === "--contract" || option === "--session" || option === "--cursor" || option === "--limit" || option === "--output" || option === "-o" || option === "--bearer-env" || option === "--header-env" || option === "--max-redispatches") {
|
|
156
|
+
const result = nextValue(argv, index, option);
|
|
157
|
+
index = result.next;
|
|
158
|
+
switch (option) {
|
|
159
|
+
case "--url":
|
|
160
|
+
parsed.url = result.value;
|
|
161
|
+
break;
|
|
162
|
+
case "--format":
|
|
163
|
+
if (result.value !== "human" && result.value !== "json" && result.value !== "ndjson") fail("--format must be human, json, or ndjson");
|
|
164
|
+
parsed.format = result.value;
|
|
165
|
+
break;
|
|
166
|
+
case "--contract":
|
|
167
|
+
parsed.contract = result.value;
|
|
168
|
+
break;
|
|
169
|
+
case "--session":
|
|
170
|
+
parsed.session = result.value;
|
|
171
|
+
break;
|
|
172
|
+
case "--cursor":
|
|
173
|
+
parsed.cursor = result.value;
|
|
174
|
+
break;
|
|
175
|
+
case "--limit":
|
|
176
|
+
parsed.limit = positiveInteger(result.value, "--limit");
|
|
177
|
+
break;
|
|
178
|
+
case "--output":
|
|
179
|
+
case "-o":
|
|
180
|
+
parsed.output = result.value;
|
|
181
|
+
break;
|
|
182
|
+
case "--bearer-env":
|
|
183
|
+
parsed.bearerEnv = result.value;
|
|
184
|
+
break;
|
|
185
|
+
case "--header-env":
|
|
186
|
+
parsed.headerEnvs.push(result.value);
|
|
187
|
+
break;
|
|
188
|
+
case "--max-redispatches": parsed.maxRedispatches = nonNegativeInteger(result.value, "--max-redispatches");
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (token.startsWith("-")) fail(`unknown option '${token}'`);
|
|
193
|
+
if (parsed.command === null && commandNames.has(token)) {
|
|
194
|
+
parsed.command = token;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (parsed.command === null) fail(`unknown devtools command '${token}'`);
|
|
198
|
+
parsed.positionals.push(token);
|
|
199
|
+
}
|
|
200
|
+
return parsed;
|
|
201
|
+
};
|
|
202
|
+
const required = (value, option) => {
|
|
203
|
+
if (value === void 0) fail(`${option} is required`);
|
|
204
|
+
return value;
|
|
205
|
+
};
|
|
206
|
+
const assertCommandArguments = (args) => {
|
|
207
|
+
if (args.command === null) return;
|
|
208
|
+
if (args.command === "verify") {
|
|
209
|
+
if (args.positionals.length !== 1) fail("verify requires exactly one capture file");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (args.positionals.length > 0) fail(`unexpected argument '${args.positionals[0]}'`);
|
|
213
|
+
if (args.command !== "contracts") required(args.contract, "--contract");
|
|
214
|
+
if (args.command === "show" || args.command === "export" || args.command === "check") required(args.session, "--session");
|
|
215
|
+
};
|
|
216
|
+
const baseUrl = (value) => {
|
|
217
|
+
const url = (() => {
|
|
218
|
+
try {
|
|
219
|
+
return new URL(value);
|
|
220
|
+
} catch {
|
|
221
|
+
return fail("--url must be an absolute HTTP(S) URL");
|
|
222
|
+
}
|
|
223
|
+
})();
|
|
224
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") fail("--url must use HTTP or HTTPS");
|
|
225
|
+
if (url.username || url.password) fail("credentials must not be embedded in --url");
|
|
226
|
+
if (url.search || url.hash) fail("--url must not contain a query string or fragment");
|
|
227
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/`;
|
|
228
|
+
return url;
|
|
229
|
+
};
|
|
230
|
+
const environmentName = (value, option) => {
|
|
231
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) fail(`${option} must reference a valid environment variable name`);
|
|
232
|
+
return value;
|
|
233
|
+
};
|
|
234
|
+
const requestHeaders = (args, env, accept) => {
|
|
235
|
+
const headers = new Headers({ accept });
|
|
236
|
+
const bearerName = args.bearerEnv === void 0 ? "A2_DEVTOOLS_TOKEN" : environmentName(args.bearerEnv, "--bearer-env");
|
|
237
|
+
const bearer = env[bearerName];
|
|
238
|
+
if (args.bearerEnv !== void 0 && !bearer) fail(`environment variable '${bearerName}' is not set`);
|
|
239
|
+
if (bearer) headers.set("authorization", `Bearer ${bearer}`);
|
|
240
|
+
for (const mapping of args.headerEnvs) {
|
|
241
|
+
const equals = mapping.indexOf("=");
|
|
242
|
+
if (equals < 1 || equals === mapping.length - 1) fail("--header-env must use HEADER=ENV");
|
|
243
|
+
const name = mapping.slice(0, equals);
|
|
244
|
+
const envName = environmentName(mapping.slice(equals + 1), "--header-env");
|
|
245
|
+
const value = env[envName];
|
|
246
|
+
if (!value) fail(`environment variable '${envName}' is not set`);
|
|
247
|
+
if (headers.has(name)) fail(`request header '${name}' was configured twice`);
|
|
248
|
+
try {
|
|
249
|
+
headers.set(name, value);
|
|
250
|
+
} catch {
|
|
251
|
+
fail(`--header-env contains an invalid header name or value`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return headers;
|
|
255
|
+
};
|
|
256
|
+
const resourceUrl = (base, resource, params) => {
|
|
257
|
+
const url = new URL(`_a2/${resource}`, base);
|
|
258
|
+
for (const [name, value] of Object.entries(params)) if (value !== void 0) url.searchParams.set(name, String(value));
|
|
259
|
+
return url;
|
|
260
|
+
};
|
|
261
|
+
const terminalText = (value) => [...value].map((character) => {
|
|
262
|
+
const codePoint = character.codePointAt(0);
|
|
263
|
+
if (!(codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 8232 || codePoint === 8233)) return character;
|
|
264
|
+
if (character === "\n") return "\\n";
|
|
265
|
+
if (character === "\r") return "\\r";
|
|
266
|
+
if (character === " ") return "\\t";
|
|
267
|
+
return codePoint <= 255 ? `\\x${codePoint.toString(16).padStart(2, "0")}` : `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
268
|
+
}).join("");
|
|
269
|
+
const fetchResponse = async (fetcher, url, headers, resource) => {
|
|
270
|
+
let response;
|
|
271
|
+
try {
|
|
272
|
+
response = await fetcher(url, {
|
|
273
|
+
method: "GET",
|
|
274
|
+
headers,
|
|
275
|
+
redirect: "manual"
|
|
276
|
+
});
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw new CliFailure(`could not fetch ${resource}: ${error instanceof Error ? error.message : String(error)}`);
|
|
279
|
+
}
|
|
280
|
+
if (!response.ok) {
|
|
281
|
+
let text = "";
|
|
282
|
+
try {
|
|
283
|
+
text = await response.text();
|
|
284
|
+
} catch {}
|
|
285
|
+
const detail = terminalText(text.trim().slice(0, 300));
|
|
286
|
+
throw new CliFailure(`${resource} returned ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
287
|
+
}
|
|
288
|
+
return response;
|
|
289
|
+
};
|
|
290
|
+
const responseText = async (fetcher, url, headers, resource) => (await fetchResponse(fetcher, url, headers, resource)).text();
|
|
291
|
+
const responseBytes = async (fetcher, url, headers, resource) => new Uint8Array(await (await fetchResponse(fetcher, url, headers, resource)).arrayBuffer());
|
|
292
|
+
const objectValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
293
|
+
const parseJson = (text, resource) => {
|
|
294
|
+
try {
|
|
295
|
+
return JSON.parse(text);
|
|
296
|
+
} catch {
|
|
297
|
+
throw new CliFailure(`${resource} did not return valid JSON`);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const assertProtocol = (value, resource) => {
|
|
301
|
+
if (value["protocolVersion"] !== 1) throw new CliFailure(`${resource} returned an unsupported protocol version`);
|
|
302
|
+
};
|
|
303
|
+
const hasContractName = (value) => {
|
|
304
|
+
const object = objectValue(value);
|
|
305
|
+
return typeof object?.["name"] === "string" && object["name"].length > 0;
|
|
306
|
+
};
|
|
307
|
+
const contractsResponse = (value) => {
|
|
308
|
+
const object = objectValue(value);
|
|
309
|
+
if (!object) throw new CliFailure("contracts returned an invalid response");
|
|
310
|
+
assertProtocol(object, "contracts");
|
|
311
|
+
if (!Array.isArray(object["contracts"]) || !object["contracts"].every(hasContractName)) throw new CliFailure("contracts returned an invalid response");
|
|
312
|
+
return object;
|
|
313
|
+
};
|
|
314
|
+
const sessionsResponse = (value) => {
|
|
315
|
+
const object = objectValue(value);
|
|
316
|
+
if (!object) throw new CliFailure("sessions returned an invalid response");
|
|
317
|
+
assertProtocol(object, "sessions");
|
|
318
|
+
if (!Array.isArray(object["sessions"]) || object["cursor"] !== null && typeof object["cursor"] !== "string") throw new CliFailure("sessions returned an invalid response");
|
|
319
|
+
return object;
|
|
320
|
+
};
|
|
321
|
+
const sessionResponse = (value) => {
|
|
322
|
+
const object = objectValue(value);
|
|
323
|
+
if (!object) throw new CliFailure("session returned an invalid response");
|
|
324
|
+
assertProtocol(object, "session");
|
|
325
|
+
if (typeof object["contract"] !== "string" || typeof object["sessionId"] !== "string" || typeof object["revision"] !== "string" || !Array.isArray(object["events"]) || !Array.isArray(object["snapshots"])) throw new CliFailure("session returned an invalid response");
|
|
326
|
+
return object;
|
|
327
|
+
};
|
|
328
|
+
const sessionPageResponse = (value) => {
|
|
329
|
+
const object = objectValue(value);
|
|
330
|
+
if (!object) throw new CliFailure("session returned an invalid response");
|
|
331
|
+
assertProtocol(object, "session");
|
|
332
|
+
if (typeof object["contract"] !== "string" || typeof object["sessionId"] !== "string" || !Number.isSafeInteger(object["throughIndex"]) || object["throughIndex"] < 0 || object["cursor"] !== null && typeof object["cursor"] !== "string" || !Array.isArray(object["events"]) || !Array.isArray(object["snapshots"])) throw new CliFailure("session returned an invalid response");
|
|
333
|
+
return object;
|
|
334
|
+
};
|
|
335
|
+
const readSessionDetail = async (fetcher, base, headers, contract, session) => {
|
|
336
|
+
const events = [];
|
|
337
|
+
let snapshots = [];
|
|
338
|
+
let cursor;
|
|
339
|
+
let throughIndex;
|
|
340
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
341
|
+
for (;;) {
|
|
342
|
+
const text = await responseText(fetcher, resourceUrl(base, "session", {
|
|
343
|
+
contract,
|
|
344
|
+
session,
|
|
345
|
+
limit: 250,
|
|
346
|
+
cursor
|
|
347
|
+
}), headers, "session");
|
|
348
|
+
const parsed = parseJson(text, "session");
|
|
349
|
+
const object = objectValue(parsed);
|
|
350
|
+
if (object && typeof object["revision"] === "string") {
|
|
351
|
+
if (events.length > 0) throw new CliFailure("session changed response shape between pages");
|
|
352
|
+
return sessionResponse(parsed);
|
|
353
|
+
}
|
|
354
|
+
const page = sessionPageResponse(parsed);
|
|
355
|
+
if (page.contract !== contract || page.sessionId !== session) throw new CliFailure("session page returned a different identity");
|
|
356
|
+
if (throughIndex === void 0) {
|
|
357
|
+
throughIndex = page.throughIndex;
|
|
358
|
+
snapshots = page.snapshots;
|
|
359
|
+
} else if (page.throughIndex !== throughIndex || page.snapshots.length > 0) throw new CliFailure("session page changed its frozen frontier");
|
|
360
|
+
for (const event of page.events) {
|
|
361
|
+
if (event.index !== events.length + 1 || event.index > throughIndex) throw new CliFailure("session event pages are not contiguous");
|
|
362
|
+
events.push(event);
|
|
363
|
+
}
|
|
364
|
+
if (page.cursor === null) break;
|
|
365
|
+
if (page.events.length === 0 || events.length === throughIndex || seenCursors.has(page.cursor)) throw new CliFailure("session cursor did not advance");
|
|
366
|
+
seenCursors.add(page.cursor);
|
|
367
|
+
cursor = page.cursor;
|
|
368
|
+
}
|
|
369
|
+
if (throughIndex !== events.length) throw new CliFailure("session ended before its frozen frontier");
|
|
370
|
+
return {
|
|
371
|
+
protocolVersion: 1,
|
|
372
|
+
contract,
|
|
373
|
+
sessionId: session,
|
|
374
|
+
revision: await devtoolsSessionRevision({
|
|
375
|
+
events,
|
|
376
|
+
snapshots
|
|
377
|
+
}),
|
|
378
|
+
events,
|
|
379
|
+
snapshots
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
const jsonText = (value) => `${JSON.stringify(value, null, 2)}\n`;
|
|
383
|
+
const ndjsonText = (values) => values.map((value) => JSON.stringify(value)).join("\n") + (values.length === 0 ? "" : "\n");
|
|
384
|
+
const humanContracts = (response) => response.contracts.length === 0 ? "No contracts.\n" : `${response.contracts.map((contract) => terminalText(contract.name)).join("\n")}\n`;
|
|
385
|
+
const humanSessions = (response) => {
|
|
386
|
+
if (response.sessions.length === 0) return "No sessions.\n";
|
|
387
|
+
return ["SESSION EVENTS PENDING DEAD ATTEMPTS FAILURES UPDATED", ...response.sessions.map((session) => [
|
|
388
|
+
terminalText(session.sessionId),
|
|
389
|
+
session.eventCount,
|
|
390
|
+
session.pendingCount,
|
|
391
|
+
session.failedCount,
|
|
392
|
+
session.attemptCount,
|
|
393
|
+
session.failureCount,
|
|
394
|
+
terminalText(session.updatedAt)
|
|
395
|
+
].join(" "))].join("\n") + "\n";
|
|
396
|
+
};
|
|
397
|
+
const humanSession = (detail) => {
|
|
398
|
+
const rows = detail.events.map((event) => {
|
|
399
|
+
const status = event.failedAt ? "dead-lettered" : event.processedAt ? "completed" : "pending";
|
|
400
|
+
return `${event.index}\t${status}\t${terminalText(event.type)}\t${terminalText(event.id)}`;
|
|
401
|
+
});
|
|
402
|
+
const heading = `${terminalText(detail.contract)}/${terminalText(detail.sessionId)}: ${detail.events.length} events, ${detail.snapshots.length} snapshots\n`;
|
|
403
|
+
return rows.length === 0 ? heading : `${heading}INDEX\tSTATUS\tTYPE\tID\n${rows.join("\n")}\n`;
|
|
404
|
+
};
|
|
405
|
+
const emitResponse = (format, value, ndjson, human, stdout) => {
|
|
406
|
+
if (format === "json") stdout(jsonText(value));
|
|
407
|
+
else if (format === "ndjson") stdout(ndjsonText(ndjson));
|
|
408
|
+
else stdout(human);
|
|
409
|
+
};
|
|
410
|
+
const parseAndVerifyCapture = async (bytes) => {
|
|
411
|
+
let text;
|
|
412
|
+
try {
|
|
413
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
414
|
+
} catch {
|
|
415
|
+
throw new IntegrityFailure("capture is not valid UTF-8");
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
const capture = parseDevtoolsCapture(text);
|
|
419
|
+
await verifyDevtoolsCapture(capture);
|
|
420
|
+
return capture;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
throw new IntegrityFailure(error instanceof Error ? error.message : String(error));
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const captureSummary = (capture) => {
|
|
426
|
+
let pendingCount = 0;
|
|
427
|
+
let deadLetterCount = 0;
|
|
428
|
+
let caughtFailureCount = 0;
|
|
429
|
+
let redispatchCount = 0;
|
|
430
|
+
for (const event of capture.events) {
|
|
431
|
+
if (event.processedAt === null && event.failedAt === null) pendingCount += 1;
|
|
432
|
+
if (event.failedAt !== null) deadLetterCount += 1;
|
|
433
|
+
if (!Number.isSafeInteger(event.failureCount) || event.failureCount < 0 || !Number.isSafeInteger(event.attemptCount) || event.attemptCount < 0) throw new IntegrityFailure(`event #${event.index} has invalid attempt or failure counts`);
|
|
434
|
+
caughtFailureCount += event.failureCount;
|
|
435
|
+
redispatchCount += Math.max(0, event.attemptCount - 1);
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
ok: true,
|
|
439
|
+
contract: capture.contract,
|
|
440
|
+
sessionId: capture.sessionId,
|
|
441
|
+
revision: capture.revision,
|
|
442
|
+
eventCount: capture.events.length,
|
|
443
|
+
snapshotCount: capture.snapshots.length,
|
|
444
|
+
pendingCount,
|
|
445
|
+
deadLetterCount,
|
|
446
|
+
caughtFailureCount,
|
|
447
|
+
redispatchCount,
|
|
448
|
+
failures: []
|
|
449
|
+
};
|
|
450
|
+
};
|
|
451
|
+
const checkSummary = (capture, args) => {
|
|
452
|
+
const summary = captureSummary(capture);
|
|
453
|
+
if (capture.contract !== args.contract) summary.failures.push(`capture contract '${capture.contract}' does not match '${args.contract}'`);
|
|
454
|
+
if (capture.sessionId !== args.session) summary.failures.push(`capture session '${capture.sessionId}' does not match '${args.session}'`);
|
|
455
|
+
if (args.settled && summary.pendingCount > 0) summary.failures.push(`${summary.pendingCount} event(s) are pending`);
|
|
456
|
+
if (args.noDeadLetters && summary.deadLetterCount > 0) summary.failures.push(`${summary.deadLetterCount} event(s) are dead-lettered`);
|
|
457
|
+
if (args.noCaughtFailures && summary.caughtFailureCount > 0) summary.failures.push(`${summary.caughtFailureCount} caught handler failure(s) were recorded`);
|
|
458
|
+
if (args.maxRedispatches !== void 0 && summary.redispatchCount > args.maxRedispatches) summary.failures.push(`${summary.redispatchCount} redispatch(es) exceed the limit of ${args.maxRedispatches}`);
|
|
459
|
+
summary.ok = summary.failures.length === 0;
|
|
460
|
+
return summary;
|
|
461
|
+
};
|
|
462
|
+
const assertCaptureIdentity = (capture, contract, sessionId) => {
|
|
463
|
+
if (capture.contract !== contract) throw new IntegrityFailure(`capture contract '${capture.contract}' does not match '${contract}'`);
|
|
464
|
+
if (capture.sessionId !== sessionId) throw new IntegrityFailure(`capture session '${capture.sessionId}' does not match '${sessionId}'`);
|
|
465
|
+
};
|
|
466
|
+
const humanCheck = (summary, verb) => `${verb} ${terminalText(summary.contract)}/${terminalText(summary.sessionId)}: ${summary.eventCount} events, ${summary.snapshotCount} snapshots, ${summary.pendingCount} pending, ${summary.deadLetterCount} dead-lettered, ${summary.caughtFailureCount} caught failures, ${summary.redispatchCount} redispatches\n`;
|
|
467
|
+
const defaultCaptureName = (contract, sessionId) => {
|
|
468
|
+
return `${`${contract}-${sessionId}`.replaceAll(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "a2-session"}.a2log`;
|
|
469
|
+
};
|
|
470
|
+
const errorCode = (error) => {
|
|
471
|
+
const object = objectValue(error);
|
|
472
|
+
return typeof object?.["code"] === "string" ? object["code"] : void 0;
|
|
473
|
+
};
|
|
474
|
+
const writeCaptureFile = async (path, data, options) => {
|
|
475
|
+
const file = await open(path, options.flag, options.mode);
|
|
476
|
+
try {
|
|
477
|
+
await file.chmod(options.mode);
|
|
478
|
+
await file.writeFile(data);
|
|
479
|
+
} finally {
|
|
480
|
+
await file.close();
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
/** Run the `a2 devtools` command namespace and return its process exit code. */
|
|
484
|
+
async function runCli(argv, options = {}) {
|
|
485
|
+
const stdout = options.stdout ?? ((text) => process.stdout.write(text));
|
|
486
|
+
const stderr = options.stderr ?? ((text) => process.stderr.write(text));
|
|
487
|
+
const env = options.env ?? process.env;
|
|
488
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
489
|
+
const cwd = options.cwd ?? process.cwd();
|
|
490
|
+
const readFile$1 = options.readFile ?? readFile;
|
|
491
|
+
const writeFile = options.writeFile ?? writeCaptureFile;
|
|
492
|
+
try {
|
|
493
|
+
const args = parseArguments(argv);
|
|
494
|
+
if (args.help || args.command === null) {
|
|
495
|
+
stdout(USAGE);
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
assertCommandArguments(args);
|
|
499
|
+
if (args.command === "verify") {
|
|
500
|
+
const path = resolve(cwd, args.positionals[0]);
|
|
501
|
+
let bytes;
|
|
502
|
+
try {
|
|
503
|
+
bytes = await readFile$1(path);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
throw new CliFailure(`could not read '${path}': ${error instanceof Error ? error.message : String(error)}`);
|
|
506
|
+
}
|
|
507
|
+
const summary = captureSummary(await parseAndVerifyCapture(bytes));
|
|
508
|
+
emitResponse(args.format, summary, [summary], humanCheck(summary, "Verified"), stdout);
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
const configuredUrl = args.url ?? env["A2_DEVTOOLS_URL"];
|
|
512
|
+
if (!configuredUrl) fail("--url or A2_DEVTOOLS_URL is required");
|
|
513
|
+
const base = baseUrl(configuredUrl);
|
|
514
|
+
const headers = requestHeaders(args, env, "application/json");
|
|
515
|
+
if (args.command === "contracts") {
|
|
516
|
+
const text = await responseText(fetcher, resourceUrl(base, "contracts", {}), headers, "contracts");
|
|
517
|
+
const response = contractsResponse(parseJson(text, "contracts"));
|
|
518
|
+
emitResponse(args.format, response, response.contracts, humanContracts(response), stdout);
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
const contract = required(args.contract, "--contract");
|
|
522
|
+
if (args.command === "sessions") {
|
|
523
|
+
const text = await responseText(fetcher, resourceUrl(base, "sessions", {
|
|
524
|
+
contract,
|
|
525
|
+
cursor: args.cursor,
|
|
526
|
+
limit: args.limit
|
|
527
|
+
}), headers, "sessions");
|
|
528
|
+
const response = sessionsResponse(parseJson(text, "sessions"));
|
|
529
|
+
emitResponse(args.format, response, response.sessions, humanSessions(response), stdout);
|
|
530
|
+
return 0;
|
|
531
|
+
}
|
|
532
|
+
const session = required(args.session, "--session");
|
|
533
|
+
if (args.command === "show") {
|
|
534
|
+
const detail = await readSessionDetail(fetcher, base, headers, contract, session);
|
|
535
|
+
emitResponse(args.format, detail, [
|
|
536
|
+
{
|
|
537
|
+
kind: "session",
|
|
538
|
+
protocolVersion: detail.protocolVersion,
|
|
539
|
+
contract: detail.contract,
|
|
540
|
+
sessionId: detail.sessionId,
|
|
541
|
+
revision: detail.revision
|
|
542
|
+
},
|
|
543
|
+
...detail.events.map((event) => ({
|
|
544
|
+
...event,
|
|
545
|
+
kind: "event"
|
|
546
|
+
})),
|
|
547
|
+
...detail.snapshots.map((snapshot) => ({
|
|
548
|
+
...snapshot,
|
|
549
|
+
kind: "snapshot"
|
|
550
|
+
}))
|
|
551
|
+
], humanSession(detail), stdout);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
const exportHeaders = requestHeaders(args, env, "application/x-ndjson");
|
|
555
|
+
const captureBytes = await responseBytes(fetcher, resourceUrl(base, "export", {
|
|
556
|
+
contract,
|
|
557
|
+
session
|
|
558
|
+
}), exportHeaders, "export");
|
|
559
|
+
const capture = await parseAndVerifyCapture(captureBytes);
|
|
560
|
+
if (args.command === "check") {
|
|
561
|
+
const summary = checkSummary(capture, args);
|
|
562
|
+
emitResponse(args.format, summary, [summary], humanCheck(summary, summary.ok ? "Checked" : "Check failed for"), stdout);
|
|
563
|
+
if (!summary.ok) {
|
|
564
|
+
for (const failure of summary.failures) stderr(`a2 devtools: ${terminalText(failure)}\n`);
|
|
565
|
+
return 2;
|
|
566
|
+
}
|
|
567
|
+
return 0;
|
|
568
|
+
}
|
|
569
|
+
assertCaptureIdentity(capture, contract, session);
|
|
570
|
+
const output = resolve(cwd, args.output ?? defaultCaptureName(contract, session));
|
|
571
|
+
try {
|
|
572
|
+
await writeFile(output, captureBytes, {
|
|
573
|
+
flag: args.force ? "w" : "wx",
|
|
574
|
+
mode: 384
|
|
575
|
+
});
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (errorCode(error) === "EEXIST") throw new CliFailure(`'${output}' already exists; pass --force to replace it`);
|
|
578
|
+
throw new CliFailure(`could not write '${output}': ${error instanceof Error ? error.message : String(error)}`);
|
|
579
|
+
}
|
|
580
|
+
const result = {
|
|
581
|
+
path: output,
|
|
582
|
+
contract: capture.contract,
|
|
583
|
+
sessionId: capture.sessionId,
|
|
584
|
+
eventCount: capture.events.length,
|
|
585
|
+
snapshotCount: capture.snapshots.length,
|
|
586
|
+
sha256: capture.footer.sha256
|
|
587
|
+
};
|
|
588
|
+
emitResponse(args.format, result, [result], `Exported ${terminalText(capture.contract)}/${terminalText(capture.sessionId)}: ${capture.events.length} events, ${capture.snapshots.length} snapshots -> ${terminalText(output)}\n`, stdout);
|
|
589
|
+
return 0;
|
|
590
|
+
} catch (error) {
|
|
591
|
+
const failure = error instanceof CliFailure ? error : new CliFailure(error instanceof Error ? error.message : String(error));
|
|
592
|
+
stderr(`a2 devtools: ${terminalText(failure.message)}\n`);
|
|
593
|
+
return failure.exitCode;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
//#endregion
|
|
597
|
+
export { DEVTOOLS_CLI_EXIT_ERROR as n, runCli as r, DEVTOOLS_CLI_EXIT_CHECK_FAILED as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli-bin.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/cli.d.ts
|
|
2
|
+
declare const DEVTOOLS_CLI_EXIT_ERROR: 1;
|
|
3
|
+
declare const DEVTOOLS_CLI_EXIT_CHECK_FAILED: 2;
|
|
4
|
+
type DevtoolsCliOptions = {
|
|
5
|
+
fetch?: typeof globalThis.fetch;
|
|
6
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
stdout?: (text: string) => void;
|
|
9
|
+
stderr?: (text: string) => void;
|
|
10
|
+
readFile?: (path: string) => Promise<Uint8Array>;
|
|
11
|
+
writeFile?: (path: string, data: Uint8Array, options: {
|
|
12
|
+
flag: "w" | "wx";
|
|
13
|
+
mode: number;
|
|
14
|
+
}) => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
/** Run the `a2 devtools` command namespace and return its process exit code. */
|
|
17
|
+
declare function runCli(argv: readonly string[], options?: DevtoolsCliOptions): Promise<number>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { DEVTOOLS_CLI_EXIT_CHECK_FAILED, DEVTOOLS_CLI_EXIT_ERROR, DevtoolsCliOptions, runCli };
|
package/dist/cli.js
ADDED