pinomad 0.11.0 → 0.12.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/README.md +1 -1
- package/dist/{chunk-b457q0tm.js → chunk-41q0saz0.js} +11 -8
- package/dist/chunk-42nqtx5r.js +22 -0
- package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
- package/dist/{chunk-xqycs43g.js → chunk-47ff1d1t.js} +35 -7
- package/dist/{chunk-n1m1tjrk.js → chunk-5qf1grkk.js} +19 -13
- package/dist/{chunk-ncbqa8jk.js → chunk-7mn38n4f.js} +1 -1
- package/dist/{chunk-sd5fkxjj.js → chunk-7wj0jh3t.js} +1 -1
- package/dist/{chunk-4dcs8sbj.js → chunk-agq4tn9r.js} +43 -21
- package/dist/{chunk-as6pyf00.js → chunk-aqj9wfmb.js} +1 -1
- package/dist/{chunk-y127fe1a.js → chunk-g3ahp7b9.js} +11 -0
- package/dist/{chunk-gtdpjqa3.js → chunk-gg2dcqbg.js} +186 -45
- package/dist/{chunk-6d16c2fv.js → chunk-jeqsdcj1.js} +13 -6
- package/dist/{chunk-67gq282t.js → chunk-ksfxqd8s.js} +491 -34
- package/dist/{chunk-9h5vawnm.js → chunk-mnmqyq7d.js} +237 -27
- package/dist/{chunk-jt34g8st.js → chunk-np4jd070.js} +154 -2
- package/dist/{chunk-44pnts98.js → chunk-nppgxh6c.js} +1 -1
- package/dist/chunk-nq57fdnq.js +106 -0
- package/dist/{chunk-vvj9b1gc.js → chunk-t5abqsva.js} +174 -52
- package/dist/{chunk-tj57aqav.js → chunk-vqkb70d7.js} +2 -2
- package/dist/{chunk-h5q2y57m.js → chunk-vx7dy07y.js} +10 -4
- package/dist/pinomad.js +15 -14
- package/package.json +2 -1
- package/web/assets/astro-DOly5024.js +1 -0
- package/web/assets/c-BIGW1oBm.js +1 -0
- package/web/assets/core-DnpzcQzs.js +4 -0
- package/web/assets/cpp-DddhVWhK.js +1 -0
- package/web/assets/csharp-B_TYUkTI.js +1 -0
- package/web/assets/css-Z72CF80r.js +1 -0
- package/web/assets/dart-CtKP3W-j.js +1 -0
- package/web/assets/diff-D97Zzqfu.js +1 -0
- package/web/assets/dockerfile-BcOcwvcX.js +1 -0
- package/web/assets/elixir-DkJLSxtU.js +1 -0
- package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
- package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
- package/web/assets/fish-BvzEVeQv.js +1 -0
- package/web/assets/go-C27-OAKa.js +1 -0
- package/web/assets/graphql-ChdNCCLP.js +1 -0
- package/web/assets/haskell-Df6bDoY_.js +1 -0
- package/web/assets/hcl-BWvSN4gD.js +1 -0
- package/web/assets/highlighted-diff-Dn5ifsvK.js +175 -0
- package/web/assets/highlights-DnHv6NBM.js +4 -0
- package/web/assets/highlights-themes-CUdrlZk3.js +1 -0
- package/web/assets/html-BlS82ilI.js +1 -0
- package/web/assets/{index-DoyHMPwf.css → index-BV0ecolo.css} +1 -1
- package/web/assets/index-DNsTTgPE.js +396 -0
- package/web/assets/ini-BEwlwnbL.js +1 -0
- package/web/assets/java-CylS5w8V.js +1 -0
- package/web/assets/javascript-wDzz0qaB.js +1 -0
- package/web/assets/json-Cp-IABpG.js +1 -0
- package/web/assets/jsonc-Des-eS-w.js +1 -0
- package/web/assets/jsx-g9-lgVsj.js +1 -0
- package/web/assets/kotlin-BdnUsdx6.js +1 -0
- package/web/assets/less-B1dDrJ26.js +1 -0
- package/web/assets/lua-BaeVxFsk.js +1 -0
- package/web/assets/make-CHLpvVh8.js +1 -0
- package/web/assets/markdown-Cvjx9yec.js +1 -0
- package/web/assets/nix-CwoSXNpI.js +1 -0
- package/web/assets/objective-c-DXmwc3jG.js +1 -0
- package/web/assets/php-DoruYMF0.js +1 -0
- package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
- package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
- package/web/assets/pierre-light-480U9XYS.js +1 -0
- package/web/assets/pierre-light-BVSW-Xev.js +1 -0
- package/web/assets/postcss-DnzrXTVv.js +1 -0
- package/web/assets/powershell-Dd0kTxKP.js +1 -0
- package/web/assets/prisma-Vru482bI.js +1 -0
- package/web/assets/proto-C7zT0LnQ.js +1 -0
- package/web/assets/python-B6aJPvgy.js +1 -0
- package/web/assets/ruby-B43yHlf9.js +1 -0
- package/web/assets/rust-DPvrY3IQ.js +1 -0
- package/web/assets/scala-CqE71os6.js +1 -0
- package/web/assets/scss-Cw6XMfVs.js +1 -0
- package/web/assets/shellscript-Yzrsuije.js +1 -0
- package/web/assets/shiki-BuPP5HCg.js +2 -0
- package/web/assets/shiki-langs-FJk0v0Ga.js +2 -0
- package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
- package/web/assets/sql-DkvoVjww.js +1 -0
- package/web/assets/stream-tokenizer-Bl6bisIa.js +4 -0
- package/web/assets/svelte-DoIXUgJc.js +1 -0
- package/web/assets/swift-C2oV4EkX.js +1 -0
- package/web/assets/terraform-BETggiCN.js +1 -0
- package/web/assets/theming-themes-BL_CC-Mm.js +1 -0
- package/web/assets/toml-vGWfd6FD.js +1 -0
- package/web/assets/tsx-COt5Ahok.js +1 -0
- package/web/assets/typescript-BPQ3VLAy.js +1 -0
- package/web/assets/vue-CwSeJimo.js +1 -0
- package/web/assets/xml-sdJ4AIDG.js +1 -0
- package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
- package/web/assets/xterm-view-ZsuzJb2C.js +38 -0
- package/web/assets/yaml-Buea-lGh.js +1 -0
- package/web/assets/zig-VOosw3JB.js +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-etf7ef97.js +0 -55
- package/dist/chunk-greb3dp6.js +0 -22
- package/web/assets/index-BhHrKpoo.js +0 -395
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
organize,
|
|
16
16
|
parseRtcMaxMessageSize,
|
|
17
17
|
rtcSelectedPair
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-47ff1d1t.js";
|
|
19
19
|
import {
|
|
20
20
|
SECURE_PROLOGUE,
|
|
21
21
|
SecureSession,
|
|
@@ -286,6 +286,13 @@ function webSocketTransport(url, token) {
|
|
|
286
286
|
};
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// packages/protocol/src/terminals.ts
|
|
290
|
+
function isTerminalsHello(value) {
|
|
291
|
+
return value !== null && typeof value === "object" && typeof value.limit === "number";
|
|
292
|
+
}
|
|
293
|
+
var terminalsStream = (id) => `terminals:${id}`;
|
|
294
|
+
var terminalStream = (id) => `terminal:${id}`;
|
|
295
|
+
|
|
289
296
|
// packages/protocol/src/transcript.ts
|
|
290
297
|
function isBusy(view) {
|
|
291
298
|
const live = view.docs?.["pi.live"];
|
|
@@ -458,6 +465,11 @@ class RemoteClient {
|
|
|
458
465
|
#helloModels = [];
|
|
459
466
|
#helloDefaults = {};
|
|
460
467
|
#hostVersion;
|
|
468
|
+
#hostTerminals;
|
|
469
|
+
#terminalWatchers = new Map;
|
|
470
|
+
#terminalSinks = new Map;
|
|
471
|
+
#terminalPositions = new Map;
|
|
472
|
+
#terminalSizes = new Map;
|
|
461
473
|
#attempt = 0;
|
|
462
474
|
#rtcAttemptNonce = randomHex(8);
|
|
463
475
|
#closed = false;
|
|
@@ -710,6 +722,7 @@ class RemoteClient {
|
|
|
710
722
|
this.#helloModels = frame.models;
|
|
711
723
|
this.#helloDefaults = frame.defaults ?? {};
|
|
712
724
|
this.#hostVersion = frame.hostVersion;
|
|
725
|
+
this.#hostTerminals = isTerminalsHello(frame.terminals) ? frame.terminals : undefined;
|
|
713
726
|
this.#fresh.clear();
|
|
714
727
|
const covered = new Set(hostStreams(hostMinor));
|
|
715
728
|
for (const name of HOST_STREAM_NAMES) {
|
|
@@ -727,6 +740,7 @@ class RemoteClient {
|
|
|
727
740
|
if (this.#side !== undefined)
|
|
728
741
|
for (const stream of this.#conversationStreams(this.#side))
|
|
729
742
|
this.#wanted.add(stream);
|
|
743
|
+
this.#terminalsAvailability();
|
|
730
744
|
for (const stream of this.#wanted)
|
|
731
745
|
this.#subscribe(stream);
|
|
732
746
|
if (frame.endpointSet !== undefined) {
|
|
@@ -815,6 +829,7 @@ class RemoteClient {
|
|
|
815
829
|
this.#resolveReady({
|
|
816
830
|
view: this.#viewSource(),
|
|
817
831
|
controller: this.#controller(),
|
|
832
|
+
terminals: this.#terminals(),
|
|
818
833
|
setConnectionMode: (mode) => this.setConnectionMode(mode),
|
|
819
834
|
rtcDiagnostics: () => this.rtcDiagnostics(),
|
|
820
835
|
close: () => this.#close()
|
|
@@ -828,6 +843,9 @@ class RemoteClient {
|
|
|
828
843
|
await this.#recoverSubmissions();
|
|
829
844
|
if (!this.#isActive(dial))
|
|
830
845
|
return;
|
|
846
|
+
for (const [terminalId, size] of this.#terminalSizes) {
|
|
847
|
+
this.#callNow("resizeTerminal", { terminalId, ...size }).catch(() => {});
|
|
848
|
+
}
|
|
831
849
|
this.#finishSyncGate();
|
|
832
850
|
this.#afterCarrierReady(dial);
|
|
833
851
|
}
|
|
@@ -1961,6 +1979,11 @@ class RemoteClient {
|
|
|
1961
1979
|
case "snapshot":
|
|
1962
1980
|
if (!this.#wanted.has(frame.stream))
|
|
1963
1981
|
return;
|
|
1982
|
+
if (frame.stream.startsWith("terminals:")) {
|
|
1983
|
+
this.#values.set(frame.stream, frame.value);
|
|
1984
|
+
this.#notifyTerminals(frame.stream, { kind: "ready", list: frame.value });
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1964
1987
|
this.#values.set(frame.stream, frame.value);
|
|
1965
1988
|
this.#fresh.add(frame.stream);
|
|
1966
1989
|
this.#streamVersions.set(frame.stream, (this.#streamVersions.get(frame.stream) ?? 0) + 1);
|
|
@@ -1974,7 +1997,19 @@ class RemoteClient {
|
|
|
1974
1997
|
this.#values.set(frame.stream, applyImmutable(this.#values.get(frame.stream), frame.ops));
|
|
1975
1998
|
this.#refresh();
|
|
1976
1999
|
return;
|
|
2000
|
+
case "output": {
|
|
2001
|
+
const sink = this.#terminalSinks.get(frame.stream);
|
|
2002
|
+
if (sink === undefined || !this.#wanted.has(frame.stream))
|
|
2003
|
+
return;
|
|
2004
|
+
this.#terminalPositions.set(frame.stream, frame.seq);
|
|
2005
|
+
sink.output(frame.data, frame.reset === true);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
1977
2008
|
case "ended": {
|
|
2009
|
+
if (frame.stream.startsWith("terminals:") || frame.stream.startsWith("terminal:")) {
|
|
2010
|
+
this.#endTerminalStream(frame.stream, frame.reason);
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
1978
2013
|
this.#wanted.delete(frame.stream);
|
|
1979
2014
|
this.#values.delete(frame.stream);
|
|
1980
2015
|
this.#fresh.delete(frame.stream);
|
|
@@ -2070,7 +2105,124 @@ class RemoteClient {
|
|
|
2070
2105
|
}
|
|
2071
2106
|
#subscribe(stream) {
|
|
2072
2107
|
const refs = stream.startsWith("conversation:") && hostSupportsFeature(this.#hostMinor, "imageRefs");
|
|
2073
|
-
this.#
|
|
2108
|
+
const since = this.#terminalPositions.get(stream);
|
|
2109
|
+
this.#send({
|
|
2110
|
+
type: "subscribe",
|
|
2111
|
+
stream,
|
|
2112
|
+
...refs ? { images: "ref" } : {},
|
|
2113
|
+
...since === undefined ? {} : { since }
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
#terminalsUnavailable() {
|
|
2117
|
+
if (!hostSupportsFeature(this.#hostMinor, "terminals")) {
|
|
2118
|
+
return "The host is too old for terminals (needs protocol 5.27); run `pinomad upgrade` on the host";
|
|
2119
|
+
}
|
|
2120
|
+
if (this.#hostTerminals === undefined)
|
|
2121
|
+
return "The host couldn't load its terminal module; its log says which package is missing";
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
#terminalsAvailability() {
|
|
2125
|
+
const reason = this.#terminalsUnavailable();
|
|
2126
|
+
if (reason === undefined) {
|
|
2127
|
+
for (const stream of this.#terminalWatchers.keys())
|
|
2128
|
+
this.#wanted.add(stream);
|
|
2129
|
+
for (const stream of this.#terminalSinks.keys())
|
|
2130
|
+
this.#wanted.add(stream);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
for (const stream of [...this.#terminalWatchers.keys()]) {
|
|
2134
|
+
this.#wanted.delete(stream);
|
|
2135
|
+
this.#values.delete(stream);
|
|
2136
|
+
this.#notifyTerminals(stream, { kind: "unavailable", reason });
|
|
2137
|
+
}
|
|
2138
|
+
for (const stream of [...this.#terminalSinks.keys()])
|
|
2139
|
+
this.#endTerminalStream(stream, reason);
|
|
2140
|
+
}
|
|
2141
|
+
#notifyTerminals(stream, state) {
|
|
2142
|
+
for (const listener of [...this.#terminalWatchers.get(stream) ?? []])
|
|
2143
|
+
listener(state);
|
|
2144
|
+
}
|
|
2145
|
+
#endTerminalStream(stream, reason) {
|
|
2146
|
+
this.#wanted.delete(stream);
|
|
2147
|
+
this.#values.delete(stream);
|
|
2148
|
+
if (stream.startsWith("terminals:")) {
|
|
2149
|
+
this.#notifyTerminals(stream, { kind: "unavailable", reason });
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
const sink = this.#terminalSinks.get(stream);
|
|
2153
|
+
this.#terminalSinks.delete(stream);
|
|
2154
|
+
this.#terminalPositions.delete(stream);
|
|
2155
|
+
this.#terminalSizes.delete(stream.slice("terminal:".length));
|
|
2156
|
+
sink?.ended(reason);
|
|
2157
|
+
}
|
|
2158
|
+
#terminalCall(method, args) {
|
|
2159
|
+
const reason = this.#terminalsUnavailable();
|
|
2160
|
+
return reason === undefined ? this.#call(method, args) : Promise.reject(new Error(reason));
|
|
2161
|
+
}
|
|
2162
|
+
#terminals() {
|
|
2163
|
+
return {
|
|
2164
|
+
watch: (conversationId, listener) => {
|
|
2165
|
+
const stream = terminalsStream(conversationId);
|
|
2166
|
+
let listeners = this.#terminalWatchers.get(stream);
|
|
2167
|
+
const first = listeners === undefined;
|
|
2168
|
+
if (listeners === undefined)
|
|
2169
|
+
this.#terminalWatchers.set(stream, listeners = new Set);
|
|
2170
|
+
listeners.add(listener);
|
|
2171
|
+
const reason = this.#terminalsUnavailable();
|
|
2172
|
+
const list = this.#values.get(stream);
|
|
2173
|
+
listener(reason !== undefined ? { kind: "unavailable", reason } : list !== undefined ? { kind: "ready", list } : { kind: "loading" });
|
|
2174
|
+
if (first && reason === undefined) {
|
|
2175
|
+
this.#wanted.add(stream);
|
|
2176
|
+
if (this.#helloReceived)
|
|
2177
|
+
this.#subscribe(stream);
|
|
2178
|
+
}
|
|
2179
|
+
return () => {
|
|
2180
|
+
if (!listeners.delete(listener) || listeners.size > 0)
|
|
2181
|
+
return;
|
|
2182
|
+
this.#terminalWatchers.delete(stream);
|
|
2183
|
+
if (!this.#wanted.delete(stream))
|
|
2184
|
+
return;
|
|
2185
|
+
this.#values.delete(stream);
|
|
2186
|
+
this.#send({ type: "unsubscribe", stream });
|
|
2187
|
+
};
|
|
2188
|
+
},
|
|
2189
|
+
attach: (terminalId, sink) => {
|
|
2190
|
+
const stream = terminalStream(terminalId);
|
|
2191
|
+
const reason = this.#terminalsUnavailable();
|
|
2192
|
+
if (reason !== undefined) {
|
|
2193
|
+
sink.ended(reason);
|
|
2194
|
+
return () => {};
|
|
2195
|
+
}
|
|
2196
|
+
if (this.#terminalSinks.has(stream) && this.#wanted.has(stream))
|
|
2197
|
+
this.#send({ type: "unsubscribe", stream });
|
|
2198
|
+
this.#terminalSinks.set(stream, sink);
|
|
2199
|
+
this.#terminalPositions.delete(stream);
|
|
2200
|
+
this.#wanted.add(stream);
|
|
2201
|
+
if (this.#helloReceived)
|
|
2202
|
+
this.#subscribe(stream);
|
|
2203
|
+
return () => {
|
|
2204
|
+
if (this.#terminalSinks.get(stream) !== sink)
|
|
2205
|
+
return;
|
|
2206
|
+
this.#terminalSinks.delete(stream);
|
|
2207
|
+
this.#terminalPositions.delete(stream);
|
|
2208
|
+
this.#terminalSizes.delete(terminalId);
|
|
2209
|
+
if (this.#wanted.delete(stream))
|
|
2210
|
+
this.#send({ type: "unsubscribe", stream });
|
|
2211
|
+
};
|
|
2212
|
+
},
|
|
2213
|
+
open: (conversationId, size) => this.#terminalCall("openTerminal", { conversationId, ...size }).then(({ terminalId }) => terminalId),
|
|
2214
|
+
input: (terminalId, data) => {
|
|
2215
|
+
this.#terminalCall("terminalInput", { terminalId, data }).catch(() => {});
|
|
2216
|
+
},
|
|
2217
|
+
resize: (terminalId, size) => {
|
|
2218
|
+
if (this.#terminalSinks.has(terminalStream(terminalId)))
|
|
2219
|
+
this.#terminalSizes.set(terminalId, size);
|
|
2220
|
+
this.#terminalCall("resizeTerminal", { terminalId, ...size }).catch(() => {});
|
|
2221
|
+
},
|
|
2222
|
+
rename: (terminalId, name) => this.#terminalCall("renameTerminal", { terminalId, name }).then(() => {}),
|
|
2223
|
+
restart: (terminalId, size) => this.#terminalCall("restartTerminal", { terminalId, ...this.#terminalSizes.get(terminalId) ?? size }).then(() => {}),
|
|
2224
|
+
close: (terminalId) => this.#terminalCall("closeTerminal", { terminalId }).then(() => {})
|
|
2225
|
+
};
|
|
2074
2226
|
}
|
|
2075
2227
|
#host(name) {
|
|
2076
2228
|
return this.#values.get(name);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// apps/host/src/server-config.ts
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
var SERVER_CONFIG_FILE = "server.json";
|
|
6
|
+
var REMOVAL_NOTICE_FILE = "server-removed.json";
|
|
7
|
+
var normalizeOrigin = (origin) => origin.replace(/\/+$/, "");
|
|
8
|
+
async function readServerConfig(dataDir) {
|
|
9
|
+
const config = await readServerConfigAt(join(dataDir, SERVER_CONFIG_FILE));
|
|
10
|
+
if (config === undefined)
|
|
11
|
+
return;
|
|
12
|
+
const notice = await readRemovalNotice(dataDir).catch(() => {
|
|
13
|
+
return;
|
|
14
|
+
});
|
|
15
|
+
return notice !== undefined && endsJoin(notice, config) ? undefined : config;
|
|
16
|
+
}
|
|
17
|
+
var endsJoin = (notice, config) => normalizeOrigin(notice.origin) === normalizeOrigin(config.origin) && notice.binding === config.binding;
|
|
18
|
+
async function readServerConfigAt(path) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = await readFile(path, "utf8");
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code === "ENOENT")
|
|
24
|
+
return;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
let value;
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(text);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error(`${path} is not valid JSON — delete it or run \`pinomad leave\``);
|
|
32
|
+
}
|
|
33
|
+
const origin = value?.origin;
|
|
34
|
+
if (typeof origin !== "string" || origin.trim() === "") {
|
|
35
|
+
throw new Error(`${path} is missing a string "origin"`);
|
|
36
|
+
}
|
|
37
|
+
const { binding, username } = value;
|
|
38
|
+
return {
|
|
39
|
+
origin,
|
|
40
|
+
...typeof binding === "string" && binding !== "" ? { binding } : {},
|
|
41
|
+
...typeof username === "string" && username !== "" ? { username } : {}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function writeJsonFile(path, value) {
|
|
45
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
46
|
+
await writeFile(tmp, `${JSON.stringify(value)}
|
|
47
|
+
`, { mode: 384 });
|
|
48
|
+
await rename(tmp, path);
|
|
49
|
+
}
|
|
50
|
+
async function writeServerConfig(dataDir, config) {
|
|
51
|
+
await writeJsonFile(join(dataDir, SERVER_CONFIG_FILE), {
|
|
52
|
+
origin: normalizeOrigin(config.origin),
|
|
53
|
+
binding: randomBytes(16).toString("base64url"),
|
|
54
|
+
...config.username === undefined ? {} : { username: config.username }
|
|
55
|
+
});
|
|
56
|
+
await rm(join(dataDir, REMOVAL_NOTICE_FILE), { force: true });
|
|
57
|
+
}
|
|
58
|
+
async function markRemoved(dataDir, removed, removedAt = Date.now()) {
|
|
59
|
+
await writeJsonFile(join(dataDir, REMOVAL_NOTICE_FILE), {
|
|
60
|
+
origin: normalizeOrigin(removed.origin),
|
|
61
|
+
...removed.binding === undefined ? {} : { binding: removed.binding },
|
|
62
|
+
...removed.username === undefined ? {} : { username: removed.username },
|
|
63
|
+
removedAt
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function readRemovalNotice(dataDir) {
|
|
67
|
+
const path = join(dataDir, REMOVAL_NOTICE_FILE);
|
|
68
|
+
let text;
|
|
69
|
+
try {
|
|
70
|
+
text = await readFile(path, "utf8");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error.code === "ENOENT")
|
|
73
|
+
return;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
let value;
|
|
77
|
+
try {
|
|
78
|
+
value = JSON.parse(text);
|
|
79
|
+
} catch {
|
|
80
|
+
throw new Error(`${path} is not valid JSON — delete it`);
|
|
81
|
+
}
|
|
82
|
+
const { origin, binding, username, removedAt } = value ?? {};
|
|
83
|
+
if (typeof origin !== "string" || typeof removedAt !== "number")
|
|
84
|
+
throw new Error(`${path} is missing "origin" or "removedAt" — delete it`);
|
|
85
|
+
return {
|
|
86
|
+
origin,
|
|
87
|
+
...typeof binding === "string" && binding !== "" ? { binding } : {},
|
|
88
|
+
...typeof username === "string" && username !== "" ? { username } : {},
|
|
89
|
+
removedAt
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function removeServerConfig(dataDir) {
|
|
93
|
+
await rm(join(dataDir, SERVER_CONFIG_FILE), { force: true });
|
|
94
|
+
await rm(join(dataDir, REMOVAL_NOTICE_FILE), { force: true });
|
|
95
|
+
}
|
|
96
|
+
async function readServerBinding(dataDir) {
|
|
97
|
+
return (await readServerConfig(dataDir))?.binding;
|
|
98
|
+
}
|
|
99
|
+
function resolveRelayOrigin(flag, config) {
|
|
100
|
+
if (flag !== undefined && config !== undefined && normalizeOrigin(flag) !== normalizeOrigin(config.origin)) {
|
|
101
|
+
throw new Error(`--relay ${flag} disagrees with ${SERVER_CONFIG_FILE} (${config.origin}) — remove one`);
|
|
102
|
+
}
|
|
103
|
+
return flag ?? config?.origin;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { REMOVAL_NOTICE_FILE, normalizeOrigin, readServerConfig, writeServerConfig, markRemoved, readRemovalNotice, removeServerConfig, readServerBinding, resolveRelayOrigin };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ProtocolMismatchError,
|
|
3
3
|
connectRemoteDurable
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-np4jd070.js";
|
|
5
5
|
import {
|
|
6
6
|
isParseArgsError
|
|
7
7
|
} from "./chunk-kzbzacz9.js";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
commandHint,
|
|
14
14
|
hostEntry,
|
|
15
15
|
packagedVersion
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-7wj0jh3t.js";
|
|
17
17
|
|
|
18
18
|
// apps/host/src/cli/service.ts
|
|
19
19
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -245,6 +245,55 @@ import { homedir, userInfo } from "node:os";
|
|
|
245
245
|
import { dirname, join as join2 } from "node:path";
|
|
246
246
|
import { promisify } from "node:util";
|
|
247
247
|
var run = promisify(execFile);
|
|
248
|
+
var NOT_INSTALLED = { installed: false, stopped: false, running: false, detail: "not installed" };
|
|
249
|
+
function launchdDisabled(printed) {
|
|
250
|
+
const value = /^\s*"pinomad\.host" => (\w+)\s*$/m.exec(printed)?.[1];
|
|
251
|
+
return value === "disabled" || value === "true";
|
|
252
|
+
}
|
|
253
|
+
function launchdState(disabled, printed) {
|
|
254
|
+
if (printed === undefined) {
|
|
255
|
+
return disabled ? { installed: true, stopped: true, running: false, detail: "stopped" } : { installed: true, stopped: false, running: false, detail: "not loaded" };
|
|
256
|
+
}
|
|
257
|
+
const field = (name) => printed.match(new RegExp(`^\\s*${name} = (.+)$`, "m"))?.[1];
|
|
258
|
+
const state = field("state") ?? "unknown";
|
|
259
|
+
const pid = field("pid");
|
|
260
|
+
const exit = field("last exit code");
|
|
261
|
+
return {
|
|
262
|
+
installed: true,
|
|
263
|
+
stopped: false,
|
|
264
|
+
running: state === "running" && pid !== undefined,
|
|
265
|
+
...pid === undefined ? {} : { pid: Number(pid) },
|
|
266
|
+
detail: [
|
|
267
|
+
state,
|
|
268
|
+
...pid === undefined ? [] : [`pid ${pid}`],
|
|
269
|
+
...exit === undefined || exit === "0" || exit === "(never exited)" ? [] : [`last exit ${exit}`],
|
|
270
|
+
...disabled ? ["turned off but still loaded"] : []
|
|
271
|
+
].join(", ")
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function systemdState(shown) {
|
|
275
|
+
const fields = Object.fromEntries(shown.trim().split(`
|
|
276
|
+
`).map((line) => line.split("=", 2)));
|
|
277
|
+
if (fields.LoadState === "not-found")
|
|
278
|
+
return NOT_INSTALLED;
|
|
279
|
+
const pid = Number(fields.MainPID ?? "0");
|
|
280
|
+
const active = fields.ActiveState === "active";
|
|
281
|
+
const done = fields.ActiveState === "inactive" || fields.ActiveState === "failed";
|
|
282
|
+
if (fields.UnitFileState === "disabled" && done)
|
|
283
|
+
return { installed: true, stopped: true, running: false, detail: "stopped" };
|
|
284
|
+
return {
|
|
285
|
+
installed: true,
|
|
286
|
+
stopped: false,
|
|
287
|
+
running: active && pid > 0,
|
|
288
|
+
...pid > 0 ? { pid } : {},
|
|
289
|
+
detail: [
|
|
290
|
+
fields.ActiveState ?? "unknown",
|
|
291
|
+
fields.SubState ?? "",
|
|
292
|
+
...pid > 0 ? [`pid ${pid}`] : [],
|
|
293
|
+
...fields.ExecMainStatus === undefined || fields.ExecMainStatus === "0" ? [] : [`last exit ${fields.ExecMainStatus}`]
|
|
294
|
+
].filter((part) => part !== "").join(", ")
|
|
295
|
+
};
|
|
296
|
+
}
|
|
248
297
|
var launchd = {
|
|
249
298
|
async install(spec) {
|
|
250
299
|
await mkdir(join2(spec.dataDir, "logs"), { recursive: true });
|
|
@@ -253,6 +302,7 @@ var launchd = {
|
|
|
253
302
|
await writeFile(path, launchdPlist(spec));
|
|
254
303
|
const domain = `gui/${process.getuid()}/pinomad.host`;
|
|
255
304
|
await run("launchctl", ["bootout", domain]).catch(() => {});
|
|
305
|
+
await run("launchctl", ["enable", domain]);
|
|
256
306
|
await run("launchctl", ["bootstrap", `gui/${process.getuid()}`, path]);
|
|
257
307
|
return path;
|
|
258
308
|
},
|
|
@@ -263,26 +313,27 @@ var launchd = {
|
|
|
263
313
|
async restart() {
|
|
264
314
|
await run("launchctl", ["kickstart", "-k", `gui/${process.getuid()}/pinomad.host`]);
|
|
265
315
|
},
|
|
316
|
+
async start() {
|
|
317
|
+
const domain = `gui/${process.getuid()}/pinomad.host`;
|
|
318
|
+
await run("launchctl", ["enable", domain]);
|
|
319
|
+
if (await run("launchctl", ["print", domain]).then(() => true, () => false))
|
|
320
|
+
return;
|
|
321
|
+
await run("launchctl", ["bootstrap", `gui/${process.getuid()}`, launchdPlistPath(homedir())]);
|
|
322
|
+
},
|
|
323
|
+
async stop() {
|
|
324
|
+
const domain = `gui/${process.getuid()}/pinomad.host`;
|
|
325
|
+
await run("launchctl", ["disable", domain]);
|
|
326
|
+
await run("launchctl", ["bootout", domain]).catch(() => {});
|
|
327
|
+
},
|
|
266
328
|
async state() {
|
|
329
|
+
const defined = await readFile(launchdPlistPath(homedir())).then(() => true, () => false);
|
|
330
|
+
if (!defined)
|
|
331
|
+
return NOT_INSTALLED;
|
|
332
|
+
const disabled = await run("launchctl", ["print-disabled", `gui/${process.getuid()}`]).then(({ stdout }) => launchdDisabled(stdout), () => false);
|
|
267
333
|
const printed = await run("launchctl", ["print", `gui/${process.getuid()}/pinomad.host`]).then(({ stdout }) => stdout, () => {
|
|
268
334
|
return;
|
|
269
335
|
});
|
|
270
|
-
|
|
271
|
-
return { installed: false, running: false, detail: "not installed" };
|
|
272
|
-
const field = (name) => printed.match(new RegExp(`^\\s*${name} = (.+)$`, "m"))?.[1];
|
|
273
|
-
const state = field("state") ?? "unknown";
|
|
274
|
-
const pid = field("pid");
|
|
275
|
-
const exit = field("last exit code");
|
|
276
|
-
return {
|
|
277
|
-
installed: true,
|
|
278
|
-
running: state === "running" && pid !== undefined,
|
|
279
|
-
...pid === undefined ? {} : { pid: Number(pid) },
|
|
280
|
-
detail: [
|
|
281
|
-
state,
|
|
282
|
-
...pid === undefined ? [] : [`pid ${pid}`],
|
|
283
|
-
...exit === undefined || exit === "0" || exit === "(never exited)" ? [] : [`last exit ${exit}`]
|
|
284
|
-
].join(", ")
|
|
285
|
-
};
|
|
336
|
+
return launchdState(disabled, printed);
|
|
286
337
|
},
|
|
287
338
|
async logs(dataDir, lines, follow) {
|
|
288
339
|
const file = join2(dataDir, "logs", "host.log");
|
|
@@ -328,34 +379,23 @@ var systemd = {
|
|
|
328
379
|
async restart() {
|
|
329
380
|
await run("systemctl", ["--user", "restart", "pinomad.service"]);
|
|
330
381
|
},
|
|
382
|
+
async start() {
|
|
383
|
+
await run("systemctl", ["--user", "enable", "--now", "pinomad.service"]);
|
|
384
|
+
},
|
|
385
|
+
async stop() {
|
|
386
|
+
await run("systemctl", ["--user", "disable", "--now", "pinomad.service"]);
|
|
387
|
+
},
|
|
331
388
|
async state() {
|
|
332
389
|
const shown = await run("systemctl", [
|
|
333
390
|
"--user",
|
|
334
391
|
"show",
|
|
335
392
|
"pinomad.service",
|
|
336
393
|
"-p",
|
|
337
|
-
"LoadState,ActiveState,SubState,MainPID,ExecMainStatus"
|
|
394
|
+
"LoadState,ActiveState,SubState,MainPID,ExecMainStatus,UnitFileState"
|
|
338
395
|
]).then(({ stdout }) => stdout, () => {
|
|
339
396
|
return;
|
|
340
397
|
});
|
|
341
|
-
|
|
342
|
-
return { installed: false, running: false, detail: "not installed" };
|
|
343
|
-
const fields = Object.fromEntries(shown.trim().split(`
|
|
344
|
-
`).map((line) => line.split("=", 2)));
|
|
345
|
-
if (fields.LoadState === "not-found")
|
|
346
|
-
return { installed: false, running: false, detail: "not installed" };
|
|
347
|
-
const pid = Number(fields.MainPID ?? "0");
|
|
348
|
-
return {
|
|
349
|
-
installed: true,
|
|
350
|
-
running: fields.ActiveState === "active" && pid > 0,
|
|
351
|
-
...pid > 0 ? { pid } : {},
|
|
352
|
-
detail: [
|
|
353
|
-
fields.ActiveState ?? "unknown",
|
|
354
|
-
fields.SubState ?? "",
|
|
355
|
-
...pid > 0 ? [`pid ${pid}`] : [],
|
|
356
|
-
...fields.ExecMainStatus === undefined || fields.ExecMainStatus === "0" ? [] : [`last exit ${fields.ExecMainStatus}`]
|
|
357
|
-
].filter((part) => part !== "").join(", ")
|
|
358
|
-
};
|
|
398
|
+
return shown === undefined ? NOT_INSTALLED : systemdState(shown);
|
|
359
399
|
},
|
|
360
400
|
async logs(_dataDir, lines, follow) {
|
|
361
401
|
const journal = spawn("journalctl", ["--user", "-u", "pinomad.service", "-n", String(lines), ...follow ? ["-f"] : []], {
|
|
@@ -410,6 +450,74 @@ async function install(argv) {
|
|
|
410
450
|
console.log(`data dir: ${spec.dataDir}`);
|
|
411
451
|
console.log(`browser link: ${commandHint("link")} pairing QR: ${commandHint("pair")}`);
|
|
412
452
|
}
|
|
453
|
+
async function waitUntilIdle(address, connect, log) {
|
|
454
|
+
try {
|
|
455
|
+
const remote = await connect(address.url, address.token);
|
|
456
|
+
await waitForIdle(remote, (tasks) => {
|
|
457
|
+
log("waiting for running tasks to finish (Ctrl-C to cancel)");
|
|
458
|
+
for (const task of Object.values(tasks.tasks)) {
|
|
459
|
+
log(` ${task.kind} in conversation ${task.conversationId}`);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
remote.close();
|
|
463
|
+
} catch {}
|
|
464
|
+
}
|
|
465
|
+
async function stopService({
|
|
466
|
+
force,
|
|
467
|
+
argv,
|
|
468
|
+
manager = serviceManager(),
|
|
469
|
+
connect = connectToHost,
|
|
470
|
+
log = (line) => console.log(line),
|
|
471
|
+
logError = (line) => console.error(line)
|
|
472
|
+
}) {
|
|
473
|
+
const state = await manager.state();
|
|
474
|
+
if (!state.installed) {
|
|
475
|
+
log(`service not installed (${commandHint("service")} install)`);
|
|
476
|
+
return 1;
|
|
477
|
+
}
|
|
478
|
+
if (state.stopped) {
|
|
479
|
+
log("already stopped");
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
if (!force) {
|
|
483
|
+
const spec = argv.length === 0 ? await manager.read() : undefined;
|
|
484
|
+
const port = spec === undefined ? undefined : argValue(spec.hostArgs, "--port");
|
|
485
|
+
const flags = spec === undefined ? argv : ["--data-dir", spec.dataDir, ...port === undefined ? [] : ["--url", `ws://127.0.0.1:${port}`]];
|
|
486
|
+
const address = await hostAddress(flags).catch((error) => {
|
|
487
|
+
if (isParseArgsError(error))
|
|
488
|
+
throw error;
|
|
489
|
+
return;
|
|
490
|
+
});
|
|
491
|
+
if (address !== undefined)
|
|
492
|
+
await waitUntilIdle(address, connect, log);
|
|
493
|
+
}
|
|
494
|
+
const holdInterrupt = () => {};
|
|
495
|
+
process.on("SIGINT", holdInterrupt);
|
|
496
|
+
try {
|
|
497
|
+
await manager.stop();
|
|
498
|
+
} finally {
|
|
499
|
+
process.off("SIGINT", holdInterrupt);
|
|
500
|
+
}
|
|
501
|
+
const after = await manager.state();
|
|
502
|
+
if (!after.stopped) {
|
|
503
|
+
logError(`the host is still running (${after.detail}); run \`${commandHint("service")} stop\` again`);
|
|
504
|
+
return 1;
|
|
505
|
+
}
|
|
506
|
+
log(`stopped; it stays off, also after a restart, until \`${commandHint("service")} start\``);
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
async function startService({
|
|
510
|
+
manager = serviceManager(),
|
|
511
|
+
log = (line) => console.log(line)
|
|
512
|
+
} = {}) {
|
|
513
|
+
if (!(await manager.state()).installed) {
|
|
514
|
+
log(`service not installed (${commandHint("service")} install)`);
|
|
515
|
+
return 1;
|
|
516
|
+
}
|
|
517
|
+
await manager.start();
|
|
518
|
+
log("started");
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
413
521
|
async function versionLine() {
|
|
414
522
|
if (packagedVersion !== undefined)
|
|
415
523
|
return `version: ${packagedVersion}`;
|
|
@@ -432,8 +540,19 @@ async function reachabilityLine(argv) {
|
|
|
432
540
|
return `host: unreachable (${message(error)})`;
|
|
433
541
|
}
|
|
434
542
|
}
|
|
543
|
+
var serviceStatusJson = (state) => JSON.stringify({
|
|
544
|
+
installed: state.installed,
|
|
545
|
+
stopped: state.stopped,
|
|
546
|
+
running: state.running,
|
|
547
|
+
...state.pid === undefined ? {} : { pid: state.pid },
|
|
548
|
+
detail: state.detail
|
|
549
|
+
});
|
|
435
550
|
async function status(argv) {
|
|
436
551
|
const state = await serviceManager().state();
|
|
552
|
+
if (argv.includes("--json")) {
|
|
553
|
+
console.log(serviceStatusJson(state));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
437
556
|
console.log(`service: ${state.installed ? state.detail : "not installed"}`);
|
|
438
557
|
console.log(await versionLine());
|
|
439
558
|
console.log(await reachabilityLine(argv));
|
|
@@ -447,27 +566,22 @@ async function restartService({
|
|
|
447
566
|
pollTimeoutMs = 30000,
|
|
448
567
|
pollDelayMs = 500
|
|
449
568
|
}) {
|
|
450
|
-
|
|
569
|
+
const state = await manager.state();
|
|
570
|
+
if (!state.installed) {
|
|
451
571
|
console.log(`service not installed (${commandHint("service")} install)`);
|
|
452
572
|
return 1;
|
|
453
573
|
}
|
|
574
|
+
if (state.stopped && restart === undefined) {
|
|
575
|
+
console.log(`service stopped (${commandHint("service")} start)`);
|
|
576
|
+
return 1;
|
|
577
|
+
}
|
|
454
578
|
const address = await hostAddress(argv).catch((error) => {
|
|
455
579
|
if (isParseArgsError(error))
|
|
456
580
|
throw error;
|
|
457
581
|
return;
|
|
458
582
|
});
|
|
459
|
-
if (!force && address !== undefined)
|
|
460
|
-
|
|
461
|
-
const remote = await connect(address.url, address.token);
|
|
462
|
-
await waitForIdle(remote, (tasks) => {
|
|
463
|
-
console.log("waiting for running tasks to finish (Ctrl-C to cancel)");
|
|
464
|
-
for (const task of Object.values(tasks.tasks)) {
|
|
465
|
-
console.log(` ${task.kind} in conversation ${task.conversationId}`);
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
remote.close();
|
|
469
|
-
} catch {}
|
|
470
|
-
}
|
|
583
|
+
if (!force && address !== undefined)
|
|
584
|
+
await waitUntilIdle(address, connect, (line) => console.log(line));
|
|
471
585
|
await (restart?.() ?? manager.restart());
|
|
472
586
|
if (address === undefined) {
|
|
473
587
|
console.log("restart requested (host address unknown; not polling)");
|
|
@@ -525,11 +639,19 @@ async function main(argv) {
|
|
|
525
639
|
process.exitCode = await restartService({ force, argv: args.filter((arg) => arg !== "--force") });
|
|
526
640
|
break;
|
|
527
641
|
}
|
|
642
|
+
case "stop": {
|
|
643
|
+
const force = args.includes("--force");
|
|
644
|
+
process.exitCode = await stopService({ force, argv: args.filter((arg) => arg !== "--force") });
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
case "start":
|
|
648
|
+
process.exitCode = await startService();
|
|
649
|
+
break;
|
|
528
650
|
case "logs":
|
|
529
651
|
await logs(args);
|
|
530
652
|
break;
|
|
531
653
|
default:
|
|
532
|
-
console.error(`usage: ${commandHint("service")} install|uninstall|status|restart|logs`);
|
|
654
|
+
console.error(`usage: ${commandHint("service")} install|uninstall|status|restart|stop|start|logs`);
|
|
533
655
|
process.exitCode = 1;
|
|
534
656
|
}
|
|
535
657
|
} catch (error) {
|
|
@@ -541,4 +663,4 @@ async function main(argv) {
|
|
|
541
663
|
}
|
|
542
664
|
if (false) {}
|
|
543
665
|
|
|
544
|
-
export { installSpec, argValue, withoutArg, withoutRelayArgs, serviceManager, versionLine, reachabilityLine, restartService, main };
|
|
666
|
+
export { installSpec, argValue, withoutArg, withoutRelayArgs, serviceManager, stopService, startService, versionLine, reachabilityLine, serviceStatusJson, restartService, main };
|