@phpsandbox/sdk 0.1.9 → 0.1.11

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 (61) hide show
  1. package/README.md +42 -50
  2. package/dist/beacon/index.d.ts +1 -7
  3. package/dist/beacon/index.js +14 -13
  4. package/dist/beacon/navigator.d.ts +8 -36
  5. package/dist/beacon/navigator.js +1 -62
  6. package/dist/browser/phpsandbox-sdk.esm.js +552 -692
  7. package/dist/browser/phpsandbox-sdk.esm.min.js +24 -24
  8. package/dist/browser/phpsandbox-sdk.iife.js +552 -692
  9. package/dist/browser/phpsandbox-sdk.iife.min.js +35 -35
  10. package/dist/composer.d.ts +10 -8
  11. package/dist/composer.js +18 -18
  12. package/dist/errors/index.d.ts +43 -8
  13. package/dist/errors/index.js +120 -14
  14. package/dist/feedback.js +1 -2
  15. package/dist/filesystem.d.ts +0 -17
  16. package/dist/filesystem.js +2 -21
  17. package/dist/git.d.ts +1 -5
  18. package/dist/git.js +6 -8
  19. package/dist/http.d.ts +6 -0
  20. package/dist/http.js +11 -0
  21. package/dist/index.d.ts +54 -144
  22. package/dist/index.js +170 -289
  23. package/dist/integrations.d.ts +0 -2
  24. package/dist/integrations.js +5 -11
  25. package/dist/log.d.ts +0 -2
  26. package/dist/lsp.js +1 -1
  27. package/dist/metrics.d.ts +3 -3
  28. package/dist/ports.d.ts +1 -1
  29. package/dist/publications.d.ts +4 -54
  30. package/dist/publications.js +25 -103
  31. package/dist/runtime/rest.js +35 -18
  32. package/dist/runtime.d.ts +68 -0
  33. package/dist/runtime.js +46 -0
  34. package/dist/sandbox-integrations.d.ts +1 -2
  35. package/dist/sandbox-integrations.js +11 -14
  36. package/dist/schemas/publications.d.ts +0 -6
  37. package/dist/schemas/publications.js +0 -1
  38. package/dist/servers.d.ts +4 -2
  39. package/dist/servers.js +5 -8
  40. package/dist/shell.d.ts +2 -14
  41. package/dist/shell.js +1 -21
  42. package/dist/socket/index.d.ts +6 -7
  43. package/dist/socket/index.js +39 -25
  44. package/dist/types.d.ts +0 -12
  45. package/dist/types.js +1 -15
  46. package/docs/api-summary.md +28 -28
  47. package/docs/cdn-usage.md +43 -55
  48. package/docs/getting-started.md +43 -26
  49. package/examples/README.md +5 -35
  50. package/examples/basic.ts +9 -18
  51. package/examples/get.ts +2 -2
  52. package/examples/git-merge-smoke.ts +2 -3
  53. package/examples/laravel-load-test.ts +31 -36
  54. package/examples/notebook-readonly-smoke.ts +12 -26
  55. package/examples/publication-provider-label.ts +1 -1
  56. package/examples/publish-cloudflare-container.ts +2 -2
  57. package/examples/publish-laravel-cloud.ts +2 -2
  58. package/examples/publish-ssh-agent.ts +2 -2
  59. package/package.json +4 -3
  60. package/dist/container.d.ts +0 -65
  61. package/dist/container.js +0 -30
@@ -5,7 +5,8 @@ This guide walks through the fastest path from install to a working notebook.
5
5
  ## Prerequisites
6
6
 
7
7
  - Node.js 18+
8
- - A PHPSandbox API token
8
+ - A PHPSandbox account with an active subscription and available notebook capacity
9
+ - SDK access enabled and an API key created in **API keys**
9
10
 
10
11
  ## Install
11
12
 
@@ -13,6 +14,10 @@ This guide walks through the fastest path from install to a working notebook.
13
14
  npm install @phpsandbox/sdk
14
15
  ```
15
16
 
17
+ Copy the API key when shown and store it in `PHPSANDBOX_TOKEN` in your server environment or secret store. Keys are shown once and grant notebook read and write access; never include them in frontend bundles or commit them. If **API keys** is unavailable, request SDK access first.
18
+
19
+ For a complete runnable example, save the [root quick start](../README.md#quick-start) as `quickstart.mjs` and run `node quickstart.mjs`. The snippets below explain individual operations.
20
+
16
21
  ## 1. Create a Client
17
22
 
18
23
  ```ts
@@ -21,23 +26,20 @@ import { PHPSandbox } from '@phpsandbox/sdk';
21
26
  const token = process.env.PHPSANDBOX_TOKEN;
