@phpsandbox/sdk 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/terminal.js CHANGED
@@ -2,30 +2,34 @@ import { nanoid } from 'nanoid';
2
2
  import { once } from './utils/promise.js';
3
3
  const TERMINAL_INPUT_FLUSH_DELAY_MS = 8;
4
4
  const terminalInputFlushPattern = /[\x00-\x1f\x7f]/;
5
- export default class Terminal {
5
+ export default class Terminals {
6
6
  constructor(okra) {
7
7
  this.okra = okra;
8
8
  }
9
9
  list() {
10
10
  return this.okra.invoke('terminal.list');
11
11
  }
12
- create(input) {
13
- return this.okra.invoke('terminal.create', input);
14
- }
15
- onStarted(handler) {
16
- return this.okra.listen('terminal.started', handler);
17
- }
18
- resize(id, size) {
19
- return this.okra.invoke('terminal.resize', { id, width: size[0], height: size[1] });
20
- }
21
- onOutput(id, handler) {
22
- return this.okra.listen(`terminal.output.${id}`, handler);
23
- }
24
- input(id, input) {
25
- return Promise.resolve(this.okra.send('terminal.input', { id, input }));
26
- }
27
- listen(event, handler) {
28
- return this.okra.listen(event, handler);
12
+ async create(options = {}) {
13
+ const id = options.id ?? nanoid();
14
+ const handle = this.processHandle(id, options.abortSignal);
15
+ const command = typeof options.command === 'string' && options.args?.length
16
+ ? [options.command, ...options.args]
17
+ : options.command;
18
+ const { abortSignal, args: _args, ...input } = options;
19
+ try {
20
+ const task = await this.okra.invoke('terminal.create', {
21
+ ...input,
22
+ id,
23
+ kind: options.kind ?? 'bash',
24
+ size: options.size ?? [80, 24],
25
+ command,
26
+ }, { abortSignal });
27
+ return { ...handle.process, ...task };
28
+ }
29
+ catch (error) {
30
+ handle.dispose();
31
+ throw error;
32
+ }
29
33
  }
30
34
  async attach(id, opts) {
31
35
  const handle = this.processHandle(id, opts?.abortSignal);
@@ -48,28 +52,6 @@ export default class Terminal {
48
52
  throw error;
49
53
  }
50
54
  }
51
- async spawn(command, args, opts) {
52
- const id = opts?.id || nanoid();
53
- const handle = this.processHandle(id, opts?.abortSignal);
54
- const { abortSignal, ...otherOpts } = opts ?? {};
55
- try {
56
- const result = await this.okra.invoke('terminal.spawn', {
57
- command: [command, ...args],
58
- opts: {
59
- id,
60
- ...otherOpts,
61
- },
62
- }, { abortSignal });
63
- return {
64
- ...handle.process,
65
- ...result,
66
- };
67
- }
68
- catch (error) {
69
- handle.dispose();
70
- throw error;
71
- }
72
- }
73
55
  processHandle(id, abortSignal) {
74
56
  const disposables = new Set();
75
57
  let abortHandler;
@@ -95,7 +77,7 @@ export default class Terminal {
95
77
  }
96
78
  const input = pendingInput;
97
79
  pendingInput = '';
98
- void this.input(id, input);
80
+ void this.okra.send('terminal.input', { id, input });
99
81
  };
100
82
  const scheduleInputFlush = () => {
101
83
  if (inputFlushTimer) {
@@ -103,7 +85,8 @@ export default class Terminal {
103
85
  }
104
86
  inputFlushTimer = setTimeout(flushInput, TERMINAL_INPUT_FLUSH_DELAY_MS);
105
87
  };
106
- const queueInput = (chunk) => {
88
+ const queueInput = (value) => {
89
+ const chunk = typeof value === 'string' ? value : new TextDecoder().decode(value);
107
90
  pendingInput += chunk;
108
91
  if (terminalInputFlushPattern.test(chunk)) {
109
92
  flushInput();
@@ -124,7 +107,7 @@ export default class Terminal {
124
107
  const output = new ReadableStream({
125
108
  start: (_controller) => {
126
109
  controller = _controller;
127
- disposables.add(this.listen(`terminal.output.${id}`, (data) => {
110
+ disposables.add(this.okra.listen(`terminal.output.${id}`, (data) => {
128
111
  controller?.enqueue(data.output);
129
112
  }));
130
113
  },
@@ -134,7 +117,7 @@ export default class Terminal {
134
117
  },
135
118
  });
136
119
  const completion = new Promise((resolve) => {
137
- disposables.add(this.listen(`terminal.close.${id}`, (data) => {
120
+ disposables.add(this.okra.listen(`terminal.close.${id}`, (data) => {
138
121
  if (controller) {
139
122
  try {
140
123
  controller.close();
@@ -32,36 +32,46 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
32
32
 
33
33
  - Lifecycle: `ready()`, `connected()`, `whenConnected()`, `reconnect()`, `dispose()`
34
34
  - Notebook actions: `fork(input?)`, `destroy()`, `stop()`, `restart()`, `ping()`, `update()`
35
- - Processes: `run(command, args?, options?)` returning native `stdin`, `stdout`, `stderr`, ordered `output`, `exit`, and `kill()`
35
+ - Processes: `exec(command, args?, options?)`, or `run(command, args?, options?)` returning native `stdin`, `stdout`, `stderr`, ordered `output`, `wait()`, and `kill()`
36
36
  - Events: `listen(event, handler)`, `onDidConnect(handler)`, `onDidDisconnect(handler)`, `onDidInitialize(handler)`
37
37
  - Raw call: `invoke(action, data?, options?)`
38
38
 
39
39
  ## Services
40
40
 
41
- ### Filesystem (`notebook.file`)
41
+ ### Filesystem (`notebook.files`)
42
42
 
43
- - File data: `info()`, `readFile()`, `write()`, `writeFile()`, `tail()`, `stat()`, `exists()`
44
- - File management: `move()`, `rename()`, `copy()`, `remove()`, `delete()`
45
- - Directory: `mkdir()`, `createDirectory()`, `readDirectory()`, `tree()`
43
+ - File data: `read()`, `readText()`, `readStream()`, `write()`, `tail()`, `stat()`, `exists()`
44
+ - File management: `move()`, `copy()`, `remove()`
45
+ - Directory: `createDirectory()`, `list()`, `tree()`
46
46
  - Search: `find()`, `search()`
47
- - Streaming/monitoring: `watch()`, `download()`
47
+ - Streaming/monitoring: `readStream()`, `watch()`, `download()`
48
+ - `tail()` is an eager last-lines snapshot; it does not follow future appends.
48
49
 
49
- ### Terminal (`notebook.terminal`)
50
+ ### Terminal (`notebook.terminals`)
50
51
 
51
- - Terminal control: `list()`, `create()`, `attach()`, `resize()`, `input()`
52
- - Process API: `spawn(command, args, opts?)`, `attach(id, opts?)`
53
- - Events: `listen()`, `onStarted()`, `onOutput(id, handler)`
52
+ - `list()` returns terminal metadata
53
+ - `create(options?)` returns a terminal handle
54
+ - `attach(id, options?)` returns a terminal handle
55
+ - Handles expose `input`, `output`, `wait()`, `resize()`, and `kill()`
54
56
 
55
57
  ### Container (`notebook.container`)
56
58
 
57
59
  - Lifecycle: `start()`, `stop()`, `state()`
58
- - Ports: `openedPorts()`, `onPort(handler)`
59
60
  - Runtime config: `setPhp(version)`
60
- - Telemetry: `enableTelemetry(features)`, `stopTelemetry()`, `listen(event, handler)`
61
61
 
62
- ### Shell (`notebook.shell`)
62
+ ### Runtime observability
63
63
 
64
- - `run(command, args?, options?)` returning the same process handle as `notebook.run()`
64
+ - `notebook.logs.follow({ signal? })` returns `ReadableStream<RuntimeLogEntry>`
65
+ - `notebook.ports.list()` returns the current preview ports
66
+ - `notebook.ports.watch({ signal? })` returns `ReadableStream<PortInfo[]>`
67
+ - `notebook.metrics.current()` returns the current CPU, memory, and disk sample
68
+ - `notebook.metrics.watch({ signal? })` returns `ReadableStream<ContainerStats>`
69
+
70
+ ### Processes
71
+
72
+ - `notebook.run(command, args?, options?)` returns a streaming process handle
73
+ - `notebook.exec(command, args?, options?)` returns a buffered `ProcessResult` for one-shot commands
74
+ - Process handles expose `output`, `stdout`, `stderr`, `stdin`, `wait()`, and `kill()`
65
75
 
66
76
  ### Services (`notebook.services`)
67
77
 
@@ -70,13 +80,13 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
70
80
  - `run(name, command?)`
71
81
  - `stop(name)`
72
82
  - `logs(name, { tail? })`
73
- - `logs(name, { follow: true, tail?, id? })` returning `{ id, output, stop() }`
74
- - Events: `listen()`, `onLog(id, handler)`
83
+ - `logs(name, { follow: true, tail?, id? })` returning `ReadableStream<string>`; cancel the stream to stop following
75
84
 
76
85
  ### Composer (`notebook.composer`)
77
86
 
78
- - Core: `invoke(command, args?, options?)`
79
- - Helpers: `install()`, `update()`, `require()`, `remove()`, `dumpAutoload()`, `packages()`, `stream(handler)`
87
+ - Core: `run(command, args?, options?)`
88
+ - Helpers: `install()`, `update()`, `require()`, `remove()`, `dumpAutoload()`, `packages()`
89
+ - Command handles expose `output`, `stdout`, `stderr`, `wait()`, and `kill()`
80
90
 
81
91
  ### Git (`notebook.git`)
82
92
 
@@ -126,11 +136,6 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
126
136
  - A publish run exposes `events()` as an `AsyncIterable`, `result()`, and `publication()`.
127
137
  - Built-in provider names are `cloudflare-containers`, `ssh-server`, and `laravel-cloud`. Provider maps support TypeScript declaration merging.
128
138
 
129
- ### Log (`notebook.log`)
130
-
131
- - `stream(handler)`
132
- - `listen(event, handler)`
133
-
134
139
  ## Common Errors
135
140
 
136
141
  - `ApiError` for HTTP API failures
package/docs/cdn-usage.md CHANGED
@@ -18,7 +18,7 @@ Do not hardcode long-lived private API tokens in public frontend code. Use short
18
18
  const notebook = await client.notebook.get('your-notebook-id');
19
19
  await notebook.ready();
20
20
 
21
- const result = await notebook.run('php -v', { tty: false }).exit;
21
+ const result = await notebook.exec('php -v', { tty: false });
22
22
  result.throw();
23
23
  console.log(result.output);
24
24
  </script>
@@ -45,7 +45,7 @@ Do not hardcode long-lived private API tokens in public frontend code. Use short
45
45
 
46
46
  client.notebook.get('your-notebook-id').then(async (notebook) => {
47
47
  await notebook.ready();
48
- const result = await notebook.run('php -v', { tty: false }).exit;
48
+ const result = await notebook.exec('php -v', { tty: false });
49
49
  console.log(result.output);
50
50
  notebook.dispose();
51
51
  });
@@ -48,18 +48,18 @@ API notebook creates and forks are ephemeral by default. Use `persistent: true`
48
48
  ## 3. Write and Read Files
49
49
 
50
50
  ```ts
51
- await notebook.file.write('hello.php', '<?php echo "Hello";');
51
+ await notebook.files.write('hello.php', '<?php echo "Hello";');
52
52
 
53
- const data = await notebook.file.readFile('hello.php');
53
+ const data = await notebook.files.read('hello.php');
54
54
  const text = new TextDecoder().decode(data);
55
55
 
56
56
  console.log(text);
57
57
  ```
58
58
 
59
- If you need binary-safe writes, use `writeFile`:
59
+ The same `write()` method accepts text, bytes, readable streams, and async iterables:
60
60
 
61
61
  ```ts
62
- await notebook.file.writeFile('binary.dat', new Uint8Array([1, 2, 3]), {
62
+ await notebook.files.write('binary.dat', new Uint8Array([1, 2, 3]), {
63
63
  create: true,
64
64
  overwrite: true,
65
65
  unlock: false,
@@ -72,7 +72,7 @@ await notebook.file.writeFile('binary.dat', new Uint8Array([1, 2, 3]), {
72
72
  ### Terminal (stream output)
73
73
 
74
74
  ```ts
75
- const task = await notebook.terminal.spawn('php', ['hello.php']);
75
+ const task = await notebook.terminals.create({ command: 'php', args: ['hello.php'] });
76
76
  const reader = task.output.getReader();
77
77
 
78
78
  try {
@@ -85,14 +85,14 @@ try {
85
85
  reader.releaseLock();
86
86
  }
87
87
 
88
- const exitCode = await task.exit;
88
+ const exitCode = await task.wait();
89
89
  console.log(`\nExit code: ${exitCode}`);
90
90
  ```
91
91
 
92
92
  ### Shell (single command result)
93
93
 
94
94
  ```ts
95
- const result = await notebook.run('php -v', { tty: false }).exit;
95
+ const result = await notebook.exec('php -v', { tty: false });
96
96
  result.throw();
97
97
  console.log(result.stdout);
98
98
  ```
@@ -100,14 +100,14 @@ console.log(result.stdout);
100
100
  ## 5. Search and Watch Files
101
101
 
102
102
  ```ts
103
- const matches = await notebook.file.find('*.php', {
103
+ const matches = await notebook.files.find('*.php', {
104
104
  includes: ['app/**'],
105
105
  excludes: ['vendor/**'],
106
106
  });
107
107
 
108
108
  console.log(matches.map((m) => m.path));
109
109
 
110
- const watcher = notebook.file.watch(
110
+ const watcher = notebook.files.watch(
111
111
  '/app',
112
112
  {
113
113
  recursive: true,
@@ -125,8 +125,8 @@ watcher.dispose();
125
125
  ## 6. Use Composer and Git
126
126
 
127
127
  ```ts
128
- await notebook.composer.install({ noInteraction: true });
129
- await notebook.composer.require({ packages: ['monolog/monolog'] });
128
+ (await notebook.composer.install().wait()).throw();
129
+ (await notebook.composer.require({ packages: ['monolog/monolog'] }).wait()).throw();
130
130
 
131
131
  await notebook.git.checkpoint('Jane Doe <jane@example.com>', 'Initial checkpoint');
132
132
  ```
@@ -142,8 +142,8 @@ notebook.dispose();
142
142
  ## Common Pitfalls
143
143
 
144
144
  - `get()` notebooks must call `await notebook.ready()` before tool calls.
145
- - `file.watch()` requires an `excludes` array.
146
- - Await `process.exit`, then call `result.throw()` to fail on non-zero exit codes.
145
+ - `files.watch()` requires an `excludes` array.
146
+ - Use `notebook.exec()` for one-shot commands; for streaming shell handles, await `process.wait()` and call `result.throw()` to fail on non-zero exit codes.
147
147
 
148
148
  ## Next Reading
149
149
 
package/examples/basic.ts CHANGED
@@ -16,7 +16,7 @@ console.log('\n');
16
16
 
17
17
  console.log('====== Read routes/web.php in the notebook ======');
18
18
  const decoder = new TextDecoder();
19
- const content = await notebook.file.readFile('routes/web.php');
19
+ const content = await notebook.files.read('routes/web.php');
20
20
  console.log(decoder.decode(content));
21
21
 
22
22
  // Always close the notebook when done to clean up the connection.
@@ -54,10 +54,7 @@ async function main(): Promise<void> {
54
54
 
55
55
  await fork.ready();
56
56
  const editContent = buildEditContent(base.data.id, forkNotebookId, options);
57
- const written = await fork.file.write(options.editPath, editContent);
58
- if (!written) {
59
- throw new Error(`fs.write returned false for ${options.editPath}.`);
60
- }
57
+ await fork.files.write(options.editPath, editContent);
61
58
  console.log(`Edited fork file: ${options.editPath}`);
62
59
 
63
60
  const forkCheckpoint = await fork.git.checkpoint(options.author, `Smoke edit in fork ${forkNotebookId}`, options.ref, false);
@@ -389,11 +389,10 @@ async function runScenario(
389
389
  let notebook: NotebookInstance | null = null;
390
390
  let previewUrl: string | undefined;
391
391
  let logsCaptured = 0;
392
- let logListener: Disposable | null = null;
392
+ let logReader: ReadableStreamDefaultReader<import('../src/log.js').RuntimeLogEntry> | null = null;
393
393
  let initTrace: InitTrace | undefined;
394
394
  let readyResult: NotebookInitSuccessResult | undefined;
395
395
  let initTraceDisposables: Disposable[] = [];
396
- let telemetryEnabled = false;
397
396
  let redisRunning = false;
398
397
 
399
398
  const routeSlug = id.toLowerCase();
@@ -560,12 +559,19 @@ async function runScenario(
560
559
  }
561
560
 
562
561
  if (options.mode !== 'init') {
563
- logListener = notebook.log.stream((line) => {
564
- if (line.trim().length > 0) {
565
- logsCaptured += 1;
566
- ui.updateScenarioLogs(id, logsCaptured);
562
+ logReader = notebook.logs.follow().getReader();
563
+ void (async () => {
564
+ while (logReader) {
565
+ const entry = await logReader.read();
566
+ if (entry.done) {
567
+ return;
568
+ }
569
+ if (entry.value.message.trim().length > 0) {
570
+ logsCaptured += 1;
571
+ ui.updateScenarioLogs(id, logsCaptured);
572
+ }
567
573
  }
568
- });
574
+ })().catch(() => {});
569
575
 
570
576
  await runCheckedStep('notebook.ping', () => notebook!.ping(), { continueOnError: false });
571
577
 
@@ -599,8 +605,8 @@ async function runScenario(
599
605
  });
600
606
 
601
607
  await runCheckedStep('container.state', async () => {
602
- const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.container.openedPorts()]);
603
- const portInfo = assertPortInfoList(ports, 'container.openedPorts');
608
+ const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.ports.list()]);
609
+ const portInfo = assertPortInfoList(ports, 'ports.list');
604
610
  return `${state.state} (${describePorts(portInfo)})`;
605
611
  });
606
612
 
@@ -609,11 +615,11 @@ async function runScenario(
609
615
  'container.restart',
610
616
  async () => {
611
617
  await notebook!.container.start();
612
- const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.container.openedPorts()]);
618
+ const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.ports.list()]);
613
619
  if (state.state !== 'running') {
614
620
  throw new Error(`Expected running container after restart, got ${state.state}`);
615
621
  }
616
- return `${state.state} (${describePorts(assertPortInfoList(ports, 'container.openedPorts after restart'))})`;
622
+ return `${state.state} (${describePorts(assertPortInfoList(ports, 'ports.list after restart'))})`;
617
623
  },
618
624
  { timeoutMs: options.stepTimeoutMs * 3, continueOnError: false }
619
625
  );
@@ -626,29 +632,20 @@ async function runScenario(
626
632
  async () =>
627
633
  retryWithBackoff(
628
634
  async () => {
629
- const statsPromise = waitForNextEvent<ContainerStats>(
630
- (handler) => notebook!.container.listen('container.stats', handler),
631
- options.telemetryTimeoutMs,
632
- 'container.stats'
633
- );
634
- void statsPromise.catch(() => undefined);
635
- const portsPromise = waitForNextEvent<PortInfo[]>(
636
- (handler) => notebook!.container.listen('container.ports', handler),
637
- options.telemetryTimeoutMs,
638
- 'container.ports'
639
- );
640
- void portsPromise.catch(() => undefined);
641
-
642
- await notebook!.container.enableTelemetry(new Set(['stats', 'ports']));
643
- telemetryEnabled = true;
644
-
645
- return await firstFulfilled([
646
- statsPromise.then(
647
- (stats) =>
648
- `cpu=${stats.cpu.usage}/${stats.cpu.limit} memory=${stats.memory.usage}/${stats.memory.limit}`
649
- ),
650
- portsPromise.then((ports) => describePorts(assertPortInfoList(ports, 'container.ports'))),
651
- ]);
635
+ const statsReader = notebook!.metrics.watch().getReader();
636
+ const portsReader = notebook!.ports.watch().getReader();
637
+ try {
638
+ const statsPromise = withTimeout(statsReader.read(), options.telemetryTimeoutMs, 'metrics.watch')
639
+ .then(({ value }) => {
640
+ if (!value) throw new Error('metrics.watch ended before a sample was received.');
641
+ return `cpu=${value.cpu.usage}/${value.cpu.limit} memory=${value.memory.usage}/${value.memory.limit}`;
642
+ });
643
+ const portsPromise = withTimeout(portsReader.read(), options.telemetryTimeoutMs, 'ports.watch')
644
+ .then(({ value }) => describePorts(assertPortInfoList(value ?? [], 'ports.watch')));
645
+ return await firstFulfilled([statsPromise, portsPromise]);
646
+ } finally {
647
+ await Promise.all([statsReader.cancel(), portsReader.cancel()]);
648
+ }
652
649
  },
653
650
  TELEMETRY_RETRY_DELAYS_MS,
654
651
  shouldRetryTelemetryError
@@ -660,7 +657,7 @@ async function runScenario(
660
657
  'filesystem.watch-and-write',
661
658
  async () => {
662
659
  const changeSeen = createDeferred<FileChange>();
663
- const watch = await notebook!.file.watch(
660
+ const watch = await notebook!.files.watch(
664
661
  'routes',
665
662
  { recursive: true, excludes: ['vendor/**', 'node_modules/**'] },
666
663
  (change) => {
@@ -671,12 +668,9 @@ async function runScenario(
671
668
  );
672
669
 
673
670
  try {
674
- const current = textDecoder.decode(await notebook!.file.readFile(routePath));
671
+ const current = textDecoder.decode(await notebook!.files.read(routePath));
675
672
  const next = appendRouteDefinition(current, id, routeUrlPath);
676
- const written = await notebook!.file.write(routePath, next);
677
- if (!written) {
678
- throw new Error(`fs.write returned false for ${routePath}.`);
679
- }
673
+ await notebook!.files.write(routePath, next);
680
674
 
681
675
  const change = await withTimeout(
682
676
  changeSeen.promise,
@@ -692,14 +686,13 @@ async function runScenario(
692
686
  );
693
687
 
694
688
  await runCheckedStep('filesystem.inspect', async () => {
695
- const [raw, info, stat, directory, tree, found, tail] = await Promise.all([
696
- notebook!.file.readFile(routePath),
697
- notebook!.file.info(routePath),
698
- notebook!.file.stat(routePath),
699
- notebook!.file.readDirectory('routes'),
700
- notebook!.file.tree('routes'),
701
- notebook!.file.find(routeFileName, { includes: ['routes/**'], maxResults: 10 }),
702
- notebook!.file.tail(routePath, 20),
689
+ const [raw, stat, directory, tree, found, tail] = await Promise.all([
690
+ notebook!.files.read(routePath),
691
+ notebook!.files.stat(routePath),
692
+ notebook!.files.list('routes'),
693
+ notebook!.files.tree('routes'),
694
+ notebook!.files.find(routeFileName, { includes: ['routes/**'], maxResults: 10 }),
695
+ notebook!.files.tail(routePath, { lines: 20 }),
703
696
  ]);
704
697
 
705
698
  const text = textDecoder.decode(raw);
@@ -707,7 +700,7 @@ async function runScenario(
707
700
  throw new Error(`Expected ${routeUrlPath} in ${routePath}.`);
708
701
  }
709
702
 
710
- const [, matches] = await notebook!.file.search(
703
+ const [, matches] = await notebook!.files.search(
711
704
  { pattern: routeUrlPath },
712
705
  { includes: [routePath], excludes: [], maxResults: 10 }
713
706
  );
@@ -716,12 +709,12 @@ async function runScenario(
716
709
  throw new Error(`file.search() did not find ${routeUrlPath}.`);
717
710
  }
718
711
 
719
- const exists = await notebook!.file.exists(routePath);
712
+ const exists = await notebook!.files.exists(routePath);
720
713
  if (!exists) {
721
714
  throw new Error(`${routePath} does not exist after write.`);
722
715
  }
723
716
 
724
- return `size=${stat.size} info=${info.language} dir=${directory.length} tree-lines=${tree.split('\n').length} find=${found.length} tail=${tail.length}`;
717
+ return `size=${stat.size} dir=${directory.length} tree-lines=${tree.split('\n').length} find=${found.length} tail=${tail.length}`;
725
718
  });
726
719
 
727
720
  if (options.includePreview) {
@@ -732,7 +725,10 @@ async function runScenario(
732
725
 
733
726
  if (options.includeDownload) {
734
727
  await runCheckedStep('filesystem.download', async () => {
735
- const archive = await notebook!.file.download(undefined, ['vendor/**', 'node_modules/**'], [routePath, 'composer.json']);
728
+ const archive = await notebook!.files.download({
729
+ exclude: ['vendor/**', 'node_modules/**'],
730
+ include: [routePath, 'composer.json'],
731
+ });
736
732
  if (archive.size === 0) {
737
733
  throw new Error('file.download() returned an empty blob.');
738
734
  }
@@ -784,12 +780,12 @@ async function runScenario(
784
780
  );
785
781
 
786
782
  await runCheckedStep(
787
- 'terminal.spawn',
783
+ 'terminals.create',
788
784
  async () => {
789
- const process = await notebook!.terminal.spawn(
790
- 'php',
791
- ['artisan', 'route:list', `--path=${routeUrlPath}`]
792
- );
785
+ const process = await notebook!.terminals.create({
786
+ command: 'php',
787
+ args: ['artisan', 'route:list', `--path=${routeUrlPath}`],
788
+ });
793
789
 
794
790
  try {
795
791
  const outputPromise = readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars);
@@ -797,7 +793,7 @@ async function runScenario(
797
793
  const [output, exitCode] = await Promise.all([outputPromise, exitPromise]);
798
794
 
799
795
  if (exitCode !== 0) {
800
- throw new Error(`terminal.spawn() exited with ${exitCode}: ${trimDetail(output)}`);
796
+ throw new Error(`terminals.create() exited with ${exitCode}: ${trimDetail(output)}`);
801
797
  }
802
798
 
803
799
  if (!output.includes(routeUrlPath)) {
@@ -820,7 +816,7 @@ async function runScenario(
820
816
  redisRunning = started.running || started.state === 'running' || started.state === 'starting';
821
817
 
822
818
  const stream = notebook!.services.logs('redis', { follow: true, tail: 25 });
823
- const reader = stream.output.getReader();
819
+ const reader = stream.getReader();
824
820
 
825
821
  try {
826
822
  const [snapshot, chunk] = await Promise.all([
@@ -837,7 +833,6 @@ async function runScenario(
837
833
  } finally {
838
834
  await reader.cancel().catch(() => undefined);
839
835
  reader.releaseLock();
840
- await stream.stop().catch(() => false);
841
836
  await notebook!.services.stop('redis').catch(() => started);
842
837
  redisRunning = false;
843
838
  }
@@ -847,25 +842,20 @@ async function runScenario(
847
842
 
848
843
  await runCheckedStep('composer.metadata', async () => {
849
844
  let composerLogChunks = 0;
850
- const composerLogs = notebook!.composer.stream((line) => {
851
- if (line.trim().length > 0) {
845
+ const dump = notebook!.composer.dumpAutoload();
846
+ for await (const chunk of dump.output) {
847
+ if (chunk.data.byteLength > 0) {
852
848
  composerLogChunks += 1;
853
849
  }
854
- });
855
-
856
- try {
857
- const [packages, dump] = await Promise.all([notebook!.composer.packages(), notebook!.composer.dumpAutoload()]);
858
- if (!dump.success) {
859
- throw new Error(dump.message || 'composer.dumpAutoload() failed.');
860
- }
861
- if (packages.length === 0) {
862
- throw new Error('composer.packages() returned no packages.');
863
- }
850
+ }
864
851
 
865
- return `packages=${packages.length} composer-log-events=${composerLogChunks}`;
866
- } finally {
867
- composerLogs.dispose();
852
+ (await dump.wait()).throw();
853
+ const packages = await notebook!.composer.packages();
854
+ if (packages.length === 0) {
855
+ throw new Error('composer.packages() returned no packages.');
868
856
  }
857
+
858
+ return `packages=${packages.length} composer-log-events=${composerLogChunks}`;
869
859
  }, { timeoutMs: Math.max(options.stepTimeoutMs * 2, 120_000) });
870
860
 
871
861
  await runCheckedStep('git.checkpoint', async () => {
@@ -960,11 +950,8 @@ async function runScenario(
960
950
  await notebook.services.stop('redis').catch(() => undefined);
961
951
  }
962
952
 
963
- if (telemetryEnabled) {
964
- await notebook.container.stopTelemetry().catch(() => undefined);
965
- }
966
-
967
- logListener?.dispose();
953
+ await logReader?.cancel().catch(() => undefined);
954
+ logReader = null;
968
955
  for (const disposable of initTraceDisposables) {
969
956
  disposable.dispose();
970
957
  }