@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.
- package/README.md +42 -50
- package/dist/beacon/index.d.ts +1 -7
- package/dist/beacon/index.js +14 -13
- package/dist/beacon/navigator.d.ts +8 -36
- package/dist/beacon/navigator.js +1 -62
- package/dist/browser/phpsandbox-sdk.esm.js +552 -692
- package/dist/browser/phpsandbox-sdk.esm.min.js +24 -24
- package/dist/browser/phpsandbox-sdk.iife.js +552 -692
- package/dist/browser/phpsandbox-sdk.iife.min.js +35 -35
- package/dist/composer.d.ts +10 -8
- package/dist/composer.js +18 -18
- package/dist/errors/index.d.ts +43 -8
- package/dist/errors/index.js +120 -14
- package/dist/feedback.js +1 -2
- package/dist/filesystem.d.ts +0 -17
- package/dist/filesystem.js +2 -21
- package/dist/git.d.ts +1 -5
- package/dist/git.js +6 -8
- package/dist/http.d.ts +6 -0
- package/dist/http.js +11 -0
- package/dist/index.d.ts +54 -144
- package/dist/index.js +170 -289
- package/dist/integrations.d.ts +0 -2
- package/dist/integrations.js +5 -11
- package/dist/log.d.ts +0 -2
- package/dist/lsp.js +1 -1
- package/dist/metrics.d.ts +3 -3
- package/dist/ports.d.ts +1 -1
- package/dist/publications.d.ts +4 -54
- package/dist/publications.js +25 -103
- package/dist/runtime/rest.js +35 -18
- package/dist/runtime.d.ts +68 -0
- package/dist/runtime.js +46 -0
- package/dist/sandbox-integrations.d.ts +1 -2
- package/dist/sandbox-integrations.js +11 -14
- package/dist/schemas/publications.d.ts +0 -6
- package/dist/schemas/publications.js +0 -1
- package/dist/servers.d.ts +4 -2
- package/dist/servers.js +5 -8
- package/dist/shell.d.ts +2 -14
- package/dist/shell.js +1 -21
- package/dist/socket/index.d.ts +6 -7
- package/dist/socket/index.js +39 -25
- package/dist/types.d.ts +0 -12
- package/dist/types.js +1 -15
- package/docs/api-summary.md +28 -28
- package/docs/cdn-usage.md +43 -55
- package/docs/getting-started.md +43 -26
- package/examples/README.md +5 -35
- package/examples/basic.ts +9 -18
- package/examples/get.ts +2 -2
- package/examples/git-merge-smoke.ts +2 -3
- package/examples/laravel-load-test.ts +31 -36
- package/examples/notebook-readonly-smoke.ts +12 -26
- package/examples/publication-provider-label.ts +1 -1
- package/examples/publish-cloudflare-container.ts +2 -2
- package/examples/publish-laravel-cloud.ts +2 -2
- package/examples/publish-ssh-agent.ts +2 -2
- package/package.json +4 -3
- package/dist/container.d.ts +0 -65
- package/dist/container.js +0 -30
package/docs/getting-started.md
CHANGED
|
@@ -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
|
|
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 =
|
|
29
|
+
const client = PHPSandbox.realtime(token);
|
|
25
30
|
```
|
|
26
31
|
|
|
27
32
|
## 2. Create or Open a Notebook
|
|
28
33
|
|
|
29
34
|
```ts
|
|
30
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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((
|
|
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
|
-
|
|
129
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
152
|
+
- Runtime actions wait for readiness. Call `ready()` only when you need its provisioning metadata.
|
|
145
153
|
- `files.watch()` requires an `excludes` array.
|
|
146
|
-
-
|
|
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:
|
|
151
|
-
- CDN/browser usage:
|
|
152
|
-
- Root quick start:
|
|
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)
|
package/examples/README.md
CHANGED
|
@@ -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
|
|
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
|
-
###
|
|
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
|
-
|
|
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 '
|
|
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
|
|
9
|
-
const notebook = await
|
|
6
|
+
const client = PHPSandbox.realtime(token);
|
|
7
|
+
const notebook = await client.notebook.create('standard');
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|
|
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 =
|
|
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 '
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
578
|
+
shouldRetryRemoteRateLimitError
|
|
584
579
|
);
|
|
585
580
|
|
|
586
581
|
const secrets = await retryWithBackoff(
|
|
587
582
|
() => notebook!.secrets.list(),
|
|
588
583
|
SECRETS_RETRY_DELAYS_MS,
|
|
589
|
-
|
|
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
|
-
|
|
595
|
+
shouldRetryRemoteRateLimitError
|
|
601
596
|
);
|
|
602
597
|
|
|
603
598
|
return secrets.length;
|
|
604
599
|
});
|
|
605
600
|
});
|
|
606
601
|
|
|
607
|
-
await runCheckedStep('container.
|
|
608
|
-
const
|
|
609
|
-
|
|
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!.
|
|
618
|
-
const
|
|
619
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|
2124
|
-
|
|
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 '
|
|
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 =
|
|
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
|
|
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 =
|
|
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]
|
|
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.
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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.
|
|
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",
|