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
@@ -3,28 +3,28 @@ import {
3
3
  isSecureServerOrigin,
4
4
  probeServerAccounts,
5
5
  runServerOperation
6
- } from "./chunk-ncbqa8jk.js";
6
+ } from "./chunk-7mn38n4f.js";
7
7
  import {
8
8
  constantValue,
9
9
  openHostSettings
10
- } from "./chunk-44pnts98.js";
10
+ } from "./chunk-nppgxh6c.js";
11
11
  import {
12
12
  loadHostKey,
13
13
  loadRelayKey
14
14
  } from "./chunk-t91zt5f0.js";
15
15
  import {
16
16
  writeServerConfig
17
- } from "./chunk-etf7ef97.js";
17
+ } from "./chunk-nq57fdnq.js";
18
18
  import {
19
19
  relayHostId
20
- } from "./chunk-y127fe1a.js";
20
+ } from "./chunk-g3ahp7b9.js";
21
21
  import {
22
22
  argValue,
23
23
  installSpec,
24
24
  restartService,
25
25
  serviceManager,
26
26
  withoutRelayArgs
27
- } from "./chunk-vvj9b1gc.js";
27
+ } from "./chunk-t5abqsva.js";
28
28
  import {
29
29
  cleanHostName,
30
30
  normalizeHostName
@@ -35,7 +35,7 @@ import {
35
35
  import {
36
36
  commandHint,
37
37
  hostEntry
38
- } from "./chunk-sd5fkxjj.js";
38
+ } from "./chunk-7wj0jh3t.js";
39
39
 
40
40
  // apps/host/src/cli/join.ts
41
41
  import { mkdir } from "node:fs/promises";
@@ -45,7 +45,10 @@ import { parseArgs } from "node:util";
45
45
 
46
46
  // apps/host/src/cli/host-link.ts
47
47
  import { spawn } from "node:child_process";
48
+ import { createHash, randomBytes } from "node:crypto";
49
+ import { createServer } from "node:http";
48
50
  import { arch, platform } from "node:os";
51
+ var CODE_EXPIRED = "the code expired before anyone confirmed it — run the command again for a new one";
49
52
  var SLOW_DOWN_STEP_MS = 5000;
50
53
  var REQUEST_TIMEOUT_MS = 15000;
51
54
  var EXPIRY_GRACE_MS = 2 * 60000;
@@ -58,18 +61,28 @@ function browserCommand(os, env) {
58
61
  return "xdg-open";
59
62
  return;
60
63
  }
61
- function openInBrowser(url) {
62
- const command = browserCommand(process.platform, process.env);
64
+ var OPENER_SETTLE_MS = 2000;
65
+ function openInBrowser(url, command = browserCommand(process.platform, process.env), settleMs = OPENER_SETTLE_MS) {
63
66
  if (command === undefined)
64
- return false;
65
- try {
66
- const child = spawn(command, [url], { detached: true, stdio: "ignore" });
67
- child.on("error", () => {});
68
- child.unref();
69
- return true;
70
- } catch {
71
- return false;
72
- }
67
+ return Promise.resolve(false);
68
+ return new Promise((resolve) => {
69
+ let timer;
70
+ const settle = (opened) => {
71
+ clearTimeout(timer);
72
+ resolve(opened);
73
+ };
74
+ try {
75
+ const child = spawn(command, [url], { detached: true, stdio: "ignore" });
76
+ child.once("spawn", () => {
77
+ child.unref();
78
+ timer = setTimeout(() => settle(true), settleMs);
79
+ });
80
+ child.once("error", () => settle(false));
81
+ child.once("exit", (code) => settle(code === 0));
82
+ } catch {
83
+ settle(false);
84
+ }
85
+ });
73
86
  }
74
87
  function hostOsLabel() {
75
88
  const os = platform();
@@ -78,6 +91,29 @@ function hostOsLabel() {
78
91
  }
79
92
  var isRecord = (value) => typeof value === "object" && value !== null;
80
93
  var errorCode = (body) => isRecord(body) && typeof body.error === "string" ? body.error : undefined;
94
+ var escapeHtml = (text) => text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
95
+ function answerPage(response, status, text) {
96
+ response.writeHead(status, {
97
+ "content-type": "text/html; charset=utf-8",
98
+ "cache-control": "no-store",
99
+ "referrer-policy": "no-referrer",
100
+ "content-security-policy": "default-src 'none'",
101
+ connection: "close"
102
+ });
103
+ response.end(`<!doctype html><meta charset="utf-8"><title>PiNomad</title><p>${escapeHtml(text)}</p>
104
+ `);
105
+ }
106
+ function pkcePair() {
107
+ const verifier = randomBytes(32).toString("base64url");
108
+ return { verifier, challenge: createHash("sha256").update(verifier).digest("base64url") };
109
+ }
110
+ var codeStart = (body) => isRecord(body) && typeof body.deviceCode === "string" && typeof body.userCode === "string" && typeof body.verifyUrl === "string" && typeof body.interval === "number" && typeof body.expiresAt === "number" && isSecureServerOrigin(body.verifyUrl) ? {
111
+ deviceCode: body.deviceCode,
112
+ userCode: body.userCode,
113
+ verifyUrl: body.verifyUrl,
114
+ expiresAt: body.expiresAt,
115
+ interval: body.interval
116
+ } : undefined;
81
117
  async function requestJoinToken(args, deps) {
82
118
  const fetchImpl = deps?.fetch ?? fetch;
83
119
  const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -103,13 +139,13 @@ async function requestJoinToken(args, deps) {
103
139
  signal: controller.signal
104
140
  });
105
141
  const text = await response.text();
106
- let body2;
142
+ let body;
107
143
  try {
108
- body2 = JSON.parse(text);
144
+ body = JSON.parse(text);
109
145
  } catch {
110
- body2 = undefined;
146
+ body = undefined;
111
147
  }
112
- return { status: response.status, body: body2 };
148
+ return { status: response.status, body };
113
149
  })().catch(() => {
114
150
  return;
115
151
  });
@@ -119,41 +155,144 @@ async function requestJoinToken(args, deps) {
119
155
  clearTimeout(timer);
120
156
  }
121
157
  };
