pinomad 0.10.0 → 0.11.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.
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ joinServer,
4
+ main
5
+ } from "./chunk-gtdpjqa3.js";
6
+ import"./chunk-ncbqa8jk.js";
7
+ import"./chunk-44pnts98.js";
8
+ import"./chunk-t91zt5f0.js";
9
+ import"./chunk-etf7ef97.js";
10
+ import"./chunk-y127fe1a.js";
11
+ import"./chunk-vvj9b1gc.js";
12
+ import"./chunk-jt34g8st.js";
13
+ import"./chunk-kzbzacz9.js";
14
+ import"./chunk-xqycs43g.js";
15
+ import"./chunk-pqe7q1s7.js";
16
+ import"./chunk-ngf1zatm.js";
17
+ import"./chunk-sd5fkxjj.js";
18
+ import"./chunk-df32avw6.js";
19
+ export {
20
+ main,
21
+ joinServer
22
+ };
@@ -0,0 +1,317 @@
1
+ import {
2
+ assertSecureServerOrigin,
3
+ isSecureServerOrigin,
4
+ probeServerAccounts,
5
+ runServerOperation
6
+ } from "./chunk-ncbqa8jk.js";
7
+ import {
8
+ constantValue,
9
+ openHostSettings
10
+ } from "./chunk-44pnts98.js";
11
+ import {
12
+ loadHostKey,
13
+ loadRelayKey
14
+ } from "./chunk-t91zt5f0.js";
15
+ import {
16
+ writeServerConfig
17
+ } from "./chunk-etf7ef97.js";
18
+ import {
19
+ relayHostId
20
+ } from "./chunk-y127fe1a.js";
21
+ import {
22
+ argValue,
23
+ installSpec,
24
+ restartService,
25
+ serviceManager,
26
+ withoutRelayArgs
27
+ } from "./chunk-vvj9b1gc.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-sd5fkxjj.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 { arch, platform } from "node:os";
49
+ var SLOW_DOWN_STEP_MS = 5000;
50
+ var REQUEST_TIMEOUT_MS = 15000;
51
+ var EXPIRY_GRACE_MS = 2 * 60000;
52
+ function browserCommand(os, env) {
53
+ if (env.SSH_CONNECTION !== undefined || env.SSH_TTY !== undefined)
54
+ return;
55
+ if (os === "darwin")
56
+ return "open";
57
+ if (os === "linux" && (env.DISPLAY !== undefined || env.WAYLAND_DISPLAY !== undefined))
58
+ return "xdg-open";
59
+ return;
60
+ }
61
+ function openInBrowser(url) {
62
+ const command = browserCommand(process.platform, process.env);
63
+ 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
+ }
73
+ }
74
+ function hostOsLabel() {
75
+ const os = platform();
76
+ const name = os === "darwin" ? "macOS" : os === "linux" ? "Linux" : os === "win32" ? "Windows" : os;
77
+ return `${name} (${arch()})`;
78
+ }
79
+ var isRecord = (value) => typeof value === "object" && value !== null;
80
+ var errorCode = (body) => isRecord(body) && typeof body.error === "string" ? body.error : undefined;
81
+ async function requestJoinToken(args, deps) {
82
+ const fetchImpl = deps?.fetch ?? fetch;
83
+ const sleep = deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
84
+ const log = deps?.log ?? ((line) => console.log(line));
85
+ const now = deps?.now ?? Date.now;
86
+ const { origin } = args;
87
+ const timeoutMs = deps?.requestTimeoutMs ?? REQUEST_TIMEOUT_MS;
88
+ const post = async (path, payload) => {
89
+ const controller = new AbortController;
90
+ let timer;
91
+ const expired = new Promise((resolve) => {
92
+ timer = setTimeout(() => {
93
+ controller.abort();
94
+ resolve(undefined);
95
+ }, timeoutMs);
96
+ });
97
+ const attempt = (async () => {
98
+ const response = await fetchImpl(`${origin}${path}`, {
99
+ method: "POST",
100
+ headers: { "content-type": "application/json" },
101
+ body: JSON.stringify(payload),
102
+ redirect: "manual",
103
+ signal: controller.signal
104
+ });
105
+ const text = await response.text();
106
+ let body2;
107
+ try {
108
+ body2 = JSON.parse(text);
109
+ } catch {
110
+ body2 = undefined;
111
+ }
112
+ return { status: response.status, body: body2 };
113
+ })().catch(() => {
114
+ return;
115
+ });
116
+ try {
117
+ return await Promise.race([attempt, expired]);
118
+ } finally {
119
+ clearTimeout(timer);
120
+ }
121
+ };
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) {
129
+ 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
+ }
131
+ if (code === "rate_limited")
132
+ throw new Error("too many join requests from this address — wait a few minutes and try again");
133
+ if (code === "busy")
134
+ 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`);
139
+ }
140
+ const { deviceCode, userCode, verifyUrl, expiresAt } = body;
141
+ let intervalMs = Math.max(1, body.interval) * 1000;
142
+ const minutes = Math.max(1, Math.round((expiresAt - now()) / 60000));
143
+ log(`To add this host to your account on ${origin}:`);
144
+ log(` 1. Open ${verifyUrl} and sign in.`);
145
+ log(` 2. Enter this code: ${userCode}`);
146
+ log("");
147
+ 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);
149
+ log(`${opened ? "Opened the page in your browser. " : ""}Waiting for confirmation (the code expires in ${minutes} min)…`);
150
+ for (;; ) {
151
+ await sleep(intervalMs);
152
+ const response = await post("/api/host-link/poll", { deviceCode });
153
+ 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
+ }
157
+ continue;
158
+ }
159
+ const answer = response.body;
160
+ if (response.status === 200 && isRecord(answer) && typeof answer.token === "string") {
161
+ log("Confirmed.");
162
+ return answer.token;
163
+ }
164
+ const code = response.status === 400 ? errorCode(answer) : undefined;
165
+ if (code === "access_denied")
166
+ throw new Error(`the join was denied on ${origin}`);
167
+ if (code === "expired_token")
168
+ throw new Error(`the code expired before anyone confirmed it — run the command again for a new one`);
169
+ if (code === "slow_down") {
170
+ const asked = isRecord(answer) && typeof answer.interval === "number" ? answer.interval * 1000 : 0;
171
+ intervalMs = Math.max(intervalMs + SLOW_DOWN_STEP_MS, asked);
172
+ } else if (code !== "authorization_pending") {
173
+ throw new Error(`${origin} refused the join: ${code ?? `HTTP ${response.status}`}`);
174
+ }
175
+ }
176
+ }
177
+
178
+ // apps/host/src/cli/service-slot.ts
179
+ import { homedir } from "node:os";
180
+ import { resolve } from "node:path";
181
+ async function openServiceSlot(dataDir, manager) {
182
+ const state = await manager.state().catch(() => ({ installed: false, running: false, detail: "unknown" }));
183
+ const previous = state.installed ? await manager.read() : undefined;
184
+ if (previous !== undefined && resolve(previous.dataDir) !== dataDir) {
185
+ 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`);
186
+ }
187
+ return { dataDir, manager, state, previous };
188
+ }
189
+ async function writeService(slot, edit, options) {
190
+ const { dataDir, manager, previous } = slot;
191
+ const fresh = installSpec(["--data-dir", dataDir, ...edit([])], {
192
+ execPath: process.execPath,
193
+ env: process.env,
194
+ home: homedir(),
195
+ cwd: process.cwd(),
196
+ mainPath: hostEntry.script,
197
+ entryArgs: hostEntry.args
198
+ });
199
+ if (!slot.state.installed) {
200
+ await manager.install(fresh);
201
+ return "installed";
202
+ }
203
+ const spec = previous === undefined ? fresh : { ...previous, hostArgs: edit(previous.hostArgs) };
204
+ const same = previous !== undefined && spec.hostArgs.join("\x00") === previous.hostArgs.join("\x00");
205
+ if (same && !options.changed && slot.state.running)
206
+ return "unchanged";
207
+ const port = argValue(spec.hostArgs, "--port");
208
+ const code = await restartService({
209
+ force: false,
210
+ argv: ["--data-dir", dataDir, ...port === undefined ? [] : ["--url", `ws://127.0.0.1:${port}`]],
211
+ manager,
212
+ restart: async () => {
213
+ await manager.install(spec);
214
+ }
215
+ });
216
+ return code === 0 ? "updated" : "failed";
217
+ }
218
+
219
+ // apps/host/src/cli/join.ts
220
+ var message = (error) => error instanceof Error ? error.message : String(error);
221
+ async function joinServer(args, deps) {
222
+ const origin = args.origin.replace(/\/+$/, "");
223
+ const dataDir = resolve2(args.dataDir);
224
+ assertSecureServerOrigin(origin);
225
+ if (args.name !== undefined && cleanHostName(args.name) === "") {
226
+ throw new Error(`Invalid host name: ${JSON.stringify(args.name)}`);
227
+ }
228
+ await mkdir(dataDir, { recursive: true });
229
+ const signingKey = await loadRelayKey(dataDir);
230
+ const hostKey = await loadHostKey(dataDir);
231
+ const hostId = relayHostId(signingKey.publicKey);
232
+ const slot = await openServiceSlot(dataDir, deps?.manager ?? serviceManager());
233
+ const probe = await probeServerAccounts(origin, deps?.fetch);
234
+ if (probe === "unreachable")
235
+ throw new Error(`cannot reach ${origin} — check the address and that the server is up`);
236
+ if (probe === "disabled") {
237
+ throw new Error(`${origin} does not run server accounts — the server is too old or was started without a data dir`);
238
+ }
239
+ const settings = await openHostSettings({
240
+ path: join(dataDir, "settings.json"),
241
+ fallback: {
242
+ defaultModel: () => null,
243
+ defaultThinkingLevel: () => null,
244
+ steeringMode: () => "one-at-a-time",
245
+ followUpMode: () => "one-at-a-time"
246
+ },
247
+ models: constantValue([])
248
+ });
249
+ const current = settings.value;
250
+ const storedName = current.custom.includes("hostName") ? current.hostName : undefined;
251
+ const name = normalizeHostName(args.name ?? storedName ?? deps?.hostname ?? hostname());
252
+ const joinToken = args.token ?? await requestJoinToken({ origin, hostId, name }, deps);
253
+ const reply = await runServerOperation({
254
+ origin,
255
+ signingKey,
256
+ noisePublicKey: hostKey.publicKey,
257
+ op: "join",
258
+ joinToken,
259
+ name
260
+ });
261
+ if (reply.t === "refused") {
262
+ 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}`);
263
+ }
264
+ if (reply.t !== "joined")
265
+ throw new Error(`unexpected answer to join: ${reply.t}`);
266
+ const username = reply.username;
267
+ let service;
268
+ try {
269
+ await writeServerConfig(dataDir, { origin, username });
270
+ if (args.name !== undefined)
271
+ await settings.update({ hostName: args.name });
272
+ const edit = args.editHostArgs ?? ((hostArgs) => hostArgs);
273
+ service = await writeService(slot, (hostArgs) => edit(withoutRelayArgs(hostArgs)), { changed: true });
274
+ } catch (error) {
275
+ 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)`));
277
+ }
278
+ return { hostId, username, origin, service };
279
+ }
280
+ async function main(argv, deps) {
281
+ const { values, positionals } = parseArgs({
282
+ args: [...argv],
283
+ allowPositionals: true,
284
+ options: { "data-dir": { type: "string" }, name: { type: "string" } }
285
+ });
286
+ const [origin, token] = positionals;
287
+ 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");
290
+ process.exit(1);
291
+ }
292
+ try {
293
+ const outcome = await joinServer({
294
+ origin,
295
+ ...token === undefined ? {} : { token },
296
+ dataDir: values["data-dir"] ?? DEFAULT_DATA_DIR,
297
+ ...values.name === undefined ? {} : { name: values.name }
298
+ }, deps);
299
+ console.log(`host id: ${outcome.hostId}`);
300
+ console.log(`joined ${outcome.username} at ${outcome.origin}`);
301
+ if (outcome.service === "installed")
302
+ console.log("service installed and started");
303
+ else if (outcome.service === "updated")
304
+ console.log("service definition updated; host restarted when idle");
305
+ else {
306
+ console.error("joined, but the service restart did not finish — check `service status`");
307
+ process.exitCode = 1;
308
+ }
309
+ } catch (error) {
310
+ console.error(message(error));
311
+ process.exitCode = 1;
312
+ }
313
+ }
314
+ if (false)
315
+ ;
316
+
317
+ export { openServiceSlot, writeService, joinServer, main };
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ main,
4
+ reachabilityLine,
5
+ restartService,
6
+ versionLine
7
+ } from "./chunk-vvj9b1gc.js";
8
+ import"./chunk-jt34g8st.js";
9
+ import"./chunk-kzbzacz9.js";
10
+ import"./chunk-xqycs43g.js";
11
+ import"./chunk-pqe7q1s7.js";
12
+ import"./chunk-ngf1zatm.js";
13
+ import"./chunk-sd5fkxjj.js";
14
+ import"./chunk-df32avw6.js";
15
+ export {
16
+ versionLine,
17
+ restartService,
18
+ reachabilityLine,
19
+ main
20
+ };
@@ -15,7 +15,7 @@ import {
15
15
  organize,
16
16
  parseRtcMaxMessageSize,
17
17
  rtcSelectedPair
18
- } from "./chunk-jgd38rna.js";
18
+ } from "./chunk-xqycs43g.js";
19
19
  import {
20
20
  SECURE_PROLOGUE,
21
21
  SecureSession,
@@ -24,7 +24,7 @@ import {
24
24
  initiateIK,
25
25
  isLoopbackUrl,
26
26
  parseEndpointSet
27
- } from "./chunk-sygeb9qb.js";
27
+ } from "./chunk-pqe7q1s7.js";
28
28
 
