@phpsandbox/sdk 0.0.68 → 0.1.1

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 (41) hide show
  1. package/README.md +10 -13
  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 +306 -1010
  7. package/dist/browser/phpsandbox-sdk.esm.min.js +24 -24
  8. package/dist/browser/phpsandbox-sdk.iife.js +306 -1010
  9. package/dist/browser/phpsandbox-sdk.iife.min.js +24 -24
  10. package/dist/filesystem.d.ts +2 -1
  11. package/dist/filesystem.js +35 -2
  12. package/dist/index.d.ts +14 -19
  13. package/dist/index.js +16 -24
  14. package/dist/publications.d.ts +222 -0
  15. package/dist/{deployments.js → publications.js} +64 -69
  16. package/dist/repl.d.ts +0 -2
  17. package/dist/repl.js +0 -3
  18. package/dist/schemas/{deployments.d.ts → publications.d.ts} +68 -68
  19. package/dist/schemas/publications.js +133 -0
  20. package/dist/shell.d.ts +43 -22
  21. package/dist/shell.js +103 -75
  22. package/dist/terminal.d.ts +1 -3
  23. package/dist/terminal.js +2 -5
  24. package/docs/api-summary.md +13 -10
  25. package/docs/cdn-usage.md +4 -4
  26. package/docs/getting-started.md +6 -6
  27. package/examples/README.md +8 -9
  28. package/examples/basic.ts +1 -1
  29. package/examples/git-merge-smoke.ts +2 -2
  30. package/examples/laravel-load-test.ts +13 -18
  31. package/examples/notebook-readonly-smoke.ts +8 -8
  32. package/examples/publication-provider-label.ts +11 -0
  33. package/examples/{deploy-cloudflare-container.ts → publish-cloudflare-container.ts} +11 -11
  34. package/examples/{deploy-laravel-cloud.ts → publish-laravel-cloud.ts} +11 -11
  35. package/examples/{deploy-ssh-agent.ts → publish-ssh-agent.ts} +15 -16
  36. package/package.json +7 -2
  37. package/dist/deployments.d.ts +0 -251
  38. package/dist/laravel.d.ts +0 -22
  39. package/dist/laravel.js +0 -11
  40. package/dist/schemas/deployments.js +0 -133
  41. package/examples/deployment-provider-label.ts +0 -11
package/dist/shell.js CHANGED
@@ -1,108 +1,136 @@
1
1
  import { nanoid } from 'nanoid';
