@phpsandbox/sdk 0.0.68 → 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.
Files changed (56) hide show
  1. package/README.md +66 -32
  2. package/dist/auth.d.ts +0 -1
  3. package/dist/auth.js +0 -3
  4. package/dist/beacon/index.d.ts +2 -0
  5. package/dist/beacon/index.js +5 -0
  6. package/dist/browser/phpsandbox-sdk.esm.js +842 -1320
  7. package/dist/browser/phpsandbox-sdk.esm.min.js +24 -24
  8. package/dist/browser/phpsandbox-sdk.iife.js +842 -1320
  9. package/dist/browser/phpsandbox-sdk.iife.min.js +23 -23
  10. package/dist/composer.d.ts +36 -23
  11. package/dist/composer.js +61 -11
  12. package/dist/container.d.ts +2 -6
  13. package/dist/container.js +0 -28
  14. package/dist/filesystem.d.ts +58 -17
  15. package/dist/filesystem.js +169 -51
  16. package/dist/index.d.ts +30 -26
  17. package/dist/index.js +37 -32
  18. package/dist/log.d.ts +11 -5
  19. package/dist/log.js +10 -7
  20. package/dist/metrics.d.ts +10 -0
  21. package/dist/metrics.js +13 -0
  22. package/dist/ports.d.ts +10 -0
  23. package/dist/ports.js +13 -0
  24. package/dist/publications.d.ts +222 -0
  25. package/dist/{deployments.js → publications.js} +64 -69
  26. package/dist/repl.d.ts +0 -2
  27. package/dist/repl.js +0 -3
  28. package/dist/runtime/rest.js +1 -4
  29. package/dist/runtime-telemetry.d.ts +15 -0
  30. package/dist/runtime-telemetry.js +111 -0
  31. package/dist/schemas/{deployments.d.ts → publications.d.ts} +76 -76
  32. package/dist/schemas/publications.js +133 -0
  33. package/dist/services.d.ts +2 -9
  34. package/dist/services.js +3 -10
  35. package/dist/shell.d.ts +52 -22
  36. package/dist/shell.js +103 -75
  37. package/dist/terminal.d.ts +18 -14
  38. package/dist/terminal.js +29 -49
  39. package/docs/api-summary.md +37 -29
  40. package/docs/cdn-usage.md +4 -4
  41. package/docs/getting-started.md +17 -17
  42. package/examples/README.md +8 -9
  43. package/examples/basic.ts +2 -2
  44. package/examples/git-merge-smoke.ts +3 -6
  45. package/examples/laravel-load-test.ts +80 -98
  46. package/examples/notebook-readonly-smoke.ts +38 -41
  47. package/examples/publication-provider-label.ts +11 -0
  48. package/examples/{deploy-cloudflare-container.ts → publish-cloudflare-container.ts} +11 -11
  49. package/examples/{deploy-laravel-cloud.ts → publish-laravel-cloud.ts} +11 -11
  50. package/examples/{deploy-ssh-agent.ts → publish-ssh-agent.ts} +15 -16
  51. package/package.json +7 -2
  52. package/dist/deployments.d.ts +0 -251
  53. package/dist/laravel.d.ts +0 -22
  54. package/dist/laravel.js +0 -11
  55. package/dist/schemas/deployments.js +0 -133
  56. package/examples/deployment-provider-label.ts +0 -11
@@ -6,20 +6,20 @@
6
6
  PHPSANDBOX_TOKEN=your-token npx tsx examples/basic.ts
7
7
  ```
8
8
 
9
- ### Running Deployment Examples
9
+ ### Running Publication Examples
10
10
 
11
11
  Cloudflare Containers target:
12
12
 
13
13
  ```bash
14
14
  PHPSANDBOX_TOKEN=your-token PHPSANDBOX_NOTEBOOK_ID=notebook-id \
15
- npx tsx examples/deploy-cloudflare-container.ts
15
+ npx tsx examples/publish-cloudflare-container.ts
16
16
  ```
17
17
 
18
18
  SSH agent target:
19
19
 
20
20
  ```bash
