@openagt/servers 0.0.0-stage → 0.0.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/dist/steps.js ADDED
@@ -0,0 +1,340 @@
1
+ /**
2
+ * The steps of an install, in order: what each is called on the page, who runs it on the server,
3
+ * and the script itself. A step first looks whether its work is already done and then does
4
+ * nothing, so the same list is the first install, a second try after a failure, and an update.
5
+ *
6
+ * `root` is only who installs. What runs afterwards runs as the server's own user `openagent`:
7
+ * the programs are root's, so an agent that works there cannot change OpenAgent itself.
8
+ */
9
+ export const STEP_IDS = ['login', 'user', 'tools', 'openagent', 'agent', 'service', 'door'];
10
+ /** The server's user everything runs as once it is installed. */
11
+ export const SERVER_USER = 'openagent';
12
+ /** Where the install keeps what it brings: Node, and OpenAgent inside it. */
13
+ const ROOT = '/opt/openagent';
14
+ /** The Node the server gets: the newest of this line, read off nodejs.org with its checksum. */
15
+ const NODE_LINE = 24;
16
+ /** Where the server keeps the Claude login: root's file, which the service reads as it starts. An agent on the server works with the login, and so can read it; it cannot change the file. */
17
+ export const CLAUDE_FILE = '/etc/openagent/claude.env';
18
+ /** The name Claude Code reads a login under that was made for a computer nobody sits at. */
19
+ const CLAUDE_NAME = 'CLAUDE_CODE_OAUTH_TOKEN';
20
+ /** The door's own command, inside the OpenAgent the install brought. */
21
+ const DOOR = `${ROOT}/node/bin/node ${ROOT}/node/lib/node_modules/@openagt/dashboard/node_modules/@openagt/remote-access/bin/remote-access`;
22
+ /** `value` as one word of a shell script, whatever it holds. */
23
+ export function quoted(value) {
24
+ return `'${value.replaceAll("'", `'\\''`)}'`;
25
+ }
26
+ /**
27
+ * Held by every script root runs, for as long as it runs: a script still at work after this computer stopped waiting for it is not joined by a second one.
28
+ * In a folder only root writes: the server's user cannot put a file of its own there first.
29
+ */
30
+ const LOCK = '/run/openagent-install.lock';
31
+ /**
32
+ * A whole script around `body`. The body is a function called once the shell has read all of
33
+ * it, with nothing to read from: the script itself comes in where a program it starts would
34
+ * otherwise read, and would be eaten by the first one that does.
35
+ *
36
+ * Root finds its programs in the system's own folders only, and nothing the server's user can
37
+ * write is ever on its way to one.
38
+ */
39
+ function script(as, vars, body) {
40
+ const system = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
41
+ return [
42
+ 'set -euo pipefail',
43
+ 'umask 022',
44
+ 'export DEBIAN_FRONTEND=noninteractive',
45
+ `export PATH="${as === 'root' ? system : `$HOME/.local/bin:${ROOT}/node/bin:${system}`}"`,
46
+ ...Object.entries(vars).map(([name, value]) => `${name}=${quoted(value)}`),
47
+ 'fail() { echo "$1" >&2; exit 1; }',
48
+ ...(as === 'root' ? [`exec 9>${LOCK}`, 'flock -n 9 || fail "Another install is still at work on this server. Try again in a moment."'] : []),
49
+ 'main() {',
50
+ body.trim(),
51
+ '}',
52
+ 'main </dev/null',
53
+ '',
54
+ ].join('\n');
55
+ }
56
+ const MINUTE = 60_000;
57
+ /** Defines `started`, which waits until OpenAgent answers on the server; when it does not, it shows what the service last said and answers no. */
58
+ const STARTED = `
59
+ started() {
60
+ for _ in $(seq 30); do
61
+ curl -fsS -o /dev/null http://127.0.0.1:4200/ 2>/dev/null && return 0
62
+ sleep 1
63
+ done
64
+ journalctl -u openagent -n 20 --no-pager >&2 || true
65
+ return 1
66
+ }`;
67
+ const NOT_STARTED = 'fail "OpenAgent did not start on this server."';
68
+ /** What the service file holds. `KillMode=process`: a run is a process of its own and goes on when OpenAgent starts again, as on a laptop. */
69
+ export const SERVICE_UNIT = `[Unit]
70
+ Description=OpenAgent
71
+ After=network-online.target
72
+ Wants=network-online.target
73
+
74
+ [Service]
75
+ User=${SERVER_USER}
76
+ WorkingDirectory=/home/${SERVER_USER}
77
+ Environment=PATH=/home/${SERVER_USER}/.local/bin:${ROOT}/node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
78
+ EnvironmentFile=-${CLAUDE_FILE}
79
+ ExecStart=${ROOT}/node/bin/openagent
80
+ Restart=always
81
+ RestartSec=5
82
+ KillMode=process
83
+ AmbientCapabilities=CAP_NET_BIND_SERVICE
84
+
85
+ [Install]
86
+ WantedBy=multi-user.target`;
87
+ export const STEPS = [
88
+ {
89
+ id: 'login',
90
+ label: 'Log in',
91
+ as: 'root',
92
+ timeoutMs: 2 * MINUTE,
93
+ script: () => script('root', {}, `
94
+ . /etc/os-release 2>/dev/null || true
95
+ case "\${ID:-}" in
96
+ ubuntu | debian) ;;
97
+ *) fail "OpenAgent installs on Ubuntu and Debian. This server runs \${PRETTY_NAME:-something else}." ;;
98
+ esac
99
+ case "$(uname -m)" in
100
+ x86_64 | aarch64) ;;
101
+ *) fail "OpenAgent has no install for this server's chip, $(uname -m)." ;;
102
+ esac
103
+ [ -d /run/systemd/system ] || fail "This server does not start its programs with systemd, which OpenAgent's service needs."
104
+ # Nothing is changed under an agent at work: its own programs would be replaced while it runs.
105
+ if pgrep -u ${SERVER_USER} -f 'agent-runner/bin/agent-runner run' >/dev/null 2>&1; then
106
+ fail "An agent is working on this server. Stop it, or wait until it ends, then try again."
107
+ fi
108
+ echo "\${PRETTY_NAME:-$ID}, $(uname -m)"
109
+ `),
110
+ },
111
+ {
112
+ id: 'user',
113
+ label: `The user ${SERVER_USER}`,
114
+ as: 'root',
115
+ timeoutMs: 2 * MINUTE,
116
+ script: () => script('root', {}, `
117
+ id ${SERVER_USER} >/dev/null 2>&1 || useradd --create-home --shell /bin/bash ${SERVER_USER}
118
+ `),
119
+ },
120
+ {
121
+ id: 'tools',
122
+ label: 'git, gh, Node and Chrome',
123
+ as: 'root',
124
+ timeoutMs: 20 * MINUTE,
125
+ script: () => script('root', {}, `
126
+ . /etc/os-release
127
+ # A server that was just made often installs its own updates in its first minutes: the install waits its turn.
128
+ apt() { apt-get -y -q -o DPkg::Lock::Timeout=600 "$@"; }
129
+ apt update
130
+ apt install git curl ca-certificates xz-utils procps
131
+ dir=$(mktemp -d)
132
+ trap 'rm -rf "$dir"' EXIT
133
+
134
+ # From GitHub's own package source, signed with its key: the gh these systems bring themselves is years old.
135
+ if ! command -v gh >/dev/null; then
136
+ install -d -m 755 /etc/apt/keyrings
137
+ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o "$dir/gh.gpg"
138
+ install -m 644 "$dir/gh.gpg" /etc/apt/keyrings/githubcli-archive-keyring.gpg
139
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list
140
+ apt update
141
+ apt install gh
142
+ fi
143
+
144
+ have=$(${ROOT}/node/bin/node -p 'process.versions.node.split(".")[0]' 2>/dev/null || true)
145
+ if [ "$have" != ${NODE_LINE} ]; then
146
+ arch=x64
147
+ [ "$(uname -m)" = aarch64 ] && arch=arm64
148
+ from=https://nodejs.org/dist/latest-v${NODE_LINE}.x
149
+ curl -fsSL "$from/SHASUMS256.txt" -o "$dir/SHASUMS256.txt"
150
+ file=$(grep -o "node-v[0-9.]*-linux-$arch\\.tar\\.xz" "$dir/SHASUMS256.txt" | head -n 1 || true)
151
+ [ -n "$file" ] || fail "nodejs.org lists no Node ${NODE_LINE} for this server's chip."
152
+ curl -fsSL "$from/$file" -o "$dir/$file"
153
+ (cd "$dir" && grep " $file\\$" SHASUMS256.txt | sha256sum -c - >/dev/null) || fail "The Node that was downloaded is not the one nodejs.org lists."
154
+ rm -rf ${ROOT}/node.new
155
+ mkdir -p ${ROOT}/node.new
156
+ # Root's, whoever the download names as the files' owner: on a server that owner could be the user the agents run as.
157
+ tar -xJf "$dir/$file" -C ${ROOT}/node.new --strip-components=1 --no-same-owner
158
+ rm -rf ${ROOT}/node
159
+ mv ${ROOT}/node.new ${ROOT}/node
160
+ touch ${ROOT}/restart
161
+ fi
162
+ for bin in node npm npx; do ln -sf ${ROOT}/node/bin/$bin /usr/local/bin/$bin; done
163
+
164
+ # A browser that is there and does not start is none: Ubuntu's own package is only a pointer to its snap store.
165
+ browser() { for name in google-chrome chromium chromium-browser; do "$name" --version >/dev/null 2>&1 && return 0; done; return 1; }
166
+ if ! browser; then
167
+ if [ "$(uname -m)" = x86_64 ]; then
168
+ curl -fsSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o "$dir/chrome.deb"
169
+ apt install "$dir/chrome.deb"
170
+ elif [ "$ID" = debian ]; then
171
+ # Google ships no Chrome for this chip: Chromium is the same browser without its name.
172
+ apt install chromium
173
+ else
174
+ apt install chromium-browser
175
+ fi
176
+ browser || fail "Chrome was installed, and it does not start on this server."
177
+ fi
178
+ `),
179
+ },
180
+ {
181
+ id: 'openagent',
182
+ label: 'OpenAgent',
183
+ as: 'root',
184
+ timeoutMs: 20 * MINUTE,
185
+ script: ({ version }) => script('root', { VERSION: version }, `
186
+ # Read off its file, not asked of OpenAgent itself: nothing a package brought runs as root, neither here nor at the install.
187
+ have=$(${ROOT}/node/bin/node -p 'require("${ROOT}/node/lib/node_modules/@openagt/dashboard/package.json").version' 2>/dev/null || true)
188
+ if [ "$have" != "$VERSION" ]; then
189
+ ${ROOT}/node/bin/npm install --global --ignore-scripts --no-fund --no-audit --loglevel=error "@openagt/dashboard@$VERSION"
190
+ touch ${ROOT}/restart
191
+ fi
192
+ ln -sf ${ROOT}/node/bin/openagent /usr/local/bin/openagent
193
+ `),
194
+ },
195
+ {
196
+ id: 'agent',
197
+ label: 'Claude Code',
198
+ as: 'openagent',
199
+ timeoutMs: 10 * MINUTE,
200
+ script: () => script('openagent', {}, `
201
+ # Claude Code's own installer, into this user's home: it keeps itself up to date there, as on a laptop.
202
+ command -v claude >/dev/null || curl -fsSL https://claude.ai/install.sh | bash
203
+ command -v claude >/dev/null || fail "Claude Code's installer ended without a claude command."
204
+ `),
205
+ },
206
+ {
207
+ id: 'service',
208
+ label: 'The service',
209
+ as: 'root',
210
+ timeoutMs: 3 * MINUTE,
211
+ script: () => script('root', { UNIT: SERVICE_UNIT }, `${STARTED}
212
+ file=/etc/systemd/system/openagent.service
213
+ if [ "$(cat "$file" 2>/dev/null || true)" != "$UNIT" ]; then
214
+ printf '%s\\n' "$UNIT" > "$file"
215
+ systemctl daemon-reload
216
+ touch ${ROOT}/restart
217
+ fi
218
+ # A firewall that is on lets the door's two ports through; one that is off is left off.
219
+ if command -v ufw >/dev/null && ufw status | grep '^Status: active' >/dev/null; then
220
+ ufw allow 80/tcp >/dev/null
221
+ ufw allow 443/tcp >/dev/null
222
+ fi
223
+ systemctl enable --quiet openagent
224
+ if [ -e ${ROOT}/restart ] || ! systemctl is-active --quiet openagent; then
225
+ systemctl restart openagent
226
+ rm -f ${ROOT}/restart
227
+ fi
228
+ started || ${NOT_STARTED}
229
+ `),
230
+ },
231
+ {
232
+ id: 'door',
233
+ label: 'The door',
234
+ as: 'openagent',
235
+ timeoutMs: 6 * MINUTE,
236
+ script: ({ address }) => script('openagent', { ADDRESS: address }, `
237
+ open() { case "$(${DOOR} status)" in *'"listening":true'*'"address":"'"$ADDRESS"'"'*) return 0 ;; *) return 1 ;; esac; }
238
+ open && exit 0
239
+ ${DOOR} https "$ADDRESS" >/dev/null
240
+ # A certificate takes a moment: its office reads the proof on port 80 first.
241
+ for _ in $(seq 150); do
242
+ open && exit 0
243
+ sleep 2
244
+ done
245
+ ${DOOR} status | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.error(JSON.parse(s).problem??"The door did not open."))' || true
246
+ fail "The door needs ports 80 and 443 of this server open to the internet: check the firewall of the company the server is rented from."
247
+ `),
248
+ },
249
+ ];
250
+ /** The script that asks the server's door for a link a new machine gets in with. Run as the server's own user. */
251
+ export function linkScript() {
252
+ return script(SERVER_USER, {}, `\n ${DOOR} add\n`);
253
+ }
254
+ /** How long a login's script may take: a server that stopped answering is not waited for longer. */
255
+ export const LOGIN_TIMEOUT_MS = 2 * MINUTE;
256
+ /** `gh` reads and writes its own file in the server user's home, whatever that user's shell says of another place or another login. */
257
+ const GH_OWN = 'unset GH_TOKEN GITHUB_TOKEN GH_CONFIG_DIR XDG_CONFIG_HOME';
258
+ const GH_FILE = '"$HOME/.config/gh/hosts.yml"';
259
+ /**
260
+ * Give the server a GitHub login: `gh` keeps it in its own file, and git asks `gh` for it. The
261
+ * server then holds this one login, and answers whose it is; when any of that fails, it holds none.
262
+ *
263
+ * The login is a word of the script, which the server's shell reads where a program would read:
264
+ * it is on no command line, and no program the script starts finds it among what it was given.
265
+ */
266
+ export function githubLoginScript(login) {
267
+ return {
268
+ as: SERVER_USER,
269
+ script: script(SERVER_USER, { LOGIN: login }, `
270
+ ${GH_OWN}
271
+ rm -f ${GH_FILE}
272
+ given() {
273
+ printf '%s\\n' "$LOGIN" | gh auth login --hostname github.com --git-protocol https --with-token &&
274
+ gh auth setup-git --hostname github.com &&
275
+ gh api --hostname github.com user --jq .login
276
+ }
277
+ # All of it or none: a login that is on the server and not known of on this computer could not be taken off from there.
278
+ given || { rm -f ${GH_FILE}; exit 1; }
279
+ `),
280
+ };
281
+ }
282
+ /** Take the GitHub login off the server. */
283
+ export function githubLogoutScript() {
284
+ return {
285
+ as: SERVER_USER,
286
+ script: script(SERVER_USER, {}, `
287
+ ${GH_OWN}
288
+ rm -f ${GH_FILE}
289
+ `),
290
+ };
291
+ }
292
+ /**
293
+ * Give the server a Claude login: Claude Code is first asked, as the server's own user, whether
294
+ * it reads the login; then the login is written to root's file and OpenAgent is started again
295
+ * with it. A run that is at work goes on with the login it started with. When OpenAgent does not
296
+ * start with it, the file is put back as it was.
297
+ *
298
+ * Claude Code is the server user's own program: root asks it as that user, for half a minute at
299
+ * most, and without the lock root holds, which a program that never ended would hold with it.
300
+ */
301
+ export function claudeLoginScript(login) {
302
+ return {
303
+ as: 'root',
304
+ script: script('root', { LOGIN: login }, `${STARTED}
305
+ # What is written where the service reads its settings is a login's letters and nothing else.
306
+ case "$LOGIN" in '' | *[!A-Za-z0-9_-]*) fail "What was sent is no Claude login." ;; esac
307
+ said=$(${CLAUDE_NAME}="$LOGIN" timeout 30 runuser -u ${SERVER_USER} -- env HOME=/home/${SERVER_USER} /home/${SERVER_USER}/.local/bin/claude auth status 2>&1 9>&- || true)
308
+ printf '%s' "$said" | grep -Eq '"loggedIn": *true' || fail "Claude Code on this server does not take the login."
309
+ install -d -m 755 "$(dirname ${CLAUDE_FILE})"
310
+ rm -f ${CLAUDE_FILE}.old
311
+ [ ! -e ${CLAUDE_FILE} ] || cp -p ${CLAUDE_FILE} ${CLAUDE_FILE}.old
312
+ (umask 077 && printf '${CLAUDE_NAME}=%s\\n' "$LOGIN" > ${CLAUDE_FILE}.new)
313
+ mv ${CLAUDE_FILE}.new ${CLAUDE_FILE}
314
+ with_it() {
315
+ systemctl restart openagent && started &&
316
+ tr '\\0' '\\n' < "/proc/$(systemctl show -p MainPID --value openagent)/environ" | grep -q '^${CLAUDE_NAME}='
317
+ }
318
+ if ! with_it; then
319
+ # As it was: a login that is on the server and not known of on this computer could not be taken off from there.
320
+ if [ -e ${CLAUDE_FILE}.old ]; then mv ${CLAUDE_FILE}.old ${CLAUDE_FILE}; else rm -f ${CLAUDE_FILE}; fi
321
+ systemctl restart openagent || true
322
+ fail "OpenAgent did not start with the login. The server's Claude login is as it was before."
323
+ fi
324
+ rm -f ${CLAUDE_FILE}.old
325
+ `),
326
+ };
327
+ }
328
+ /** Take the Claude login off the server, and start OpenAgent again without it. */
329
+ export function claudeLogoutScript() {
330
+ return {
331
+ as: 'root',
332
+ script: script('root', {}, `${STARTED}
333
+ [ -e ${CLAUDE_FILE} ] || exit 0
334
+ rm -f ${CLAUDE_FILE} ${CLAUDE_FILE}.old
335
+ systemctl restart openagent
336
+ started || ${NOT_STARTED}
337
+ `),
338
+ };
339
+ }
340
+ //# sourceMappingURL=steps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"steps.js","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ import { type StepId } from './steps.js';
2
+ /**
3
+ * The servers this computer installed OpenAgent on, in the person's home folder: where each is,
4
+ * who logs in there, and how far its install got. The command and the install's own process both
5
+ * write it, so every change goes through {@link update}.
6
+ *
7
+ * It holds no key, no password and no login: the way in is this computer's own SSH keys, the key
8
+ * the server's door gives is OpenAgent's, kept with its machines, and a GitHub or Claude login
9
+ * goes to the server and is not kept here.
10
+ */
11
+ export type StepState = 'waiting' | 'running' | 'done' | 'failed';
12
+ export interface Step {
13
+ id: StepId;
14
+ state: StepState;
15
+ /** For a step that failed: what the server said, its last lines. */
16
+ said?: string;
17
+ }
18
+ /** The install's own process. */
19
+ export interface Worker {
20
+ pid: number;
21
+ /** ISO 8601: written again every few seconds for as long as the process works. */
22
+ seen: string;
23
+ }
24
+ /** The two logins a server works with. */
25
+ export declare const LOGIN_KINDS: readonly ['github', 'claude'];
26
+ export type LoginKind = (typeof LOGIN_KINDS)[number];
27
+ /**
28
+ * One login of a server, as this computer left it. The login itself is never kept here: only
29
+ * that it was put there, when, and for GitHub whose it is.
30
+ */
31
+ export interface Login {
32
+ /** ISO 8601: when this computer put the login on the server. Absent while the server has none from here. */
33
+ since?: string;
34
+ /** GitHub: the account, as the server's own `gh` answered. */
35
+ account?: string;
36
+ /** GitHub: to be copied once the install's steps are done. */
37
+ wanted?: boolean;
38
+ /** Why the last try ended without a login. */
39
+ failed?: string;
40
+ /** The process that changes the login right now, and when it last said it still does. */
41
+ worker?: Worker;
42
+ /** Claude: that process waits for the person's yes in the browser, and has sent nothing yet. */
43
+ asking?: boolean;
44
+ }
45
+ export interface Server {
46
+ /** The server's number or name: what a person typed, and what its certificate is for. */
47
+ address: string;
48
+ /** Who logs in over SSH: `root`, or a user that becomes root without a password. */
49
+ user: string;
50
+ /** What the server's key is known by, as SSH showed it when this computer first met it. */
51
+ fingerprint?: string;
52
+ steps: Step[];
53
+ /** The process that installs right now, and when it last said it still does. */
54
+ worker?: Worker;
55
+ /** The install was asked for and the server is not among the machines yet: the page adds it once the steps are done. */
56
+ adding: boolean;
57
+ /** The GitHub login this computer copied to the server. */
58
+ github?: Login;
59
+ /** The Claude login this computer made for the server. */
60
+ claude?: Login;
61
+ }
62
+ export interface Store {
63
+ servers: Server[];
64
+ }
65
+ /** A cap, so a file edited by hand cannot bloat it. */
66
+ export declare const MAX_SERVERS = 50;
67
+ /** The file's path: beside OpenAgent's own home file, by the same rule. */
68
+ export declare function storePath(env: NodeJS.ProcessEnv): string;
69
+ /**
70
+ * `said` as a server's address is kept, or `undefined` for what is none: a number, or a name, in
71
+ * the one form the server's door keeps it in too, so the two always read the same. Nothing that
72
+ * could be read as an option of a command passes.
73
+ */
74
+ export declare function addressOf(said: string): string | undefined;
75
+ /** Whether `said` is a user name a Linux server has. */
76
+ export declare const isUser: (said: unknown) => said is string;
77
+ /** Whether `said` is a version npm keeps a package under. */
78
+ export declare const isVersion: (said: unknown) => said is string;
79
+ /** How often the install's process says it still works, and after how long of silence it is taken for gone. */
80
+ export declare const BEAT_MS = 10000;
81
+ export declare const SILENT_MS = 45000;
82
+ /**
83
+ * Whether the server's install runs right now: its process is there, and said so a moment ago.
84
+ * The process's number alone is no proof: one that died without a word (a laptop that was
85
+ * switched off) leaves its number in the file, and the system gives it to another program later.
86
+ */
87
+ export declare function working(server: Server, now?: number): boolean;
88
+ /** Whether a process that wrote itself to the file is still at work: it is there, and said so a moment ago. */
89
+ export declare function atWork(worker: Worker | undefined, now?: number): boolean;
90
+ /** Whether `said` is a GitHub account's name. */
91
+ export declare const isAccount: (said: unknown) => said is string;
92
+ /** Where the door of the server at `address` answers: what OpenAgent's machines know it by. */
93
+ export declare function doorUrl(address: string): string;
94
+ /** Every step, not started. */
95
+ export declare const freshSteps: () => Step[];
96
+ /** Read what a file holds, forgivingly: anything malformed is left out, never a reason to fail. */
97
+ export declare function sanitize(value: unknown): Store;
98
+ /** The file is there and cannot be read as what it should hold: a typo made by hand, a disk that failed. */
99
+ export declare class Unreadable extends Error {
100
+ constructor(path: string);
101
+ }
102
+ /** The file as it stands; no file reads as no server. Throws {@link Unreadable} for a file that is there and cannot be read. */
103
+ export declare function readStore(env: NodeJS.ProcessEnv): Promise<Store>;
104
+ /**
105
+ * Change the file: read it, apply `change`, write it back, with no other writer in between.
106
+ * Written to a temporary file and renamed, so a reader never sees half a file, owner-only.
107
+ * `change` answers what the caller wants to know of the change.
108
+ */
109
+ export declare function update<T>(env: NodeJS.ProcessEnv, change: (store: Store) => T): Promise<T>;
110
+ /** Change the one server at `address`, when the file still holds it; answers what `apply` did, or `undefined` for a server that is gone. */
111
+ export declare function changeServer<T>(env: NodeJS.ProcessEnv, address: string, apply: (server: Server) => T): Promise<T | undefined>;
112
+ /** Whether a process with this id is running. */
113
+ export declare function pidAlive(pid: number): boolean;
114
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}