pinomad 0.10.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 (98) hide show
  1. package/README.md +11 -9
  2. package/dist/chunk-41q0saz0.js +73 -0
  3. package/dist/chunk-42nqtx5r.js +22 -0
  4. package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
  5. package/dist/{chunk-jgd38rna.js → chunk-47ff1d1t.js} +206 -178
  6. package/dist/chunk-5qf1grkk.js +220 -0
  7. package/dist/{chunk-4h7pkkzr.js → chunk-7mn38n4f.js} +3 -3
  8. package/dist/{chunk-zehwx009.js → chunk-7wj0jh3t.js} +1 -1
  9. package/dist/{chunk-5791mb0b.js → chunk-agq4tn9r.js} +47 -23
  10. package/dist/{chunk-c5p9xkd8.js → chunk-aqj9wfmb.js} +3 -3
  11. package/dist/{chunk-cx8k63by.js → chunk-g3ahp7b9.js} +12 -1
  12. package/dist/chunk-gg2dcqbg.js +458 -0
  13. package/dist/{chunk-73efzaf1.js → chunk-jeqsdcj1.js} +30 -13
  14. package/dist/{chunk-czj9nfrz.js → chunk-ksfxqd8s.js} +500 -41
  15. package/dist/{chunk-p978qj9m.js → chunk-mnmqyq7d.js} +565 -39
  16. package/dist/{chunk-4sepcfpy.js → chunk-np4jd070.js} +155 -3
  17. package/dist/{chunk-v414b5pp.js → chunk-nppgxh6c.js} +2 -2
  18. package/dist/chunk-nq57fdnq.js +106 -0
  19. package/dist/{chunk-sygeb9qb.js → chunk-pqe7q1s7.js} +1 -1
  20. package/dist/{chunk-w2m9xkks.js → chunk-t5abqsva.js} +199 -72
  21. package/dist/{chunk-8gywfw5n.js → chunk-t91zt5f0.js} +2 -51
  22. package/dist/{chunk-vax4zspw.js → chunk-vqkb70d7.js} +3 -3
  23. package/dist/chunk-vx7dy07y.js +26 -0
  24. package/dist/pinomad.js +19 -12
  25. package/package.json +2 -1
  26. package/web/assets/astro-DOly5024.js +1 -0
  27. package/web/assets/c-BIGW1oBm.js +1 -0
  28. package/web/assets/core-DnpzcQzs.js +4 -0
  29. package/web/assets/cpp-DddhVWhK.js +1 -0
  30. package/web/assets/csharp-B_TYUkTI.js +1 -0
  31. package/web/assets/css-Z72CF80r.js +1 -0
  32. package/web/assets/dart-CtKP3W-j.js +1 -0
  33. package/web/assets/diff-D97Zzqfu.js +1 -0
  34. package/web/assets/dockerfile-BcOcwvcX.js +1 -0
  35. package/web/assets/elixir-DkJLSxtU.js +1 -0
  36. package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
  37. package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
  38. package/web/assets/fish-BvzEVeQv.js +1 -0
  39. package/web/assets/go-C27-OAKa.js +1 -0
  40. package/web/assets/graphql-ChdNCCLP.js +1 -0
  41. package/web/assets/haskell-Df6bDoY_.js +1 -0
  42. package/web/assets/hcl-BWvSN4gD.js +1 -0
  43. package/web/assets/highlighted-diff-Dn5ifsvK.js +175 -0
  44. package/web/assets/highlights-DnHv6NBM.js +4 -0
  45. package/web/assets/highlights-themes-CUdrlZk3.js +1 -0
  46. package/web/assets/html-BlS82ilI.js +1 -0
  47. package/web/assets/{index-DoyHMPwf.css → index-BV0ecolo.css} +1 -1
  48. package/web/assets/index-DNsTTgPE.js +396 -0
  49. package/web/assets/ini-BEwlwnbL.js +1 -0
  50. package/web/assets/java-CylS5w8V.js +1 -0
  51. package/web/assets/javascript-wDzz0qaB.js +1 -0
  52. package/web/assets/json-Cp-IABpG.js +1 -0
  53. package/web/assets/jsonc-Des-eS-w.js +1 -0
  54. package/web/assets/jsx-g9-lgVsj.js +1 -0
  55. package/web/assets/kotlin-BdnUsdx6.js +1 -0
  56. package/web/assets/less-B1dDrJ26.js +1 -0
  57. package/web/assets/lua-BaeVxFsk.js +1 -0
  58. package/web/assets/make-CHLpvVh8.js +1 -0
  59. package/web/assets/markdown-Cvjx9yec.js +1 -0
  60. package/web/assets/nix-CwoSXNpI.js +1 -0
  61. package/web/assets/objective-c-DXmwc3jG.js +1 -0
  62. package/web/assets/php-DoruYMF0.js +1 -0
  63. package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
  64. package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
  65. package/web/assets/pierre-light-480U9XYS.js +1 -0
  66. package/web/assets/pierre-light-BVSW-Xev.js +1 -0
  67. package/web/assets/postcss-DnzrXTVv.js +1 -0
  68. package/web/assets/powershell-Dd0kTxKP.js +1 -0
  69. package/web/assets/prisma-Vru482bI.js +1 -0
  70. package/web/assets/proto-C7zT0LnQ.js +1 -0
  71. package/web/assets/python-B6aJPvgy.js +1 -0
  72. package/web/assets/ruby-B43yHlf9.js +1 -0
  73. package/web/assets/rust-DPvrY3IQ.js +1 -0
  74. package/web/assets/scala-CqE71os6.js +1 -0
  75. package/web/assets/scss-Cw6XMfVs.js +1 -0
  76. package/web/assets/shellscript-Yzrsuije.js +1 -0
  77. package/web/assets/shiki-BuPP5HCg.js +2 -0
  78. package/web/assets/shiki-langs-FJk0v0Ga.js +2 -0
  79. package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
  80. package/web/assets/sql-DkvoVjww.js +1 -0
  81. package/web/assets/stream-tokenizer-Bl6bisIa.js +4 -0
  82. package/web/assets/svelte-DoIXUgJc.js +1 -0
  83. package/web/assets/swift-C2oV4EkX.js +1 -0
  84. package/web/assets/terraform-BETggiCN.js +1 -0
  85. package/web/assets/theming-themes-BL_CC-Mm.js +1 -0
  86. package/web/assets/toml-vGWfd6FD.js +1 -0
  87. package/web/assets/tsx-COt5Ahok.js +1 -0
  88. package/web/assets/typescript-BPQ3VLAy.js +1 -0
  89. package/web/assets/vue-CwSeJimo.js +1 -0
  90. package/web/assets/xml-sdJ4AIDG.js +1 -0
  91. package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
  92. package/web/assets/xterm-view-ZsuzJb2C.js +38 -0
  93. package/web/assets/yaml-Buea-lGh.js +1 -0
  94. package/web/assets/zig-VOosw3JB.js +1 -0
  95. package/web/index.html +2 -2
  96. package/dist/chunk-bhtx2c8a.js +0 -16
  97. package/dist/chunk-c05g401k.js +0 -170
  98. package/web/assets/index-BdBjP1qJ.js +0 -395