21
21
  PHPSANDBOX_TOKEN=your-token PHPSANDBOX_NOTEBOOK_ID=notebook-id \
22
- npx tsx examples/deploy-ssh-agent.ts
22
+ npx tsx examples/publish-ssh-agent.ts
23
23
  ```
24
24
 
25
25
  Laravel Cloud target:
@@ -29,10 +29,10 @@ PHPSANDBOX_TOKEN=your-token PHPSANDBOX_NOTEBOOK_ID=notebook-id \
29
29
  PHPSANDBOX_LARAVEL_CLOUD_REPOSITORY=owner/repo \
30
30
  PHPSANDBOX_GITHUB_TOKEN=github-token \
31
31
  PHPSANDBOX_LARAVEL_CLOUD_API_KEY=lc-api-key \
32
- npx tsx examples/deploy-laravel-cloud.ts
32
+ npx tsx examples/publish-laravel-cloud.ts
33
33
  ```
34
34
 
35
- Deployment examples accept `PHPSANDBOX_API_URL` and `PHPSANDBOX_DEPLOY_SLUG`. On the first run they create the notebook's deployment; later runs publish the existing deployment without changing slug or provider. The SSH example creates a server registration when `PHPSANDBOX_DEPLOY_SERVER_ID` is not set; on local `.test` APIs it also starts local Rook from `PHPSANDBOX_ROOK_DIR` or the sibling `rook` repo. The Laravel Cloud example syncs the notebook to `PHPSANDBOX_LARAVEL_CLOUD_REPOSITORY`, upserts `PHPSANDBOX_GITHUB_TOKEN` into `GITHUB_TOKEN`, and upserts `PHPSANDBOX_LARAVEL_CLOUD_API_KEY` into `LARAVEL_CLOUD_API_KEY` on first publish.
35
+ Publication examples accept `PHPSANDBOX_API_URL` and `PHPSANDBOX_PUBLISH_SLUG`. On the first run they create the notebook's publication; later runs publish it again without changing slug or provider. The SSH example creates a server registration when `PHPSANDBOX_PUBLISH_SERVER_ID` is not set; on local `.test` APIs it also starts local Rook from `PHPSANDBOX_ROOK_DIR` or the sibling `rook` repo. The Laravel Cloud example syncs the notebook to `PHPSANDBOX_LARAVEL_CLOUD_REPOSITORY`, upserts `PHPSANDBOX_GITHUB_TOKEN` into `GITHUB_TOKEN`, and upserts `PHPSANDBOX_LARAVEL_CLOUD_API_KEY` into `LARAVEL_CLOUD_API_KEY` on first publish.
36
36
 
37
37
  ### Running The Laravel Load Test
38
38
 
@@ -105,7 +105,7 @@ Useful flags:
105
105
  - `--expect-provisioned=true` asserts the `notebook.ready()` initialization result reports provisioning work.
106
106
  - `--expect-provisioning-action=template` asserts the initialization result action.
107
107
  - `--reauthorization-url=wss://...?ticket=...` exercises `notebook.auth.login()` / `logout()` with a fresh ticket-bearing runtime URL.
108
- - `--mode=init` limits the scenario to notebook create, ready, and delete so you can isolate init capacity.
108
+ - `--mode=init` limits the scenario to notebook create, ready, and destroy so you can isolate init capacity.
109
109
  - `--init-only` is shorthand for `--mode=init`.
110
110
  - `--stop-on-failure-level` stops after the first level that has at least one failing scenario.
111
111
  - `--local` switches the default API URL to `https://api.phpsandbox.test/v1` and defaults TLS to insecure unless you also pass `--ca-file` or `--no-insecure-tls`.
@@ -116,12 +116,12 @@ Useful flags:
116
116
 
117
117
  Default baseline coverage:
118
118
 
119
- - notebook lifecycle: create, ready, ping, delete
119
+ - notebook lifecycle: create, ready, ping, destroy
120
120
  - secrets
121
121
  - container state and telemetry; optional restart with `--include-container-restart`
122
122
  - filesystem watch/write/read/search/find/tree/tail