29
29
  // packages/protocol/src/remote-durable.ts
30
30
  import { applyImmutable } from "@earendil-works/chord/delta";
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ joinServer,
4
+ openServiceSlot,
5
+ writeService
6
+ } from "./chunk-gtdpjqa3.js";
7
+ import"./chunk-ncbqa8jk.js";
8
+ import"./chunk-44pnts98.js";
9
+ import"./chunk-t91zt5f0.js";
10
+ import {
11
+ normalizeOrigin,
12
+ readServerConfig
13
+ } from "./chunk-etf7ef97.js";
14
+ import"./chunk-y127fe1a.js";
15
+ import {
16
+ argValue,
17
+ serviceManager,
18
+ withoutArg,
19
+ withoutRelayArgs
20
+ } from "./chunk-vvj9b1gc.js";
21
+ import"./chunk-jt34g8st.js";
22
+ import"./chunk-kzbzacz9.js";
23
+ import"./chunk-xqycs43g.js";
24
+ import"./chunk-pqe7q1s7.js";
25
+ import {
26
+ DEFAULT_DATA_DIR
27
+ } from "./chunk-ngf1zatm.js";
28
+ import {
29
+ commandHint
30
+ } from "./chunk-sd5fkxjj.js";
31
+ import"./chunk-df32avw6.js";
32
+
33
+ // apps/host/src/cli/up.ts
34
+ import { resolve } from "node:path";
35
+ import { createInterface } from "node:readline/promises";
36
+ import { parseArgs } from "node:util";
37
+ var OFFICIAL_SERVER_ORIGIN = "https://app.pinomad.si";
38
+ var DEFAULT_REMOTE_PORT = "7422";
39
+ var message = (error) => error instanceof Error ? error.message : String(error);
40
+ function resolveReach(flags, current) {
41
+ if (flags.server === undefined && flags.lan === undefined)
42
+ return;
43
+ if (current !== undefined)
44
+ return { server: flags.server ?? current.server, lan: flags.lan ?? current.lan };
45
+ return { server: flags.server ?? !flags.lan, lan: flags.lan ?? !flags.server };
46
+ }
47
+ async function currentReach(dataDir, deps) {
48
+ const slot = await openServiceSlot(dataDir, deps?.manager ?? serviceManager());
49
+ const config = await readServerConfig(dataDir);
50
+ if (config === undefined && !slot.state.installed)
51
+ return;
52
+ return {
53
+ server: config !== undefined,
54
+ lan: argValue(slot.previous?.hostArgs ?? [], "--remote-port") !== undefined
55
+ };
56
+ }
57
+ function withLan(hostArgs, port) {
58
+ if (port === undefined)
59
+ return withoutArg(withoutArg(hostArgs, "--remote-port"), "--public-url");
60
+ if (argValue(hostArgs, "--remote-port") === port)
61
+ return hostArgs;
62
+ return [...withoutArg(hostArgs, "--remote-port"), "--remote-port", port];
63
+ }
64
+ async function upHost(args, deps) {
65
+ const dataDir = resolve(args.dataDir);
66
+ const { reach } = args;
67
+ if (!reach.server && !reach.lan)
68
+ throw new Error("nothing to set up: choose the server, the LAN, or both");
69
+ const port = args.remotePort;
70
+ if (port !== undefined && !(/^\d+$/.test(port) && Number(port) >= 1 && Number(port) <= 65535)) {
71
+ throw new Error(`--remote-port must be a port number from 1 to 65535, got ${port}`);
72
+ }
73
+ const manager = deps?.manager ?? serviceManager();
74
+ const slot = await openServiceSlot(dataDir, manager);
75
+ const config = await readServerConfig(dataDir);
76
+ const origin = normalizeOrigin(args.origin ?? config?.origin ?? OFFICIAL_SERVER_ORIGIN);
77
+ if (reach.server && config !== undefined && normalizeOrigin(config.origin) !== origin) {
78
+ throw new Error(`this host is joined to ${config.origin} — run \`${commandHint("leave")}\` first to join ${origin}, ` + `or leave out --origin to stay`);
79
+ }
80
+ const lanPort = reach.lan ? args.remotePort ?? argValue(slot.previous?.hostArgs ?? [], "--remote-port") ?? DEFAULT_REMOTE_PORT : undefined;
81
+ const lan = (hostArgs) => withLan(hostArgs, lanPort);
82
+ const notes = [];
83
+ if (reach.server && config === undefined) {
84
+ const joined = await joinServer({ origin, dataDir, editHostArgs: lan }, { ...deps, manager });
85
+ return {
86
+ joined: { origin: joined.origin, username: joined.username, fresh: true },
87
+ ...lanPort === undefined ? {} : { lanPort },
88
+ service: joined.service,
89
+ notes
90
+ };
91
+ }
92
+ const edit = reach.server ? (hostArgs) => lan(withoutRelayArgs(hostArgs)) : lan;
93
+ const service = await writeService(slot, edit, { changed: reach.server });
94
+ if (!reach.server && config !== undefined) {
95
+ notes.push(`still joined to ${config.origin} — run \`${commandHint("leave")}\` to leave it`);
96
+ }
97
+ return {
98
+ ...reach.server && config !== undefined ? { joined: { origin: config.origin, ...config.username === undefined ? {} : { username: config.username }, fresh: false } } : {},
99
+ ...lanPort === undefined ? {} : { lanPort },
100
+ service,
101
+ notes
102
+ };
103
+ }
104
+ async function askOnTerminal(question) {
105
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
106
+ try {
107
+ return await terminal.question(question);
108
+ } finally {
109
+ terminal.close();
110
+ }
111
+ }
112
+ var CHOICES = [
113
+ { server: true, lan: false },
114
+ { server: false, lan: true },
115
+ { server: true, lan: true }
116
+ ];
117
+ async function askReach(origin, fallback, ask) {
118
+ const preset = CHOICES.findIndex((choice) => choice.server === fallback.server && choice.lan === fallback.lan) + 1;
119
+ let question = `How should your other devices reach this host?
120
+ ` + ` 1) Through ${origin}: from anywhere, with push notifications
121
+ ` + ` 2) Only on your local network and tailnet
122
+ ` + ` 3) Both
123
+ ` + `Choose 1, 2 or 3 [${preset}]: `;
124
+ for (;; ) {
125
+ const answer = (await ask(question)).trim();
126
+ if (answer === "")
127
+ return CHOICES[preset - 1];
128
+ const picked = CHOICES[Number(answer) - 1];
129
+ if (/^[123]$/.test(answer) && picked !== undefined)
130
+ return picked;
131
+ question = "Choose 1, 2 or 3: ";
132
+ }
133
+ }
134
+ var FLAGS = `choose with flags:
135
+ --server [--origin URL] through a PiNomad server (default ${OFFICIAL_SERVER_ORIGIN})
136
+ --lan [--remote-port N] only on the local network and tailnet (default port ${DEFAULT_REMOTE_PORT})
137
+ --server --lan both
138
+ --yes keep what this host has, or the server on a new host`;
139
+ async function main(argv, deps) {
140
+ const { values } = parseArgs({
141
+ args: [...argv],
142
+ allowNegative: true,
143
+ options: {
144
+ server: { type: "boolean" },
145
+ lan: { type: "boolean" },
146
+ yes: { type: "boolean", short: "y" },
147
+ origin: { type: "string" },
148
+ "remote-port": { type: "string" },
149
+ "data-dir": { type: "string" }
150
+ }
151
+ });
152
+ const log = deps?.log ?? ((line) => console.log(line));
153
+ try {
154
+ const dataDir = resolve(values["data-dir"] ?? DEFAULT_DATA_DIR);
155
+ const origin = normalizeOrigin(values.origin ?? (await readServerConfig(dataDir))?.origin ?? OFFICIAL_SERVER_ORIGIN);
156
+ const current = await currentReach(dataDir, deps);
157
+ let reach = resolveReach({ server: values.server, lan: values.lan }, current);
158
+ if (reach === undefined) {
159
+ const fallback = current !== undefined && (current.server || current.lan) ? current : { server: true, lan: false };
160
+ if (values.yes === true) {
161
+ reach = fallback;
162
+ } else if (!(deps?.interactive ?? (process.stdin.isTTY && process.stdout.isTTY))) {
163
+ console.error(`${commandHint("up")}: no terminal to ask how other devices reach this host — ${FLAGS}`);
164
+ process.exitCode = 2;
165
+ return;
166
+ } else {
167
+ reach = await askReach(origin, fallback, deps?.ask ?? askOnTerminal);
168
+ }
169
+ }
170
+ if (!reach.server && !reach.lan) {
171
+ console.error(`${commandHint("up")}: neither the server nor the LAN would be on — ` + `\`${commandHint("service")} install\` runs a host only this machine can reach`);
172
+ process.exitCode = 2;
173
+ return;
174
+ }
175
+ const outcome = await upHost({
176
+ dataDir,
177
+ origin,
178
+ reach,
179
+ ...values["remote-port"] === undefined ? {} : { remotePort: values["remote-port"] }
180
+ }, deps);
181
+ if (outcome.joined !== undefined) {
182
+ const who = outcome.joined.username ?? "an account";
183
+ log(outcome.joined.fresh ? `joined ${who} at ${outcome.joined.origin}` : `already joined ${who} at ${outcome.joined.origin}`);
184
+ log(`Sign in at ${outcome.joined.origin} on your other devices; they pair with this host on their own.`);
185
+ }
186
+ if (outcome.lanPort !== undefined) {
187
+ log(`LAN and tailnet: port ${outcome.lanPort}. Pair a device with \`${commandHint("pair")}\`.`);
188
+ }
189
+ for (const note of outcome.notes)
190
+ log(`note: ${note}`);
191
+ if (outcome.service === "installed")
192
+ log("service installed and started");
193
+ else if (outcome.service === "updated")
194
+ log("service updated; host restarted when idle");
195
+ else if (outcome.service === "unchanged")
196
+ log("service already running with these settings");
197
+ else {
198
+ console.error(`the service restart did not finish — check \`${commandHint("status")}\``);
199
+ process.exitCode = 1;
200
+ }
201
+ } catch (error) {
202
+ console.error(message(error));
203
+ process.exitCode = 1;
204
+ }
205
+ }
206
+ if (false)
207
+ ;
208
+ export {
209
+ upHost,
210
+ resolveReach,
211
+ main,
212
+ OFFICIAL_SERVER_ORIGIN,
213
+ DEFAULT_REMOTE_PORT
214
+ };
@@ -6,10 +6,10 @@ import {
6
6
  relayWsBase,
7
7
  signRelayChallenge,
8
8
  signRelayChallengeV2
9
- } from "./chunk-cx8k63by.js";
9
+ } from "./chunk-y127fe1a.js";
10
10
  import {
11
11
  toBase64Url
12
- } from "./chunk-sygeb9qb.js";
12
+ } from "./chunk-pqe7q1s7.js";
13
13
 