122
- const started = await post("/api/host-link/start", { hostId: args.hostId, name: args.name, os: args.os ?? hostOsLabel() });
123
- if (started === undefined)
124
- throw new Error(`cannot reach ${origin} — check the address and that the server is up`);
125
- const { body } = started;
126
- if (started.status !== 200) {
127
- const code = errorCode(body);
128
- if (started.status === 404) {
158
+ const startBody = { hostId: args.hostId, name: args.name, os: args.os ?? hostOsLabel() };
159
+ const openBrowser = async (url) => Promise.resolve((deps?.openBrowser ?? openInBrowser)(url)).catch(() => false);
160
+ const start = async (payload) => {
161
+ const started2 = await post("/api/host-link/start", payload);
162
+ if (started2 === undefined)
163
+ throw new Error(`cannot reach ${origin} — check the address and that the server is up`);
164
+ if (started2.status === 200)
165
+ return started2.body;
166
+ const code = errorCode(started2.body);
167
+ if (started2.status === 404) {
129
168
  throw new Error(`${origin} can't confirm hosts in the browser yet — create a join token under "Add a host" and run the join command with it`);
130
169
  }
131
170
  if (code === "rate_limited")
132
171
  throw new Error("too many join requests from this address — wait a few minutes and try again");
133
172
  if (code === "busy")
134
173
  throw new Error(`${origin} has too many hosts waiting to join right now — try again in a few minutes`);
135
- throw new Error(`${origin} refused to start the join: ${code ?? `HTTP ${started.status}`}`);
136
- }
137
- if (!isRecord(body) || typeof body.deviceCode !== "string" || typeof body.userCode !== "string" || typeof body.verifyUrl !== "string" || typeof body.interval !== "number" || typeof body.expiresAt !== "number" || !isSecureServerOrigin(body.verifyUrl)) {
138
- throw new Error(`${origin} answered the join request with something unexpected`);
174
+ throw new Error(`${origin} refused to start the join: ${code ?? `HTTP ${started2.status}`}`);
175
+ };
176
+ const unexpected = () => new Error(`${origin} answered the join request with something unexpected`);
177
+ const viaLoopback = async () => {
178
+ const { verifier, challenge } = pkcePair();
179
+ const state = randomBytes(16).toString("base64url");
180
+ let settle;
181
+ const callback = new Promise((resolve) => {
182
+ settle = resolve;
183
+ });
184
+ const listener = createServer((request, response) => {
185
+ const url = URL.parse(request.url ?? "/", "http://127.0.0.1");
186
+ if (url === null) {
187
+ answerPage(response, 400, "Bad request.");
188
+ return;
189
+ }
190
+ if (request.method !== "GET" || url.pathname !== "/callback" || url.searchParams.get("state") !== state) {
191
+ answerPage(response, 404, "Not found.");
192
+ return;
193
+ }
194
+ if (settle === undefined) {
195
+ answerPage(response, 409, "This join was already answered. You can close this page.");
196
+ return;
197
+ }
198
+ const deliver = settle;
199
+ settle = undefined;
200
+ const closed = new Promise((resolve) => response.once("close", () => resolve()));
201
+ deliver({
202
+ params: url.searchParams,
203
+ reply: async (status, text) => {
204
+ answerPage(response, status, text);
205
+ await closed;
206
+ }
207
+ });
208
+ });
209
+ await new Promise((resolve, reject) => {
210
+ listener.once("error", reject);
211
+ listener.listen(0, "127.0.0.1", () => resolve());
212
+ });
213
+ try {
214
+ const { port } = listener.address();
215
+ const body = await start({
216
+ ...startBody,
217
+ redirectUri: `http://127.0.0.1:${port}/callback`,
218
+ codeChallenge: challenge,
219
+ state
220
+ });
221
+ if (!isRecord(body) || typeof body.requestId !== "string") {
222
+ const fallback = codeStart(body);
223
+ if (fallback === undefined)
224
+ throw unexpected();
225
+ return { fallback };
226
+ }
227
+ if (typeof body.verifyUrl !== "string" || typeof body.expiresAt !== "number" || !isSecureServerOrigin(body.verifyUrl)) {
228
+ throw unexpected();
229
+ }
230
+ const { verifyUrl: verifyUrl2, expiresAt: expiresAt2 } = body;
231
+ if (!await openBrowser(verifyUrl2)) {
232
+ browserOpens = false;
233
+ return { fallback: undefined };
234
+ }
235
+ const minutes2 = Math.max(1, Math.round((expiresAt2 - now()) / 60000));
236
+ log(`Opened ${verifyUrl2} in your browser to add this host to your account on ${origin}.`);
237
+ log(`Sign in there and confirm it on this computer — only if you started this on ${args.name} (host id ${args.hostId}).`);
238
+ log(`To confirm from another device instead, press Ctrl-C and run the command again with --code.`);
239
+ log(`Waiting for confirmation (the request expires in ${minutes2} min)…`);
240
+ const wait = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms).unref()));
241
+ const answer = await Promise.race([
242
+ callback,
243
+ wait(Math.max(0, expiresAt2 + EXPIRY_GRACE_MS - now())).then(() => {
244
+ return;
245
+ })
246
+ ]);
247
+ if (answer === undefined)
248
+ throw new Error("the request expired before anyone confirmed it — run the command again");
249
+ const { params, reply } = answer;
250
+ if (params.get("error") === "access_denied") {
251
+ await reply(200, `Denied: ${args.name} was not added. You can close this page.`);
252
+ throw new Error(`the join was denied on ${origin}`);
253
+ }
254
+ const code = params.get("code");
255
+ const exchanged = code === null ? undefined : await post("/api/host-link/token", { code, codeVerifier: verifier });
256
+ const token = exchanged?.status === 200 && isRecord(exchanged.body) ? exchanged.body.token : undefined;
257
+ if (typeof token !== "string") {
258
+ await reply(502, "The join did not finish. Go back to the terminal and run the command again.");
259
+ throw new Error(`${origin} did not hand over the join token: ${errorCode(exchanged?.body) ?? (exchanged === undefined ? "no answer" : `HTTP ${exchanged.status}`)} — run the command again`);
260
+ }
261
+ await reply(200, `Confirmed: ${args.name} is joining your account. You can close this page.`);
262
+ log("Confirmed.");
263
+ return { token };
264
+ } finally {
265
+ listener.close();
266
+ listener.closeAllConnections();
267
+ }
268
+ };
269
+ let browserOpens = args.code !== true;
270
+ let started;
271
+ if (args.code !== true && (deps?.loopback ?? browserCommand(process.platform, process.env) !== undefined)) {
272
+ const loopback = await viaLoopback();
273
+ if ("token" in loopback)
274
+ return loopback.token;
275
+ started = loopback.fallback;
139
276
  }
