@struktoai/mirage-cli 0.0.7-alpha.1 → 0.0.7-alpha.3

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,603 @@
1
+ // src/credentials.ts
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ openSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ writeSync,
10
+ closeSync
11
+ } from "fs";
12
+ import { dirname, join } from "path";
13
+ import { mirageHome } from "@struktoai/mirage-server/paths";
14
+ var LOGIN_FILE = "login.json";
15
+ var LOGIN_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
16
+ var REFRESH_MARGIN_SECONDS = 60;
17
+ var LoginError = class extends Error {
18
+ name = "LoginError";
19
+ };
20
+ function renewBy(login) {
21
+ const ends = login.logged_in_at + LOGIN_MAX_AGE_SECONDS;
22
+ if (login.refresh_token === null && login.expires_at !== null) {
23
+ return Math.min(ends, login.expires_at);
24
+ }
25
+ return ends;
26
+ }
27
+ function loginPath(env = process.env) {
28
+ return join(mirageHome(env), LOGIN_FILE);
29
+ }
30
+ function readLogin(path = loginPath()) {
31
+ if (!existsSync(path)) return null;
32
+ return JSON.parse(readFileSync(path, "utf-8"));
33
+ }
34
+ function writeLogin(login, path = loginPath()) {
35
+ mkdirSync(dirname(path), { recursive: true });
36
+ const staged = join(dirname(path), `.${LOGIN_FILE}.${String(process.pid)}`);
37
+ const fd = openSync(staged, "w", 384);
38
+ try {
39
+ writeSync(fd, JSON.stringify(login, null, 2));
40
+ } finally {
41
+ closeSync(fd);
42
+ }
43
+ renameSync(staged, path);
44
+ }
45
+ function removeLogin(path = loginPath()) {
46
+ const login = readLogin(path);
47
+ rmSync(path, { force: true });
48
+ return login;
49
+ }
50
+ function tokenClaims(token) {
51
+ const parts = token.split(".");
52
+ if (parts.length !== 3) return {};
53
+ try {
54
+ const claims = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf-8"));
55
+ return typeof claims === "object" && claims !== null && !Array.isArray(claims) ? claims : {};
56
+ } catch {
57
+ return {};
58
+ }
59
+ }
60
+ async function jsonBody(reply) {
61
+ try {
62
+ const body = await reply.json();
63
+ return typeof body === "object" && body !== null && !Array.isArray(body) ? body : {};
64
+ } catch {
65
+ return {};
66
+ }
67
+ }
68
+ function endsAt(body, now) {
69
+ const expiresIn = body.expires_in;
70
+ return typeof expiresIn === "number" && expiresIn > 0 ? now + expiresIn : null;
71
+ }
72
+ async function freshToken(login, path = loginPath()) {
73
+ const now = Date.now() / 1e3;
74
+ const again = `run \`mirage login\` to log in to ${login.url} again`;
75
+ if (now >= login.logged_in_at + LOGIN_MAX_AGE_SECONDS) {
76
+ throw new LoginError(`the login is 30 days old; ${again}`);
77
+ }
78
+ if (login.expires_at === null || now < login.expires_at - REFRESH_MARGIN_SECONDS) {
79
+ return login.access_token;
80
+ }
81
+ if (login.refresh_token === null || login.token_endpoint === null || login.client_id === null) {
82
+ throw new LoginError(`the token has expired; ${again}`);
83
+ }
84
+ let reply;
85
+ try {
86
+ reply = await fetch(login.token_endpoint, {
87
+ method: "POST",
88
+ body: new URLSearchParams({
89
+ grant_type: "refresh_token",
90
+ refresh_token: login.refresh_token,
91
+ client_id: login.client_id
92
+ }),
93
+ signal: AbortSignal.timeout(3e4)
94
+ });
95
+ } catch (error) {
96
+ throw new LoginError(`could not reach the issuer: ${String(error)}`);
97
+ }
98
+ const body = await jsonBody(reply);
99
+ if (reply.status !== 200 || typeof body.access_token !== "string") {
100
+ const reason = typeof body.error === "string" ? body.error : String(reply.status);
101
+ throw new LoginError(`the issuer refused to refresh the login (${reason}); ${again}`);
102
+ }
103
+ login.access_token = body.access_token;
104
+ if (typeof body.refresh_token === "string" && body.refresh_token !== "") {
105
+ login.refresh_token = body.refresh_token;
106
+ }
107
+ login.expires_at = endsAt(body, now);
108
+ writeLogin(login, path);
109
+ return login.access_token;
110
+ }
111
+
112
+ // src/settings.ts
113
+ import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
114
+ import { dirname as dirname2, join as join2 } from "path";
115
+ import { rstripSlash } from "@struktoai/mirage-core/utils/slash";
116
+ import { compareCodePoints } from "@struktoai/mirage-core/utils/sort";
117
+ import { CONFIG_ENV_KEYS } from "@struktoai/mirage-server/auth/config";
118
+ import { defaultTokenFile, readTokenFile } from "@struktoai/mirage-server/auth/storage";
119
+ import {
120
+ ENV_ALLOWED_HOSTS,
121
+ ENV_DAEMON_PORT as ENV_DAEMON_PORT2,
122
+ ENV_IDLE_GRACE_SECONDS as ENV_IDLE_GRACE_SECONDS2
123
+ } from "@struktoai/mirage-server/env";
124
+ import {
125
+ ALLOWED_KEYS,
126
+ DaemonConfigError,
127
+ NUMERIC_KEYS,
128
+ parseDaemonTable,
129
+ readDaemonTable
130
+ } from "@struktoai/mirage-server/daemon_config";
131
+ import { DEFAULT_ALLOWED_HOSTS } from "@struktoai/mirage-server/host_validation_constants";
132
+ import { mirageHome as mirageHome2 } from "@struktoai/mirage-server/paths";
133
+ import {
134
+ AUTHORIZED_KEYS_NAME,
135
+ DEFAULT_SSH_HOST,
136
+ HOST_KEY_NAME,
137
+ SSH_ENV_KEYS
138
+ } from "@struktoai/mirage-server/ssh/constants";
139
+ import { defaultSSHDir } from "@struktoai/mirage-server/ssh/config";
140
+
141
+ // src/env.ts
142
+ import { ENV_AUTH_MODE, ENV_AUTH_TOKEN } from "@struktoai/mirage-server/auth/config";
143
+ import { ENV_DAEMON_PORT, ENV_IDLE_GRACE_SECONDS } from "@struktoai/mirage-server/env";
144
+ var ENV_DAEMON_URL = "MIRAGE_DAEMON_URL";
145
+ var ENV_TOKEN = "MIRAGE_TOKEN";
146
+
147
+ // src/settings.ts
148
+ var DEFAULT_DAEMON_URL = "http://127.0.0.1:8765";
149
+ function isLocalUrl(url) {
150
+ let host;
151
+ try {
152
+ host = new URL(url).hostname;
153
+ } catch {
154
+ return false;
155
+ }
156
+ return DEFAULT_ALLOWED_HOSTS.includes(host.replace(/^\[(.*)\]$/, "$1"));
157
+ }
158
+ function loadDaemonSettings(options = {}) {
159
+ const env = options.env ?? process.env;
160
+ const table = options.configPath !== void 0 ? existsSync2(options.configPath) ? parseDaemonTable(readFileSync2(options.configPath, "utf-8")) : {} : readDaemonTable(mirageHome2(env));
161
+ const settings = {
162
+ url: table.url ?? DEFAULT_DAEMON_URL,
163
+ authToken: table.auth_token ?? "",
164
+ idleGraceSeconds: Number(table.idle_grace_seconds ?? "30")
165
+ };
166
+ const envUrl = env[ENV_DAEMON_URL];
167
+ if (envUrl !== void 0 && envUrl !== "") {
168
+ settings.url = envUrl;
169
+ }
170
+ const envToken = env[ENV_TOKEN];
171
+ if (envToken !== void 0 && envToken !== "") {
172
+ settings.authToken = envToken;
173
+ }
174
+ if (settings.authToken === "") {
175
+ const login = readLogin(loginPath(env));
176
+ if (login !== null && login.url === rstripSlash(settings.url)) {
177
+ settings.login = login;
178
+ return settings;
179
+ }
180
+ }
181
+ if (settings.authToken === "" && isLocalUrl(settings.url)) {
182
+ const fileToken = readTokenFile(options.tokenFile ?? defaultTokenFile(env));
183
+ if (fileToken !== void 0 && fileToken !== "") {
184
+ settings.authToken = fileToken;
185
+ }
186
+ }
187
+ return settings;
188
+ }
189
+ function configPath(env = process.env) {
190
+ return join2(mirageHome2(env), "config.toml");
191
+ }
192
+ var ENV_FOR_KEY = {
193
+ url: ENV_DAEMON_URL,
194
+ allowed_hosts: ENV_ALLOWED_HOSTS,
195
+ ...CONFIG_ENV_KEYS,
196
+ auth_token: ENV_TOKEN,
197
+ idle_grace_seconds: ENV_IDLE_GRACE_SECONDS2,
198
+ port: ENV_DAEMON_PORT2,
199
+ ...SSH_ENV_KEYS
200
+ };
201
+ function defaultForKey(key, home) {
202
+ const defaults = {
203
+ url: DEFAULT_DAEMON_URL,
204
+ allowed_hosts: DEFAULT_ALLOWED_HOSTS.join(","),
205
+ auth_mode: "local",
206
+ jwt_alg: "",
207
+ jwt_issuer: "",
208
+ jwt_audience: "",
209
+ jwt_pubkey_file: "",
210
+ jwt_jwks_url: "",
211
+ jwt_clock_skew: "5",
212
+ jwt_authorized_parties: "",
213
+ login_client_id: "",
214
+ socket: "",
215
+ auth_token: "",
216
+ idle_grace_seconds: "30",
217
+ port: "8765",
218
+ ssh_port: "",
219
+ ssh_host: DEFAULT_SSH_HOST,
220
+ ssh_host_key_file: join2(defaultSSHDir(home), HOST_KEY_NAME),
221
+ ssh_authorized_keys: join2(defaultSSHDir(home), AUTHORIZED_KEYS_NAME)
222
+ };
223
+ return defaults[key] ?? "";
224
+ }
225
+ function resolvedConfig(env = process.env) {
226
+ const home = mirageHome2(env);
227
+ const table = readDaemonTable(home);
228
+ const out = {};
229
+ for (const key of [...ALLOWED_KEYS].sort(compareCodePoints)) {
230
+ const envName = ENV_FOR_KEY[key];
231
+ if (envName !== void 0) {
232
+ const envValue = env[envName];
233
+ if (envValue !== void 0 && envValue !== "") {
234
+ out[key] = [envValue, `env ${envName}`];
235
+ continue;
236
+ }
237
+ }
238
+ const fileValue = table[key];
239
+ if (fileValue !== void 0 && fileValue !== "") {
240
+ out[key] = [fileValue, "file"];
241
+ } else {
242
+ out[key] = [defaultForKey(key, home), "default"];
243
+ }
244
+ }
245
+ return out;
246
+ }
247
+ function checkKey(key) {
248
+ if (!ALLOWED_KEYS.has(key)) {
249
+ throw new DaemonConfigError(
250
+ `unknown config key: '${key}'; allowed: ${[...ALLOWED_KEYS].sort(compareCodePoints).join(", ")}`
251
+ );
252
+ }
253
+ }
254
+ function formatValue(key, value) {
255
+ if (NUMERIC_KEYS.has(key)) return value;
256
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
257
+ }
258
+ function listConfig(path) {
259
+ const p = path ?? configPath(process.env);
260
+ if (!existsSync2(p)) return {};
261
+ return parseDaemonTable(readFileSync2(p, "utf-8"));
262
+ }
263
+ function getConfig(key, path) {
264
+ checkKey(key);
265
+ return listConfig(path)[key];
266
+ }
267
+ function setConfig(key, value, path) {
268
+ checkKey(key);
269
+ const p = path ?? configPath(process.env);
270
+ const lines = existsSync2(p) ? readFileSync2(p, "utf-8").split("\n") : [];
271
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
272
+ const rendered = `${key} = ${formatValue(key, value)}`;
273
+ let headerIdx = -1;
274
+ for (let i = 0; i < lines.length; i++) {
275
+ if ((lines[i] ?? "").trim() === "[daemon]") {
276
+ headerIdx = i;
277
+ break;
278
+ }
279
+ }
280
+ if (headerIdx < 0) {
281
+ if (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() !== "") lines.push("");
282
+ lines.push("[daemon]", rendered);
283
+ } else {
284
+ let end = lines.length;
285
+ for (let i = headerIdx + 1; i < lines.length; i++) {
286
+ if ((lines[i] ?? "").trim().startsWith("[")) {
287
+ end = i;
288
+ break;
289
+ }
290
+ }
291
+ let replaced = false;
292
+ for (let i = headerIdx + 1; i < end; i++) {
293
+ const t = (lines[i] ?? "").trim();
294
+ if (t.startsWith("#") || !t.includes("=")) continue;
295
+ if (t.slice(0, t.indexOf("=")).trim() === key) {
296
+ lines[i] = rendered;
297
+ replaced = true;
298
+ break;
299
+ }
300
+ }
301
+ if (!replaced) lines.splice(end, 0, rendered);
302
+ }
303
+ mkdirSync2(dirname2(p), { recursive: true });
304
+ writeFileSync(p, lines.join("\n") + "\n");
305
+ chmodSync(p, 384);
306
+ }
307
+ function unsetConfig(key, path) {
308
+ const p = path ?? configPath(process.env);
309
+ if (!existsSync2(p)) return;
310
+ const lines = readFileSync2(p, "utf-8").split("\n");
311
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
312
+ const kept = [];
313
+ let inDaemon = false;
314
+ for (const line of lines) {
315
+ const t = line.trim();
316
+ if (t === "[daemon]") {
317
+ inDaemon = true;
318
+ kept.push(line);
319
+ continue;
320
+ }
321
+ if (t.startsWith("[")) inDaemon = false;
322
+ if (inDaemon && t.includes("=") && !t.startsWith("#") && t.slice(0, t.indexOf("=")).trim() === key)
323
+ continue;
324
+ kept.push(line);
325
+ }
326
+ writeFileSync(p, kept.join("\n") + "\n");
327
+ chmodSync(p, 384);
328
+ }
329
+
330
+ // src/client.ts
331
+ import { spawn } from "child_process";
332
+ import { randomUUID } from "crypto";
333
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, openSync as openSync2 } from "fs";
334
+ import { createRequire } from "module";
335
+ import { connect } from "net";
336
+ import { join as join3 } from "path";
337
+ import { AuthMode } from "@struktoai/mirage-server/auth/config";
338
+ import { defaultTokenFile as defaultTokenFile2, ensureTokenFile } from "@struktoai/mirage-server/auth/storage";
339
+ import { readDaemonTable as readDaemonTable2, validateDaemonTable } from "@struktoai/mirage-server/daemon_config";
340
+ import { mirageHome as mirageHome3 } from "@struktoai/mirage-server/paths";
341
+ var requireFromHere = createRequire(import.meta.url);
342
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
343
+ var DaemonUnreachable = class extends Error {
344
+ name = "DaemonUnreachable";
345
+ };
346
+ var DaemonClient = class {
347
+ settings;
348
+ refreshing;
349
+ /** The last login token this client sent. */
350
+ held = "";
351
+ constructor(settings) {
352
+ this.settings = settings;
353
+ }
354
+ /**
355
+ * The bearer token to send: the settings' own, else the login's,
356
+ * refreshed when it is about to end; empty when there is none. The client
357
+ * stays bound to the login it started with. That login is read from its
358
+ * file each time, so a refresh made by another process is shared, while
359
+ * `mirage logout` or a new login stops this one; requests sent at once
360
+ * share one read, so they refresh it once. The token sent is kept as
361
+ * `held`. Throws `LoginError` when the login ended or changed, or cannot
362
+ * give a token.
363
+ */
364
+ async token() {
365
+ const login = this.settings.login;
366
+ if (this.settings.authToken !== "" || login === void 0) {
367
+ this.held = this.settings.authToken;
368
+ return this.held;
369
+ }
370
+ this.refreshing ??= (async () => {
371
+ const stored = readLogin();
372
+ if (stored?.url !== login.url) {
373
+ throw new LoginError(`not logged in to ${login.url} any more; run \`mirage login\``);
374
+ }
375
+ if (stored.logged_in_at !== login.logged_in_at) {
376
+ throw new LoginError(`the login to ${login.url} changed; run the command again`);
377
+ }
378
+ this.held = await freshToken(stored);
379
+ return this.held;
380
+ })().finally(() => {
381
+ this.refreshing = void 0;
382
+ });
383
+ return this.refreshing;
384
+ }
385
+ async headers(extra) {
386
+ const h = { ...extra };
387
+ const token = await this.token();
388
+ if (token !== "") h.Authorization = `Bearer ${token}`;
389
+ return h;
390
+ }
391
+ async request(method, path, init = {}) {
392
+ const { timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, ...rest } = init;
393
+ const headers = {
394
+ ...await this.headers(),
395
+ ...rest.headers ?? {}
396
+ };
397
+ if (rest.body !== void 0 && headers["Content-Type"] === void 0) {
398
+ headers["Content-Type"] = "application/json";
399
+ }
400
+ if (timeoutMs === null) return fetch(this.settings.url + path, { ...rest, method, headers });
401
+ const ctrl = new AbortController();
402
+ const t = setTimeout(() => {
403
+ ctrl.abort();
404
+ }, timeoutMs);
405
+ try {
406
+ return await fetch(this.settings.url + path, {
407
+ ...rest,
408
+ method,
409
+ headers,
410
+ signal: ctrl.signal
411
+ });
412
+ } finally {
413
+ clearTimeout(t);
414
+ }
415
+ }
416
+ /**
417
+ * Send a JSON `request` part and then `part`, streamed from its data
418
+ * as it is read, as one multipart body. There is no overall timeout,
419
+ * since the upload lasts as long as its input; `signal` aborts it.
420
+ */
421
+ async requestUpload(method, path, request, part, signal) {
422
+ const boundary = `mirage-${randomUUID()}`;
423
+ const text = new TextEncoder();
424
+ const head = text.encode(
425
+ `--${boundary}\r
426
+ Content-Disposition: form-data; name="request"; filename="request.json"\r
427
+ Content-Type: application/json\r
428
+ \r
429
+ ${JSON.stringify(request)}\r
430
+ --${boundary}\r
431
+ Content-Disposition: form-data; name="${part.name}"; filename="${part.name}.bin"\r
432
+ Content-Type: application/octet-stream\r
433
+ \r
434
+ `
435
+ );
436
+ const tail = text.encode(`\r
437
+ --${boundary}--\r
438
+ `);
439
+ async function* parts() {
440
+ yield head;
441
+ for await (const chunk of part.data) yield chunk;
442
+ yield tail;
443
+ }
444
+ return fetch(this.settings.url + path, {
445
+ method,
446
+ headers: await this.headers({
447
+ "Content-Type": `multipart/form-data; boundary=${boundary}`
448
+ }),
449
+ body: ReadableStream.from(parts()),
450
+ duplex: "half",
451
+ ...signal !== void 0 ? { signal } : {}
452
+ });
453
+ }
454
+ async isReachable(timeoutMs = 500) {
455
+ const headers = await this.headers();
456
+ const ctrl = new AbortController();
457
+ const t = setTimeout(() => {
458
+ ctrl.abort();
459
+ }, timeoutMs);
460
+ try {
461
+ const r = await fetch(this.settings.url + "/v1/health", {
462
+ headers,
463
+ signal: ctrl.signal
464
+ });
465
+ return r.status === 200;
466
+ } catch {
467
+ return false;
468
+ } finally {
469
+ clearTimeout(t);
470
+ }
471
+ }
472
+ /**
473
+ * Ensure the daemon is reachable, optionally spawning it. `timeoutMs` is
474
+ * how long a spawned daemon has to answer `/v1/health`: a first start
475
+ * loads the whole server. A daemon that exits first ends the wait, unless
476
+ * another daemon already holds the port. Mirrors Python's
477
+ * `ensure_running`.
478
+ */
479
+ async ensureRunning(opts = {}) {
480
+ const allowSpawn = opts.allowSpawn ?? true;
481
+ const timeoutMs = opts.timeoutMs ?? 3e4;
482
+ if (await this.isReachable()) return;
483
+ if (!isLocalUrl(this.settings.url)) {
484
+ throw new DaemonUnreachable(`daemon not reachable at ${this.settings.url}`);
485
+ }
486
+ if (!allowSpawn) {
487
+ throw new DaemonUnreachable(
488
+ `daemon not reachable at ${this.settings.url}; run \`mirage workspace create CONFIG.yaml\` to spawn one`
489
+ );
490
+ }
491
+ const daemon = this.spawnDaemon();
492
+ const deadline = Date.now() + timeoutMs;
493
+ while (Date.now() < deadline) {
494
+ if (await this.isReachable(300)) return;
495
+ const code = daemon.exitCode ?? daemon.signalCode;
496
+ if (code !== null && !await this.listening()) {
497
+ throw new DaemonUnreachable(
498
+ `daemon exited with code ${String(code)} before answering /v1/health; see ${join3(mirageHome3(), "daemon.log")}`
499
+ );
500
+ }
501
+ await new Promise((r) => setTimeout(r, 100));
502
+ }
503
+ throw new DaemonUnreachable(
504
+ `daemon spawned but did not answer /v1/health within ${String(timeoutMs)}ms`
505
+ );
506
+ }
507
+ /** Whether anything accepts a connection on the daemon's port. */
508
+ listening() {
509
+ return new Promise((resolve) => {
510
+ const sock = connect({ host: new URL(this.settings.url).hostname, port: this.portFromUrl() });
511
+ const done = (up) => {
512
+ sock.destroy();
513
+ resolve(up);
514
+ };
515
+ sock.setTimeout(300);
516
+ sock.once("connect", () => {
517
+ done(true);
518
+ });
519
+ sock.once("timeout", () => {
520
+ done(false);
521
+ });
522
+ sock.once("error", (err) => {
523
+ console.debug(`nothing accepts on the daemon's port: ${String(err)}`);
524
+ done(false);
525
+ });
526
+ });
527
+ }
528
+ spawnDaemon() {
529
+ const table = readDaemonTable2(mirageHome3());
530
+ validateDaemonTable(table);
531
+ const env = {};
532
+ for (const [k, v] of Object.entries(process.env)) {
533
+ if (typeof v === "string") env[k] = v;
534
+ }
535
+ env[ENV_DAEMON_PORT] = String(this.resolvePort(table));
536
+ if ((env[ENV_IDLE_GRACE_SECONDS] ?? "") === "") {
537
+ env[ENV_IDLE_GRACE_SECONDS] = String(this.settings.idleGraceSeconds);
538
+ }
539
+ if (this.settings.authToken === "") {
540
+ this.settings.authToken = ensureTokenFile(defaultTokenFile2());
541
+ }
542
+ env[ENV_AUTH_TOKEN] = this.settings.authToken;
543
+ if ((env[ENV_AUTH_MODE] ?? "") === "" && (table.auth_mode ?? "") === "") {
544
+ env[ENV_AUTH_MODE] = AuthMode.Local;
545
+ }
546
+ const logDir = mirageHome3();
547
+ mkdirSync3(logDir, { recursive: true });
548
+ const out = openSync2(join3(logDir, "daemon.log"), "a");
549
+ const daemonEntry = requireFromHere.resolve("@struktoai/mirage-server/bin/daemon");
550
+ if (!existsSync3(daemonEntry)) {
551
+ throw new Error(
552
+ `daemon binary not found at ${daemonEntry}; reinstall @struktoai/mirage-server`
553
+ );
554
+ }
555
+ const child = spawn(process.execPath, [daemonEntry], {
556
+ env,
557
+ detached: true,
558
+ stdio: ["ignore", out, out]
559
+ });
560
+ child.on("error", (err) => {
561
+ console.error("failed to spawn daemon:", err);
562
+ });
563
+ child.unref();
564
+ return child;
565
+ }
566
+ resolvePort(table) {
567
+ const envPort = process.env[ENV_DAEMON_PORT];
568
+ if (envPort !== void 0 && envPort !== "") return Number(envPort);
569
+ const configPort = table.port;
570
+ if (configPort !== void 0 && configPort !== "") return Number(configPort);
571
+ return this.portFromUrl();
572
+ }
573
+ portFromUrl() {
574
+ const u = new URL(this.settings.url);
575
+ return Number(u.port) || 8765;
576
+ }
577
+ };
578
+ function makeClient(settings) {
579
+ return new DaemonClient(settings);
580
+ }
581
+
582
+ export {
583
+ LoginError,
584
+ renewBy,
585
+ readLogin,
586
+ writeLogin,
587
+ removeLogin,
588
+ tokenClaims,
589
+ jsonBody,
590
+ endsAt,
591
+ DEFAULT_DAEMON_URL,
592
+ isLocalUrl,
593
+ loadDaemonSettings,
594
+ resolvedConfig,
595
+ listConfig,
596
+ getConfig,
597
+ setConfig,
598
+ unsetConfig,
599
+ DaemonUnreachable,
600
+ DaemonClient,
601
+ makeClient
602
+ };
603
+ //# sourceMappingURL=chunk-IJUWWUKK.js.map