firedrill-run 3.0.2 → 3.0.5

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 CHANGED
@@ -11,19 +11,19 @@ permissions.
11
11
 
12
12
  ## Get started
13
13
 
14
- Use this version explicitly while the unversioned package still points to the
15
- previous platform:
14
+ Install and sign in:
16
15
 
17
16
  ```sh
18
- npx firedrill-run@3.0.1 init
19
- npx firedrill-run@3.0.1 --help
17
+ npx firedrill-run init
18
+ npx firedrill-run --help
20
19
  ```
21
20
 
22
- For automation, `firedrill --help` and `--json` describe the installed version.
21
+ Use `firedrill --version` to read the installed package version without signing
22
+ in. Add `--json` for structured output. Use `firedrill --help` for commands.
23
23
 
24
24
  ## Requirements
25
25
 
26
- - Node.js `>=22.23.4 <23`.
26
+ - Node.js `>=22.23.4 <23 || >=24.11.1 <25 || >=26.13.1 <27`. Other major versions are not supported.
27
27
  - macOS, Windows or Linux are source-supported targets. A release qualification
28
28
  must name the actual OS/architecture used; support is not proof each platform
29
29
  was exercised. Native keyring availability also depends on its platform build.
package/bin/firedrill.mjs CHANGED
@@ -2,7 +2,9 @@
2
2
  // Keep this bootstrap plain JavaScript. An unsupported Node runtime must not
3
3
  // try to parse TypeScript before it can explain the required toolchain.
4
4
  const [major, minor, patch] = process.versions.node.split('.').map(Number);
