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.
Files changed (96) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-b457q0tm.js → chunk-41q0saz0.js} +11 -8
  3. package/dist/chunk-42nqtx5r.js +22 -0
  4. package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
  5. package/dist/{chunk-xqycs43g.js → chunk-47ff1d1t.js} +35 -7
  6. package/dist/{chunk-n1m1tjrk.js → chunk-5qf1grkk.js} +19 -13
  7. package/dist/{chunk-ncbqa8jk.js → chunk-7mn38n4f.js} +1 -1
  8. package/dist/{chunk-sd5fkxjj.js → chunk-7wj0jh3t.js} +1 -1
  9. package/dist/{chunk-4dcs8sbj.js → chunk-agq4tn9r.js} +43 -21
  10. package/dist/{chunk-as6pyf00.js → chunk-aqj9wfmb.js} +1 -1
  11. package/dist/{chunk-y127fe1a.js → chunk-g3ahp7b9.js} +11 -0
  12. package/dist/{chunk-gtdpjqa3.js → chunk-gg2dcqbg.js} +186 -45
  13. package/dist/{chunk-6d16c2fv.js → chunk-jeqsdcj1.js} +13 -6
  14. package/dist/{chunk-67gq282t.js → chunk-ksfxqd8s.js} +491 -34
  15. package/dist/{chunk-9h5vawnm.js → chunk-mnmqyq7d.js} +237 -27
  16. package/dist/{chunk-jt34g8st.js → chunk-np4jd070.js} +154 -2
  17. package/dist/{chunk-44pnts98.js → chunk-nppgxh6c.js} +1 -1
  18. package/dist/chunk-nq57fdnq.js +106 -0
  19. package/dist/{chunk-vvj9b1gc.js → chunk-t5abqsva.js} +174 -52
  20. package/dist/{chunk-tj57aqav.js → chunk-vqkb70d7.js} +2 -2
  21. package/dist/{chunk-h5q2y57m.js → chunk-vx7dy07y.js} +10 -4
  22. package/dist/pinomad.js +15 -14
  23. package/package.json +2 -1
  24. package/web/assets/astro-DOly5024.js +1 -0
  25. package/web/assets/c-BIGW1oBm.js +1 -0
  26. package/web/assets/core-DnpzcQzs.js +4 -0
  27. package/web/assets/cpp-DddhVWhK.js +1 -0
  28. package/web/assets/csharp-B_TYUkTI.js +1 -0
  29. package/web/assets/css-Z72CF80r.js +1 -0
  30. package/web/assets/dart-CtKP3W-j.js +1 -0
  31. package/web/assets/diff-D97Zzqfu.js +1 -0
  32. package/web/assets/dockerfile-BcOcwvcX.js +1 -0
  33. package/web/assets/elixir-DkJLSxtU.js +1 -0
  34. package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
  35. package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
  36. package/web/assets/fish-BvzEVeQv.js +1 -0
  37. package/web/assets/go-C27-OAKa.js +1 -0
  38. package/web/assets/graphql-ChdNCCLP.js +1 -0
  39. package/web/assets/haskell-Df6bDoY_.js +1 -0
  40. package/web/assets/hcl-BWvSN4gD.js +1 -0
  41. package/web/assets/highlighted-diff-Dn5ifsvK.js +175 -0
  42. package/web/assets/highlights-DnHv6NBM.js +4 -0
  43. package/web/assets/highlights-themes-CUdrlZk3.js +1 -0
  44. package/web/assets/html-BlS82ilI.js +1 -0
  45. package/web/assets/{index-DoyHMPwf.css → index-BV0ecolo.css} +1 -1
  46. package/web/assets/index-DNsTTgPE.js +396 -0
  47. package/web/assets/ini-BEwlwnbL.js +1 -0
  48. package/web/assets/java-CylS5w8V.js +1 -0
  49. package/web/assets/javascript-wDzz0qaB.js +1 -0
  50. package/web/assets/json-Cp-IABpG.js +1 -0
  51. package/web/assets/jsonc-Des-eS-w.js +1 -0
  52. package/web/assets/jsx-g9-lgVsj.js +1 -0
  53. package/web/assets/kotlin-BdnUsdx6.js +1 -0
  54. package/web/assets/less-B1dDrJ26.js +1 -0
  55. package/web/assets/lua-BaeVxFsk.js +1 -0
  56. package/web/assets/make-CHLpvVh8.js +1 -0
  57. package/web/assets/markdown-Cvjx9yec.js +1 -0
  58. package/web/assets/nix-CwoSXNpI.js +1 -0
  59. package/web/assets/objective-c-DXmwc3jG.js +1 -0
  60. package/web/assets/php-DoruYMF0.js +1 -0
  61. package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
  62. package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
  63. package/web/assets/pierre-light-480U9XYS.js +1 -0
  64. package/web/assets/pierre-light-BVSW-Xev.js +1 -0
  65. package/web/assets/postcss-DnzrXTVv.js +1 -0
  66. package/web/assets/powershell-Dd0kTxKP.js +1 -0
  67. package/web/assets/prisma-Vru482bI.js +1 -0
  68. package/web/assets/proto-C7zT0LnQ.js +1 -0
  69. package/web/assets/python-B6aJPvgy.js +1 -0
  70. package/web/assets/ruby-B43yHlf9.js +1 -0
  71. package/web/assets/rust-DPvrY3IQ.js +1 -0
  72. package/web/assets/scala-CqE71os6.js +1 -0
  73. package/web/assets/scss-Cw6XMfVs.js +1 -0
  74. package/web/assets/shellscript-Yzrsuije.js +1 -0
  75. package/web/assets/shiki-BuPP5HCg.js +2 -0
  76. package/web/assets/shiki-langs-FJk0v0Ga.js +2 -0
  77. package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
  78. package/web/assets/sql-DkvoVjww.js +1 -0
  79. package/web/assets/stream-tokenizer-Bl6bisIa.js +4 -0
  80. package/web/assets/svelte-DoIXUgJc.js +1 -0
  81. package/web/assets/swift-C2oV4EkX.js +1 -0
  82. package/web/assets/terraform-BETggiCN.js +1 -0
  83. package/web/assets/theming-themes-BL_CC-Mm.js +1 -0
  84. package/web/assets/toml-vGWfd6FD.js +1 -0
  85. package/web/assets/tsx-COt5Ahok.js +1 -0
  86. package/web/assets/typescript-BPQ3VLAy.js +1 -0
  87. package/web/assets/vue-CwSeJimo.js +1 -0
  88. package/web/assets/xml-sdJ4AIDG.js +1 -0
  89. package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
  90. package/web/assets/xterm-view-ZsuzJb2C.js +38 -0
  91. package/web/assets/yaml-Buea-lGh.js +1 -0
  92. package/web/assets/zig-VOosw3JB.js +1 -0
  93. package/web/index.html +2 -2
  94. package/dist/chunk-etf7ef97.js +0 -55
  95. package/dist/chunk-greb3dp6.js +0 -22
  96. 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-xqycs43g.js";
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.#send({ type: "subscribe", stream, ...refs ? { images: "ref" } : {} });
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);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RTC_DEFAULT_STUN
3
- } from "./chunk-xqycs43g.js";
3
+ } from "./chunk-47ff1d1t.js";
4
4
  import {
5
5
  HOST_SETTING_KEYS,
6
6
  cleanHostName,
@@ -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-jt34g8st.js";
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-sd5fkxjj.js";
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
- if (printed === undefined)
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
- if (shown === undefined)
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
- if (!(await manager.state()).installed) {
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
- try {
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 };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  connectRemoteDurable
3
- } from "./chunk-jt34g8st.js";
4
- import"./chunk-xqycs43g.js";
3
+ } from "./chunk-np4jd070.js";
4
+ import"./chunk-47ff1d1t.js";
5
5
  import {
6
6
  isLoopbackUrl
7
7
  } from "./chunk-pqe7q1s7.js";