@@ -0,0 +1,458 @@
1
+ import {
2
+ assertSecureServerOrigin,
3
+ isSecureServerOrigin,
4
+ probeServerAccounts,
5
+ runServerOperation
6
+ } from "./chunk-7mn38n4f.js";
7
+ import {
8
+ constantValue,
9
+ openHostSettings
10
+ } from "./chunk-nppgxh6c.js";
11
+ import {
12
+ loadHostKey,
13
+ loadRelayKey
14
+ } from "./chunk-t91zt5f0.js";
15
+ import {
16
+ writeServerConfig
17
+ } from "./chunk-nq57fdnq.js";
18
+ import {
19
+ relayHostId
20
+ } from "./chunk-g3ahp7b9.js";
21
+ import {
22
+ argValue,
23
+ installSpec,
24
+ restartService,
25
+ serviceManager,
26
+ withoutRelayArgs
27
+ } from "./chunk-t5abqsva.js";
28
+ import {
29
+ cleanHostName,
30
+ normalizeHostName
31
+ } from "./chunk-pqe7q1s7.js";
32
+ import {
33
+ DEFAULT_DATA_DIR
34
+ } from "./chunk-ngf1zatm.js";
35
+ import {
36
+ commandHint,
37
+ hostEntry
38
+ } from "./chunk-7wj0jh3t.js";
39
+
40
+ // apps/host/src/cli/join.ts
41
+ import { mkdir } from "node:fs/promises";
42
+ import { hostname } from "node:os";
43
+ import { join, resolve as resolve2 } from "node:path";
44
+ import { parseArgs } from "node:util";
45
+
46
+ // apps/host/src/cli/host-link.ts
47
+ import { spawn } from "node:child_process";
48
+ import { createHash, randomBytes } from "node:crypto";
49
+ import { createServer } from "node:http";
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";
52
+ var SLOW_DOWN_STEP_MS = 5000;
53
+ var REQUEST_TIMEOUT_MS = 15000;
54
+ var EXPIRY_GRACE_MS = 2 * 60000;
55
+ function browserCommand(os, env) {
56
+ if (env.SSH_CONNECTION !== undefined || env.SSH_TTY !== undefined)
57
+ return;
58
+ if (os === "darwin")
59
+ return "open";
60
+ if (os === "linux" && (env.DISPLAY !== undefined || env.WAYLAND_DISPLAY !== undefined))
61
+ return "xdg-open";
62
+ return;
63
+ }
64
+ var OPENER_SETTLE_MS = 2000;
65
+ function openInBrowser(url, command = browserCommand(process.platform, process.env), settleMs = OPENER_SETTLE_MS) {
66
+ if (command === undefined)
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
+ });
86
+ }
87
+ function hostOsLabel() {
88
+ const os = platform();
89
+ const name = os === "darwin" ? "macOS" : os === "linux" ? "Linux" : os === "win32" ? "Windows" : os;
90
+ return `${name} (${arch()})`;
91
+ }
92
+ var isRecord = (value) => typeof value === "object" && value !== null;
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;
117
+ async function requestJoinToken(args, deps) {
118
+ const fetchImpl = deps?.fetch ?? fetch;
119
+ const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
120
+ const log = deps?.log ?? ((line) => console.log(line));
121
+ const now = deps?.now ?? Date.now;
122
+ const { origin } = args;
123
+ const timeoutMs = deps?.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
124
+ const post = async (path, payload) => {
125
+ const controller = new AbortController;
126
+ let timer;
127
+ const expired = new Promise((resolve) => {
128
+ timer = setTimeout(() => {
129
+ controller.abort();
130
+ resolve(undefined);
131
+ }, timeoutMs);
132
+ });
133
+ const attempt = (async () => {
134
+ const response = await fetchImpl(`${origin}${path}`, {
135
+ method: "POST",
136
+ headers: { "content-type": "application/json" },
137
+ body: JSON.stringify(payload),
138
+ redirect: "manual",
139
+ signal: controller.signal
140
+ });
141
+ const text = await response.text();
142
+ let body;
143
+ try {
144
+ body = JSON.parse(text);
145
+ } catch {
146
+ body = undefined;
147
+ }
148
+ return { status: response.status, body };
149
+ })().catch(() => {
150
+ return;
151
+ });
152
+ try {
153
+ return await Promise.race([attempt, expired]);
154
+ } finally {
155
+ clearTimeout(timer);
156
+ }
157
+ };
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) {
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`);
169
+ }
170
+ if (code === "rate_limited")
171
+ throw new Error("too many join requests from this address — wait a few minutes and try again");
172
+ if (code === "busy")
173
+ throw new Error(`${origin} has too many hosts waiting to join right now — try again in a few minutes`);
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;
276
+ }
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;
282
+ const minutes = Math.max(1, Math.round((expiresAt - now()) / 60000));
283
+ log(`To add this host to your account on ${origin}:`);
284
+ log(` 1. Open ${verifyUrl} and sign in.`);
285
+ log(` 2. Enter this code: ${userCode}`);
286
+ log("");
287
+ log(`Only confirm it if you started this on ${args.name} (host id ${args.hostId}).`);
288
+ const opened = browserOpens && await openBrowser(verifyUrl);
289
+ log(`${opened ? "Opened the page in your browser. " : ""}Waiting for confirmation (the code expires in ${minutes} min)…`);
290
+ for (;; ) {
291
+ await sleep(intervalMs);
292
+ const response = await post("/api/host-link/poll", { deviceCode });
293
+ if (response === undefined || response.status >= 500) {
294
+ if (now() > expiresAt + EXPIRY_GRACE_MS)
295
+ throw new Error(CODE_EXPIRED);
296
+ continue;
297
+ }
298
+ const answer = response.body;
299
+ if (response.status === 200 && isRecord(answer) && typeof answer.token === "string") {
300
+ log("Confirmed.");
301
+ return answer.token;
302
+ }
303
+ const code = response.status === 400 ? errorCode(answer) : undefined;
304
+ if (code === "access_denied")
305
+ throw new Error(`the join was denied on ${origin}`);
306
+ if (code === "expired_token")
307
+ throw new Error(CODE_EXPIRED);
308
+ if (code === "slow_down") {
309
+ const asked = isRecord(answer) && typeof answer.interval === "number" ? answer.interval * 1000 : 0;
310
+ intervalMs = Math.max(intervalMs + SLOW_DOWN_STEP_MS, asked);
311
+ } else if (code !== "authorization_pending") {
312
+ throw new Error(`${origin} refused the join: ${code ?? `HTTP ${response.status}`}`);
313
+ }
314
+ }
315
+ }
316
+
317
+ // apps/host/src/cli/service-slot.ts
318
+ import { homedir } from "node:os";
319
+ import { resolve } from "node:path";
320
+ async function openServiceSlot(dataDir, manager) {
321
+ const state = await manager.state().catch(() => ({ installed: false, stopped: false, running: false, detail: "unknown" }));
322
+ const previous = state.installed ? await manager.read() : undefined;
323
+ if (previous !== undefined && resolve(previous.dataDir) !== dataDir) {
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`);
325
+ }
326
+ return { dataDir, manager, state, previous };
327
+ }
328
+ async function writeService(slot, edit, options) {
329
+ const { dataDir, manager, previous } = slot;
330
+ const fresh = installSpec(["--data-dir", dataDir, ...edit([])], {
331
+ execPath: process.execPath,
332
+ env: process.env,
333
+ home: homedir(),
334
+ cwd: process.cwd(),
335
+ mainPath: hostEntry.script,
336
+ entryArgs: hostEntry.args
337
+ });
338
+ if (!slot.state.installed) {
339
+ await manager.install(fresh);
340
+ return "installed";
341
+ }
342
+ const spec = previous === undefined ? fresh : { ...previous, hostArgs: edit(previous.hostArgs) };
343
+ const same = previous !== undefined && spec.hostArgs.join("\x00") === previous.hostArgs.join("\x00");
344
+ if (same && !options.changed && slot.state.running)
345
+ return "unchanged";
346
+ const port = argValue(spec.hostArgs, "--port");
347
+ const code = await restartService({
348
+ force: false,
349
+ argv: ["--data-dir", dataDir, ...port === undefined ? [] : ["--url", `ws://127.0.0.1:${port}`]],
350
+ manager,
351
+ restart: async () => {
352
+ await manager.install(spec);
353
+ }
354
+ });
355
+ return code === 0 ? "updated" : "failed";
356
+ }
357
+
358
+ // apps/host/src/cli/join.ts
359
+ var message = (error) => error instanceof Error ? error.message : String(error);
360
+ async function joinServer(args, deps) {
361
+ const origin = args.origin.replace(/\/+$/, "");
362
+ const dataDir = resolve2(args.dataDir);
363
+ assertSecureServerOrigin(origin);
364
+ if (args.name !== undefined && cleanHostName(args.name) === "") {
365
+ throw new Error(`Invalid host name: ${JSON.stringify(args.name)}`);
366
+ }
367
+ await mkdir(dataDir, { recursive: true });
368
+ const signingKey = await loadRelayKey(dataDir);
369
+ const hostKey = await loadHostKey(dataDir);
370
+ const hostId = relayHostId(signingKey.publicKey);
371
+ const slot = await openServiceSlot(dataDir, deps?.manager ?? serviceManager());
372
+ const probe = await probeServerAccounts(origin, deps?.fetch);
373
+ if (probe === "unreachable")
374
+ throw new Error(`cannot reach ${origin} — check the address and that the server is up`);
375
+ if (probe === "disabled") {
376
+ throw new Error(`${origin} does not run server accounts — the server is too old or was started without a data dir`);
377
+ }
378
+ const settings = await openHostSettings({
379
+ path: join(dataDir, "settings.json"),
380
+ fallback: {
381
+ defaultModel: () => null,
382
+ defaultThinkingLevel: () => null,
383
+ steeringMode: () => "one-at-a-time",
384
+ followUpMode: () => "one-at-a-time"
385
+ },
386
+ models: constantValue([])
387
+ });
388
+ const current = settings.value;
389
+ const storedName = current.custom.includes("hostName") ? current.hostName : undefined;
390
+ const name = normalizeHostName(args.name ?? storedName ?? deps?.hostname ?? hostname());
391
+ const joinToken = args.token ?? await requestJoinToken({ origin, hostId, name, code: args.code }, deps);
392
+ const reply = await runServerOperation({
393
+ origin,
394
+ signingKey,
395
+ noisePublicKey: hostKey.publicKey,
396
+ op: "join",
397
+ joinToken,
398
+ name
399
+ });
400
+ if (reply.t === "refused") {
401
+ throw new Error(reply.reason === "expired_token" ? "the join token expired — generate a fresh one" : reply.reason === "invalid_token" ? "the join token is invalid or already spent — generate a fresh one" : `the server refused the join: ${reply.reason}`);
402
+ }
403
+ if (reply.t !== "joined")
404
+ throw new Error(`unexpected answer to join: ${reply.t}`);
405
+ const username = reply.username;
406
+ let service;
407
+ try {
408
+ await writeServerConfig(dataDir, { origin, username });
409
+ if (args.name !== undefined)
410
+ await settings.update({ hostName: args.name });
411
+ const edit = args.editHostArgs ?? ((hostArgs) => hostArgs);
412
+ service = await writeService(slot, (hostArgs) => edit(withoutRelayArgs(hostArgs)), { changed: true });
413
+ } catch (error) {
414
+ throw new Error(`joined ${username} at ${origin}, but local setup failed: ${message(error)}
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)`));
416
+ }
417
+ return { hostId, username, origin, service };
418
+ }
419
+ async function main(argv, deps) {
420
+ const { values, positionals } = parseArgs({
421
+ args: [...argv],
422
+ allowPositionals: true,
423
+ options: { "data-dir": { type: "string" }, name: { type: "string" }, code: { type: "boolean" } }
424
+ });
425
+ const [origin, token] = positionals;
426
+ if (origin === undefined || positionals.length > 2) {
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");
430
+ process.exit(1);
431
+ }
432
+ try {
433
+ const outcome = await joinServer({
434
+ origin,
435
+ ...token === undefined ? {} : { token },
436
+ ...values.code === true ? { code: true } : {},
437
+ dataDir: values["data-dir"] ?? DEFAULT_DATA_DIR,
438
+ ...values.name === undefined ? {} : { name: values.name }
439
+ }, deps);
440
+ console.log(`host id: ${outcome.hostId}`);
441
+ console.log(`joined ${outcome.username} at ${outcome.origin}`);
442
+ if (outcome.service === "installed")
443
+ console.log("service installed and started");
444
+ else if (outcome.service === "updated")
445
+ console.log("service definition updated; host restarted when idle");
446
+ else {
447
+ console.error("joined, but the service restart did not finish — check `service status`");
448
+ process.exitCode = 1;
449
+ }
450
+ } catch (error) {
451
+ console.error(message(error));
452
+ process.exitCode = 1;
453
+ }
454
+ }
455
+ if (false)
456
+ ;
457
+
458
+ export { openServiceSlot, writeService, joinServer, main };
@@ -2,31 +2,40 @@
2
2
  import {
3
3
  restartService,
4
4
  serviceManager
5
- } from "./chunk-w2m9xkks.js";
5
+ } from "./chunk-t5abqsva.js";
6
+ import"./chunk-np4jd070.js";
6
7
  import"./chunk-kzbzacz9.js";
