@zgeoff/imp-client 0.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Geoff Whatley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @zgeoff/imp-client
2
+
3
+ A typed client for [impd](https://github.com/zgeoff/imp), the imp host daemon. It runs in Bun, in
4
+ Node 22 or later, and in browsers with `Promise.withResolvers`: Chrome 119, Firefox 121 and Safari
5
+ 17.4 or later.
6
+
7
+ ```sh
8
+ npm install @zgeoff/imp-client
9
+ ```
10
+
11
+ `@opentelemetry/api` is an optional peer: oRPC's type declarations name it, so a project that
12
+ type-checks its dependencies (no `skipLibCheck`) installs it too.
13
+
14
+ ## Connect
15
+
16
+ ```ts
17
+ import { createImpClient } from '@zgeoff/imp-client';
18
+
19
+ const imp = createImpClient({
20
+ url: 'http://localhost:7070',
21
+ token: process.env.IMP_TOKEN,
22
+ });
23
+ ```
24
+
25
+ - `url` is impd's API. A path prefix is kept, so impd can sit behind a proxy at `/impd/`.
26
+ - `token` is the bearer token from `<IMP_DATA_DIR>/token`. It controls every imp on the host, so
27
+ keep it on a server. A browser app sends its requests through a proxy that adds the token, and
28
+ leaves `token` out.
29
+ - `fetch` swaps the transport, for example to call an in-process app in tests.
30
+
31
+ ## Imps, checkpoints and forks
32
+
33
+ Every procedure of impd's API is on the client, with its input and output types.
34
+
35
+ ```ts
36
+ const dev = await imp.imps.create({ name: 'dev', memoryMib: 2048 });
37
+
38
+ const checkpoint = await imp.checkpoints.create({ name: 'dev', label: 'clean' });
39
+
40
+ // a new imp from the checkpoint's disk
41
+ await imp.imps.fork({ source: 'dev', name: 'dev-2', checkpoint: checkpoint.id });
42
+
43
+ await imp.imps.sleep({ name: 'dev' });
44
+ ```
45
+
46
+ `imp.requireAwake(name)` makes one wake call. It wakes a sleeping imp, boots a stopped one and
47
+ returns a running one as it is. impd refuses an imp in the `error` state with `INVALID_STATE`,
48
+ because a wake would restart it; pass `{ restartError: true }` to restart it anyway. While impd
49
+ stops, calls fail with `SERVICE_UNAVAILABLE`, and while it restarts, `fetch` cannot connect; pass
50
+ `{ retryUnavailable: { attempts, delayMs } }` to wait for it to come back. `RAM_BUDGET_EXCEEDED` is
51
+ never retried.
52
+
53
+ ## Run commands
54
+
55
+ `imp.run` runs a command to its exit and collects its output. Without `stdin`, stdin closes at once.
56
+
57
+ ```ts
58
+ const result = await imp.run('dev', ['sh', '-c', 'uname -a; cat'], { stdin: 'hi\n' });
59
+
60
+ console.log(result.code, new TextDecoder().decode(result.stdout));
61
+ ```
62
+
63
+ `imp.openExec` streams instead. `stdout` and `stderr` are `ReadableStream`s that end with the
64
+ session. `write` takes a string or bytes and waits while the socket's buffer is full, so a writer in
65
+ a loop keeps to the network's pace.
66
+
67
+ Read or cancel both streams. Output nobody reads waits in memory, and past 8 MiB on one stream
68
+ (`maxUnreadBytes`) the session ends with `OUTPUT_OVERFLOW`. Cancelling both streams ends the session
69
+ and stops the command; so do `close()` and the `signal` option's abort.
70
+
71
+ ```ts
72
+ const tail = await imp.openExec('dev', ['tail', '-f', '/var/log/app.log']);
73
+
74
+ void tail.stderr.cancel();
75
+
76
+ try {
77
+ for await (const chunk of tail.stdout) {
78
+ if (process.stdout.write(chunk) === false) {
79
+ break;
80
+ }
81
+ }
82
+ } finally {
83
+ tail.close();
84
+ }
85
+ ```
86
+
87
+ `imp.openConsole` opens the root user's login shell with a tty, as `imp console` does. With
88
+ [xterm.js](https://xtermjs.org):
89
+
90
+ ```ts
91
+ const shell = await imp.openConsole('dev', { cols: term.cols, rows: term.rows });
92
+
93
+ term.onData((data) => {
94
+ shell.write(data).catch(() => {});
95
+ });
96
+ term.onResize(({ cols, rows }) => shell.resize(cols, rows));
97
+
98
+ const reader = shell.stdout.getReader();
99
+
100
+ for (let read = await reader.read(); !read.done; read = await reader.read()) {
101
+ term.write(read.value);
102
+ }
103
+ ```
104
+
105
+ With `session: 'main'`, `openConsole` and `openExec` start that session, or attach to it if it runs,
106
+ and the shell outlives the handle: `close()` detaches.
107
+ `imp.openAttach('dev', 'main', { cols, rows })` attaches to a running session; its `stdout` starts
108
+ with a replay of the recent output. `sessions.list` names them without waking the imp. `started`
109
+ resolves with `{ pid, session, created }`. When impd ends the socket while the session runs on,
110
+ `exit` rejects with `DETACHED`, and its `data.reason` says why: `taken_over` (another client
111
+ attached; one is attached at a time), `slow` (the client fell too far behind) or `lost` (impd lost
112
+ the guest, as when the imp slept; attach again).
113
+
114
+ With a tty, `sendSignal('SIGINT')` and `sendSignal('SIGQUIT')` send ^C and ^\ as keys, so they reach
115
+ the foreground job; other signals, and every signal without a tty, go to the process. A write after
116
+ the session ended rejects with `CLOSED`.
117
+
118
+ The socket authenticates with a single-use exec ticket from `exec.ticket`, so the token never goes
119
+ in a URL; a browser behind a proxy that adds the token works the same way.
120
+
121
+ `exit` resolves with `{ code, signal }`, where `code` is null when a signal ended the command. When
122
+ the command did not run to its exit, `exit` rejects with an `ExecError` whose `code` is impd's (as
123
+ in the table below, plus `EXEC_FAILED` when the command cannot start) or one of `UNAUTHORIZED` (also
124
+ for an exec ticket that expired or was used), `UNREACHABLE`, `RESTARTING`, `CONNECTION_CLOSED`,
125
+ `DETACHED`, `BAD_MESSAGE`, `CLOSED`, `OUTPUT_OVERFLOW` and `LOCAL_ERROR`. Its `data` is impd's error
126
+ data.
127
+
128
+ ## Errors
129
+
130
+ A failed call throws an `ORPCError`. impd's errors carry a `code` and typed `data`:
131
+
132
+ | Code | When | `data` |
133
+ | --------------------- | ---------------------------------------------------- | -------------------------------------- |
134
+ | `NOT_FOUND` | No imp, image, checkpoint or session has that name. | `{ kind, name }` |
135
+ | `CONFLICT` | The name is taken. | `{ kind, name }` |
136
+ | `INVALID_STATE` | The imp's state does not allow the call. | `{ state, allowed }` |
137
+ | `RAM_BUDGET_EXCEEDED` | The host has no room, even after sleeping idle imps. | `{ budgetMib, usedMib, requestedMib }` |
138
+ | `SERVICE_UNAVAILABLE` | impd is stopping. | |
139
+ | `FORBIDDEN` | An exec ticket was used for another imp. | |
140
+ | `AGENT_OUTDATED` | The imp's agent has no sessions; stop and start it. | |
141
+
142
+ ```ts
143
+ import { isDefinedError, safe } from '@zgeoff/imp-client';
144
+
145
+ const [error, created] = await safe(imp.imps.create({ name: 'dev' }));
146
+
147
+ if (isDefinedError(error) && error.code === 'RAM_BUDGET_EXCEEDED') {
148
+ console.log(`needs ${error.data.requestedMib} MiB`);
149
+ }
150
+ ```
151
+
152
+ A 401 means the token is wrong.
153
+
154
+ ## Versions
155
+
156
+ The client and impd are released together with the same version. `imp.checkServer()` tells whether
157
+ impd speaks this client's API: the major version must match, and before 1.0 the minor version too.
158
+
159
+ ```ts
160
+ const check = await imp.checkServer();
161
+
162
+ if (!check.compatible) {
163
+ throw new Error(`impd ${check.serverVersion} does not match client ${check.clientVersion}`);
164
+ }
165
+ ```