22
27
  if (!token) throw new Error('Missing PHPSANDBOX_TOKEN');
23
28
 
24
- const client = new PHPSandbox(token);
29
+ const client = PHPSandbox.realtime(token);
25
30
  ```
26
31
 
27
32
  ## 2. Create or Open a Notebook
28
33
 
29
34
  ```ts
30
- // create() initializes by default
31
- const notebook = await client.notebook.create('php');
35
+ const notebook = await client.notebook.create('standard');
32
36
 
33
37
  const persistent = await client.notebook.create('laravel', {
34
38
  title: 'Persistent Laravel',
35
39
  persistent: true,
36
40
  });
37
41
 
38
- // get() needs ready()
39
42
  const existing = await client.notebook.get('notebook-id');
40
- await existing.ready();
41
43
 
42
44
  const fork = await existing.fork();
43
45
  const persistentFork = await existing.fork({ persistent: true });
@@ -75,15 +77,12 @@ await notebook.files.write('binary.dat', new Uint8Array([1, 2, 3]), {
75
77
  const task = await notebook.terminals.create({ command: 'php', args: ['hello.php'] });
76
78
  const reader = task.output.getReader();
77
79
 
78
- try {
79
- while (true) {
80
- const { done, value } = await reader.read();
81
- if (done) break;
82
- process.stdout.write(value);
83
- }
84
- } finally {
85
- reader.releaseLock();
80
+ while (true) {
81
+ const { done, value } = await reader.read();
82
+ if (done) break;
83
+ process.stdout.write(value);
86
84
  }
85
+ reader.releaseLock();
87
86
 
88
87
  const exitCode = await task.wait();
89
88
  console.log(`\nExit code: ${exitCode}`);
@@ -93,7 +92,7 @@ console.log(`\nExit code: ${exitCode}`);
93
92
 
94
93
  ```ts
95
94
  const result = await notebook.exec('php -v', { tty: false });
96
- result.throw();
95
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.output);
97
96
  console.log(result.stdout);
98
97
  ```
99
98
 
@@ -105,9 +104,9 @@ const matches = await notebook.files.find('*.php', {
105
104
  excludes: ['vendor/**'],
106
105
  });
107
106
 
108
- console.log(matches.map((m) => m.path));
107
+ console.log(matches.map((match) => match.path));
109
108
 
110
- const watcher = notebook.files.watch(
109
+ const watcher = await notebook.files.watch(
111
110
  '/app',
112
111
  {
113
112
  recursive: true,
@@ -125,28 +124,46 @@ watcher.dispose();
125
124
  ## 6. Use Composer and Git
126
125
 
127
126
  ```ts
128
- (await notebook.composer.install().wait()).throw();
129
- (await notebook.composer.require({ packages: ['monolog/monolog'] }).wait()).throw();
127
+ const installResult = await notebook.composer.run('install').wait();
128
+ if (installResult.exitCode !== 0) throw new Error(installResult.stderr || installResult.output);
129
+
130
+ const requireResult = await notebook.composer.run('require', { packages: ['monolog/monolog'] }).wait();
131
+ if (requireResult.exitCode !== 0) throw new Error(requireResult.stderr || requireResult.output);
130
132
 
131
133
  await notebook.git.checkpoint('Jane Doe <jane@example.com>', 'Initial checkpoint');
132
134
  ```
133
135
 
134
136
  ## 7. Clean Up
135
137
 
136
- Always dispose notebook connections when finished:
138
+ Use a `finally` block to dispose every connection, including forks and notebooks opened with `get()`. `dispose()` only closes local resources. For temporary automation, delete the remote notebook as well:
137
139
 
138
140
  ```ts
139
- notebook.dispose();
141
+ try {
142
+ await notebook.destroy();
143
+ } finally {
144
+ notebook.dispose();
145
+ }
140
146
  ```
141
147
 
148
+ Omit `destroy()` only when you intend to keep the notebook. Stopping a notebook does not delete it. Ephemeral notebooks are eventually pruned according to `notebook.data.policy`; do not rely on that as immediate cleanup.
149
+
142
150
  ## Common Pitfalls
143
151
 
144
- - `get()` notebooks must call `await notebook.ready()` before tool calls.
152
+ - Runtime actions wait for readiness. Call `ready()` only when you need its provisioning metadata.
145
153
  - `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.
154
+ - Command results include `exitCode`; decide explicitly whether a non-zero exit should fail your workflow.
155
+
156
+ ## Troubleshooting
157
+
158
+ - `401`: check that the API key is set and has not been revoked.
159
+ - `403`: check permissions, account subscription, notebook capacity, and persistence entitlement.
160
+ - `404`: the notebook is missing or inaccessible to your account.
161
+ - `422`: use `RemoteError.isValidation(error)` to inspect field errors; `standard` is the plain PHP template ID.
162
+ - `429`: respect `error.response.headers.get('Retry-After')` and reduce concurrency. Avoid blind retries of resource creation or publication.
163
+ - A process stays alive after the script finishes: ensure all notebook instances are disposed in `finally`.
147
164
 
148
165
  ## Next Reading
149
166
 
150
- - API overview: `docs/api-summary.md`
151
- - CDN/browser usage: `docs/cdn-usage.md`
152
- - Root quick start: `README.md`
167
+ - API overview: [API overview](api-summary.md)
168
+ - CDN/browser usage: [Browser usage](cdn-usage.md)
169
+ - Root quick start: [Root quick start](../README.md)
@@ -1,5 +1,7 @@
1
1
  ## Examples
2
2
 
3
+ Examples import the published `@phpsandbox/sdk` package. From a source checkout, run `npm ci && npm run build` first. Set `PHPSANDBOX_TOKEN` using your environment or secret store. The basic example creates and deletes one temporary notebook; publication examples can change deployments and provider resources.
4
+
3
5
  ### Running Basic Example
4
6
 
5
7
  ```bash
@@ -97,7 +99,7 @@ Useful flags:
97
99
  - `--no-cleanup` keeps every notebook after the run.
98
100
  - `--keep-failed` keeps failed notebooks for inspection. By default, failures are deleted too.
99
101
  - `--runtime-origin=ws://127.0.0.1:9511` preserves the SDK-issued websocket path/auth but routes runtime traffic to another host.
100
- - `--include-container-restart` adds a mutable `container.start()` restart check on the fresh notebook.
102
+ - `--include-container-restart` adds a runtime restart check on the fresh notebook.
101
103
  - `--include-download` adds `file.download()` to the baseline.
102
104
  - `--include-preview` fetches the initialized preview URL in init mode, or the route written by the scenario in full mode.
103
105
  - `--include-lsp` adds an LSP initialize handshake to the baseline.
@@ -196,38 +198,6 @@ PHPSANDBOX_TOKEN=your-token npx tsx examples/git-merge-smoke.ts \
196
198
 
197
199
  By default the created fork is kept for inspection. Pass `--delete-fork` when you want the script to delete it after the run.
198
200
 
199
- ### Okra Operator Loop
200
-
201
- For repeatable prod investigations, use the operator CLI:
202
-
203
- ```bash
204
- npx tsx tools/okra-loop.ts run --label=init-64-96 -- --levels=64,80,96 --waves=2 --mode=init
205
- ```
206
-
207
- If you omit the load-test level flags entirely, the operator loop defaults to a conservative staircase:
208
-
209
- ```bash
210
- npx tsx tools/okra-loop.ts run --label=quick-probe
211
- ```
201
+ ### Repository-only operator tools
212
202
 
213
- That default probe uses `--levels=1,2,4,8,12,16,24,32,48,64,80,96` and enables `--stop-on-failure-level` so you get the first failing boundary quickly.
214
-
215
- This will:
216
-
217
- - run the Laravel load test and save `load-test.json`, stdout, and stderr into `artifacts/okra-loop/<timestamp>-<label>/`
218
- - capture before/after snapshots from `okra-main`, `okra-helfer`, and `okra-helfer-prime`
219
- - sample remote load/memory/supervisor state during the run
220
- - write `analysis.json` plus a `report.md` with hotspot heuristics and next actions
221
-
222
- Useful operator flags:
223
-
224
- - `--hosts=none` disables SSH collection, which is useful for local API runs.
225
- - `--deploy-hosts=workers` deploys the local okra checkout to `okra-helfer` and `okra-helfer-prime` before the run.
226
- - `--artifact-root=...` changes where run artifacts are stored.
227
- - `--sample-interval-ms=15000` adjusts the SSH sampling interval.
228
-
229
- Deploy only:
230
-
231
- ```bash
232
- npx tsx tools/okra-loop.ts deploy --deploy-hosts=workers
233
- ```
203
+ The `tools/okra-loop.ts` operator utility requires the source checkout and maintainer infrastructure access. It is not included in the published package and is not part of consumer onboarding.
package/examples/basic.ts CHANGED
@@ -1,24 +1,15 @@
1
- import { PHPSandbox } from '../src/index.js';
1
+ import { PHPSandbox } from '@phpsandbox/sdk';
2
2
 
3
3
  const token = process.env.PHPSANDBOX_TOKEN;
4
- if (!token) {
5
- throw new Error('Missing PHPSANDBOX_TOKEN');
6
- }
4
+ if (!token) throw new Error('Missing PHPSANDBOX_TOKEN');
7
5
 
8
- const psb = new PHPSandbox(token);
9
- const notebook = await psb.notebook.create('laravel');
6
+ const client = PHPSandbox.realtime(token);
7
+ const notebook = await client.notebook.create('standard');
10
8
 
11
- console.log('====== Ping the notebook ======');
12
- console.log('Response:', await notebook.ping());
13
- console.log('====== Notebook is ready ======');
9
+ await notebook.files.write('hello.php', '<?php echo "Hello from PHPSandbox";');
10
+ const result = await notebook.exec('php', ['hello.php']);
11
+ if (result.exitCode !== 0) throw new Error(result.stderr || result.output);
12
+ console.log(result.stdout.trim());
14
13
 
15
- console.log('\n');
16
-
17
- console.log('====== Read routes/web.php in the notebook ======');
18
- const decoder = new TextDecoder();
19
- const content = await notebook.files.read('routes/web.php');
20
- console.log(decoder.decode(content));
21
-
22
- // Always close the notebook when done to clean up the connection.
23
- notebook.dispose();
24
14
  await notebook.destroy();
15
+ notebook.dispose();
package/examples/get.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Client } from '../src/index.js';
1
+ import { PHPSandbox } from '@phpsandbox/sdk';
2
2
 
3
3
  const token = process.env.PHPSANDBOX_TOKEN;
4
4
  if (!token) {
@@ -11,7 +11,7 @@ if (!notebookId) {
11
11
  }
12
12
 
13
13
  const apiUrl = process.env.PHPSANDBOX_API_URL ?? 'https://api.phpsandbox.io/v1';
14
- const client = new Client(token, apiUrl);
14
+ const client = PHPSandbox.realtime(token, apiUrl);
15
15
  const notebook = await client.notebook.get(notebookId);
16
16
 
17
17
  try {
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import https from 'node:https';
3
3
  import axios from 'axios';
4
4
  import NodeWebSocket from 'ws';
5
- import { PHPSandbox, type GitDiffPreview, type GitMergeResult, type NotebookInstance } from '../src/index.js';
5
+ import { PHPSandbox, type GitDiffPreview, type GitMergeResult, type NotebookInstance } from '@phpsandbox/sdk';
6
6
 
7
7
  const DEFAULT_API_URL = 'https://api.phpsandbox.io/v1';
8
8
  const DEFAULT_LOCAL_API_URL = 'https://api.phpsandbox.test/v1';
@@ -31,8 +31,7 @@ interface Options {
31
31
  async function main(): Promise<void> {
32
32
  const options = parseOptions(process.argv.slice(2));
33
33
  const transport = await createNodeTransport(options);
34
- const client = new PHPSandbox(options.token, options.apiUrl, {
35
- startClosed: true,
34
+ const client = PHPSandbox.realtime(options.token, options.apiUrl, {
36
35
  ...transport,
37
36
  });
38
37
 
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import https from 'node:https';
3
3
  import {
4
- type ContainerStats,
5
4
  type Disposable,
6
5
  type FileChange,
7
6
  type NotebookInitProgress,
@@ -12,7 +11,8 @@ import {
12
11
  NotebookInstance,
13
12
  type PortInfo,
14
13
  PHPSandbox,
15
- } from '../src/index.js';
14
+ RemoteError,
15
+ } from '@phpsandbox/sdk';
16
16
  import axios from 'axios';
17
17
  import NodeWebSocket from 'ws';
18
18
 
@@ -72,7 +72,6 @@ interface LoadTestOptions {
72
72
  cleanup: boolean;
73
73
  keepFailed: boolean;
74
74
  debug: boolean;
75
- startClosed: boolean;
76
75
  stepTimeoutMs: number;
77
76
  streamTimeoutMs: number;
78
77
  telemetryTimeoutMs: number;
@@ -255,9 +254,8 @@ async function main(): Promise<void> {
255
254
  const transport = await createNodeTransport(options);
256
255
  const ui = new LoadTestUi(options);
257
256
  const interrupts = new InterruptController();
258
- const client = new PHPSandbox(options.token, options.apiUrl, {
257
+ const client = PHPSandbox.realtime(options.token, options.apiUrl, {
259
258
  debug: options.debug,
260
- startClosed: options.startClosed,
261
259
  fetch: transport.fetch,
262
260
  webSocket: transport.webSocket,
263
261
  });
@@ -471,8 +469,7 @@ async function runScenario(
471
469
  {
472
470
  title: `${options.titlePrefix} ${id}`,
473
471
  visibility: options.visibility,
474
- },
475
- false
472
+ }
476
473
  );
477
474
 
478
475
  const data = {
@@ -481,7 +478,7 @@ async function runScenario(
481
478
  };
482
479
  created.dispose();
483
480
 
484
- return new NotebookInstance(data, client);
481
+ return client.notebook.open(data);
485
482
  },
486
483
  { continueOnError: false }
487
484
  );
@@ -559,7 +556,7 @@ async function runScenario(
559
556
  }
560
557
 
561
558
  if (options.mode !== 'init') {
562
- logReader = notebook.logs.follow().getReader();
559
+ logReader = notebook.runtime.logs.follow().getReader();
563
560
  void (async () => {
564
561
  while (logReader) {
565
562
  const entry = await logReader.read();
@@ -573,20 +570,18 @@ async function runScenario(
573
570
  }
574
571
  })().catch(() => {});
575
572
 
576
- await runCheckedStep('notebook.ping', () => notebook!.ping(), { continueOnError: false });
577
-
578
573
  await runCheckedStep('notebook.secrets', async () => {
579
574
  return secretsStepLimiter.run(async () => {
580
575
  await retryWithBackoff(
581
576
  () => notebook!.secrets.set(secretName, `value-${id}`),
582
577
  SECRETS_RETRY_DELAYS_MS,
583
- shouldRetryApiRateLimitError
578
+ shouldRetryRemoteRateLimitError
584
579
  );
585
580
 
586
581
  const secrets = await retryWithBackoff(
587
582
  () => notebook!.secrets.list(),
588
583
  SECRETS_RETRY_DELAYS_MS,
589
- shouldRetryApiRateLimitError
584
+ shouldRetryRemoteRateLimitError
590
585
  );
591
586
 
592
587
  const found = secrets.find((secret) => secret.name === secretName);
@@ -597,29 +592,25 @@ async function runScenario(
597
592
  await retryWithBackoff(
598
593
  () => notebook!.secrets.delete(secretName),
599
594
  SECRETS_RETRY_DELAYS_MS,
600
- shouldRetryApiRateLimitError
595
+ shouldRetryRemoteRateLimitError
601
596
  );
602
597
 
603
598
  return secrets.length;
604
599
  });
605
600
  });
606
601
 
607
- await runCheckedStep('container.state', async () => {
608
- const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.ports.list()]);
609
- const portInfo = assertPortInfoList(ports, 'ports.list');
610
- return `${state.state} (${describePorts(portInfo)})`;
602
+ await runCheckedStep('container.ports', async () => {
603
+ const ports = await notebook!.runtime.ports.list();
604
+ return describePorts(assertPortInfoList(ports, 'ports.list'));
611
605
  });
612
606
 
613
607
  if (options.includeContainerRestart) {
614
608
  await runCheckedStep(
615
609
  'container.restart',
616
610
  async () => {
617
- await notebook!.container.start();
618
- const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.ports.list()]);
619
- if (state.state !== 'running') {
620
- throw new Error(`Expected running container after restart, got ${state.state}`);
621
- }
622
- return `${state.state} (${describePorts(assertPortInfoList(ports, 'ports.list after restart'))})`;
611
+ await notebook!.restart();
612
+ const ports = await notebook!.runtime.ports.list();
613
+ return describePorts(assertPortInfoList(ports, 'ports.list after restart'));
623
614
  },
624
615
  { timeoutMs: options.stepTimeoutMs * 3, continueOnError: false }
625
616
  );
@@ -632,8 +623,8 @@ async function runScenario(
632
623
  async () =>
633
624
  retryWithBackoff(
634
625
  async () => {
635
- const statsReader = notebook!.metrics.watch().getReader();
636
- const portsReader = notebook!.ports.watch().getReader();
626
+ const statsReader = notebook!.runtime.metrics.watch().getReader();
627
+ const portsReader = notebook!.runtime.ports.watch().getReader();
637
628
  try {
638
629
  const statsPromise = withTimeout(statsReader.read(), options.telemetryTimeoutMs, 'metrics.watch')
639
630
  .then(({ value }) => {
@@ -741,7 +732,9 @@ async function runScenario(
741
732
 
742
733
  await runCheckedStep('shell.run', async () => {
743
734
  const result = await notebook!.run(['php', 'artisan', '--version']).wait();
744
- result.throw();
735
+ if (result.exitCode !== 0) {
736
+ throw new Error(result.stderr || result.output);
737
+ }
745
738
  if (!result.output.toLowerCase().includes('laravel')) {
746
739
  throw new Error(`Unexpected artisan version output: ${trimDetail(result.output)}`);
747
740
  }
@@ -768,7 +761,9 @@ async function runScenario(
768
761
  withTimeout(execution.wait(), options.stepTimeoutMs, 'waiting for process exit'),
769
762
  ]);
770
763
 
771
- result.throw();
764
+ if (result.exitCode !== 0) {
765
+ throw new Error(result.stderr || result.output);
766
+ }
772
767
  const combined = [streamOutput, result.output].filter(Boolean).join('\n');
773
768
  if (!combined.includes(routeUrlPath)) {
774
769
  throw new Error(`route:list output did not contain ${routeUrlPath}.`);
@@ -842,14 +837,17 @@ async function runScenario(
842
837
 
843
838
  await runCheckedStep('composer.metadata', async () => {
844
839
  let composerLogChunks = 0;
845
- const dump = notebook!.composer.dumpAutoload();
840
+ const dump = notebook!.composer.run('dump-autoload');
846
841
  for await (const chunk of dump.output) {
847
842
  if (chunk.data.byteLength > 0) {
848
843
  composerLogChunks += 1;
849
844
  }
850
845
  }
851
846
 
852
- (await dump.wait()).throw();
847
+ const result = await dump.wait();
848
+ if (result.exitCode !== 0) {
849
+ throw new Error(result.stderr || result.output);
850
+ }
853
851
  const packages = await notebook!.composer.packages();
854
852
  if (packages.length === 0) {
855
853
  throw new Error('composer.packages() returned no packages.');
@@ -1002,7 +1000,7 @@ async function cleanupNotebook(notebook: NotebookInstance, steps: StepResult[]):
1002
1000
  const notebookId = notebook.data.id;
1003
1001
  await cleanupDeleteLimiter.run(async () => {
1004
1002
  try {
1005
- await retryWithBackoff(() => notebook.destroy(), DELETE_RETRY_DELAYS_MS, shouldRetryApiRateLimitError);
1003
+ await retryWithBackoff(() => notebook.destroy(), DELETE_RETRY_DELAYS_MS, shouldRetryRemoteRateLimitError);
1006
1004
  } catch (error) {
1007
1005
  if (formatError(error).includes('Notebook has been deleted')) {
1008
1006
  return;
@@ -1059,7 +1057,6 @@ function renderConfiguration(options: LoadTestOptions): string {
1059
1057
  ` waves per level: ${options.waves}`,
1060
1058
  ` visibility: ${options.visibility}`,
1061
1059
  ` cleanup: ${options.cleanup} (keep failed: ${options.keepFailed})`,
1062
- ` startClosed: ${options.startClosed}`,
1063
1060
  ` include container restart: ${options.includeContainerRestart}`,
1064
1061
  ` include download: ${options.includeDownload}`,
1065
1062
  ` include preview: ${options.includePreview}`,
@@ -2120,9 +2117,8 @@ function formatError(error: unknown): string {
2120
2117
  return String(error);
2121
2118
  }
2122
2119
 
2123
- function shouldRetryApiRateLimitError(error: unknown): boolean {
2124
- const message = formatError(error);
2125
- return message.includes('429') || message.includes('Too Many Attempts');
2120
+ function shouldRetryRemoteRateLimitError(error: unknown): boolean {
2121
+ return RemoteError.is(error, 'RateLimited');
2126
2122
  }
2127
2123
 
2128
2124
  function shouldRetryTelemetryError(error: unknown): boolean {
@@ -2395,7 +2391,6 @@ function parseOptions(argv: string[]): LoadTestOptions {
2395
2391
  cleanup: booleanArg(args, 'cleanup') ?? true,
2396
2392
  keepFailed: booleanArg(args, 'keep-failed') ?? false,
2397
2393
  debug: booleanArg(args, 'debug') ?? false,
2398
- startClosed: booleanArg(args, 'start-closed') ?? true,
2399
2394
  stepTimeoutMs: numberArg(args, 'step-timeout-ms') ?? 60_000,
2400
2395
  streamTimeoutMs: numberArg(args, 'stream-timeout-ms') ?? 20_000,
2401
2396
  telemetryTimeoutMs: numberArg(args, 'telemetry-timeout-ms') ?? 15_000,
@@ -10,7 +10,7 @@ import {
10
10
  type NotebookInitProgress,
11
11
  type NotebookInitResult,
12
12
  type PortInfo,
13
- } from '../src/index.js';
13
+ } from '@phpsandbox/sdk';
14
14
 
15
15
  const DEFAULT_API_URL = 'https://api.phpsandbox.io/v1';
16
16
  const DEFAULT_LOCAL_API_URL = 'https://api.phpsandbox.test/v1';
@@ -30,7 +30,6 @@ interface SmokeOptions {
30
30
  includeDownload: boolean;
31
31
  includePreview: boolean;
32
32
  debug: boolean;
33
- startClosed: boolean;
34
33
  insecureTls: boolean;
35
34
  caFile?: string;
36
35
  stepTimeoutMs: number;
@@ -89,9 +88,8 @@ async function main(): Promise<void> {
89
88
  const steps: StepArtifact[] = [];
90
89
  const initTrace: InitTraceArtifact = { events: [] };
91
90
  const transport = await createNodeTransport(options);
92
- const client = new PHPSandbox(options.token, options.apiUrl, {
91
+ const client = PHPSandbox.realtime(options.token, options.apiUrl, {
93
92
  debug: options.debug,
94
- startClosed: options.startClosed,
95
93
  ...transport,
96
94
  });
97
95
 
@@ -100,7 +98,8 @@ async function main(): Promise<void> {
100
98
 
101
99
  try {
102
100
  const response = await recordStep(steps, 'api.get-notebook', options.stepTimeoutMs, async () => {
103
- const result = await client.get<NotebookData>(`/notebook/${options.notebookId}`);
101
+ const result = await client.notebook.get(options.notebookId);
102
+ result.dispose();
104
103
  return {
105
104
  data: result.data,
106
105
  detail: `id=${result.data.id} type=${result.data.type ?? ''} title=${result.data.title ?? ''} runtimeUrl=${redactUrl(result.data.runtimeUrl)}`,
@@ -112,7 +111,7 @@ async function main(): Promise<void> {
112
111
  runtimeUrl: resolveRuntimeUrl(response.data.runtimeUrl, options),
113
112
  };
114
113
 
115
- notebook = new NotebookInstance(notebookData, client);
114
+ notebook = client.notebook.open(notebookData);
116
115
  const initStartedAt = performance.now();
117
116
  notebook.listen('init.event', (event: NotebookInitProgress) => {
118
117
  initTrace.events.push({
@@ -138,24 +137,8 @@ async function main(): Promise<void> {
138
137
  });
139
138
  const ready = readyStep.result;
140
139
 
141
- await recordStep(steps, 'notebook.ping', options.stepTimeoutMs, async () => {
142
- const result = await notebook!.ping();
143
- if (result !== 'pong') {
144
- throw new Error(`Expected pong, got ${String(result)}`);
145
- }
146
- return result;
147
- }, false);
148
-
149
- await recordStep(steps, 'container.state', options.stepTimeoutMs, async () => {
150
- const state = await notebook!.container.state();
151
- if (!state || typeof state.state !== 'string') {
152
- throw new Error(`Unexpected container.state response: ${JSON.stringify(state)}`);
153
- }
154
- return state.state;
155
- }, false);
156
-
157
140
  await recordStep(steps, 'container.opened-ports', options.stepTimeoutMs, async () => {
158
- const ports = await notebook!.ports.list();
141
+ const ports = await notebook!.runtime.ports.list();
159
142
  return describePorts(assertPortInfoList(ports, 'ports.list'));
160
143
  }, false);
161
144
 
@@ -186,7 +169,9 @@ async function main(): Promise<void> {
186
169
  notebook!.files.find('composer.json', { maxResults: 10 }),
187
170
  notebook!.files.find('index.php', { maxResults: 10 }),
188
171
  ]);
189
- const candidate = [...composerFiles, ...indexFiles].find((file) => typeof file.path === 'string')?.path;
172
+ const candidate = [...composerFiles, ...indexFiles]
173
+ .find((file) => typeof file.path === 'string')
174
+ ?.path;
190
175
  return {
191
176
  detail: `composerFiles=${composerFiles.length} indexFiles=${indexFiles.length}`,
192
177
  candidate,
@@ -326,7 +311,9 @@ async function main(): Promise<void> {
326
311
  async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifact[], options: SmokeOptions): Promise<void> {
327
312
  await recordStep(steps, 'shell.run', options.stepTimeoutMs, async () => {
328
313
  const result = await notebook.run(['php', '-r', 'echo "sdk-smoke";']).wait();
329
- result.throw();
314
+ if (result.exitCode !== 0) {
315
+ throw new Error(result.stderr || result.output);
316
+ }
330
317
  if (!result.output.includes('sdk-smoke')) {
331
318
  throw new Error(`Unexpected shell output: ${result.output}`);
332
319
  }
@@ -511,7 +498,6 @@ function parseOptions(argv: string[]): SmokeOptions {
511
498
  includeDownload: booleanArg(args, 'include-download') ?? false,
512
499
  includePreview: booleanArg(args, 'include-preview') ?? false,
513
500
  debug: booleanArg(args, 'debug') ?? false,
514
- startClosed: booleanArg(args, 'start-closed') ?? true,
515
501
  insecureTls: booleanArg(args, 'insecure-tls') ?? ((booleanArg(args, 'local') ?? false) && !stringArg(args, 'ca-file')),
516
502
  caFile: stringArg(args, 'ca-file'),
517
503
  stepTimeoutMs: numberArg(args, 'step-timeout-ms') ?? 60_000,
@@ -1,4 +1,4 @@
1
- import type { PublicationProviderName } from '../src/index.js';
1
+ import type { PublicationProviderName } from '@phpsandbox/sdk';
2
2
 
3
3
  const publicationProviderLabels: Record<PublicationProviderName, string> = {
4
4
  'cloudflare-containers': 'Cloudflare Containers',
@@ -1,4 +1,4 @@
1
- import { Client } from '../src/index.js';
1
+ import { PHPSandbox } from '@phpsandbox/sdk';
2
2
  import { publicationProviderLabel } from './publication-provider-label.js';
3
3
 
4
4
  const token = process.env.PHPSANDBOX_TOKEN;
@@ -18,7 +18,7 @@ const cloudflareApiToken = process.env.CLOUDFLARE_API_TOKEN;
18
18
  if (!cloudflareAccountId || !cloudflareApiToken) {
19
19
  throw new Error('Missing CLOUDFLARE_ACCOUNT_ID or CLOUDFLARE_API_TOKEN');
20
20
  }
21
- const client = new Client(token, apiUrl);
21
+ const client = PHPSandbox.realtime(token, apiUrl);
22
22
  const notebook = await client.notebook.get(notebookId);
23
23
 
24
24
  try {
@@ -1,4 +1,4 @@
1
- import { Client, type Integration, type LaravelCloudRegion } from '../src/index.js';
1
+ import { PHPSandbox, type Integration, type LaravelCloudRegion } from '@phpsandbox/sdk';
2
2
  import { publicationProviderLabel } from './publication-provider-label.js';
3
3
 
4
4
  const token = process.env.PHPSANDBOX_TOKEN;
@@ -19,7 +19,7 @@ if (new URL(apiUrl).hostname.endsWith('.test') && process.env.NODE_TLS_REJECT_UN
19
19
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
20
20
  }
21
21
 
22
- const client = new Client(token, apiUrl);
22
+ const client = PHPSandbox.realtime(token, apiUrl);
23
23
  const notebook = await client.notebook.get(notebookId);
24
24
 
25
25
  try {
@@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { Client, type ServerInstance } from '../src/index.js';
6
+ import { PHPSandbox, type ServerInstance } from '@phpsandbox/sdk';
7
7
  import { publicationProviderLabel } from './publication-provider-label.js';
8
8
 
9
9
  const token = process.env.PHPSANDBOX_TOKEN;
@@ -24,7 +24,7 @@ if (isLocal && process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
24
24
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
25
25
  }
26
26
 
27
- const client = new Client(token, apiUrl);
27
+ const client = PHPSandbox.realtime(token, apiUrl);
28
28
  const notebook = await client.notebook.get(notebookId);
29
29
  let agent: ChildProcessWithoutNullStreams | null = null;
30
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phpsandbox/sdk",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "A comprehensive TypeScript SDK for interacting with cloud-based PHP development environments",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -43,8 +43,9 @@
43
43
  "smoke:reaper": "npm run build:tsc && vitest run --reporter=verbose --config tests/smoke/vitest.config.ts tests/smoke/maintenance/resources.reaper.smoke.ts",
44
44
  "smoke:typecheck": "npm run build:tsc && tsc --project tests/smoke/tsconfig.json --noEmit",
45
45
  "check:examples": "tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2020 --types node --skipLibCheck --allowSyntheticDefaultImports --esModuleInterop examples/basic.ts examples/publish-cloudflare-container.ts examples/publish-laravel-cloud.ts examples/publish-ssh-agent.ts examples/get.ts examples/git-merge-smoke.ts examples/laravel-load-test.ts examples/notebook-readonly-smoke.ts",
46
- "prepublishOnly": "npm test && npm run check:examples && npm run clean && npm run build",
47
- "bumpAndPublish": "npm version patch && npm publish --access public"
46
+ "prepublishOnly": "npm test && npm run check:examples && npm run check:docs && npm run clean && npm run build",
47
+ "bumpAndPublish": "npm version patch && npm publish --access public",
48
+ "check:docs": "npm run build:tsc && node tools/check-documentation.mjs"
48
49
  },
49
50
  "keywords": [
50
51
  "php",