123
123
  - optional preview fetch with `--include-preview`
124
- - shell exec and stream
124
+ - shell process execution and streams
125
125
  - terminal spawn streaming
126
126
  - services with Redis logs
127
127
  - composer metadata and autoload dump
@@ -132,7 +132,6 @@ Intentionally excluded by default:
132
132
 
133
133
  - `beacon`, because it needs a browser iframe runtime
134
134
  - `auth`, because it needs a fresh ticket-bearing runtime URL
135
- - `laravel.maintenance.*`, because it is not part of the current okra websocket action set
136
135
  - `lsp`, because it is useful but more brittle than the rest of the headless baseline
137
136
 
138
137
  ### Running A Read-Only Existing Notebook Smoke Test
package/examples/basic.ts CHANGED
@@ -16,9 +16,9 @@ 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.
23
23
  notebook.dispose();
24
- await notebook.delete();
24
+ await notebook.destroy();
@@ -41,7 +41,7 @@ async function main(): Promise<void> {
41
41
  let shouldDeleteFork = false;
42
42
 
43
43
  try {
44
- base = await client.notebook.open(options.notebookId);
44
+ base = await client.notebook.get(options.notebookId);
45
45
  console.log(`Base notebook: ${base.data.id}`);
46
46
 
47
47
  await base.ready();
@@ -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);
@@ -98,7 +95,7 @@ async function main(): Promise<void> {
98
95
  } finally {
99
96
  if (shouldDeleteFork && fork) {
100
97
  try {
101
- await fork.delete();
98
+ await fork.destroy();
102
99
  console.log(`Deleted fork notebook: ${fork.data.id}`);
103
100
  } catch (error) {
104
101
  console.error(`Could not delete fork notebook ${fork.data.id}: ${formatError(error)}`);
@@ -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
  }
@@ -743,8 +739,8 @@ async function runScenario(
743
739
  recordSkippedStep(steps, 'filesystem.download', 'Enable with --include-download.');
744
740
  }
745
741
 
746
- await runCheckedStep('shell.exec', async () => {
747
- const result = await notebook!.shell.exec(['php', 'artisan', '--version']);
742
+ await runCheckedStep('shell.run', async () => {
743
+ const result = await notebook!.run(['php', 'artisan', '--version']).wait();
748
744
  result.throw();
749
745
  if (!result.output.toLowerCase().includes('laravel')) {
750
746
  throw new Error(`Unexpected artisan version output: ${trimDetail(result.output)}`);
@@ -754,21 +750,22 @@ async function runScenario(
754
750
  }, { skipIf: explainUnsupportedShell });
755
751
 
756
752
  await runCheckedStep(
757
- 'shell.exec-stream',
753
+ 'shell.run-stream',
758
754
  async () => {
759
- const execution = notebook!.shell.execStream(
760
- ['php', 'artisan', 'route:list', `--path=${routeUrlPath}`]
755
+ const execution = notebook!.run(
756
+ ['php', 'artisan', 'route:list', `--path=${routeUrlPath}`],
757
+ { tty: false },
761
758
  );
762
759
 
763
760
  const [streamOutput, result] = await Promise.all([
764
- readStreamToString(execution.output, options.streamTimeoutMs, options.maxStreamChars).catch((error) => {
761
+ readStreamToString(execution.stdout, options.streamTimeoutMs, options.maxStreamChars).catch((error) => {
765
762
  if (isStreamInactivityError(error)) {
766
763
  return '';
767
764
  }
768
765
 
769
766
  throw error;
770
767
  }),
771
- withTimeout(execution.result, options.stepTimeoutMs, 'waiting for shell.execStream() result'),
768
+ withTimeout(execution.wait(), options.stepTimeoutMs, 'waiting for process exit'),
772
769
  ]);
773
770
 
774
771
  result.throw();
@@ -783,20 +780,20 @@ async function runScenario(
783
780
  );
784
781
 
785
782
  await runCheckedStep(
786
- 'terminal.spawn',
783
+ 'terminals.create',
787
784
  async () => {
788
- const process = await notebook!.terminal.spawn(
789
- 'php',
790
- ['artisan', 'route:list', `--path=${routeUrlPath}`]
791
- );
785
+ const process = await notebook!.terminals.create({
786
+ command: 'php',
787
+ args: ['artisan', 'route:list', `--path=${routeUrlPath}`],
788
+ });
792
789
 
793
790
  try {
794
791
  const outputPromise = readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars);
795
- const exitPromise = withTimeout(process.exit, options.stepTimeoutMs, 'waiting for terminal process to exit');
792
+ const exitPromise = withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal process to exit');
796
793
  const [output, exitCode] = await Promise.all([outputPromise, exitPromise]);
797
794
 
798
795
  if (exitCode !== 0) {
799
- throw new Error(`terminal.spawn() exited with ${exitCode}: ${trimDetail(output)}`);
796
+ throw new Error(`terminals.create() exited with ${exitCode}: ${trimDetail(output)}`);
800
797
  }
801
798
 
802
799
  if (!output.includes(routeUrlPath)) {
@@ -819,7 +816,7 @@ async function runScenario(
819
816
  redisRunning = started.running || started.state === 'running' || started.state === 'starting';
820
817
 
821
818
  const stream = notebook!.services.logs('redis', { follow: true, tail: 25 });
822
- const reader = stream.output.getReader();
819
+ const reader = stream.getReader();
823
820
 
824
821
  try {
825
822
  const [snapshot, chunk] = await Promise.all([
@@ -836,7 +833,6 @@ async function runScenario(
836
833
  } finally {
837
834
  await reader.cancel().catch(() => undefined);
838
835
  reader.releaseLock();
839
- await stream.stop().catch(() => false);
840
836
  await notebook!.services.stop('redis').catch(() => started);
841
837
  redisRunning = false;
842
838
  }
@@ -844,33 +840,22 @@ async function runScenario(
844
840
  { timeoutMs: options.stepTimeoutMs * 2 }
845
841
  );
846
842
 
847
- recordSkippedStep(
848
- steps,
849
- 'laravel.maintenance',
850
- 'Not part of the current okra websocket action set.'
851
- );
852
-
853
843
  await runCheckedStep('composer.metadata', async () => {
854
844
  let composerLogChunks = 0;
855
- const composerLogs = notebook!.composer.stream((line) => {
856
- 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) {
857
848
  composerLogChunks += 1;
858
849
  }
859
- });
860
-
861
- try {
862
- const [packages, dump] = await Promise.all([notebook!.composer.packages(), notebook!.composer.dumpAutoload()]);
863
- if (!dump.success) {
864
- throw new Error(dump.message || 'composer.dumpAutoload() failed.');
865
- }
866
- if (packages.length === 0) {
867
- throw new Error('composer.packages() returned no packages.');
868
- }
850
+ }
869
851
 
870
- return `packages=${packages.length} composer-log-events=${composerLogChunks}`;
871
- } finally {
872
- 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.');
873
856
  }
857
+
858
+ return `packages=${packages.length} composer-log-events=${composerLogChunks}`;
874
859
  }, { timeoutMs: Math.max(options.stepTimeoutMs * 2, 120_000) });
875
860
 
876
861
  await runCheckedStep('git.checkpoint', async () => {
@@ -950,7 +935,7 @@ async function runScenario(
950
935
  if (options.reauthorizationUrl) {
951
936
  await runCheckedStep('auth.login-logout', async () => {
952
937
  await notebook!.auth.login(options.reauthorizationUrl!);
953
- await notebook!.auth.logout();
938
+ notebook!.dispose();
954
939
  return 'ok';
955
940
  });
956
941
  } else {
@@ -965,11 +950,8 @@ async function runScenario(
965
950
  await notebook.services.stop('redis').catch(() => undefined);
966
951
  }
967
952
 
968
- if (telemetryEnabled) {
969
- await notebook.container.stopTelemetry().catch(() => undefined);
970
- }
971
-
972
- logListener?.dispose();
953
+ await logReader?.cancel().catch(() => undefined);
954
+ logReader = null;
973
955
  for (const disposable of initTraceDisposables) {
974
956
  disposable.dispose();
975
957
  }
@@ -1020,7 +1002,7 @@ async function cleanupNotebook(notebook: NotebookInstance, steps: StepResult[]):
1020
1002
  const notebookId = notebook.data.id;
1021
1003
  await cleanupDeleteLimiter.run(async () => {
1022
1004
  try {
1023
- await retryWithBackoff(() => notebook.delete(), DELETE_RETRY_DELAYS_MS, shouldRetryApiRateLimitError);
1005
+ await retryWithBackoff(() => notebook.destroy(), DELETE_RETRY_DELAYS_MS, shouldRetryApiRateLimitError);
1024
1006
  } catch (error) {
1025
1007
  if (formatError(error).includes('Notebook has been deleted')) {
1026
1008
  return;
@@ -1912,7 +1894,7 @@ function firstFulfilled<T>(promises: Array<Promise<T>>): Promise<T> {
1912
1894
  }
1913
1895
 
1914
1896
  async function readStreamToString(
1915
- stream: ReadableStream<string>,
1897
+ stream: ReadableStream<string | Uint8Array>,
1916
1898
  timeoutMs: number,
1917
1899
  maxChars: number
1918
1900
  ): Promise<string> {
@@ -1926,7 +1908,7 @@ async function readStreamToString(
1926
1908
  break;
1927
1909
  }
1928
1910
 
1929
- output += chunk.value ?? '';
1911
+ output += chunk.value instanceof Uint8Array ? new TextDecoder().decode(chunk.value) : (chunk.value ?? '');
1930
1912
  }
1931
1913
 
1932
1914
  return output;
@@ -155,12 +155,12 @@ async function main(): Promise<void> {
155
155
  }, false);
156
156
 
157
157
  await recordStep(steps, 'container.opened-ports', options.stepTimeoutMs, async () => {
158
- const ports = await notebook!.container.openedPorts();
159
- return describePorts(assertPortInfoList(ports, 'container.openedPorts'));
158
+ const ports = await notebook!.ports.list();
159
+ return describePorts(assertPortInfoList(ports, 'ports.list'));
160
160
  }, false);
161
161
 
162
162
  const rootSnapshot = await recordStep(steps, 'filesystem.root', options.stepTimeoutMs, async () => {
163
- const rootEntries = await notebook!.file.readDirectory('/');
163
+ const rootEntries = await notebook!.files.list('/');
164
164
 
165
165
  const preferred = ['composer.json', 'index.php', 'artisan'];
166
166
  const rootNames = new Set(rootEntries.map(([name]) => name));
@@ -174,7 +174,7 @@ async function main(): Promise<void> {
174
174
  const treePath = rootSnapshot.candidate === 'artisan' ? 'routes' : undefined;
175
175
  if (treePath) {
176
176
  await recordStep(steps, 'filesystem.tree', options.stepTimeoutMs, async () => {
177
- const tree = await notebook!.file.tree(treePath);
177
+ const tree = await notebook!.files.tree(treePath);
178
178
  return `path=${treePath} bytes=${tree.length}`;
179
179
  }, false);
180
180
  } else {
@@ -183,8 +183,8 @@ async function main(): Promise<void> {
183
183
 
184
184
  const findSnapshot = await recordStep(steps, 'filesystem.find', options.stepTimeoutMs, async () => {
185
185
  const [composerFiles, indexFiles] = await Promise.all([
186
- notebook!.file.find('composer.json', { maxResults: 10 }),
187
- notebook!.file.find('index.php', { maxResults: 10 }),
186
+ notebook!.files.find('composer.json', { maxResults: 10 }),
187
+ notebook!.files.find('index.php', { maxResults: 10 }),
188
188
  ]);
189
189
  const candidate = [...composerFiles, ...indexFiles].find((file) => typeof file.path === 'string')?.path;
190
190
  return {
@@ -197,15 +197,13 @@ async function main(): Promise<void> {
197
197
 
198
198
  if (candidate) {
199
199
  await recordStep(steps, 'filesystem.read-candidate', options.stepTimeoutMs, async () => {
200
- const [info, stat, content, tail] = await Promise.all([
201
- notebook!.file.info(candidate),
202
- notebook!.file.stat(candidate),
203
- notebook!.file.readFile(candidate),
204
- notebook!.file.tail(candidate, 10),
200
+ const [stat, content, tail] = await Promise.all([
201
+ notebook!.files.stat(candidate),
202
+ notebook!.files.read(candidate),
203
+ notebook!.files.tail(candidate, { lines: 10 }),
205
204
  ]);
206
205
 
207
- const bytes = content instanceof Uint8Array ? content : new TextEncoder().encode(JSON.stringify(content));
208
- return `path=${candidate} size=${stat.size} binary=${info.binary} sha256=${sha256(bytes)} tailBytes=${tail.length}`;
206
+ return `path=${candidate} size=${stat.size} sha256=${sha256(content)} tailBytes=${tail.length}`;
209
207
  }, false);
210
208
  } else {
211
209
  recordSkippedStep(steps, 'filesystem.read-candidate', 'No readable candidate file found.');
@@ -214,7 +212,10 @@ async function main(): Promise<void> {
214
212
  if (options.includeDownload) {
215
213
  if (candidate) {
216
214
  await recordStep(steps, 'filesystem.download', options.stepTimeoutMs, async () => {
217
- const archive = await notebook!.file.download(undefined, ['vendor/**', 'node_modules/**'], [candidate]);
215
+ const archive = await notebook!.files.download({
216
+ exclude: ['vendor/**', 'node_modules/**'],
217
+ include: [candidate],
218
+ });
218
219
  if (archive.size === 0) {
219
220
  throw new Error('file.download() returned an empty blob.');
220
221
  }
@@ -228,7 +229,7 @@ async function main(): Promise<void> {
228
229
  }
229
230
 
230
231
  await recordStep(steps, 'filesystem.search', options.stepTimeoutMs, async () => {
231
- const [hasMore, matches] = await notebook!.file.search({
232
+ const [hasMore, matches] = await notebook!.files.search({
232
233
  pattern: 'php',
233
234
  isRegExp: false,
234
235
  isCaseSensitive: false,
@@ -259,8 +260,8 @@ async function main(): Promise<void> {
259
260
  if (options.includeExec) {
260
261
  await runExecutionChecks(notebook, steps, options);
261
262
  } else {
262
- recordSkippedStep(steps, 'shell.exec', 'Enable with --include-exec.');
263
- recordSkippedStep(steps, 'terminal.spawn', 'Enable with --include-exec.');
263
+ recordSkippedStep(steps, 'shell.run', 'Enable with --include-exec.');
264
+ recordSkippedStep(steps, 'terminals.create', 'Enable with --include-exec.');
264
265
  recordSkippedStep(steps, 'terminal.interactive', 'Enable with --include-exec.');
265
266
  recordSkippedStep(steps, 'terminal.osc', 'Enable with --include-exec.');
266
267
  recordSkippedStep(steps, 'repl.eval', 'Enable with --include-exec.');
@@ -323,8 +324,8 @@ async function main(): Promise<void> {
323
324
  }
324
325
 
325
326
  async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifact[], options: SmokeOptions): Promise<void> {
326
- await recordStep(steps, 'shell.exec', options.stepTimeoutMs, async () => {
327
- const result = await notebook.shell.exec(['php', '-r', 'echo "sdk-smoke";']);
327
+ await recordStep(steps, 'shell.run', options.stepTimeoutMs, async () => {
328
+ const result = await notebook.run(['php', '-r', 'echo "sdk-smoke";']).wait();
328
329
  result.throw();
329
330
  if (!result.output.includes('sdk-smoke')) {
330
331
  throw new Error(`Unexpected shell output: ${result.output}`);
@@ -332,26 +333,25 @@ async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifac
332
333
  return `exit=${result.exitCode}`;
333
334
  }, false);
334
335
 
335
- await recordStep(steps, 'terminal.spawn', options.stepTimeoutMs, async () => {
336
- const process = await notebook.terminal.spawn('php', ['-r', 'echo "sdk-smoke";'], { output: true });
336
+ await recordStep(steps, 'terminals.create', options.stepTimeoutMs, async () => {
337
+ const process = await notebook.terminals.create({ command: 'php', args: ['-r', 'echo "sdk-smoke";'] });
337
338
  const [exitCode, output] = await Promise.all([
338
- withTimeout(process.exit, options.stepTimeoutMs, 'waiting for terminal process exit'),
339
+ withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal process exit'),
339
340
  readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars),
340
341
  ]);
341
342
 
342
343
  if (exitCode !== 0 || !output.includes('sdk-smoke')) {
343
- throw new Error(`terminal.spawn failed: exit=${exitCode} output=${trimDetail(output)}`);
344
+ throw new Error(`terminals.create failed: exit=${exitCode} output=${trimDetail(output)}`);
344
345
  }
345
346
 
346
347
  return `exit=${exitCode}`;
347
348
  }, false);
348
349
 
349
350
  await recordStep(steps, 'terminal.interactive', options.stepTimeoutMs, async () => {
350
- const process = await notebook.terminal.spawn(
351
- 'bash',
352
- ['-lc', 'IFS= read -r line; printf "sdk-input:%s\\n" "$line"'],
353
- { output: true }
354
- );
351
+ const process = await notebook.terminals.create({
352
+ command: 'bash',
353
+ args: ['-lc', 'IFS= read -r line; printf "sdk-input:%s\\n" "$line"'],
354
+ });
355
355
 
356
356
  try {
357
357
  const writer = process.input.getWriter();
@@ -359,7 +359,7 @@ async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifac
359
359
  writer.releaseLock();
360
360
 
361
361
  const [exitCode, output] = await Promise.all([
362
- withTimeout(process.exit, options.stepTimeoutMs, 'waiting for interactive terminal process exit'),
362
+ withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for interactive terminal process exit'),
363
363
  readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars),
364
364
  ]);
365
365
 
@@ -374,19 +374,16 @@ async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifac
374
374
  }, false);
375
375
 
376
376
  await recordStep(steps, 'terminal.osc', options.stepTimeoutMs, async () => {
377
- const process = await notebook.terminal.spawn(
378
- 'bash',
379
- ['-lc', 'printf "sdk-prompt$"'],
380
- {
381
- output: true,
382
- osc: true,
383
- terminal: { cols: 80, rows: 15 },
384
- }
385
- );
377
+ const process = await notebook.terminals.create({
378
+ command: 'bash',
379
+ args: ['-lc', 'printf "sdk-prompt$"'],
380
+ osc: true,
381
+ size: [80, 15],
382
+ });
386
383
 
387
384
  try {
388
385
  const [exitCode, output] = await Promise.all([
389
- withTimeout(process.exit, options.stepTimeoutMs, 'waiting for terminal OSC process exit'),
386
+ withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal OSC process exit'),
390
387
  readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars),
391
388
  ]);
392
389
 
@@ -453,7 +450,7 @@ function recordSkippedStep(steps: StepArtifact[], name: string, detail: string):
453
450
  console.log(`SKIP ${name} ${detail}`);
454
451
  }
455
452
 
456
- async function readStreamToString(stream: ReadableStream<string>, timeoutMs: number, maxChars: number): Promise<string> {
453
+ async function readStreamToString(stream: ReadableStream<string | Uint8Array>, timeoutMs: number, maxChars: number): Promise<string> {
457
454
  const reader = stream.getReader();
458
455
  let output = '';
459
456
 
@@ -463,7 +460,7 @@ async function readStreamToString(stream: ReadableStream<string>, timeoutMs: num
463
460
  if (chunk.done) {
464
461
  break;
465
462
  }
466
- output += chunk.value ?? '';
463
+ output += chunk.value instanceof Uint8Array ? new TextDecoder().decode(chunk.value) : (chunk.value ?? '');
467
464
  }
468
465
  return output;
469
466
  } finally {