@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 +21 -0
- package/README.md +165 -0
- package/dist/index.d.ts +2265 -0
- package/dist/index.js +678 -0
- package/package.json +38 -0
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
|
+
```
|