5
- const supported = major === 22 && (minor > 23 || minor === 23 && patch >= 4);
5
+ const supported = major === 22 && (minor > 23 || minor === 23 && patch >= 4)
6
+ || major === 24 && (minor > 11 || minor === 11 && patch >= 1)
7
+ || major === 26 && (minor > 13 || minor === 13 && patch >= 1);
6
8
  function report(code, message, exitCode = 1) {
7
9
  process.stderr.write(process.argv.includes('--json')
8
10
  ? JSON.stringify({ type: 'error', error: { code, message } }) + '\n'
@@ -10,7 +12,31 @@ function report(code, message, exitCode = 1) {
10
12
  process.exitCode = exitCode;
11
13
  }
12
14
  if (!supported) {
13
- report('node_runtime_unsupported', 'Firedrill requires Node 22.23.4 or a later 22.x release. Update Node, then run the command again.');
15
+ report('node_runtime_unsupported', 'Firedrill requires Node 22.23.4 or later 22.x, Node 24.11.1 or later 24.x, or Node 26.13.1 or later 26.x. Update Node, then run the command again.');
16
+ } else if (process.argv.slice(2).some(value => value === '--version' || value === '-v')) {
17
+ const args = process.argv.slice(2);
18
+ const versionFlags = args.filter(value => value === '--version' || value === '-v');
19
+ if (versionFlags.length !== 1 || new Set(args).size !== args.length
20
+ || args.some(value => !['--version', '-v', '--json'].includes(value))) {
21
+ report('arguments_invalid', 'Use firedrill --version or firedrill --version --json.', 2);
22
+ } else {
23
+ try {
24
+ // The public assembler supplies this manifest. Do not initialize commands,
25
+ // credentials or network access just to read the installed package version.
26
+ const { readFile } = await import('node:fs/promises');
27
+ const manifest = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
28
+ if (!['firedrill-run', '@firedrill/simulation-cli'].includes(manifest.name)
29
+ || typeof manifest.version !== 'string'
30
+ || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(manifest.version)) {
31
+ throw new Error('version_metadata_unavailable');
32
+ }
33
+ process.stdout.write(args.includes('--json')
34
+ ? JSON.stringify({ type: 'version', data: { version: manifest.version } }) + '\n'
35
+ : manifest.version + '\n');
36
+ } catch {
37
+ report('cli_installation_unavailable', 'The installed CLI version could not be read. Restore the Firedrill installation, then try again.');
38
+ }
39
+ }
14
40
  } else {
15
41
  let entry;
16
42
  try { entry = await import('../dist/cli/src/main.js'); }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema": "firedrill.cli-assembly.v1",
3
3
  "package": "firedrill-run",
4
- "version": "3.0.2",
4
+ "version": "3.0.5",
5
5
  "entry": "cli/src/main.js",
6
6
  "helpers": [
7
7
  "cli/runtime/source_snapshot.py",
@@ -2,13 +2,88 @@ import { constants } from 'node:fs';
2
2
  import { lstat, open } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
4
  import { CliError } from './http.js';
5
+ /** Consume a pipe once without leaving an abort waiting for another byte. */
6
+ async function stdinInput(signal, maximumBytes) {
7
+ signal.throwIfAborted();
8
+ const input = process.stdin;
9
+ if (input.isTTY || input.readableEncoding !== null) {
10
+ throw new CliError('request_stdin_invalid', 'Pipe one UTF-8 JSON object into --file -. Nothing was submitted.', 2);
11
+ }
12
+ const chunks = await new Promise((accept, reject) => {
13
+ const collected = [];
14
+ let size = 0;
15
+ let settled = false;
16
+ const invalid = () => new CliError('request_stdin_invalid', 'Standard input must contain one JSON object within transport capacity. Nothing was submitted.', 2);
17
+ const finish = (error) => {
18
+ if (settled)
19
+ return;
20
+ settled = true;
21
+ input.pause();
22
+ input.removeListener('data', data);
23
+ input.removeListener('end', end);
24
+ input.removeListener('error', failed);
25
+ input.removeListener('close', closed);
26
+ signal.removeEventListener('abort', aborted);
27
+ if (error !== undefined)
28
+ reject(error);
29
+ else
30
+ accept(collected);
31
+ };
32
+ const data = (chunk) => {
33
+ if (!Buffer.isBuffer(chunk)) {
34
+ finish(invalid());
35
+ return;
36
+ }
37
+ size += chunk.length;
38
+ if (size > maximumBytes) {
39
+ finish(invalid());
40
+ return;
41
+ }
42
+ collected.push(Buffer.from(chunk));
43
+ };
44
+ const end = () => finish();
45
+ const failed = () => finish(invalid());
46
+ const closed = () => { if (!input.readableEnded)
47
+ finish(invalid()); };
48
+ const aborted = () => finish(signal.reason ?? new DOMException('The operation was aborted.', 'AbortError'));
49
+ input.on('data', data);
50
+ input.once('end', end);
51
+ input.once('error', failed);
52
+ input.once('close', closed);
53
+ signal.addEventListener('abort', aborted, { once: true });
54
+ // EOF or cancellation can precede listener installation. Never wait for an
55
+ // event that has already happened, including an idle pipe on cancellation.
56
+ if (signal.aborted)
57
+ aborted();
58
+ else if (input.readableEnded)
59
+ end();
60
+ else if (input.destroyed)
61
+ failed();
62
+ else
63
+ input.resume();
64
+ });
65
+ signal.throwIfAborted();
66
+ try {
67
+ const raw = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
68
+ const value = JSON.parse(raw);
69
+ if (!value || typeof value !== 'object' || Array.isArray(value))
70
+ throw new Error();
71
+ return raw;
72
+ }
73
+ catch {
74
+ throw new CliError('request_stdin_invalid', 'Standard input is not a UTF-8 JSON object. Nothing was submitted.', 2);
75
+ }
76
+ }
5
77
  /** Read one bounded, unchanged JSON object without rewriting number literals. */
6
78
  export async function requestInput(path, signal, maximumBytes = 8 * 1024 * 1024) {
79
+ signal.throwIfAborted();
7
80
  if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 2 || maximumBytes > 8 * 1024 * 1024) {
8
81
  throw new CliError('request_capacity_invalid', 'Choose a supported request capacity.', 2);
9
82
  }
10
83
  if (!path)
11
84
  return '{}';
85
+ if (path === '-')
86
+ return stdinInput(signal, maximumBytes);
12
87
  const before = await lstat(resolve(path));
13
88
  if (!before.isFile() || before.isSymbolicLink() || before.size > maximumBytes) {
14
89
  throw new CliError('request_file_invalid', 'Choose a regular JSON file within transport capacity.', 2);
@@ -206,19 +206,45 @@ export async function toolCatalogCommand(args, options, output) {
206
206
  dataset_id: null, restore: null, duration_seconds: seconds, launch_request_key: key }, output) : null;
207
207
  const body = { release_id: releaseId, duration_seconds: seconds, install_consent: true, start_consent: seconds !== null, usage_quote_id: usageQuote };
208
208
  output('catalog_setup_intent', { request_key: key, ...body }, `Catalog request: ${key}\nRecovery: tools catalog-status ${key}`);
209
+ const unconfirmed = (error, acceptedId) => {
210
+ // Preserve only the transport's typed, bounded diagnostic. Never echo its
211
+ // response body/message, and never infer a rejected write from HTTP status.
212
+ const typed = error instanceof CliError ? error : null;
213
+ const code = typed && /^[a-z_]{1,64}$/.test(typed.code) ? typed.code : null;
214
+ const httpStatus = typed?.httpStatus;
215
+ const status = typeof httpStatus === 'number' && Number.isInteger(httpStatus)
216
+ && httpStatus >= 400 && httpStatus <= 599 ? httpStatus : null;
217
+ const detail = code || status !== null || typed?.requestId
218
+ ? ` Response: ${[status === null ? null : `HTTP ${status}`, code, typed?.requestId ? `support reference ${typed.requestId}` : null].filter(Boolean).join('; ')}.` : '';
219
+ return new CliError(acceptedId ? 'catalog_readback_unconfirmed' : 'catalog_acceptance_unconfirmed', (acceptedId ? `Setup ${acceptedId} was accepted, but its readback could not be confirmed.` : 'Setup acceptance is unconfirmed.')
220
+ + detail + ` No replacement was submitted. Read tools catalog-status ${key} --workspace ${scope.workspace}.`, 1, status, typed?.requestId ?? null);
221
+ };
222
+ let accepted;
209
223
  let row;
210
224
  try {
211
- const accepted = await api(`${prefix}/catalog-setups`, { ...auth, method: 'POST', idempotencyKey: key, body });
212
- catalogRead(accepted, catalogSetupMatches(accepted, owner, { key, input: body }));
213
- row = await readSetup(key, { id: accepted.id, input: body });
225
+ accepted = await api(`${prefix}/catalog-setups`, { ...auth, method: 'POST', idempotencyKey: key, body });
214
226
  }
215
- catch {
227
+ catch (submissionError) {
228
+ // An answered error can follow a committed write. Recover the same key;
229
+ // missing/failed readback does not prove the original was never accepted.
216
230
  try {
217
231
  row = await readSetup(key, { input: body });
218
232
  }
219
233
  catch {
220
- throw new CliError('catalog_acceptance_unconfirmed', `Setup acceptance is unconfirmed. No replacement was submitted. Read tools catalog-status ${key} --workspace ${scope.workspace}.`);
234
+ throw unconfirmed(submissionError);
221
235
  }
236
+ output('catalog_setup', row, text(row));
237
+ return;
238
+ }
239
+ // A mismatched accepted response must not be hidden by a less constrained
240
+ // fallback. Trust its ID only after the full original scope/input check.
241
+ if (!catalogSetupMatches(accepted, owner, { key, input: body }))
242
+ throw unconfirmed(undefined);
243
+ try {
244
+ row = await readSetup(key, { id: accepted.id, input: body });
245
+ }
246
+ catch (readbackError) {
247
+ throw unconfirmed(readbackError, accepted.id);
222
248
  }
223
249
  output('catalog_setup', row, text(row));
224
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firedrill-run",
3
- "version": "3.0.2",
3
+ "version": "3.0.5",
4
4
  "description": "Local AI agent simulations with cloud-hosted worlds and tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "license": "UNLICENSED",
15
15
  "engines": {
16
- "node": ">=22.23.4 <23"
16
+ "node": ">=22.23.4 <23 || >=24.11.1 <25 || >=26.13.1 <27"
17
17
  },
18
18
  "dependencies": {
19
19
  "@napi-rs/keyring": "2.1.2",