@phpsandbox/sdk 0.1.1 → 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 +59 -22
- package/dist/browser/phpsandbox-sdk.esm.js +465 -239
- package/dist/browser/phpsandbox-sdk.esm.min.js +24 -24
- package/dist/browser/phpsandbox-sdk.iife.js +465 -239
- package/dist/browser/phpsandbox-sdk.iife.min.js +24 -24
- 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 -18
- package/dist/filesystem.js +148 -63
- package/dist/index.d.ts +18 -9
- package/dist/index.js +21 -8
- 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/runtime/rest.js +1 -4
- package/dist/runtime-telemetry.d.ts +15 -0
- package/dist/runtime-telemetry.js +111 -0
- package/dist/schemas/publications.d.ts +10 -10
- package/dist/services.d.ts +2 -9
- package/dist/services.js +3 -10
- package/dist/shell.d.ts +9 -0
- package/dist/shell.js +4 -4
- package/dist/terminal.d.ts +17 -11
- package/dist/terminal.js +27 -44
- package/docs/api-summary.md +28 -23
- package/docs/cdn-usage.md +2 -2
- package/docs/getting-started.md +13 -13
- package/examples/basic.ts +1 -1
- package/examples/git-merge-smoke.ts +1 -4
- package/examples/laravel-load-test.ts +67 -80
- package/examples/notebook-readonly-smoke.ts +30 -33
- package/package.json +1 -1
package/dist/terminal.js
CHANGED
|
@@ -2,30 +2,34 @@ import { nanoid } from 'nanoid';
|
|
|
2
2
|
import { once } from './utils/promise.js';
|
|
3
3
|
const TERMINAL_INPUT_FLUSH_DELAY_MS = 8;
|
|
4
4
|
const terminalInputFlushPattern = /[\x00-\x1f\x7f]/;
|
|
5
|
-
export default class
|
|
5
|
+
export default class Terminals {
|
|
6
6
|
constructor(okra) {
|
|
7
7
|
this.okra = okra;
|
|
8
8
|
}
|
|
9
9
|
list() {
|
|
10
10
|
return this.okra.invoke('terminal.list');
|
|
11
11
|
}
|
|
12
|
-
create(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
async create(options = {}) {
|
|
13
|
+
const id = options.id ?? nanoid();
|
|
14
|
+
const handle = this.processHandle(id, options.abortSignal);
|
|
15
|
+
const command = typeof options.command === 'string' && options.args?.length
|
|
16
|
+
? [options.command, ...options.args]
|
|
17
|
+
: options.command;
|
|
18
|
+
const { abortSignal, args: _args, ...input } = options;
|
|
19
|
+
try {
|
|
20
|
+
const task = await this.okra.invoke('terminal.create', {
|
|
21
|
+
...input,
|
|
22
|
+
id,
|
|
23
|
+
kind: options.kind ?? 'bash',
|
|
24
|
+
size: options.size ?? [80, 24],
|
|
25
|
+
command,
|
|
26
|
+
}, { abortSignal });
|
|
27
|
+
return { ...handle.process, ...task };
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
handle.dispose();
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
29
33
|
}
|
|
30
34
|
async attach(id, opts) {
|
|
31
35
|
const handle = this.processHandle(id, opts?.abortSignal);
|
|
@@ -48,28 +52,6 @@ export default class Terminal {
|
|
|
48
52
|
throw error;
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
async spawn(command, args, opts) {
|
|
52
|
-
const id = opts?.id || nanoid();
|
|
53
|
-
const handle = this.processHandle(id, opts?.abortSignal);
|
|
54
|
-
const { abortSignal, ...otherOpts } = opts ?? {};
|
|
55
|
-
try {
|
|
56
|
-
const result = await this.okra.invoke('terminal.spawn', {
|
|
57
|
-
command: [command, ...args],
|
|
58
|
-
opts: {
|
|
59
|
-
id,
|
|
60
|
-
...otherOpts,
|
|
61
|
-
},
|
|
62
|
-
}, { abortSignal });
|
|
63
|
-
return {
|
|
64
|
-
...handle.process,
|
|
65
|
-
...result,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
handle.dispose();
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
55
|
processHandle(id, abortSignal) {
|
|
74
56
|
const disposables = new Set();
|
|
75
57
|
let abortHandler;
|
|
@@ -95,7 +77,7 @@ export default class Terminal {
|
|
|
95
77
|
}
|
|
96
78
|
const input = pendingInput;
|
|
97
79
|
pendingInput = '';
|
|
98
|
-
void this.input
|
|
80
|
+
void this.okra.send('terminal.input', { id, input });
|
|
99
81
|
};
|
|
100
82
|
const scheduleInputFlush = () => {
|
|
101
83
|
if (inputFlushTimer) {
|
|
@@ -103,7 +85,8 @@ export default class Terminal {
|
|
|
103
85
|
}
|
|
104
86
|
inputFlushTimer = setTimeout(flushInput, TERMINAL_INPUT_FLUSH_DELAY_MS);
|
|
105
87
|
};
|
|
106
|
-
const queueInput = (
|
|
88
|
+
const queueInput = (value) => {
|
|
89
|
+
const chunk = typeof value === 'string' ? value : new TextDecoder().decode(value);
|
|
107
90
|
pendingInput += chunk;
|
|
108
91
|
if (terminalInputFlushPattern.test(chunk)) {
|
|
109
92
|
flushInput();
|
|
@@ -124,7 +107,7 @@ export default class Terminal {
|
|
|
124
107
|
const output = new ReadableStream({
|
|
125
108
|
start: (_controller) => {
|
|
126
109
|
controller = _controller;
|
|
127
|
-
disposables.add(this.listen(`terminal.output.${id}`, (data) => {
|
|
110
|
+
disposables.add(this.okra.listen(`terminal.output.${id}`, (data) => {
|
|
128
111
|
controller?.enqueue(data.output);
|
|
129
112
|
}));
|
|
130
113
|
},
|
|
@@ -134,7 +117,7 @@ export default class Terminal {
|
|
|
134
117
|
},
|
|
135
118
|
});
|
|
136
119
|
const completion = new Promise((resolve) => {
|
|
137
|
-
disposables.add(this.listen(`terminal.close.${id}`, (data) => {
|
|
120
|
+
disposables.add(this.okra.listen(`terminal.close.${id}`, (data) => {
|
|
138
121
|
if (controller) {
|
|
139
122
|
try {
|
|
140
123
|
controller.close();
|
package/docs/api-summary.md
CHANGED
|
@@ -32,36 +32,46 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
|
|
|
32
32
|
|
|
33
33
|
- Lifecycle: `ready()`, `connected()`, `whenConnected()`, `reconnect()`, `dispose()`
|
|
34
34
|
- Notebook actions: `fork(input?)`, `destroy()`, `stop()`, `restart()`, `ping()`, `update()`
|
|
35
|
-
- Processes: `run(command, args?, options?)` returning native `stdin`, `stdout`, `stderr`, ordered `output`, `
|
|
35
|
+
- Processes: `exec(command, args?, options?)`, or `run(command, args?, options?)` returning native `stdin`, `stdout`, `stderr`, ordered `output`, `wait()`, and `kill()`
|
|
36
36
|
- Events: `listen(event, handler)`, `onDidConnect(handler)`, `onDidDisconnect(handler)`, `onDidInitialize(handler)`
|
|
37
37
|
- Raw call: `invoke(action, data?, options?)`
|
|
38
38
|
|
|
39
39
|
## Services
|
|
40
40
|
|
|
41
|
-
### Filesystem (`notebook.
|
|
41
|
+
### Filesystem (`notebook.files`)
|
|
42
42
|
|
|
43
|
-
- File data: `
|
|
44
|
-
- File management: `move()`, `
|
|
45
|
-
- Directory: `
|
|
43
|
+
- File data: `read()`, `readText()`, `readStream()`, `write()`, `tail()`, `stat()`, `exists()`
|
|
44
|
+
- File management: `move()`, `copy()`, `remove()`
|
|
45
|
+
- Directory: `createDirectory()`, `list()`, `tree()`
|
|
46
46
|
- Search: `find()`, `search()`
|
|
47
|
-
- Streaming/monitoring: `watch()`, `download()`
|
|
47
|
+
- Streaming/monitoring: `readStream()`, `watch()`, `download()`
|
|
48
|
+
- `tail()` is an eager last-lines snapshot; it does not follow future appends.
|
|
48
49
|
|
|
49
|
-
### Terminal (`notebook.
|
|
50
|
+
### Terminal (`notebook.terminals`)
|
|
50
51
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
52
|
+
- `list()` returns terminal metadata
|
|
53
|
+
- `create(options?)` returns a terminal handle
|
|
54
|
+
- `attach(id, options?)` returns a terminal handle
|
|
55
|
+
- Handles expose `input`, `output`, `wait()`, `resize()`, and `kill()`
|
|
54
56
|
|
|
55
57
|
### Container (`notebook.container`)
|
|
56
58
|
|
|
57
59
|
- Lifecycle: `start()`, `stop()`, `state()`
|
|
58
|
-
- Ports: `openedPorts()`, `onPort(handler)`
|
|
59
60
|
- Runtime config: `setPhp(version)`
|
|
60
|
-
- Telemetry: `enableTelemetry(features)`, `stopTelemetry()`, `listen(event, handler)`
|
|
61
61
|
|
|
62
|
-
###
|
|
62
|
+
### Runtime observability
|
|
63
63
|
|
|
64
|
-
- `
|
|
64
|
+
- `notebook.logs.follow({ signal? })` returns `ReadableStream<RuntimeLogEntry>`
|
|
65
|
+
- `notebook.ports.list()` returns the current preview ports
|
|
66
|
+
- `notebook.ports.watch({ signal? })` returns `ReadableStream<PortInfo[]>`
|
|
67
|
+
- `notebook.metrics.current()` returns the current CPU, memory, and disk sample
|
|
68
|
+
- `notebook.metrics.watch({ signal? })` returns `ReadableStream<ContainerStats>`
|
|
69
|
+
|
|
70
|
+
### Processes
|
|
71
|
+
|
|
72
|
+
- `notebook.run(command, args?, options?)` returns a streaming process handle
|
|
73
|
+
- `notebook.exec(command, args?, options?)` returns a buffered `ProcessResult` for one-shot commands
|
|
74
|
+
- Process handles expose `output`, `stdout`, `stderr`, `stdin`, `wait()`, and `kill()`
|
|
65
75
|
|
|
66
76
|
### Services (`notebook.services`)
|
|
67
77
|
|
|
@@ -70,13 +80,13 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
|
|
|
70
80
|
- `run(name, command?)`
|
|
71
81
|
- `stop(name)`
|
|
72
82
|
- `logs(name, { tail? })`
|
|
73
|
-
- `logs(name, { follow: true, tail?, id? })` returning `
|
|
74
|
-
- Events: `listen()`, `onLog(id, handler)`
|
|
83
|
+
- `logs(name, { follow: true, tail?, id? })` returning `ReadableStream<string>`; cancel the stream to stop following
|
|
75
84
|
|
|
76
85
|
### Composer (`notebook.composer`)
|
|
77
86
|
|
|
78
|
-
- Core: `
|
|
79
|
-
- Helpers: `install()`, `update()`, `require()`, `remove()`, `dumpAutoload()`, `packages()
|
|
87
|
+
- Core: `run(command, args?, options?)`
|
|
88
|
+
- Helpers: `install()`, `update()`, `require()`, `remove()`, `dumpAutoload()`, `packages()`
|
|
89
|
+
- Command handles expose `output`, `stdout`, `stderr`, `wait()`, and `kill()`
|
|
80
90
|
|
|
81
91
|
### Git (`notebook.git`)
|
|
82
92
|
|
|
@@ -126,11 +136,6 @@ Connection lifecycle methods, streaming actions, and event subscriptions require
|
|
|
126
136
|
- A publish run exposes `events()` as an `AsyncIterable`, `result()`, and `publication()`.
|
|
127
137
|
- Built-in provider names are `cloudflare-containers`, `ssh-server`, and `laravel-cloud`. Provider maps support TypeScript declaration merging.
|
|
128
138
|
|
|
129
|
-
### Log (`notebook.log`)
|
|
130
|
-
|
|
131
|
-
- `stream(handler)`
|
|
132
|
-
- `listen(event, handler)`
|
|
133
|
-
|
|
134
139
|
## Common Errors
|
|
135
140
|
|
|
136
141
|
- `ApiError` for HTTP API failures
|
package/docs/cdn-usage.md
CHANGED
|
@@ -18,7 +18,7 @@ Do not hardcode long-lived private API tokens in public frontend code. Use short
|
|
|
18
18
|
const notebook = await client.notebook.get('your-notebook-id');
|
|
19
19
|
await notebook.ready();
|
|
20
20
|
|
|
21
|
-
const result = await notebook.
|
|
21
|
+
const result = await notebook.exec('php -v', { tty: false });
|
|
22
22
|
result.throw();
|
|
23
23
|
console.log(result.output);
|
|
24
24
|
</script>
|
|
@@ -45,7 +45,7 @@ Do not hardcode long-lived private API tokens in public frontend code. Use short
|
|
|
45
45
|
|
|
46
46
|
client.notebook.get('your-notebook-id').then(async (notebook) => {
|
|
47
47
|
await notebook.ready();
|
|
48
|
-
const result = await notebook.
|
|
48
|
+
const result = await notebook.exec('php -v', { tty: false });
|
|
49
49
|
console.log(result.output);
|
|
50
50
|
notebook.dispose();
|
|
51
51
|
});
|
package/docs/getting-started.md
CHANGED
|
@@ -48,18 +48,18 @@ API notebook creates and forks are ephemeral by default. Use `persistent: true`
|
|
|
48
48
|
## 3. Write and Read Files
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
|
-
await notebook.
|
|
51
|
+
await notebook.files.write('hello.php', '<?php echo "Hello";');
|
|
52
52
|
|
|
53
|
-
const data = await notebook.
|
|
53
|
+
const data = await notebook.files.read('hello.php');
|
|
54
54
|
const text = new TextDecoder().decode(data);
|
|
55
55
|
|
|
56
56
|
console.log(text);
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
The same `write()` method accepts text, bytes, readable streams, and async iterables:
|
|
60
60
|
|
|
61
61
|
```ts
|
|
62
|
-
await notebook.
|
|
62
|
+
await notebook.files.write('binary.dat', new Uint8Array([1, 2, 3]), {
|
|
63
63
|
create: true,
|
|
64
64
|
overwrite: true,
|
|
65
65
|
unlock: false,
|
|
@@ -72,7 +72,7 @@ await notebook.file.writeFile('binary.dat', new Uint8Array([1, 2, 3]), {
|
|
|
72
72
|
### Terminal (stream output)
|
|
73
73
|
|
|
74
74
|
```ts
|
|
75
|
-
const task = await notebook.
|
|
75
|
+
const task = await notebook.terminals.create({ command: 'php', args: ['hello.php'] });
|
|
76
76
|
const reader = task.output.getReader();
|
|
77
77
|
|
|
78
78
|
try {
|
|
@@ -85,14 +85,14 @@ try {
|
|
|
85
85
|
reader.releaseLock();
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const exitCode = await task.
|
|
88
|
+
const exitCode = await task.wait();
|
|
89
89
|
console.log(`\nExit code: ${exitCode}`);
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
### Shell (single command result)
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
|
-
const result = await notebook.
|
|
95
|
+
const result = await notebook.exec('php -v', { tty: false });
|
|
96
96
|
result.throw();
|
|
97
97
|
console.log(result.stdout);
|
|
98
98
|
```
|
|
@@ -100,14 +100,14 @@ console.log(result.stdout);
|
|
|
100
100
|
## 5. Search and Watch Files
|
|
101
101
|
|
|
102
102
|
```ts
|
|
103
|
-
const matches = await notebook.
|
|
103
|
+
const matches = await notebook.files.find('*.php', {
|
|
104
104
|
includes: ['app/**'],
|
|
105
105
|
excludes: ['vendor/**'],
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
console.log(matches.map((m) => m.path));
|
|
109
109
|
|
|
110
|
-
const watcher = notebook.
|
|
110
|
+
const watcher = notebook.files.watch(
|
|
111
111
|
'/app',
|
|
112
112
|
{
|
|
113
113
|
recursive: true,
|
|
@@ -125,8 +125,8 @@ watcher.dispose();
|
|
|
125
125
|
## 6. Use Composer and Git
|
|
126
126
|
|
|
127
127
|
```ts
|
|
128
|
-
await notebook.composer.install(
|
|
129
|
-
await notebook.composer.require({ packages: ['monolog/monolog'] });
|
|
128
|
+
(await notebook.composer.install().wait()).throw();
|
|
129
|
+
(await notebook.composer.require({ packages: ['monolog/monolog'] }).wait()).throw();
|
|
130
130
|
|
|
131
131
|
await notebook.git.checkpoint('Jane Doe <jane@example.com>', 'Initial checkpoint');
|
|
132
132
|
```
|
|
@@ -142,8 +142,8 @@ notebook.dispose();
|
|
|
142
142
|
## Common Pitfalls
|
|
143
143
|
|
|
144
144
|
- `get()` notebooks must call `await notebook.ready()` before tool calls.
|
|
145
|
-
- `
|
|
146
|
-
-
|
|
145
|
+
- `files.watch()` requires an `excludes` array.
|
|
146
|
+
- Use `notebook.exec()` for one-shot commands; for streaming shell handles, await `process.wait()` and call `result.throw()` to fail on non-zero exit codes.
|
|
147
147
|
|
|
148
148
|
## Next Reading
|
|
149
149
|
|
package/examples/basic.ts
CHANGED
|
@@ -16,7 +16,7 @@ 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.
|
|
@@ -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);
|
|
@@ -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
|
}
|
|
@@ -784,12 +780,12 @@ async function runScenario(
|
|
|
784
780
|
);
|
|
785
781
|
|
|
786
782
|
await runCheckedStep(
|
|
787
|
-
'
|
|
783
|
+
'terminals.create',
|
|
788
784
|
async () => {
|
|
789
|
-
const process = await notebook!.
|
|
790
|
-
'php',
|
|
791
|
-
['artisan', 'route:list', `--path=${routeUrlPath}`]
|
|
792
|
-
);
|
|
785
|
+
const process = await notebook!.terminals.create({
|
|
786
|
+
command: 'php',
|
|
787
|
+
args: ['artisan', 'route:list', `--path=${routeUrlPath}`],
|
|
788
|
+
});
|
|
793
789
|
|
|
794
790
|
try {
|
|
795
791
|
const outputPromise = readStreamToString(process.output, options.streamTimeoutMs, options.maxStreamChars);
|
|
@@ -797,7 +793,7 @@ async function runScenario(
|
|
|
797
793
|
const [output, exitCode] = await Promise.all([outputPromise, exitPromise]);
|
|
798
794
|
|
|
799
795
|
if (exitCode !== 0) {
|
|
800
|
-
throw new Error(`
|
|
796
|
+
throw new Error(`terminals.create() exited with ${exitCode}: ${trimDetail(output)}`);
|
|
801
797
|
}
|
|
802
798
|
|
|
803
799
|
if (!output.includes(routeUrlPath)) {
|
|
@@ -820,7 +816,7 @@ async function runScenario(
|
|
|
820
816
|
redisRunning = started.running || started.state === 'running' || started.state === 'starting';
|
|
821
817
|
|
|
822
818
|
const stream = notebook!.services.logs('redis', { follow: true, tail: 25 });
|
|
823
|
-
const reader = stream.
|
|
819
|
+
const reader = stream.getReader();
|
|
824
820
|
|
|
825
821
|
try {
|
|
826
822
|
const [snapshot, chunk] = await Promise.all([
|
|
@@ -837,7 +833,6 @@ async function runScenario(
|
|
|
837
833
|
} finally {
|
|
838
834
|
await reader.cancel().catch(() => undefined);
|
|
839
835
|
reader.releaseLock();
|
|
840
|
-
await stream.stop().catch(() => false);
|
|
841
836
|
await notebook!.services.stop('redis').catch(() => started);
|
|
842
837
|
redisRunning = false;
|
|
843
838
|
}
|
|
@@ -847,25 +842,20 @@ async function runScenario(
|
|
|
847
842
|
|
|
848
843
|
await runCheckedStep('composer.metadata', async () => {
|
|
849
844
|
let composerLogChunks = 0;
|
|
850
|
-
const
|
|
851
|
-
|
|
845
|
+
const dump = notebook!.composer.dumpAutoload();
|
|
846
|
+
for await (const chunk of dump.output) {
|
|
847
|
+
if (chunk.data.byteLength > 0) {
|
|
852
848
|
composerLogChunks += 1;
|
|
853
849
|
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
try {
|
|
857
|
-
const [packages, dump] = await Promise.all([notebook!.composer.packages(), notebook!.composer.dumpAutoload()]);
|
|
858
|
-
if (!dump.success) {
|
|
859
|
-
throw new Error(dump.message || 'composer.dumpAutoload() failed.');
|
|
860
|
-
}
|
|
861
|
-
if (packages.length === 0) {
|
|
862
|
-
throw new Error('composer.packages() returned no packages.');
|
|
863
|
-
}
|
|
850
|
+
}
|
|
864
851
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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.');
|
|
868
856
|
}
|
|
857
|
+
|
|
858
|
+
return `packages=${packages.length} composer-log-events=${composerLogChunks}`;
|
|
869
859
|
}, { timeoutMs: Math.max(options.stepTimeoutMs * 2, 120_000) });
|
|
870
860
|
|
|
871
861
|
await runCheckedStep('git.checkpoint', async () => {
|
|
@@ -960,11 +950,8 @@ async function runScenario(
|
|
|
960
950
|
await notebook.services.stop('redis').catch(() => undefined);
|
|
961
951
|
}
|
|
962
952
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
logListener?.dispose();
|
|
953
|
+
await logReader?.cancel().catch(() => undefined);
|
|
954
|
+
logReader = null;
|
|
968
955
|
for (const disposable of initTraceDisposables) {
|
|
969
956
|
disposable.dispose();
|
|
970
957
|
}
|