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