@phpsandbox/sdk 0.0.49 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/auth.d.ts +0 -1
  4. package/dist/auth.js +0 -1
  5. package/dist/beacon/index.d.ts +4 -5
  6. package/dist/beacon/index.js +11 -12
  7. package/dist/beacon/navigator.d.ts +1 -2
  8. package/dist/beacon/navigator.js +0 -1
  9. package/dist/beacon/types.d.ts +12 -13
  10. package/dist/beacon/types.js +0 -1
  11. package/dist/browser/phpsandbox-sdk.esm.js +282 -479
  12. package/dist/browser/phpsandbox-sdk.esm.min.js +2 -3
  13. package/dist/browser/phpsandbox-sdk.iife.js +277 -479
  14. package/dist/browser/phpsandbox-sdk.iife.min.js +2 -3
  15. package/dist/composer.d.ts +2 -3
  16. package/dist/composer.js +0 -1
  17. package/dist/config.d.ts +0 -1
  18. package/dist/config.js +0 -1
  19. package/dist/container.d.ts +0 -1
  20. package/dist/container.js +0 -1
  21. package/dist/errors/index.d.ts +0 -1
  22. package/dist/errors/index.js +0 -1
  23. package/dist/events/index.d.ts +15 -15
  24. package/dist/events/index.js +16 -9
  25. package/dist/filesystem.d.ts +6 -7
  26. package/dist/filesystem.js +36 -14
  27. package/dist/git.d.ts +0 -1
  28. package/dist/git.js +0 -1
  29. package/dist/index.d.ts +4 -3
  30. package/dist/index.js +38 -25
  31. package/dist/laravel.d.ts +1 -2
  32. package/dist/laravel.js +1 -2
  33. package/dist/log.d.ts +3 -4
  34. package/dist/log.js +1 -2
  35. package/dist/lsp.d.ts +10 -11
  36. package/dist/lsp.js +9 -10
  37. package/dist/repl.d.ts +3 -4
  38. package/dist/repl.js +2 -3
  39. package/dist/services.d.ts +0 -1
  40. package/dist/services.js +0 -1
  41. package/dist/shell.d.ts +1 -2
  42. package/dist/shell.js +1 -2
  43. package/dist/socket/index.d.ts +23 -129
  44. package/dist/socket/index.js +60 -350
  45. package/dist/terminal.d.ts +2 -3
  46. package/dist/terminal.js +45 -26
  47. package/dist/types.d.ts +0 -1
  48. package/dist/types.js +0 -1
  49. package/dist/utils/disposable.d.ts +0 -1
  50. package/dist/utils/disposable.js +0 -1
  51. package/dist/utils/promise.d.ts +0 -1
  52. package/dist/utils/promise.js +0 -1
  53. package/dist/utils/retry.d.ts +0 -1
  54. package/dist/utils/retry.js +0 -1
  55. package/dist/utils/websocket.d.ts +0 -1
  56. package/dist/utils/websocket.js +0 -1
  57. package/docs/api-summary.md +136 -0
  58. package/docs/cdn-usage.md +80 -0
  59. package/docs/getting-started.md +152 -0
  60. package/examples/README.md +206 -0
  61. package/examples/basic.ts +24 -0
  62. package/examples/get.ts +21 -0
  63. package/examples/git-merge-smoke.ts +415 -0
  64. package/examples/laravel-load-test.ts +2829 -0
  65. package/examples/notebook-readonly-smoke.ts +823 -0
  66. package/package.json +13 -7
  67. package/dist/auth.d.ts.map +0 -1
  68. package/dist/auth.js.map +0 -1
  69. package/dist/beacon/index.d.ts.map +0 -1
  70. package/dist/beacon/index.js.map +0 -1
  71. package/dist/beacon/navigator.d.ts.map +0 -1
  72. package/dist/beacon/navigator.js.map +0 -1
  73. package/dist/beacon/types.d.ts.map +0 -1
  74. package/dist/beacon/types.js.map +0 -1
  75. package/dist/browser/phpsandbox-sdk.esm.js.map +0 -7
  76. package/dist/browser/phpsandbox-sdk.esm.min.js.map +0 -7
  77. package/dist/browser/phpsandbox-sdk.iife.js.map +0 -7
  78. package/dist/browser/phpsandbox-sdk.iife.min.js.map +0 -7
  79. package/dist/composer.d.ts.map +0 -1
  80. package/dist/composer.js.map +0 -1
  81. package/dist/config.d.ts.map +0 -1
  82. package/dist/config.js.map +0 -1
  83. package/dist/container.d.ts.map +0 -1
  84. package/dist/container.js.map +0 -1
  85. package/dist/errors/index.d.ts.map +0 -1
  86. package/dist/errors/index.js.map +0 -1
  87. package/dist/events/index.d.ts.map +0 -1
  88. package/dist/events/index.js.map +0 -1
  89. package/dist/filesystem.d.ts.map +0 -1
  90. package/dist/filesystem.js.map +0 -1
  91. package/dist/git.d.ts.map +0 -1
  92. package/dist/git.js.map +0 -1
  93. package/dist/index.d.ts.map +0 -1
  94. package/dist/index.js.map +0 -1
  95. package/dist/laravel.d.ts.map +0 -1
  96. package/dist/laravel.js.map +0 -1
  97. package/dist/log.d.ts.map +0 -1
  98. package/dist/log.js.map +0 -1
  99. package/dist/lsp.d.ts.map +0 -1
  100. package/dist/lsp.js.map +0 -1
  101. package/dist/repl.d.ts.map +0 -1
  102. package/dist/repl.js.map +0 -1
  103. package/dist/services.d.ts.map +0 -1
  104. package/dist/services.js.map +0 -1
  105. package/dist/shell.d.ts.map +0 -1
  106. package/dist/shell.js.map +0 -1
  107. package/dist/socket/index.d.ts.map +0 -1
  108. package/dist/socket/index.js.map +0 -1
  109. package/dist/terminal.d.ts.map +0 -1
  110. package/dist/terminal.js.map +0 -1
  111. package/dist/types.d.ts.map +0 -1
  112. package/dist/types.js.map +0 -1
  113. package/dist/utils/disposable.d.ts.map +0 -1
  114. package/dist/utils/disposable.js.map +0 -1
  115. package/dist/utils/promise.d.ts.map +0 -1
  116. package/dist/utils/promise.js.map +0 -1
  117. package/dist/utils/retry.d.ts.map +0 -1
  118. package/dist/utils/retry.js.map +0 -1
  119. package/dist/utils/websocket.d.ts.map +0 -1
  120. package/dist/utils/websocket.js.map +0 -1