2
- function resolveExecInput(command, argsOrOpts = [], opts) {
2
+ function resolveRunInput(command, argsOrOpts = [], opts) {
3
3
  const args = Array.isArray(argsOrOpts) ? argsOrOpts : [];
4
4
  const resolvedOpts = Array.isArray(argsOrOpts) ? opts : argsOrOpts;
5
5
  return {
6
6
  command: Array.isArray(command) ? command : args.length > 0 ? [command, ...args] : command,
7
- opts: resolvedOpts,
7
+ opts: { tty: false, ...resolvedOpts },
8
8
  };
9
9
  }
10
10
  export class CommandError extends Error {
11
- constructor(output, exitCode) {
12
- super(output);
13
- this.output = output;
14
- this.exitCode = exitCode;
11
+ constructor(result) {
12
+ super(result.output);
13
+ this.result = result;
15
14
  }
16
15
  }
17
- class Result {
18
- constructor(output, exitCode) {
16
+ export class ProcessResult {
17
+ constructor(output, stdout, stderr, exitCode) {
19
18
  this.output = output;
19
+ this.stdout = stdout;
20
+ this.stderr = stderr;
20
21
  this.exitCode = exitCode;
21
22
  }
22
23
  throw() {
23
24
  if (this.exitCode !== 0) {
24
- throw new CommandError(this.output, this.exitCode);
25
+ throw new CommandError(this);
25
26
  }
26
27
  return this;
27
28
  }
28
29
  }
29
- export default class Shell {
30
- constructor(okra) {
31
- this.okra = okra;
30
+ class OutputStream {
31
+ constructor() {
32
+ this.controller = null;
33
+ this.closed = false;
34
+ this.stream = new ReadableStream({
35
+ start: (controller) => {
36
+ this.controller = controller;
37
+ },
38
+ cancel: () => {
39
+ this.closed = true;
40
+ this.controller = null;
41
+ },
42
+ });
32
43
  }
33
- onOutput(id, handler) {
34
- return this.okra.listen(`shell.output.${id}`, handler);
44
+ enqueue(value) {
45
+ if (!this.closed) {
46
+ this.controller?.enqueue(value);
47
+ }
35
48
  }
36
- listen(event, handler) {
37
- return this.okra.listen(event, handler);
49
+ close() {
50
+ if (this.closed) {
51
+ return;
52
+ }
53
+ this.closed = true;
54
+ this.controller?.close();
55
+ this.controller = null;
38
56
  }
39
- async exec(command, argsOrOpts = [], opts) {
40
- const input = resolveExecInput(command, argsOrOpts, opts);
41
- const { abortSignal, ...payloadOpts } = input.opts ?? {};
42
- const result = await this.okra.invoke('shell.exec', {
43
- command: input.command,
44
- opts: Object.keys(payloadOpts).length > 0 ? payloadOpts : undefined,
45
- }, { abortSignal });
46
- return new Result(result.output, result.exitCode);
57
+ fail(error) {
58
+ if (this.closed) {
59
+ return;
60
+ }
61
+ this.closed = true;
62
+ this.controller?.error(error);
63
+ this.controller = null;
47
64
  }
48
- execStream(command, argsOrOpts = [], opts) {
49
- const input = resolveExecInput(command, argsOrOpts, opts);
50
- const id = input.opts?.id || nanoid();
51
- const disposables = new Set();
52
- let controller = null;
53
- let closed = false;
54
- const dispose = () => {
55
- for (const disposable of disposables) {
56
- disposable.dispose();
57
- }
58
- disposables.clear();
59
- };
60
- const close = () => {
61
- if (closed) {
62
- return;
63
- }
64
- closed = true;
65
- dispose();
66
- if (controller) {
67
- try {
68
- controller.close();
69
- }
70
- catch {
71
- // Ignore close errors after cancellation.
65
+ }
66
+ export class ShellProcess {
67
+ constructor(id, okra, command, opts) {
68
+ this.id = id;
69
+ this.okra = okra;
70
+ const output = new OutputStream();
71
+ const stdout = new OutputStream();
72
+ const stderr = new OutputStream();
73
+ this.output = output.stream;
74
+ this.stdout = stdout.stream;
75
+ this.stderr = stderr.stream;
76
+ this.stdin = new WritableStream({
77
+ write: async (chunk) => {
78
+ const input = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
79
+ const written = await this.okra.invoke('shell.input', { id: this.id, input });
80
+ if (!written) {
81
+ throw new Error(`Process ${this.id} is not accepting input`);
72
82
  }
83
+ },
84
+ });
85
+ const encoder = new TextEncoder();
86
+ let subscription = this.okra.listen(`shell.output.${this.id}`, (event) => {
87
+ const data = encoder.encode(event.output);
88
+ const source = event.source ?? 'output';
89
+ output.enqueue({ source, data });
90
+ if (source === 'stderr') {
91
+ stderr.enqueue(data);
92
+ }
93
+ else {
94
+ stdout.enqueue(data);
73
95
  }
96
+ });
97
+ const finish = () => {
98
+ subscription?.dispose();
99
+ subscription = null;
100
+ output.close();
101
+ stdout.close();
102
+ stderr.close();
74
103
  };
75
104
  const fail = (error) => {
76
- if (closed) {
77
- return;
78
- }
79
- closed = true;
80
- dispose();
81
- if (controller) {
82
- controller.error(error);
83
- }
105
+ subscription?.dispose();
106
+ subscription = null;
107
+ output.fail(error);
108
+ stdout.fail(error);
109
+ stderr.fail(error);
84
110
  };
85
- const output = new ReadableStream({
86
- start: (_controller) => {
87
- controller = _controller;
88
- disposables.add(this.listen(`shell.output.${id}`, (data) => {
89
- controller?.enqueue(data.output);
90
- }));
91
- },
92
- cancel: () => {
93
- closed = true;
94
- dispose();
95
- },
96
- });
97
- const result = this.exec(input.command, { ...input.opts, id })
98
- .then((response) => {
99
- close();
100
- return response;
101
- })
102
- .catch((error) => {
111
+ const { abortSignal, ...payloadOpts } = opts;
112
+ this.completion = this.okra.invoke('shell.exec', { command, opts: { ...payloadOpts, id: this.id } }, { abortSignal }).then((result) => {
113
+ finish();
114
+ return new ProcessResult(result.output, result.stdout, result.stderr, result.exitCode);
115
+ }).catch((error) => {
103
116
  fail(error);
104
117
  throw error;
105
118
  });
106
- return { output, result };
119
+ }
120
+ wait() {
121
+ return this.completion;
122
+ }
123
+ async kill() {
124
+ await this.okra.invoke('shell.kill', { id: this.id });
125
+ }
126
+ }
127
+ export default class Shell {
128
+ constructor(okra) {
129
+ this.okra = okra;
130
+ }
131
+ run(command, argsOrOpts = [], opts) {
132
+ const input = resolveRunInput(command, argsOrOpts, opts);
133
+ const id = input.opts?.id ?? nanoid();
134
+ return new ShellProcess(id, this.okra, input.command, { ...input.opts, id });
107
135
  }
108
136
  }
@@ -49,7 +49,6 @@ export interface TerminalActions {
49
49
  input: string;
50
50
  }>;
51
51
  'terminal.list': Action<{}, Task[]>;
52
- 'terminal.start': Action;
53
52
  'terminal.create': Action<TerminalCreateInput, Task>;
54
53
  'terminal.spawn': Action<TerminalSpawnInput, Task>;
55
54
  'terminal.attach': Action<{
@@ -66,7 +65,7 @@ export interface TerminalActions {
66
65
  }, boolean>;
67
66
  }
68
67
  export interface SandboxProcess {
69
- exit: Promise<number>;
68
+ wait(): Promise<number>;
70
69
  input: WritableStream<string>;
71
70
  output: ReadableStream<string>;
72
71
  kill(): Promise<boolean>;
@@ -92,7 +91,6 @@ export default class Terminal {
92
91
  protected okra: NotebookInstance;
93
92
  constructor(okra: NotebookInstance);
94
93
  list(): Promise<Task[]>;
95
- start(): Promise<void>;
96
94
  create(input: TerminalCreateInput): Promise<Task>;
97
95
  onStarted(handler: (t: Task) => void): Disposable;
98
96
  resize(id: string, size: [number, number]): Promise<boolean>;
package/dist/terminal.js CHANGED
@@ -9,9 +9,6 @@ export default class Terminal {
9
9
  list() {
10
10
  return this.okra.invoke('terminal.list');
11
11
  }
12
- start() {
13
- return this.okra.invoke('terminal.start');
14
- }
15
12
  create(input) {
16
13
  return this.okra.invoke('terminal.create', input);
17
14
  }
@@ -136,7 +133,7 @@ export default class Terminal {
136
133
  disposeInput();
137
134
  },
138
135
  });
139
- const exit = new Promise((resolve) => {
136
+ const completion = new Promise((resolve) => {
140
137
  disposables.add(this.listen(`terminal.close.${id}`, (data) => {
141
138
  if (controller) {
142
139
  try {
@@ -165,7 +162,7 @@ export default class Terminal {
165
162
  }
166
163
  return {
167
164
  process: {
168
- exit,
165
+ wait: () => completion,
169
166
  input,
170
167
  output,
171
168
  kill,
@@ -23,16 +23,16 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
23
23
 
24
24
  - `create(template, input?, init = true)`
25
25
  - `get(id)`
26
- - `open(id)`
27
26
  - `fork(id, input?)`
28
- - `delete(id)`
27
+ - `destroy(id)`
29
28
  - `openFromData(data)`
30
29
  - `mail.status(id)`, `mail.enable(id)`, `mail.disable(id)`, `mail.list(id, options?)`, `mail.get(id, hash)`, `mail.delete(id, hash)`
31
30
 
32
31
  ## `NotebookInstance`
33
32
 
34
33
  - Lifecycle: `ready()`, `connected()`, `whenConnected()`, `reconnect()`, `dispose()`
35
- - Notebook actions: `fork(input?)`, `delete()`, `stop()`, `restart()`, `ping()`, `update()`
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()`
36
36
  - Events: `listen(event, handler)`, `onDidConnect(handler)`, `onDidDisconnect(handler)`, `onDidInitialize(handler)`
37
37
  - Raw call: `invoke(action, data?, options?)`
38
38
 
@@ -61,7 +61,7 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
61
61
 
62
62
  ### Shell (`notebook.shell`)
63
63
 
64
- - `exec(command)` returning `{ output, exitCode }` plus `throw()` helper
64
+ - `run(command, args?, options?)` returning the same process handle as `notebook.run()`
65
65
 
66
66
  ### Services (`notebook.services`)
67
67
 
@@ -114,14 +114,17 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
114
114
  - `stop()`, `eval(code, args?, replOpts?)`, `write(input)`, `resize(cols, rows)`
115
115
  - Events: `listen()`, `onOutput()`
116
116
 
117
- ### Laravel (`notebook.laravel`)
118
-
119
- - Wrapper exists in the SDK, but the current runtime action set does not expose Laravel maintenance actions.
120
-
121
117
  ### Auth (`notebook.auth`)
122
118
 
123
119
  - `login(runtimeUrl)` using a fresh ticket-bearing runtime URL
124
- - `logout()`
120
+
121
+ ## Publications
122
+
123
+ - `notebook.publish({ slug, provider, protection?, feedback?, metadata? })`
124
+ - `notebook.publication()` returns the current publication or `null`
125
+ - `client.publications.get(id)`, `destroy(id)`, `events(id)`, `logs(id)`, and `buildLogs(id, buildId)`
126
+ - A publish run exposes `events()` as an `AsyncIterable`, `result()`, and `publication()`.
127
+ - Built-in provider names are `cloudflare-containers`, `ssh-server`, and `laravel-cloud`. Provider maps support TypeScript declaration merging.
125
128
 
126
129
  ### Log (`notebook.log`)
127
130
 
@@ -137,6 +140,6 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
137
140
 
138
141
  ## Notes for Consumers
139
142
 
140
- - `open()` and `get()` do not auto-initialize; call `await notebook.ready()`.
143
+ - `get()` does not auto-initialize; call `await notebook.ready()`.
141
144
  - `create()` and `fork()` initialize by default.
142
145
  - Always call `notebook.dispose()` when finished.
package/docs/cdn-usage.md CHANGED
@@ -15,10 +15,10 @@ Do not hardcode long-lived private API tokens in public frontend code. Use short
15
15
  import { PHPSandbox } from 'https://unpkg.com/@phpsandbox/sdk@0.0.51/dist/browser/phpsandbox-sdk.esm.min.js';
16
16
 
17
17
  const client = new PHPSandbox('your-short-lived-token');
18
- const notebook = await client.notebook.open('your-notebook-id');
18
+ const notebook = await client.notebook.get('your-notebook-id');
19
19
  await notebook.ready();
20
20
 
21
- const result = await notebook.shell.exec('php -v');
21
+ const result = await notebook.run('php -v', { tty: false }).exit;
22
22
  result.throw();
23
23
  console.log(result.output);
24
24
  </script>
@@ -43,9 +43,9 @@ Do not hardcode long-lived private API tokens in public frontend code. Use short
43
43
  <script>
44
44
  const client = new PHPSandbox.PHPSandbox('your-short-lived-token');
45
45
 
46
- client.notebook.open('your-notebook-id').then(async (notebook) => {
46
+ client.notebook.get('your-notebook-id').then(async (notebook) => {
47
47
  await notebook.ready();
48
- const result = await notebook.shell.exec('php -v');
48
+ const result = await notebook.run('php -v', { tty: false }).exit;
49
49
  console.log(result.output);
50
50
  notebook.dispose();
51
51
  });
@@ -35,8 +35,8 @@ const persistent = await client.notebook.create('laravel', {
35
35
  persistent: true,
36
36
  });
37
37
 
38
- // open()/get() need ready()
39
- const existing = await client.notebook.open('notebook-id');
38
+ // get() needs ready()
39
+ const existing = await client.notebook.get('notebook-id');
40
40
  await existing.ready();
41
41
 
42
42
  const fork = await existing.fork();
@@ -92,9 +92,9 @@ console.log(`\nExit code: ${exitCode}`);
92
92
  ### Shell (single command result)
93
93
 
94
94
  ```ts
95
- const result = await notebook.shell.exec('php -v');
95
+ const result = await notebook.run('php -v', { tty: false }).exit;
96
96
  result.throw();
97
- console.log(result.output);
97
+ console.log(result.stdout);
98
98
  ```
99
99
 
100
100
  ## 5. Search and Watch Files
@@ -141,9 +141,9 @@ notebook.dispose();
141
141
 
142
142
  ## Common Pitfalls
143
143
 
144
- - `open()` / `get()` notebooks must call `await notebook.ready()` before tool calls.
144
+ - `get()` notebooks must call `await notebook.ready()` before tool calls.
145
145
  - `file.watch()` requires an `excludes` array.
146
- - `shell.exec()` does not throw by default; call `result.throw()` to fail on non-zero exit codes.
146
+ - Await `process.exit`, then call `result.throw()` to fail on non-zero exit codes.
147
147
 
148
148
  ## Next Reading
149
149
 
@@ -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
@@ -21,4 +21,4 @@ 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();
@@ -98,7 +98,7 @@ async function main(): Promise<void> {
98
98
  } finally {
99
99
  if (shouldDeleteFork && fork) {
100
100
  try {
101
- await fork.delete();
101
+ await fork.destroy();
102
102
  console.log(`Deleted fork notebook: ${fork.data.id}`);
103
103
  } catch (error) {
104
104
  console.error(`Could not delete fork notebook ${fork.data.id}: ${formatError(error)}`);
@@ -743,8 +743,8 @@ async function runScenario(
743
743
  recordSkippedStep(steps, 'filesystem.download', 'Enable with --include-download.');
744
744
  }
745
745
 
746
- await runCheckedStep('shell.exec', async () => {
747
- const result = await notebook!.shell.exec(['php', 'artisan', '--version']);
746
+ await runCheckedStep('shell.run', async () => {
747
+ const result = await notebook!.run(['php', 'artisan', '--version']).wait();
748
748
  result.throw();
749
749
  if (!result.output.toLowerCase().includes('laravel')) {
750
750
  throw new Error(`Unexpected artisan version output: ${trimDetail(result.output)}`);
@@ -754,21 +754,22 @@ async function runScenario(
754
754
  }, { skipIf: explainUnsupportedShell });
755
755
 
756
756
  await runCheckedStep(
757
- 'shell.exec-stream',
757
+ 'shell.run-stream',
758
758
  async () => {
759
- const execution = notebook!.shell.execStream(
760
- ['php', 'artisan', 'route:list', `--path=${routeUrlPath}`]
759
+ const execution = notebook!.run(
760
+ ['php', 'artisan', 'route:list', `--path=${routeUrlPath}`],
761
+ { tty: false },
761
762
  );
762
763
 
763
764
  const [streamOutput, result] = await Promise.all([
764
- readStreamToString(execution.output, options.streamTimeoutMs, options.maxStreamChars).catch((error) => {
765
+ readStreamToString(execution.stdout, options.streamTimeoutMs, options.maxStreamChars).catch((error) => {
765
766
  if (isStreamInactivityError(error)) {
766
767
  return '';
767
768
  }
768
769
 
769
770
  throw error;
770
771
  }),
771
- withTimeout(execution.result, options.stepTimeoutMs, 'waiting for shell.execStream() result'),
772
+ withTimeout(execution.wait(), options.stepTimeoutMs, 'waiting for process exit'),
772
773
  ]);
773
774
 
774
775
  result.throw();
@@ -792,7 +793,7 @@ async function runScenario(
792
793
 
793
794
  try {
794
795
  const outputPromise = readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars);
795
- const exitPromise = withTimeout(process.exit, options.stepTimeoutMs, 'waiting for terminal process to exit');
796
+ const exitPromise = withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal process to exit');
796
797
  const [output, exitCode] = await Promise.all([outputPromise, exitPromise]);
797
798
 
798
799
  if (exitCode !== 0) {
@@ -844,12 +845,6 @@ async function runScenario(
844
845
  { timeoutMs: options.stepTimeoutMs * 2 }
845
846
  );
846
847
 
847
- recordSkippedStep(
848
- steps,
849
- 'laravel.maintenance',
850
- 'Not part of the current okra websocket action set.'
851
- );
852
-
853
848
  await runCheckedStep('composer.metadata', async () => {
854
849
  let composerLogChunks = 0;
855
850
  const composerLogs = notebook!.composer.stream((line) => {
@@ -950,7 +945,7 @@ async function runScenario(
950
945
  if (options.reauthorizationUrl) {
951
946
  await runCheckedStep('auth.login-logout', async () => {
952
947
  await notebook!.auth.login(options.reauthorizationUrl!);
953
- await notebook!.auth.logout();
948
+ notebook!.dispose();
954
949
  return 'ok';
955
950
  });
956
951
  } else {
@@ -1020,7 +1015,7 @@ async function cleanupNotebook(notebook: NotebookInstance, steps: StepResult[]):
1020
1015
  const notebookId = notebook.data.id;
1021
1016
  await cleanupDeleteLimiter.run(async () => {
1022
1017
  try {
1023
- await retryWithBackoff(() => notebook.delete(), DELETE_RETRY_DELAYS_MS, shouldRetryApiRateLimitError);
1018
+ await retryWithBackoff(() => notebook.destroy(), DELETE_RETRY_DELAYS_MS, shouldRetryApiRateLimitError);
1024
1019
  } catch (error) {
1025
1020
  if (formatError(error).includes('Notebook has been deleted')) {
1026
1021
  return;
@@ -1912,7 +1907,7 @@ function firstFulfilled<T>(promises: Array<Promise<T>>): Promise<T> {
1912
1907
  }
1913
1908
 
1914
1909
  async function readStreamToString(
1915
- stream: ReadableStream<string>,
1910
+ stream: ReadableStream<string | Uint8Array>,
1916
1911
  timeoutMs: number,
1917
1912
  maxChars: number
1918
1913
  ): Promise<string> {
@@ -1926,7 +1921,7 @@ async function readStreamToString(
1926
1921
  break;
1927
1922
  }
1928
1923
 
1929
- output += chunk.value ?? '';
1924
+ output += chunk.value instanceof Uint8Array ? new TextDecoder().decode(chunk.value) : (chunk.value ?? '');
1930
1925
  }
1931
1926
 
1932
1927
  return output;
@@ -259,7 +259,7 @@ async function main(): Promise<void> {
259
259
  if (options.includeExec) {
260
260
  await runExecutionChecks(notebook, steps, options);
261
261
  } else {
262
- recordSkippedStep(steps, 'shell.exec', 'Enable with --include-exec.');
262
+ recordSkippedStep(steps, 'shell.run', 'Enable with --include-exec.');
263
263
  recordSkippedStep(steps, 'terminal.spawn', 'Enable with --include-exec.');
264
264
  recordSkippedStep(steps, 'terminal.interactive', 'Enable with --include-exec.');
265
265
  recordSkippedStep(steps, 'terminal.osc', 'Enable with --include-exec.');
@@ -323,8 +323,8 @@ async function main(): Promise<void> {
323
323
  }
324
324
 
325
325
  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";']);
326
+ await recordStep(steps, 'shell.run', options.stepTimeoutMs, async () => {
327
+ const result = await notebook.run(['php', '-r', 'echo "sdk-smoke";']).wait();
328
328
  result.throw();
329
329
  if (!result.output.includes('sdk-smoke')) {
330
330
  throw new Error(`Unexpected shell output: ${result.output}`);
@@ -335,7 +335,7 @@ async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifac
335
335
  await recordStep(steps, 'terminal.spawn', options.stepTimeoutMs, async () => {
336
336
  const process = await notebook.terminal.spawn('php', ['-r', 'echo "sdk-smoke";'], { output: true });
337
337
  const [exitCode, output] = await Promise.all([
338
- withTimeout(process.exit, options.stepTimeoutMs, 'waiting for terminal process exit'),
338
+ withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal process exit'),
339
339
  readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars),
340
340
  ]);
341
341
 
@@ -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
 
@@ -386,7 +386,7 @@ async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifac
386
386
 
387
387
  try {
388
388
  const [exitCode, output] = await Promise.all([
389
- withTimeout(process.exit, options.stepTimeoutMs, 'waiting for terminal OSC process exit'),
389
+ withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal OSC process exit'),
390
390
  readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars),
391
391
  ]);
392
392
 
@@ -453,7 +453,7 @@ function recordSkippedStep(steps: StepArtifact[], name: string, detail: string):
453
453
  console.log(`SKIP ${name} ${detail}`);
454
454
  }
455
455
 
456
- async function readStreamToString(stream: ReadableStream<string>, timeoutMs: number, maxChars: number): Promise<string> {
456
+ async function readStreamToString(stream: ReadableStream<string | Uint8Array>, timeoutMs: number, maxChars: number): Promise<string> {
457
457
  const reader = stream.getReader();
458
458
  let output = '';
459
459
 
@@ -463,7 +463,7 @@ async function readStreamToString(stream: ReadableStream<string>, timeoutMs: num
463
463
  if (chunk.done) {
464
464
  break;
465
465
  }
466
- output += chunk.value ?? '';
466
+ output += chunk.value instanceof Uint8Array ? new TextDecoder().decode(chunk.value) : (chunk.value ?? '');
467
467
  }
468
468
  return output;
469
469
  } finally {
@@ -0,0 +1,11 @@
1
+ import type { PublicationProviderName } from '../src/index.js';
2
+
3
+ const publicationProviderLabels: Record<PublicationProviderName, string> = {
4
+ 'cloudflare-containers': 'Cloudflare Containers',
5
+ 'ssh-server': 'SSH Server',
6
+ 'laravel-cloud': 'Laravel Cloud',
7
+ };
8
+
9
+ export function publicationProviderLabel(provider: PublicationProviderName): string {
10
+ return publicationProviderLabels[provider] ?? provider;
11
+ }