@phpsandbox/sdk 0.0.68 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -32
- package/dist/auth.d.ts +0 -1
- package/dist/auth.js +0 -3
- package/dist/beacon/index.d.ts +2 -0
- package/dist/beacon/index.js +5 -0
- package/dist/browser/phpsandbox-sdk.esm.js +842 -1320
- package/dist/browser/phpsandbox-sdk.esm.min.js +24 -24
- package/dist/browser/phpsandbox-sdk.iife.js +842 -1320
- package/dist/browser/phpsandbox-sdk.iife.min.js +23 -23
- package/dist/composer.d.ts +36 -23
- package/dist/composer.js +61 -11
- package/dist/container.d.ts +2 -6
- package/dist/container.js +0 -28
- package/dist/filesystem.d.ts +58 -17
- package/dist/filesystem.js +169 -51
- package/dist/index.d.ts +30 -26
- package/dist/index.js +37 -32
- package/dist/log.d.ts +11 -5
- package/dist/log.js +10 -7
- package/dist/metrics.d.ts +10 -0
- package/dist/metrics.js +13 -0
- package/dist/ports.d.ts +10 -0
- package/dist/ports.js +13 -0
- package/dist/publications.d.ts +222 -0
- package/dist/{deployments.js → publications.js} +64 -69
- package/dist/repl.d.ts +0 -2
- package/dist/repl.js +0 -3
- package/dist/runtime/rest.js +1 -4
- package/dist/runtime-telemetry.d.ts +15 -0
- package/dist/runtime-telemetry.js +111 -0
- package/dist/schemas/{deployments.d.ts → publications.d.ts} +76 -76
- package/dist/schemas/publications.js +133 -0
- package/dist/services.d.ts +2 -9
- package/dist/services.js +3 -10
- package/dist/shell.d.ts +52 -22
- package/dist/shell.js +103 -75
- package/dist/terminal.d.ts +18 -14
- package/dist/terminal.js +29 -49
- package/docs/api-summary.md +37 -29
- package/docs/cdn-usage.md +4 -4
- package/docs/getting-started.md +17 -17
- package/examples/README.md +8 -9
- package/examples/basic.ts +2 -2
- package/examples/git-merge-smoke.ts +3 -6
- package/examples/laravel-load-test.ts +80 -98
- package/examples/notebook-readonly-smoke.ts +38 -41
- package/examples/publication-provider-label.ts +11 -0
- package/examples/{deploy-cloudflare-container.ts → publish-cloudflare-container.ts} +11 -11
- package/examples/{deploy-laravel-cloud.ts → publish-laravel-cloud.ts} +11 -11
- package/examples/{deploy-ssh-agent.ts → publish-ssh-agent.ts} +15 -16
- package/package.json +7 -2
- package/dist/deployments.d.ts +0 -251
- package/dist/laravel.d.ts +0 -22
- package/dist/laravel.js +0 -11
- package/dist/schemas/deployments.js +0 -133
- package/examples/deployment-provider-label.ts +0 -11
package/examples/README.md
CHANGED
|
@@ -6,20 +6,20 @@
|
|
|
6
6
|
PHPSANDBOX_TOKEN=your-token npx tsx examples/basic.ts
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
### Running
|
|
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/
|
|
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/
|
|
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/
|
|
32
|
+
npx tsx examples/publish-laravel-cloud.ts
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
|
|
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
|
|
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,
|
|
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
|
|
124
|
+
- shell process execution and streams
|
|
125
125
|
- terminal spawn streaming
|
|
126
126
|
- services with Redis logs
|
|
127
127
|
- composer metadata and autoload dump
|
|
@@ -132,7 +132,6 @@ Intentionally excluded by default:
|
|
|
132
132
|
|
|
133
133
|
- `beacon`, because it needs a browser iframe runtime
|
|
134
134
|
- `auth`, because it needs a fresh ticket-bearing runtime URL
|
|
135
|
-
- `laravel.maintenance.*`, because it is not part of the current okra websocket action set
|
|
136
135
|
- `lsp`, because it is useful but more brittle than the rest of the headless baseline
|
|
137
136
|
|
|
138
137
|
### Running A Read-Only Existing Notebook Smoke Test
|
package/examples/basic.ts
CHANGED
|
@@ -16,9 +16,9 @@ console.log('\n');
|
|
|
16
16
|
|
|
17
17
|
console.log('====== Read routes/web.php in the notebook ======');
|
|
18
18
|
const decoder = new TextDecoder();
|
|
19
|
-
const content = await notebook.
|
|
19
|
+
const content = await notebook.files.read('routes/web.php');
|
|
20
20
|
console.log(decoder.decode(content));
|
|
21
21
|
|
|
22
22
|
// Always close the notebook when done to clean up the connection.
|
|
23
23
|
notebook.dispose();
|
|
24
|
-
await notebook.
|
|
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.
|
|
44
|
+
base = await client.notebook.get(options.notebookId);
|
|
45
45
|
console.log(`Base notebook: ${base.data.id}`);
|
|
46
46
|
|
|
47
47
|
await base.ready();
|
|
@@ -54,10 +54,7 @@ async function main(): Promise<void> {
|
|
|
54
54
|
|
|
55
55
|
await fork.ready();
|
|
56
56
|
const editContent = buildEditContent(base.data.id, forkNotebookId, options);
|
|
57
|
-
|
|
58
|
-
if (!written) {
|
|
59
|
-
throw new Error(`fs.write returned false for ${options.editPath}.`);
|
|
60
|
-
}
|
|
57
|
+
await fork.files.write(options.editPath, editContent);
|
|
61
58
|
console.log(`Edited fork file: ${options.editPath}`);
|
|
62
59
|
|
|
63
60
|
const forkCheckpoint = await fork.git.checkpoint(options.author, `Smoke edit in fork ${forkNotebookId}`, options.ref, false);
|
|
@@ -98,7 +95,7 @@ async function main(): Promise<void> {
|
|
|
98
95
|
} finally {
|
|
99
96
|
if (shouldDeleteFork && fork) {
|
|
100
97
|
try {
|
|
101
|
-
await fork.
|
|
98
|
+
await fork.destroy();
|
|
102
99
|
console.log(`Deleted fork notebook: ${fork.data.id}`);
|
|
103
100
|
} catch (error) {
|
|
104
101
|
console.error(`Could not delete fork notebook ${fork.data.id}: ${formatError(error)}`);
|
|
@@ -389,11 +389,10 @@ async function runScenario(
|
|
|
389
389
|
let notebook: NotebookInstance | null = null;
|
|
390
390
|
let previewUrl: string | undefined;
|
|
391
391
|
let logsCaptured = 0;
|
|
392
|
-
let
|
|
392
|
+
let logReader: ReadableStreamDefaultReader<import('../src/log.js').RuntimeLogEntry> | null = null;
|
|
393
393
|
let initTrace: InitTrace | undefined;
|
|
394
394
|
let readyResult: NotebookInitSuccessResult | undefined;
|
|
395
395
|
let initTraceDisposables: Disposable[] = [];
|
|
396
|
-
let telemetryEnabled = false;
|
|
397
396
|
let redisRunning = false;
|
|
398
397
|
|
|
399
398
|
const routeSlug = id.toLowerCase();
|
|
@@ -560,12 +559,19 @@ async function runScenario(
|
|
|
560
559
|
}
|
|
561
560
|
|
|
562
561
|
if (options.mode !== 'init') {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
562
|
+
logReader = notebook.logs.follow().getReader();
|
|
563
|
+
void (async () => {
|
|
564
|
+
while (logReader) {
|
|
565
|
+
const entry = await logReader.read();
|
|
566
|
+
if (entry.done) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (entry.value.message.trim().length > 0) {
|
|
570
|
+
logsCaptured += 1;
|
|
571
|
+
ui.updateScenarioLogs(id, logsCaptured);
|
|
572
|
+
}
|
|
567
573
|
}
|
|
568
|
-
});
|
|
574
|
+
})().catch(() => {});
|
|
569
575
|
|
|
570
576
|
await runCheckedStep('notebook.ping', () => notebook!.ping(), { continueOnError: false });
|
|
571
577
|
|
|
@@ -599,8 +605,8 @@ async function runScenario(
|
|
|
599
605
|
});
|
|
600
606
|
|
|
601
607
|
await runCheckedStep('container.state', async () => {
|
|
602
|
-
const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.
|
|
603
|
-
const portInfo = assertPortInfoList(ports, '
|
|
608
|
+
const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.ports.list()]);
|
|
609
|
+
const portInfo = assertPortInfoList(ports, 'ports.list');
|
|
604
610
|
return `${state.state} (${describePorts(portInfo)})`;
|
|
605
611
|
});
|
|
606
612
|
|
|
@@ -609,11 +615,11 @@ async function runScenario(
|
|
|
609
615
|
'container.restart',
|
|
610
616
|
async () => {
|
|
611
617
|
await notebook!.container.start();
|
|
612
|
-
const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.
|
|
618
|
+
const [state, ports] = await Promise.all([notebook!.container.state(), notebook!.ports.list()]);
|
|
613
619
|
if (state.state !== 'running') {
|
|
614
620
|
throw new Error(`Expected running container after restart, got ${state.state}`);
|
|
615
621
|
}
|
|
616
|
-
return `${state.state} (${describePorts(assertPortInfoList(ports, '
|
|
622
|
+
return `${state.state} (${describePorts(assertPortInfoList(ports, 'ports.list after restart'))})`;
|
|
617
623
|
},
|
|
618
624
|
{ timeoutMs: options.stepTimeoutMs * 3, continueOnError: false }
|
|
619
625
|
);
|
|
@@ -626,29 +632,20 @@ async function runScenario(
|
|
|
626
632
|
async () =>
|
|
627
633
|
retryWithBackoff(
|
|
628
634
|
async () => {
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
'
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
options.telemetryTimeoutMs,
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
telemetryEnabled = true;
|
|
644
|
-
|
|
645
|
-
return await firstFulfilled([
|
|
646
|
-
statsPromise.then(
|
|
647
|
-
(stats) =>
|
|
648
|
-
`cpu=${stats.cpu.usage}/${stats.cpu.limit} memory=${stats.memory.usage}/${stats.memory.limit}`
|
|
649
|
-
),
|
|
650
|
-
portsPromise.then((ports) => describePorts(assertPortInfoList(ports, 'container.ports'))),
|
|
651
|
-
]);
|
|
635
|
+
const statsReader = notebook!.metrics.watch().getReader();
|
|
636
|
+
const portsReader = notebook!.ports.watch().getReader();
|
|
637
|
+
try {
|
|
638
|
+
const statsPromise = withTimeout(statsReader.read(), options.telemetryTimeoutMs, 'metrics.watch')
|
|
639
|
+
.then(({ value }) => {
|
|
640
|
+
if (!value) throw new Error('metrics.watch ended before a sample was received.');
|
|
641
|
+
return `cpu=${value.cpu.usage}/${value.cpu.limit} memory=${value.memory.usage}/${value.memory.limit}`;
|
|
642
|
+
});
|
|
643
|
+
const portsPromise = withTimeout(portsReader.read(), options.telemetryTimeoutMs, 'ports.watch')
|
|
644
|
+
.then(({ value }) => describePorts(assertPortInfoList(value ?? [], 'ports.watch')));
|
|
645
|
+
return await firstFulfilled([statsPromise, portsPromise]);
|
|
646
|
+
} finally {
|
|
647
|
+
await Promise.all([statsReader.cancel(), portsReader.cancel()]);
|
|
648
|
+
}
|
|
652
649
|
},
|
|
653
650
|
TELEMETRY_RETRY_DELAYS_MS,
|
|
654
651
|
shouldRetryTelemetryError
|
|
@@ -660,7 +657,7 @@ async function runScenario(
|
|
|
660
657
|
'filesystem.watch-and-write',
|
|
661
658
|
async () => {
|
|
662
659
|
const changeSeen = createDeferred<FileChange>();
|
|
663
|
-
const watch = await notebook!.
|
|
660
|
+
const watch = await notebook!.files.watch(
|
|
664
661
|
'routes',
|
|
665
662
|
{ recursive: true, excludes: ['vendor/**', 'node_modules/**'] },
|
|
666
663
|
(change) => {
|
|
@@ -671,12 +668,9 @@ async function runScenario(
|
|
|
671
668
|
);
|
|
672
669
|
|
|
673
670
|
try {
|
|
674
|
-
const current = textDecoder.decode(await notebook!.
|
|
671
|
+
const current = textDecoder.decode(await notebook!.files.read(routePath));
|
|
675
672
|
const next = appendRouteDefinition(current, id, routeUrlPath);
|
|
676
|
-
|
|
677
|
-
if (!written) {
|
|
678
|
-
throw new Error(`fs.write returned false for ${routePath}.`);
|
|
679
|
-
}
|
|
673
|
+
await notebook!.files.write(routePath, next);
|
|
680
674
|
|
|
681
675
|
const change = await withTimeout(
|
|
682
676
|
changeSeen.promise,
|
|
@@ -692,14 +686,13 @@ async function runScenario(
|
|
|
692
686
|
);
|
|
693
687
|
|
|
694
688
|
await runCheckedStep('filesystem.inspect', async () => {
|
|
695
|
-
const [raw,
|
|
696
|
-
notebook!.
|
|
697
|
-
notebook!.
|
|
698
|
-
notebook!.
|
|
699
|
-
notebook!.
|
|
700
|
-
notebook!.
|
|
701
|
-
notebook!.
|
|
702
|
-
notebook!.file.tail(routePath, 20),
|
|
689
|
+
const [raw, stat, directory, tree, found, tail] = await Promise.all([
|
|
690
|
+
notebook!.files.read(routePath),
|
|
691
|
+
notebook!.files.stat(routePath),
|
|
692
|
+
notebook!.files.list('routes'),
|
|
693
|
+
notebook!.files.tree('routes'),
|
|
694
|
+
notebook!.files.find(routeFileName, { includes: ['routes/**'], maxResults: 10 }),
|
|
695
|
+
notebook!.files.tail(routePath, { lines: 20 }),
|
|
703
696
|
]);
|
|
704
697
|
|
|
705
698
|
const text = textDecoder.decode(raw);
|
|
@@ -707,7 +700,7 @@ async function runScenario(
|
|
|
707
700
|
throw new Error(`Expected ${routeUrlPath} in ${routePath}.`);
|
|
708
701
|
}
|
|
709
702
|
|
|
710
|
-
const [, matches] = await notebook!.
|
|
703
|
+
const [, matches] = await notebook!.files.search(
|
|
711
704
|
{ pattern: routeUrlPath },
|
|
712
705
|
{ includes: [routePath], excludes: [], maxResults: 10 }
|
|
713
706
|
);
|
|
@@ -716,12 +709,12 @@ async function runScenario(
|
|
|
716
709
|
throw new Error(`file.search() did not find ${routeUrlPath}.`);
|
|
717
710
|
}
|
|
718
711
|
|
|
719
|
-
const exists = await notebook!.
|
|
712
|
+
const exists = await notebook!.files.exists(routePath);
|
|
720
713
|
if (!exists) {
|
|
721
714
|
throw new Error(`${routePath} does not exist after write.`);
|
|
722
715
|
}
|
|
723
716
|
|
|
724
|
-
return `size=${stat.size}
|
|
717
|
+
return `size=${stat.size} dir=${directory.length} tree-lines=${tree.split('\n').length} find=${found.length} tail=${tail.length}`;
|
|
725
718
|
});
|
|
726
719
|
|
|
727
720
|
if (options.includePreview) {
|
|
@@ -732,7 +725,10 @@ async function runScenario(
|
|
|
732
725
|
|
|
733
726
|
if (options.includeDownload) {
|
|
734
727
|
await runCheckedStep('filesystem.download', async () => {
|
|
735
|
-
const archive = await notebook!.
|
|
728
|
+
const archive = await notebook!.files.download({
|
|
729
|
+
exclude: ['vendor/**', 'node_modules/**'],
|
|
730
|
+
include: [routePath, 'composer.json'],
|
|
731
|
+
});
|
|
736
732
|
if (archive.size === 0) {
|
|
737
733
|
throw new Error('file.download() returned an empty blob.');
|
|
738
734
|
}
|
|
@@ -743,8 +739,8 @@ async function runScenario(
|
|
|
743
739
|
recordSkippedStep(steps, 'filesystem.download', 'Enable with --include-download.');
|
|
744
740
|
}
|
|
745
741
|
|
|
746
|
-
await runCheckedStep('shell.
|
|
747
|
-
const result = await notebook!.
|
|
742
|
+
await runCheckedStep('shell.run', async () => {
|
|
743
|
+
const result = await notebook!.run(['php', 'artisan', '--version']).wait();
|
|
748
744
|
result.throw();
|
|
749
745
|
if (!result.output.toLowerCase().includes('laravel')) {
|
|
750
746
|
throw new Error(`Unexpected artisan version output: ${trimDetail(result.output)}`);
|
|
@@ -754,21 +750,22 @@ async function runScenario(
|
|
|
754
750
|
}, { skipIf: explainUnsupportedShell });
|
|
755
751
|
|
|
756
752
|
await runCheckedStep(
|
|
757
|
-
'shell.
|
|
753
|
+
'shell.run-stream',
|
|
758
754
|
async () => {
|
|
759
|
-
const execution = notebook!.
|
|
760
|
-
['php', 'artisan', 'route:list', `--path=${routeUrlPath}`]
|
|
755
|
+
const execution = notebook!.run(
|
|
756
|
+
['php', 'artisan', 'route:list', `--path=${routeUrlPath}`],
|
|
757
|
+
{ tty: false },
|
|
761
758
|
);
|
|
762
759
|
|
|
763
760
|
const [streamOutput, result] = await Promise.all([
|
|
764
|
-
readStreamToString(execution.
|
|
761
|
+
readStreamToString(execution.stdout, options.streamTimeoutMs, options.maxStreamChars).catch((error) => {
|
|
765
762
|
if (isStreamInactivityError(error)) {
|
|
766
763
|
return '';
|
|
767
764
|
}
|
|
768
765
|
|
|
769
766
|
throw error;
|
|
770
767
|
}),
|
|
771
|
-
withTimeout(execution.
|
|
768
|
+
withTimeout(execution.wait(), options.stepTimeoutMs, 'waiting for process exit'),
|
|
772
769
|
]);
|
|
773
770
|
|
|
774
771
|
result.throw();
|
|
@@ -783,20 +780,20 @@ async function runScenario(
|
|
|
783
780
|
);
|
|
784
781
|
|
|
785
782
|
await runCheckedStep(
|
|
786
|
-
'
|
|
783
|
+
'terminals.create',
|
|
787
784
|
async () => {
|
|
788
|
-
const process = await notebook!.
|
|
789
|
-
'php',
|
|
790
|
-
['artisan', 'route:list', `--path=${routeUrlPath}`]
|
|
791
|
-
);
|
|
785
|
+
const process = await notebook!.terminals.create({
|
|
786
|
+
command: 'php',
|
|
787
|
+
args: ['artisan', 'route:list', `--path=${routeUrlPath}`],
|
|
788
|
+
});
|
|
792
789
|
|
|
793
790
|
try {
|
|
794
791
|
const outputPromise = readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars);
|
|
795
|
-
const exitPromise = withTimeout(process.
|
|
792
|
+
const exitPromise = withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal process to exit');
|
|
796
793
|
const [output, exitCode] = await Promise.all([outputPromise, exitPromise]);
|
|
797
794
|
|
|
798
795
|
if (exitCode !== 0) {
|
|
799
|
-
throw new Error(`
|
|
796
|
+
throw new Error(`terminals.create() exited with ${exitCode}: ${trimDetail(output)}`);
|
|
800
797
|
}
|
|
801
798
|
|
|
802
799
|
if (!output.includes(routeUrlPath)) {
|
|
@@ -819,7 +816,7 @@ async function runScenario(
|
|
|
819
816
|
redisRunning = started.running || started.state === 'running' || started.state === 'starting';
|
|
820
817
|
|
|
821
818
|
const stream = notebook!.services.logs('redis', { follow: true, tail: 25 });
|
|
822
|
-
const reader = stream.
|
|
819
|
+
const reader = stream.getReader();
|
|
823
820
|
|
|
824
821
|
try {
|
|
825
822
|
const [snapshot, chunk] = await Promise.all([
|
|
@@ -836,7 +833,6 @@ async function runScenario(
|
|
|
836
833
|
} finally {
|
|
837
834
|
await reader.cancel().catch(() => undefined);
|
|
838
835
|
reader.releaseLock();
|
|
839
|
-
await stream.stop().catch(() => false);
|
|
840
836
|
await notebook!.services.stop('redis').catch(() => started);
|
|
841
837
|
redisRunning = false;
|
|
842
838
|
}
|
|
@@ -844,33 +840,22 @@ async function runScenario(
|
|
|
844
840
|
{ timeoutMs: options.stepTimeoutMs * 2 }
|
|
845
841
|
);
|
|
846
842
|
|
|
847
|
-
recordSkippedStep(
|
|
848
|
-
steps,
|
|
849
|
-
'laravel.maintenance',
|
|
850
|
-
'Not part of the current okra websocket action set.'
|
|
851
|
-
);
|
|
852
|
-
|
|
853
843
|
await runCheckedStep('composer.metadata', async () => {
|
|
854
844
|
let composerLogChunks = 0;
|
|
855
|
-
const
|
|
856
|
-
|
|
845
|
+
const dump = notebook!.composer.dumpAutoload();
|
|
846
|
+
for await (const chunk of dump.output) {
|
|
847
|
+
if (chunk.data.byteLength > 0) {
|
|
857
848
|
composerLogChunks += 1;
|
|
858
849
|
}
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
try {
|
|
862
|
-
const [packages, dump] = await Promise.all([notebook!.composer.packages(), notebook!.composer.dumpAutoload()]);
|
|
863
|
-
if (!dump.success) {
|
|
864
|
-
throw new Error(dump.message || 'composer.dumpAutoload() failed.');
|
|
865
|
-
}
|
|
866
|
-
if (packages.length === 0) {
|
|
867
|
-
throw new Error('composer.packages() returned no packages.');
|
|
868
|
-
}
|
|
850
|
+
}
|
|
869
851
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
852
|
+
(await dump.wait()).throw();
|
|
853
|
+
const packages = await notebook!.composer.packages();
|
|
854
|
+
if (packages.length === 0) {
|
|
855
|
+
throw new Error('composer.packages() returned no packages.');
|
|
873
856
|
}
|
|
857
|
+
|
|
858
|
+
return `packages=${packages.length} composer-log-events=${composerLogChunks}`;
|
|
874
859
|
}, { timeoutMs: Math.max(options.stepTimeoutMs * 2, 120_000) });
|
|
875
860
|
|
|
876
861
|
await runCheckedStep('git.checkpoint', async () => {
|
|
@@ -950,7 +935,7 @@ async function runScenario(
|
|
|
950
935
|
if (options.reauthorizationUrl) {
|
|
951
936
|
await runCheckedStep('auth.login-logout', async () => {
|
|
952
937
|
await notebook!.auth.login(options.reauthorizationUrl!);
|
|
953
|
-
|
|
938
|
+
notebook!.dispose();
|
|
954
939
|
return 'ok';
|
|
955
940
|
});
|
|
956
941
|
} else {
|
|
@@ -965,11 +950,8 @@ async function runScenario(
|
|
|
965
950
|
await notebook.services.stop('redis').catch(() => undefined);
|
|
966
951
|
}
|
|
967
952
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
logListener?.dispose();
|
|
953
|
+
await logReader?.cancel().catch(() => undefined);
|
|
954
|
+
logReader = null;
|
|
973
955
|
for (const disposable of initTraceDisposables) {
|
|
974
956
|
disposable.dispose();
|
|
975
957
|
}
|
|
@@ -1020,7 +1002,7 @@ async function cleanupNotebook(notebook: NotebookInstance, steps: StepResult[]):
|
|
|
1020
1002
|
const notebookId = notebook.data.id;
|
|
1021
1003
|
await cleanupDeleteLimiter.run(async () => {
|
|
1022
1004
|
try {
|
|
1023
|
-
await retryWithBackoff(() => notebook.
|
|
1005
|
+
await retryWithBackoff(() => notebook.destroy(), DELETE_RETRY_DELAYS_MS, shouldRetryApiRateLimitError);
|
|
1024
1006
|
} catch (error) {
|
|
1025
1007
|
if (formatError(error).includes('Notebook has been deleted')) {
|
|
1026
1008
|
return;
|
|
@@ -1912,7 +1894,7 @@ function firstFulfilled<T>(promises: Array<Promise<T>>): Promise<T> {
|
|
|
1912
1894
|
}
|
|
1913
1895
|
|
|
1914
1896
|
async function readStreamToString(
|
|
1915
|
-
stream: ReadableStream<string>,
|
|
1897
|
+
stream: ReadableStream<string | Uint8Array>,
|
|
1916
1898
|
timeoutMs: number,
|
|
1917
1899
|
maxChars: number
|
|
1918
1900
|
): Promise<string> {
|
|
@@ -1926,7 +1908,7 @@ async function readStreamToString(
|
|
|
1926
1908
|
break;
|
|
1927
1909
|
}
|
|
1928
1910
|
|
|
1929
|
-
output += chunk.value ?? '';
|
|
1911
|
+
output += chunk.value instanceof Uint8Array ? new TextDecoder().decode(chunk.value) : (chunk.value ?? '');
|
|
1930
1912
|
}
|
|
1931
1913
|
|
|
1932
1914
|
return output;
|
|
@@ -155,12 +155,12 @@ async function main(): Promise<void> {
|
|
|
155
155
|
}, false);
|
|
156
156
|
|
|
157
157
|
await recordStep(steps, 'container.opened-ports', options.stepTimeoutMs, async () => {
|
|
158
|
-
const ports = await notebook!.
|
|
159
|
-
return describePorts(assertPortInfoList(ports, '
|
|
158
|
+
const ports = await notebook!.ports.list();
|
|
159
|
+
return describePorts(assertPortInfoList(ports, 'ports.list'));
|
|
160
160
|
}, false);
|
|
161
161
|
|
|
162
162
|
const rootSnapshot = await recordStep(steps, 'filesystem.root', options.stepTimeoutMs, async () => {
|
|
163
|
-
const rootEntries = await notebook!.
|
|
163
|
+
const rootEntries = await notebook!.files.list('/');
|
|
164
164
|
|
|
165
165
|
const preferred = ['composer.json', 'index.php', 'artisan'];
|
|
166
166
|
const rootNames = new Set(rootEntries.map(([name]) => name));
|
|
@@ -174,7 +174,7 @@ async function main(): Promise<void> {
|
|
|
174
174
|
const treePath = rootSnapshot.candidate === 'artisan' ? 'routes' : undefined;
|
|
175
175
|
if (treePath) {
|
|
176
176
|
await recordStep(steps, 'filesystem.tree', options.stepTimeoutMs, async () => {
|
|
177
|
-
const tree = await notebook!.
|
|
177
|
+
const tree = await notebook!.files.tree(treePath);
|
|
178
178
|
return `path=${treePath} bytes=${tree.length}`;
|
|
179
179
|
}, false);
|
|
180
180
|
} else {
|
|
@@ -183,8 +183,8 @@ async function main(): Promise<void> {
|
|
|
183
183
|
|
|
184
184
|
const findSnapshot = await recordStep(steps, 'filesystem.find', options.stepTimeoutMs, async () => {
|
|
185
185
|
const [composerFiles, indexFiles] = await Promise.all([
|
|
186
|
-
notebook!.
|
|
187
|
-
notebook!.
|
|
186
|
+
notebook!.files.find('composer.json', { maxResults: 10 }),
|
|
187
|
+
notebook!.files.find('index.php', { maxResults: 10 }),
|
|
188
188
|
]);
|
|
189
189
|
const candidate = [...composerFiles, ...indexFiles].find((file) => typeof file.path === 'string')?.path;
|
|
190
190
|
return {
|
|
@@ -197,15 +197,13 @@ async function main(): Promise<void> {
|
|
|
197
197
|
|
|
198
198
|
if (candidate) {
|
|
199
199
|
await recordStep(steps, 'filesystem.read-candidate', options.stepTimeoutMs, async () => {
|
|
200
|
-
const [
|
|
201
|
-
notebook!.
|
|
202
|
-
notebook!.
|
|
203
|
-
notebook!.
|
|
204
|
-
notebook!.file.tail(candidate, 10),
|
|
200
|
+
const [stat, content, tail] = await Promise.all([
|
|
201
|
+
notebook!.files.stat(candidate),
|
|
202
|
+
notebook!.files.read(candidate),
|
|
203
|
+
notebook!.files.tail(candidate, { lines: 10 }),
|
|
205
204
|
]);
|
|
206
205
|
|
|
207
|
-
|
|
208
|
-
return `path=${candidate} size=${stat.size} binary=${info.binary} sha256=${sha256(bytes)} tailBytes=${tail.length}`;
|
|
206
|
+
return `path=${candidate} size=${stat.size} sha256=${sha256(content)} tailBytes=${tail.length}`;
|
|
209
207
|
}, false);
|
|
210
208
|
} else {
|
|
211
209
|
recordSkippedStep(steps, 'filesystem.read-candidate', 'No readable candidate file found.');
|
|
@@ -214,7 +212,10 @@ async function main(): Promise<void> {
|
|
|
214
212
|
if (options.includeDownload) {
|
|
215
213
|
if (candidate) {
|
|
216
214
|
await recordStep(steps, 'filesystem.download', options.stepTimeoutMs, async () => {
|
|
217
|
-
const archive = await notebook!.
|
|
215
|
+
const archive = await notebook!.files.download({
|
|
216
|
+
exclude: ['vendor/**', 'node_modules/**'],
|
|
217
|
+
include: [candidate],
|
|
218
|
+
});
|
|
218
219
|
if (archive.size === 0) {
|
|
219
220
|
throw new Error('file.download() returned an empty blob.');
|
|
220
221
|
}
|
|
@@ -228,7 +229,7 @@ async function main(): Promise<void> {
|
|
|
228
229
|
}
|
|
229
230
|
|
|
230
231
|
await recordStep(steps, 'filesystem.search', options.stepTimeoutMs, async () => {
|
|
231
|
-
const [hasMore, matches] = await notebook!.
|
|
232
|
+
const [hasMore, matches] = await notebook!.files.search({
|
|
232
233
|
pattern: 'php',
|
|
233
234
|
isRegExp: false,
|
|
234
235
|
isCaseSensitive: false,
|
|
@@ -259,8 +260,8 @@ async function main(): Promise<void> {
|
|
|
259
260
|
if (options.includeExec) {
|
|
260
261
|
await runExecutionChecks(notebook, steps, options);
|
|
261
262
|
} else {
|
|
262
|
-
recordSkippedStep(steps, 'shell.
|
|
263
|
-
recordSkippedStep(steps, '
|
|
263
|
+
recordSkippedStep(steps, 'shell.run', 'Enable with --include-exec.');
|
|
264
|
+
recordSkippedStep(steps, 'terminals.create', 'Enable with --include-exec.');
|
|
264
265
|
recordSkippedStep(steps, 'terminal.interactive', 'Enable with --include-exec.');
|
|
265
266
|
recordSkippedStep(steps, 'terminal.osc', 'Enable with --include-exec.');
|
|
266
267
|
recordSkippedStep(steps, 'repl.eval', 'Enable with --include-exec.');
|
|
@@ -323,8 +324,8 @@ async function main(): Promise<void> {
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifact[], options: SmokeOptions): Promise<void> {
|
|
326
|
-
await recordStep(steps, 'shell.
|
|
327
|
-
const result = await notebook.
|
|
327
|
+
await recordStep(steps, 'shell.run', options.stepTimeoutMs, async () => {
|
|
328
|
+
const result = await notebook.run(['php', '-r', 'echo "sdk-smoke";']).wait();
|
|
328
329
|
result.throw();
|
|
329
330
|
if (!result.output.includes('sdk-smoke')) {
|
|
330
331
|
throw new Error(`Unexpected shell output: ${result.output}`);
|
|
@@ -332,26 +333,25 @@ async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifac
|
|
|
332
333
|
return `exit=${result.exitCode}`;
|
|
333
334
|
}, false);
|
|
334
335
|
|
|
335
|
-
await recordStep(steps, '
|
|
336
|
-
const process = await notebook.
|
|
336
|
+
await recordStep(steps, 'terminals.create', options.stepTimeoutMs, async () => {
|
|
337
|
+
const process = await notebook.terminals.create({ command: 'php', args: ['-r', 'echo "sdk-smoke";'] });
|
|
337
338
|
const [exitCode, output] = await Promise.all([
|
|
338
|
-
withTimeout(process.
|
|
339
|
+
withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal process exit'),
|
|
339
340
|
readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars),
|
|
340
341
|
]);
|
|
341
342
|
|
|
342
343
|
if (exitCode !== 0 || !output.includes('sdk-smoke')) {
|
|
343
|
-
throw new Error(`
|
|
344
|
+
throw new Error(`terminals.create failed: exit=${exitCode} output=${trimDetail(output)}`);
|
|
344
345
|
}
|
|
345
346
|
|
|
346
347
|
return `exit=${exitCode}`;
|
|
347
348
|
}, false);
|
|
348
349
|
|
|
349
350
|
await recordStep(steps, 'terminal.interactive', options.stepTimeoutMs, async () => {
|
|
350
|
-
const process = await notebook.
|
|
351
|
-
'bash',
|
|
352
|
-
['-lc', 'IFS= read -r line; printf "sdk-input:%s\\n" "$line"'],
|
|
353
|
-
|
|
354
|
-
);
|
|
351
|
+
const process = await notebook.terminals.create({
|
|
352
|
+
command: 'bash',
|
|
353
|
+
args: ['-lc', 'IFS= read -r line; printf "sdk-input:%s\\n" "$line"'],
|
|
354
|
+
});
|
|
355
355
|
|
|
356
356
|
try {
|
|
357
357
|
const writer = process.input.getWriter();
|
|
@@ -359,7 +359,7 @@ async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifac
|
|
|
359
359
|
writer.releaseLock();
|
|
360
360
|
|
|
361
361
|
const [exitCode, output] = await Promise.all([
|
|
362
|
-
withTimeout(process.
|
|
362
|
+
withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for interactive terminal process exit'),
|
|
363
363
|
readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars),
|
|
364
364
|
]);
|
|
365
365
|
|
|
@@ -374,19 +374,16 @@ async function runExecutionChecks(notebook: NotebookInstance, steps: StepArtifac
|
|
|
374
374
|
}, false);
|
|
375
375
|
|
|
376
376
|
await recordStep(steps, 'terminal.osc', options.stepTimeoutMs, async () => {
|
|
377
|
-
const process = await notebook.
|
|
378
|
-
'bash',
|
|
379
|
-
['-lc', 'printf "sdk-prompt$"'],
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
terminal: { cols: 80, rows: 15 },
|
|
384
|
-
}
|
|
385
|
-
);
|
|
377
|
+
const process = await notebook.terminals.create({
|
|
378
|
+
command: 'bash',
|
|
379
|
+
args: ['-lc', 'printf "sdk-prompt$"'],
|
|
380
|
+
osc: true,
|
|
381
|
+
size: [80, 15],
|
|
382
|
+
});
|
|
386
383
|
|
|
387
384
|
try {
|
|
388
385
|
const [exitCode, output] = await Promise.all([
|
|
389
|
-
withTimeout(process.
|
|
386
|
+
withTimeout(process.wait(), options.stepTimeoutMs, 'waiting for terminal OSC process exit'),
|
|
390
387
|
readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars),
|
|
391
388
|
]);
|
|
392
389
|
|
|
@@ -453,7 +450,7 @@ function recordSkippedStep(steps: StepArtifact[], name: string, detail: string):
|
|
|
453
450
|
console.log(`SKIP ${name} ${detail}`);
|
|
454
451
|
}
|
|
455
452
|
|
|
456
|
-
async function readStreamToString(stream: ReadableStream<string>, timeoutMs: number, maxChars: number): Promise<string> {
|
|
453
|
+
async function readStreamToString(stream: ReadableStream<string | Uint8Array>, timeoutMs: number, maxChars: number): Promise<string> {
|
|
457
454
|
const reader = stream.getReader();
|
|
458
455
|
let output = '';
|
|
459
456
|
|
|
@@ -463,7 +460,7 @@ async function readStreamToString(stream: ReadableStream<string>, timeoutMs: num
|
|
|
463
460
|
if (chunk.done) {
|
|
464
461
|
break;
|
|
465
462
|
}
|
|
466
|
-
output += chunk.value ?? '';
|
|
463
|
+
output += chunk.value instanceof Uint8Array ? new TextDecoder().decode(chunk.value) : (chunk.value ?? '');
|
|
467
464
|
}
|
|
468
465
|
return output;
|
|
469
466
|
} finally {
|