package/dist/terminal.js CHANGED
@@ -16,13 +16,13 @@ export default class Terminal {
16
16
  return this.okra.invoke('terminal.create', input);
17
17
  }
18
18
  onStarted(handler) {
19
- this.okra.listen('terminal.started', handler);
19
+ return this.okra.listen('terminal.started', handler);
20
20
  }
21
21
  resize(id, size) {
22
22
  return this.okra.invoke('terminal.resize', { id, width: size[0], height: size[1] });
23
23
  }
24
24
  onOutput(id, handler) {
25
- this.okra.listen(`terminal.output.${id}`, handler);
25
+ return this.okra.listen(`terminal.output.${id}`, handler);
26
26
  }
27
27
  input(id, input) {
28
28
  return Promise.resolve(this.okra.send('terminal.input', { id, input }));
@@ -32,38 +32,55 @@ export default class Terminal {
32
32
  }
33
33
  async attach(id, opts) {
34
34
  const handle = this.processHandle(id, opts?.abortSignal);
35
- const task = await this.okra.invoke('terminal.attach', {
36
- id,
37
- replayHistory: opts?.replayHistory,
38
- }, { abortSignal: opts?.abortSignal });
39
- if (!task) {
35
+ try {
36
+ const task = await this.okra.invoke('terminal.attach', {
37
+ id,
38
+ replayHistory: opts?.replayHistory,
39
+ }, { abortSignal: opts?.abortSignal });
40
+ if (!task) {
41
+ handle.dispose();
42
+ return false;
43
+ }
44
+ return {
45
+ ...handle.process,
46
+ ...task,
47
+ };
48
+ }
49
+ catch (error) {
40
50
  handle.dispose();
41
- return false;
51
+ throw error;
42
52
  }
43
- return {
44
- ...handle.process,
45
- ...task,
46
- };
47
53
  }
48
54
  async spawn(command, args, opts) {
49
55
  const id = opts?.id || nanoid();
50
56
  const handle = this.processHandle(id, opts?.abortSignal);
51
57
  const { abortSignal, ...otherOpts } = opts ?? {};
52
- const result = await this.okra.invoke('terminal.spawn', {
53
- command: [command, ...args],
54
- opts: {
55
- id,
56
- ...otherOpts,
57
- },
58
- }, { abortSignal });
59
- return {
60
- ...handle.process,
61
- ...result,
62
- };
58
+ try {
59
+ const result = await this.okra.invoke('terminal.spawn', {
60
+ command: [command, ...args],
61
+ opts: {
62
+ id,
63
+ ...otherOpts,
64
+ },
65
+ }, { abortSignal });
66
+ return {
67
+ ...handle.process,
68
+ ...result,
69
+ };
70
+ }
71
+ catch (error) {
72
+ handle.dispose();
73
+ throw error;
74
+ }
63
75
  }
64
76
  processHandle(id, abortSignal) {
65
77
  const disposables = new Set();
78
+ let abortHandler;
66
79
  const dispose = () => {
80
+ if (abortHandler) {
81
+ abortSignal?.removeEventListener('abort', abortHandler);
82
+ abortHandler = undefined;
83
+ }
67
84
  for (const disposable of disposables) {
68
85
  disposable.dispose();
69
86
  }
@@ -140,8 +157,11 @@ export default class Terminal {
140
157
  const resize = (dimensions) => {
141
158
  this.okra.invoke('terminal.resize', { id, width: dimensions.cols, height: dimensions.rows });
142
159
  };
143
- if (abortSignal) {
144
- abortSignal.addEventListener('abort', kill);
160
+ if (abortSignal && !abortSignal.aborted) {
161
+ abortHandler = () => {
162
+ void kill();
163
+ };
164
+ abortSignal.addEventListener('abort', abortHandler, { once: true });
145
165
  }
146
166
  return {
147
167
  process: {
@@ -155,4 +175,3 @@ export default class Terminal {
155
175
  };
156
176
  }
157
177
  }
158
- //# sourceMappingURL=terminal.js.map
package/dist/types.d.ts CHANGED
@@ -13,4 +13,3 @@ export declare class PHPSandboxError extends ErrorEvent {
13
13
  export interface Disposable {
14
14
  dispose: () => void;
15
15
  }
16
- //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -13,4 +13,3 @@ export class RateLimitError extends ErrorEvent {
13
13
  }
14
14
  export class PHPSandboxError extends ErrorEvent {
15
15
  }
16
- //# sourceMappingURL=types.js.map
@@ -4,4 +4,3 @@ export declare class NamedDisposable implements Disposable {
4
4
  add(key: string, disposable: Disposable | (() => Disposable)): void;
5
5
  dispose(): void;
6
6
  }
7
- //# sourceMappingURL=disposable.d.ts.map
@@ -17,4 +17,3 @@ export class NamedDisposable {
17
17
  this.disposables.clear();
18
18
  }
19
19
  }
20
- //# sourceMappingURL=disposable.js.map
@@ -14,4 +14,3 @@ export declare const timeout: <T>(prom: Promise<T>, time: number) => Promise<T>;
14
14
  * @returns A wrapped function that memoizes the first execution
15
15
  */
16
16
  export declare const once: <T>(fn: () => Promise<T>) => (() => Promise<T>);
17
- //# sourceMappingURL=promise.d.ts.map
@@ -24,4 +24,3 @@ export const once = (fn) => {
24
24
  let result = null;
25
25
  return () => result || (result = fn());
26
26
  };
27
- //# sourceMappingURL=promise.js.map
@@ -9,4 +9,3 @@ export interface RetryOptions {
9
9
  type RetryBail = (error?: unknown) => never;
10
10
  export declare function retry<T>(fn: (bail: RetryBail, attempt: number) => T | Promise<T>, options?: RetryOptions): Promise<T>;
11
11
  export {};
12
- //# sourceMappingURL=retry.d.ts.map
@@ -43,4 +43,3 @@ export async function retry(fn, options = {}) {
43
43
  }
44
44
  throw lastError;
45
45
  }
46
- //# sourceMappingURL=retry.js.map
@@ -1,2 +1 @@
1
1
  export declare const describeWebSocketEvent: (event: unknown) => string;
2
- //# sourceMappingURL=websocket.d.ts.map
@@ -84,4 +84,3 @@ export const describeWebSocketEvent = (event) => {
84
84
  }
85
85
  return parts.length > 0 ? parts.join(' | ') : 'Unknown websocket event';
86
86
  };
87
- //# sourceMappingURL=websocket.js.map
@@ -0,0 +1,136 @@
1
+ # PHPSandbox SDK API Summary
2
+
3
+ This is a quick reference for the current SDK surface.
4
+
5
+ ## Top-Level Client
6
+
7
+ ```ts
8
+ import { PHPSandbox } from '@phpsandbox/sdk';
9
+
10
+ const client = new PHPSandbox(token, 'https://api.phpsandbox.io/v1', {
11
+ debug: false,
12
+ startClosed: true,
13
+ });
14
+ ```
15
+
16
+ ### `NotebookApi`
17
+
18
+ - `create(template, input?, init = true)`
19
+ - `get(id)`
20
+ - `open(id)`
21
+ - `fork(id, input?)`
22
+ - `delete(id)`
23
+ - `openFromData(data)`
24
+ - `mail.status(id)`, `mail.enable(id)`, `mail.disable(id)`, `mail.list(id, options?)`, `mail.get(id, hash)`, `mail.delete(id, hash)`
25
+
26
+ ## `NotebookInstance`
27
+
28
+ - Lifecycle: `ready()`, `connected()`, `whenConnected()`, `reconnect()`, `dispose()`
29
+ - Notebook actions: `fork(input?)`, `delete()`, `stop()`, `restart()`, `ping()`, `update()`
30
+ - Events: `listen(event, handler)`, `onDidConnect(handler)`, `onDidDisconnect(handler)`, `onDidInitialize(handler)`
31
+ - Raw call: `invoke(action, data?, options?)`
32
+
33
+ ## Services
34
+
35
+ ### Filesystem (`notebook.file`)
36
+
37
+ - File data: `info()`, `readFile()`, `write()`, `writeFile()`, `tail()`, `stat()`, `exists()`
38
+ - File management: `move()`, `rename()`, `copy()`, `remove()`, `delete()`
39
+ - Directory: `mkdir()`, `createDirectory()`, `readDirectory()`, `tree()`
40
+ - Search: `find()`, `search()`
41
+ - Streaming/monitoring: `watch()`, `download()`
42
+
43
+ ### Terminal (`notebook.terminal`)
44
+
45
+ - Terminal control: `list()`, `create()`, `attach()`, `resize()`, `input()`
46
+ - Process API: `spawn(command, args, opts?)`, `attach(id, opts?)`
47
+ - Events: `listen()`, `onStarted()`, `onOutput(id, handler)`
48
+
49
+ ### Container (`notebook.container`)
50
+
51
+ - Lifecycle: `start()`, `stop()`, `state()`
52
+ - Ports: `openedPorts()`, `onPort(handler)`
53
+ - Runtime config: `setPhp(version)`
54
+ - Telemetry: `enableTelemetry(features)`, `stopTelemetry()`, `listen(event, handler)`
55
+
56
+ ### Shell (`notebook.shell`)
57
+
58
+ - `exec(command)` returning `{ output, exitCode }` plus `throw()` helper
59
+
60
+ ### Services (`notebook.services`)
61
+
62
+ - Constants: `notebookBuiltinServices`, `notebookKnownServices`
63
+ - `list()`
64
+ - `run(name, command?)`
65
+ - `stop(name)`
66
+ - `logs(name, { tail? })`
67
+ - `logs(name, { follow: true, tail?, id? })` returning `{ id, output, stop() }`
68
+ - Events: `listen()`, `onLog(id, handler)`
69
+
70
+ ### Composer (`notebook.composer`)
71
+
72
+ - Core: `invoke(command, args?, options?)`
73
+ - Helpers: `install()`, `update()`, `require()`, `remove()`, `dumpAutoload()`, `packages()`, `stream(handler)`
74
+
75
+ ### Git (`notebook.git`)
76
+
77
+ - `checkpoint(author, message, branch = 'main')`
78
+ - `sync(url, author, ref?, token?, direction?, force?)`
79
+ - `diff({ url?, token?, ref?, base?, target? })`
80
+ - `merge({ url?, token?, ref?, target?, expectedBaseRef?, author?, message? })`
81
+ - `conflicts()`
82
+ - `abortMerge()`
83
+ - `continueMerge(author, message?)`
84
+ - `review()`
85
+ - `stage({ path?, paths?, all? })`
86
+ - `unstage({ path?, paths?, all? })`
87
+ - `revert({ path?, paths?, all?, staged? })`
88
+ - `log(ref = 'main')`
89
+ - `restore(ref)`
90
+
91
+ ### Mail (`notebook.mail`)
92
+
93
+ - `status()` returning non-sensitive mail configuration state
94
+ - `enable()` enabling SMTP env injection for the notebook
95
+ - `disable()` disabling SMTP env injection and revoking the active SMTP token
96
+ - `list({ page? })` returning paginated notebook mail
97
+ - `get(hash)`
98
+ - `delete(hash)`
99
+
100
+ ### LSP (`notebook.lsp`)
101
+
102
+ - Messaging: `start(id)`, `close(id)`, `message(id, payload)`
103
+ - Events: `onResponse()`, `onError()`, `onClose()`, `onClientDisconnect()`
104
+ - Connection wrapper: `connection(id)`
105
+
106
+ ### REPL (`notebook.repl`)
107
+
108
+ - `stop()`, `eval(code, args?, replOpts?)`, `write(input)`, `resize(cols, rows)`
109
+ - Events: `listen()`, `onOutput()`
110
+
111
+ ### Laravel (`notebook.laravel`)
112
+
113
+ - Wrapper exists in the SDK, but the current okra websocket action set does not expose Laravel maintenance actions.
114
+
115
+ ### Auth (`notebook.auth`)
116
+
117
+ - `login(newConnectionData)`
118
+ - `logout()`
119
+
120
+ ### Log (`notebook.log`)
121
+
122
+ - `stream(handler)`
123
+ - `listen(event, handler)`
124
+
125
+ ## Common Errors
126
+
127
+ - `ApiError` for HTTP API failures
128
+ - `NotebookInitError` when notebook initialization returns `type: 'error'`
129
+ - `FilesystemError` + `FilesystemErrorType.*` for file system operations
130
+ - `RateLimitError` for rate-limited flows
131
+
132
+ ## Notes for Consumers
133
+
134
+ - `open()` and `get()` do not auto-initialize; call `await notebook.ready()`.
135
+ - `create()` and `fork()` initialize by default.
136
+ - Always call `notebook.dispose()` when finished.
@@ -0,0 +1,80 @@
1
+ # CDN Usage Guide
2
+
3
+ The SDK ships browser bundles under `dist/browser`.
4
+
5
+ ## Security First
6
+
7
+ Do not hardcode long-lived private API tokens in public frontend code. Use short-lived tokens issued by your backend.
8
+
9
+ ## ESM in Browsers
10
+
11
+ ### unpkg
12
+
13
+ ```html
14
+ <script type="module">
15
+ import { PHPSandbox } from 'https://unpkg.com/@phpsandbox/sdk@0.0.51/dist/browser/phpsandbox-sdk.esm.min.js';
16
+
17
+ const client = new PHPSandbox('your-short-lived-token');
18
+ const notebook = await client.notebook.open('your-notebook-id');
19
+ await notebook.ready();
20
+
21
+ const result = await notebook.shell.exec('php -v');
22
+ result.throw();
23
+ console.log(result.output);
24
+ </script>
25
+ ```
26
+
27
+ ### jsDelivr
28
+
29
+ ```html
30
+ <script type="module">
31
+ import { PHPSandbox } from 'https://cdn.jsdelivr.net/npm/@phpsandbox/sdk@0.0.51/dist/browser/phpsandbox-sdk.esm.min.js';
32
+
33
+ const client = new PHPSandbox('your-short-lived-token');
34
+ </script>
35
+ ```
36
+
37
+ ## Script Tag (IIFE)
38
+
39
+ ### unpkg
40
+
41
+ ```html
42
+ <script src="https://unpkg.com/@phpsandbox/sdk@0.0.51/dist/browser/phpsandbox-sdk.iife.min.js"></script>
43
+ <script>
44
+ const client = new PHPSandbox.PHPSandbox('your-short-lived-token');
45
+
46
+ client.notebook.open('your-notebook-id').then(async (notebook) => {
47
+ await notebook.ready();
48
+ const result = await notebook.shell.exec('php -v');
49
+ console.log(result.output);
50
+ notebook.dispose();
51
+ });
52
+ </script>
53
+ ```
54
+
55
+ ### jsDelivr
56
+
57
+ ```html
58
+ <script src="https://cdn.jsdelivr.net/npm/@phpsandbox/sdk@0.0.51/dist/browser/phpsandbox-sdk.iife.min.js"></script>
59
+ ```
60
+
61
+ ## Version Pinning
62
+
63
+ Always pin a version in production URLs:
64
+
65
+ ```html
66
+ https://unpkg.com/@phpsandbox/sdk@0.0.51/dist/browser/phpsandbox-sdk.esm.min.js
67
+ https://cdn.jsdelivr.net/npm/@phpsandbox/sdk@0.0.51/dist/browser/phpsandbox-sdk.iife.min.js
68
+ ```
69
+
70
+ ## Bundle Files
71
+
72
+ - `phpsandbox-sdk.esm.js`
73
+ - `phpsandbox-sdk.esm.min.js`
74
+ - `phpsandbox-sdk.iife.js`
75
+ - `phpsandbox-sdk.iife.min.js`
76
+
77
+ ## Browser Requirements
78
+
79
+ - ES2020-compatible runtime
80
+ - `fetch`, `WebSocket`, and `ReadableStream` support
@@ -0,0 +1,152 @@
1
+ # Getting Started with PHPSandbox SDK
2
+
3
+ This guide walks through the fastest path from install to a working notebook.
4
+
5
+ ## Prerequisites
6
+
7
+ - Node.js 18+
8
+ - A PHPSandbox API token
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @phpsandbox/sdk
14
+ ```
15
+
16
+ ## 1. Create a Client
17
+
18
+ ```ts
19
+ import { PHPSandbox } from '@phpsandbox/sdk';
20
+
21
+ const token = process.env.PHPSANDBOX_TOKEN;
22
+ if (!token) throw new Error('Missing PHPSANDBOX_TOKEN');
23
+
24
+ const client = new PHPSandbox(token);
25
+ ```
26
+
27
+ ## 2. Create or Open a Notebook
28
+
29
+ ```ts
30
+ // create() initializes by default
31
+ const notebook = await client.notebook.create('php');
32
+
33
+ const persistent = await client.notebook.create('laravel', {
34
+ title: 'Persistent Laravel',
35
+ persistent: true,
36
+ });
37
+
38
+ // open()/get() need ready()
39
+ const existing = await client.notebook.open('notebook-id');
40
+ await existing.ready();
41
+
42
+ const fork = await existing.fork();
43
+ const persistentFork = await existing.fork({ persistent: true });
44
+ ```
45
+
46
+ API notebook creates and forks are ephemeral by default. Use `persistent: true` only when your account is allowed to create persistent notebooks.
47
+
48
+ ## 3. Write and Read Files
49
+
50
+ ```ts
51
+ await notebook.file.write('hello.php', '<?php echo "Hello";');
52
+
53
+ const data = await notebook.file.readFile('hello.php');
54
+ const text = new TextDecoder().decode(data);
55
+
56
+ console.log(text);
57
+ ```
58
+
59
+ If you need binary-safe writes, use `writeFile`:
60
+
61
+ ```ts
62
+ await notebook.file.writeFile('binary.dat', new Uint8Array([1, 2, 3]), {
63
+ create: true,
64
+ overwrite: true,
65
+ unlock: false,
66
+ atomic: false,
67
+ });
68
+ ```
69
+
70
+ ## 4. Run Commands
71
+
72
+ ### Terminal (stream output)
73
+
74
+ ```ts
75
+ const task = await notebook.terminal.spawn('php', ['hello.php']);
76
+ const reader = task.output.getReader();
77
+
78
+ try {
79
+ while (true) {
80
+ const { done, value } = await reader.read();
81
+ if (done) break;
82
+ process.stdout.write(value);
83
+ }
84
+ } finally {
85
+ reader.releaseLock();
86
+ }
87
+
88
+ const exitCode = await task.exit;
89
+ console.log(`\nExit code: ${exitCode}`);
90
+ ```
91
+
92
+ ### Shell (single command result)
93
+
94
+ ```ts
95
+ const result = await notebook.shell.exec('php -v');
96
+ result.throw();
97
+ console.log(result.output);
98
+ ```
99
+
100
+ ## 5. Search and Watch Files
101
+
102
+ ```ts
103
+ const matches = await notebook.file.find('*.php', {
104
+ includes: ['app/**'],
105
+ excludes: ['vendor/**'],
106
+ });
107
+
108
+ console.log(matches.map((m) => m.path));
109
+
110
+ const watcher = notebook.file.watch(
111
+ '/app',
112
+ {
113
+ recursive: true,
114
+ excludes: ['vendor/**', 'node_modules/**'],
115
+ },
116
+ (change) => {
117
+ console.log(change.type, change.path);
118
+ }
119
+ );
120
+
121
+ // later
122
+ watcher.dispose();
123
+ ```
124
+
125
+ ## 6. Use Composer and Git
126
+
127
+ ```ts
128
+ await notebook.composer.install({ noInteraction: true });
129
+ await notebook.composer.require({ packages: ['monolog/monolog'] });
130
+
131
+ await notebook.git.checkpoint('Jane Doe <jane@example.com>', 'Initial checkpoint');
132
+ ```
133
+
134
+ ## 7. Clean Up
135
+
136
+ Always dispose notebook connections when finished:
137
+
138
+ ```ts
139
+ notebook.dispose();
140
+ ```
141
+
142
+ ## Common Pitfalls
143
+
144
+ - `open()` / `get()` notebooks must call `await notebook.ready()` before tool calls.
145
+ - `file.watch()` requires an `excludes` array.
146
+ - `shell.exec()` does not throw by default; call `result.throw()` to fail on non-zero exit codes.
147
+
148
+ ## Next Reading
149
+
150
+ - API overview: `docs/api-summary.md`
151
+ - CDN/browser usage: `docs/cdn-usage.md`
152
+ - Root quick start: `README.md`