140
- const { deviceCode, userCode, verifyUrl, expiresAt } = body;
141
- let intervalMs = Math.max(1, body.interval) * 1000;
277
+ started ??= codeStart(await start(startBody));
278
+ if (started === undefined)
279
+ throw unexpected();
280
+ const { deviceCode, userCode, verifyUrl, expiresAt } = started;
281
+ let intervalMs = Math.max(1, started.interval) * 1000;
142
282
  const minutes = Math.max(1, Math.round((expiresAt - now()) / 60000));
143
283
  log(`To add this host to your account on ${origin}:`);
144
284
  log(` 1. Open ${verifyUrl} and sign in.`);
145
285
  log(` 2. Enter this code: ${userCode}`);
146
286
  log("");
147
287
  log(`Only confirm it if you started this on ${args.name} (host id ${args.hostId}).`);
148
- const opened = await Promise.resolve((deps?.openBrowser ?? openInBrowser)(verifyUrl)).catch(() => false);
288
+ const opened = browserOpens && await openBrowser(verifyUrl);
149
289
  log(`${opened ? "Opened the page in your browser. " : ""}Waiting for confirmation (the code expires in ${minutes} min)…`);
150
290
  for (;; ) {
151
291
  await sleep(intervalMs);
152
292
  const response = await post("/api/host-link/poll", { deviceCode });
153
293
  if (response === undefined || response.status >= 500) {
154
- if (now() > expiresAt + EXPIRY_GRACE_MS) {
155
- throw new Error(`the code expired before anyone confirmed it — run the command again for a new one`);
156
- }
294
+ if (now() > expiresAt + EXPIRY_GRACE_MS)
295
+ throw new Error(CODE_EXPIRED);
157
296
  continue;
158
297
  }
159
298
  const answer = response.body;
@@ -165,7 +304,7 @@ async function requestJoinToken(args, deps) {
165
304
  if (code === "access_denied")
166
305
  throw new Error(`the join was denied on ${origin}`);
167
306
  if (code === "expired_token")
168
- throw new Error(`the code expired before anyone confirmed it — run the command again for a new one`);
307
+ throw new Error(CODE_EXPIRED);
169
308
  if (code === "slow_down") {
170
309
  const asked = isRecord(answer) && typeof answer.interval === "number" ? answer.interval * 1000 : 0;
171
310
  intervalMs = Math.max(intervalMs + SLOW_DOWN_STEP_MS, asked);
@@ -179,7 +318,7 @@ async function requestJoinToken(args, deps) {
179
318
  import { homedir } from "node:os";
180
319
  import { resolve } from "node:path";
181
320
  async function openServiceSlot(dataDir, manager) {
182
- const state = await manager.state().catch(() => ({ installed: false, running: false, detail: "unknown" }));
321
+ const state = await manager.state().catch(() => ({ installed: false, stopped: false, running: false, detail: "unknown" }));
183
322
  const previous = state.installed ? await manager.read() : undefined;
184
323
  if (previous !== undefined && resolve(previous.dataDir) !== dataDir) {
185
324
  throw new Error(`the installed service uses a different data dir (${previous.dataDir}); ` + `run this again with --data-dir ${previous.dataDir} or uninstall the service first`);
@@ -249,7 +388,7 @@ async function joinServer(args, deps) {
249
388
  const current = settings.value;
250
389
  const storedName = current.custom.includes("hostName") ? current.hostName : undefined;
251
390
  const name = normalizeHostName(args.name ?? storedName ?? deps?.hostname ?? hostname());
252
- const joinToken = args.token ?? await requestJoinToken({ origin, hostId, name }, deps);
391
+ const joinToken = args.token ?? await requestJoinToken({ origin, hostId, name, code: args.code }, deps);
253
392
  const reply = await runServerOperation({
254
393
  origin,
255
394
  signingKey,
@@ -273,7 +412,7 @@ async function joinServer(args, deps) {
273
412
  service = await writeService(slot, (hostArgs) => edit(withoutRelayArgs(hostArgs)), { changed: true });
274
413
  } catch (error) {
275
414
  throw new Error(`joined ${username} at ${origin}, but local setup failed: ${message(error)}
276
- ` + (args.token === undefined ? `run the command again and confirm the new code (re-joining the same host is safe)` : `generate a fresh join token and run the command again (re-joining the same host is safe)`));
415
+ ` + (args.token === undefined ? `run the command again and confirm it again (re-joining the same host is safe)` : `generate a fresh join token and run the command again (re-joining the same host is safe)`));
277
416
  }
278
417
  return { hostId, username, origin, service };
279
418
  }
@@ -281,18 +420,20 @@ async function main(argv, deps) {
281
420
  const { values, positionals } = parseArgs({
282
421
  args: [...argv],
283
422
  allowPositionals: true,
284
- options: { "data-dir": { type: "string" }, name: { type: "string" } }
423
+ options: { "data-dir": { type: "string" }, name: { type: "string" }, code: { type: "boolean" } }
285
424
  });
286
425
  const [origin, token] = positionals;
287
426
  if (origin === undefined || positionals.length > 2) {
288
- console.error(`usage: ${commandHint("join")} <server-origin> [join-token] [--data-dir DIR] [--name NAME]`);
289
- console.error(" without a join token, confirm the printed code in the browser where you're signed in");
427
+ console.error(`usage: ${commandHint("join")} <server-origin> [join-token] [--code] [--data-dir DIR] [--name NAME]`);
428
+ console.error(" without a join token, confirm this host in the browser where you're signed in;");
429
+ console.error(" --code prints a code to confirm instead, from this or another device");
290
430
  process.exit(1);
291
431
  }
292
432
  try {
293
433
  const outcome = await joinServer({
294
434
  origin,
295
435
  ...token === undefined ? {} : { token },
436
+ ...values.code === true ? { code: true } : {},
296
437
  dataDir: values["data-dir"] ?? DEFAULT_DATA_DIR,
297
438
  ...values.name === undefined ? {} : { name: values.name }
298
439
  }, deps);
@@ -2,19 +2,20 @@
2
2
  import {
3
3
  restartService,
4
4
  serviceManager
5
- } from "./chunk-vvj9b1gc.js";
6
- import"./chunk-jt34g8st.js";
5
+ } from "./chunk-t5abqsva.js";
6
+ import"./chunk-np4jd070.js";
7
7
  import"./chunk-kzbzacz9.js";
8
- import"./chunk-xqycs43g.js";
8
+ import"./chunk-47ff1d1t.js";
9
9
  import"./chunk-pqe7q1s7.js";
10
10
  import {
11
11
  hostAddressOptions
12
12
  } from "./chunk-ngf1zatm.js";
13
13
  import {
14
+ commandHint,
14
15
  globalPrefixOf,
15
16
  packageDir,
16
17
  packagedVersion
17
- } from "./chunk-sd5fkxjj.js";
18
+ } from "./chunk-7wj0jh3t.js";
18
19
  import"./chunk-df32avw6.js";
19
20
 
20
21
  // apps/host/src/cli/upgrade.ts
@@ -34,7 +35,7 @@ function npmBeside(execPath) {
34
35
  const cli = join(binDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
35
36
  return existsSync(cli) ? { command: execPath, args: [cli], binDir } : { command: "npm", args: [] };
36
37
  }
37
- var serviceInstalled = () => Promise.resolve().then(() => serviceManager().state()).then((state) => state.installed, () => false);
38
+ var serviceState = () => Promise.resolve().then(() => serviceManager().state()).catch(() => ({ installed: false, stopped: false }));
38
39
  async function main(argv, deps = {}) {
39
40
  const { values } = parseArgs({
40
41
  args: [...argv],
@@ -75,11 +76,17 @@ async function main(argv, deps = {}) {
75
76
  return;
76
77
  }
77
78
  }
78
- if (!await (deps.serviceInstalled ?? serviceInstalled)()) {
79
+ const service = await (deps.serviceState ?? serviceState)();
80
+ if (!service.installed) {
79
81
  console.log("service not installed; upgrade done, nothing restarted");
80
82
  process.exitCode = 0;
81
83
  return;
82
84
  }
85
+ if (service.stopped) {
86
+ console.log(`service stopped; upgrade done, it stays off until \`${commandHint("service")} start\``);
87
+ process.exitCode = 0;
88
+ return;
89
+ }
83
90
  (deps.exit ?? process.exit)(await restart({ force: values.force, argv: argv.filter((arg) => arg !== "--force") }));
84
91
  }
85
92
  if (false) {}