14
14
  // apps/host/src/cli/server-ops.ts
15
15
  import { WebSocket } from "ws";
@@ -130,4 +130,4 @@ async function runServerOperation(options) {
130
130
  });
131
131
  }
132
132
 
133
- export { assertSecureServerOrigin, probeServerAccounts, runServerOperation };
133
+ export { isSecureServerOrigin, assertSecureServerOrigin, probeServerAccounts, runServerOperation };
@@ -427,4 +427,4 @@ function concatParts(parts) {
427
427
  return out;
428
428
  }
429
429
 
430
- export { ENDPOINT_SET_SCHEMA_VERSION, MAX_HOST_ENDPOINT_URL_LENGTH, isLoopbackUrl, parseEndpointSet, encodeEndpointSetParam, HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_NAME_MAX_UNITS, cleanHostName, normalizeHostName, generateKeyPair, keyPairFromPrivate, initiateIK, respondIK, SECURE_PROLOGUE, SIGNAL_PROLOGUE, encodeClientHello, encodeHostHello, decodeClientHello, decodeHostHello, toBase64Url, fromBase64Url, SecureSession };
430
+ export { ENDPOINT_SET_SCHEMA_VERSION, MAX_HOST_ENDPOINT_URL_LENGTH, isLoopbackUrl, parseEndpointSet, encodeEndpointSetParam, generateKeyPair, keyPairFromPrivate, initiateIK, respondIK, SECURE_PROLOGUE, SIGNAL_PROLOGUE, encodeClientHello, encodeHostHello, decodeClientHello, decodeHostHello, toBase64Url, fromBase64Url, SecureSession, HOST_SETTING_KEYS, THINKING_LEVELS, isThinkingLevel, isQueueMode, HOST_NAME_MAX_UNITS, cleanHostName, normalizeHostName };
@@ -1,6 +1,6 @@
1
1
  // apps/host/src/distribution.ts
2
2
  import { fileURLToPath } from "node:url";
3
- var packagedVersion = "0.10.0";
3
+ var packagedVersion = "0.11.0";
4
4
  var webRoot = fileURLToPath(new URL(packagedVersion === undefined ? "../../web/dist" : "../web/", import.meta.url));
5
5
  var hostEntry = packagedVersion === undefined ? { script: fileURLToPath(new URL("./main.ts", import.meta.url)), args: [] } : { script: fileURLToPath(new URL("./pinomad.js", import.meta.url)), args: ["host"] };
6
6
  var commandHint = (name) => packagedVersion === undefined ? `bun run ${name}` : `pinomad ${name}`;