8
+ import"./chunk-47ff1d1t.js";
9
+ import"./chunk-pqe7q1s7.js";
7
10
  import {
11
+ hostAddressOptions
12
+ } from "./chunk-ngf1zatm.js";
13
+ import {
14
+ commandHint,
8
15
  globalPrefixOf,
9
16
  packageDir,
10
17
  packagedVersion
11
- } from "./chunk-zehwx009.js";
12
- import"./chunk-4sepcfpy.js";
13
- import"./chunk-jgd38rna.js";
14
- import"./chunk-sygeb9qb.js";
15
- import {
16
- hostAddressOptions
17
- } from "./chunk-ngf1zatm.js";
18
+ } from "./chunk-7wj0jh3t.js";
18
19
  import"./chunk-df32avw6.js";
19
20
 
20
21
  // apps/host/src/cli/upgrade.ts
21
22
  import { execFile, spawn } from "node:child_process";
23
+ import { existsSync } from "node:fs";
24
+ import { dirname, join } from "node:path";
22
25
  import { fileURLToPath } from "node:url";
23
26
  import { parseArgs, promisify } from "node:util";
24
27
  var exec = promisify(execFile);
25
28
  var repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
26
- var step = (command, args) => new Promise((resolve) => {
27
- spawn(command, [...args], { cwd: repoRoot, stdio: "inherit" }).once("exit", (code) => resolve(code ?? 1));
29
+ var step = (command, args, binDir) => new Promise((resolve) => {
30
+ const env = binDir === undefined ? process.env : { ...process.env, PATH: `${binDir}:${process.env.PATH ?? ""}` };
31
+ spawn(command, [...args], { cwd: repoRoot, env, stdio: "inherit" }).once("exit", (code) => resolve(code ?? 1));
28
32
  });
29
- var serviceInstalled = () => Promise.resolve().then(() => serviceManager().state()).then((state) => state.installed, () => false);
33
+ function npmBeside(execPath) {
34
+ const binDir = dirname(execPath);
35
+ const cli = join(binDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
36
+ return existsSync(cli) ? { command: execPath, args: [cli], binDir } : { command: "npm", args: [] };
37
+ }
38
+ var serviceState = () => Promise.resolve().then(() => serviceManager().state()).catch(() => ({ installed: false, stopped: false }));
30
39
  async function main(argv, deps = {}) {
31
40
  const { values } = parseArgs({
32
41
  args: [...argv],
@@ -60,20 +69,28 @@ async function main(argv, deps = {}) {
60
69
  process.exitCode = 1;
61
70
  return;
62
71
  }
63
- const code = await runStep("npm", ["install", "-g", "--prefix", prefix, "pinomad@latest"]);
72
+ const npm = npmBeside(process.execPath);
73
+ const code = await runStep(npm.command, [...npm.args, "install", "-g", "--prefix", prefix, "pinomad@latest"], npm.binDir);
64
74
  if (code !== 0) {
65
75
  process.exitCode = code;
66
76
  return;
67
77
  }
68
78
  }
69
- if (!await (deps.serviceInstalled ?? serviceInstalled)()) {
79
+ const service = await (deps.serviceState ?? serviceState)();
80
+ if (!service.installed) {
70
81
  console.log("service not installed; upgrade done, nothing restarted");
71
82
  process.exitCode = 0;
72
83
  return;
73
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
+ }
74
90
  (deps.exit ?? process.exit)(await restart({ force: values.force, argv: argv.filter((arg) => arg !== "--force") }));
75
91
  }
76
92
  if (false) {}
77
93
  export {
94
+ npmBeside,
78
95
  main
79
96
  };