mindwire 0.1.34 → 0.1.36

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 (39) hide show
  1. package/dist/binary-cache-CG52TXCZ.js +3 -0
  2. package/dist/{binary-cache-VM7VHCEU.js.map → binary-cache-CG52TXCZ.js.map} +1 -1
  3. package/dist/chunk-67BNDGRK.js +20 -0
  4. package/dist/chunk-67BNDGRK.js.map +1 -0
  5. package/dist/{chunk-3FR7BFAW.js → chunk-PFYKJ6LX.js} +56 -4
  6. package/dist/chunk-PFYKJ6LX.js.map +1 -0
  7. package/dist/{chunk-3QFDSYKC.js → chunk-RTGZDLSG.js} +523 -172
  8. package/dist/chunk-RTGZDLSG.js.map +1 -0
  9. package/dist/cli.js +763 -32
  10. package/dist/cli.js.map +1 -1
  11. package/dist/computer/connection-change.d.ts +13 -0
  12. package/dist/computer/connection-flow.d.ts +3 -3
  13. package/dist/computer/directory-http.d.ts +8 -0
  14. package/dist/computer/discovery.d.ts +13 -0
  15. package/dist/computer/lifecycle.d.ts +2 -1
  16. package/dist/computer/managed-relay.d.ts +27 -0
  17. package/dist/computer/oblien-auth.d.ts +25 -0
  18. package/dist/computer/provider-errors.d.ts +9 -0
  19. package/dist/computer/provider-info.d.ts +7 -0
  20. package/dist/computer/provider-setup.d.ts +20 -0
  21. package/dist/computer/relay-health.d.ts +2 -2
  22. package/dist/computer/relay.d.ts +11 -3
  23. package/dist/computer/setup-prompt.d.ts +9 -0
  24. package/dist/computer.d.ts +27 -1
  25. package/dist/connection-change-IBNL42BL.js +41 -0
  26. package/dist/connection-change-IBNL42BL.js.map +1 -0
  27. package/dist/{controller-AGT6MCJG.js → controller-2SJLZMJZ.js} +199 -279
  28. package/dist/controller-2SJLZMJZ.js.map +1 -0
  29. package/dist/{controller-handoff-3LAYBJUL.js → controller-handoff-RUUHODNC.js} +4 -4
  30. package/dist/{controller-handoff-3LAYBJUL.js.map → controller-handoff-RUUHODNC.js.map} +1 -1
  31. package/dist/index.cjs +12 -3
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.js +12 -3
  34. package/dist/index.js.map +1 -1
  35. package/package.json +5 -5
  36. package/dist/binary-cache-VM7VHCEU.js +0 -4
  37. package/dist/chunk-3FR7BFAW.js.map +0 -1
  38. package/dist/chunk-3QFDSYKC.js.map +0 -1
  39. package/dist/controller-AGT6MCJG.js.map +0 -1
@@ -1,12 +1,12 @@
1
- import { MindwireError, TimeoutError, ApiError, RunFailedError } from './chunk-T6TD76UA.js';
1
+ import { MindwireError, binaryCacheDirectory, cachedExecutable, verifyReleaseBytes, cacheExecutable, executableChecksum, TimeoutError, ApiError, RunFailedError } from './chunk-PFYKJ6LX.js';
2
2
  import { execFile, spawn } from 'child_process';
3
- import * as fs from 'fs/promises';
4
- import { readFile } from 'fs/promises';
3
+ import * as fs2 from 'fs/promises';
4
+ import { readFile, open, rm, mkdtemp, writeFile } from 'fs/promises';
5
5
  import { promisify } from 'util';
6
6
  import { setTimeout as setTimeout$1 } from 'timers/promises';
7
7
  import { randomUUID } from 'crypto';
8
- import { homedir, networkInterfaces } from 'os';
9
- import * as path from 'path';
8
+ import { tmpdir, homedir, networkInterfaces } from 'os';
9
+ import * as path4 from 'path';
10
10
 
11
11
  // src/computer.ts
12
12
  var ComputerApi = class {
@@ -17,8 +17,17 @@ var ComputerApi = class {
17
17
  info() {
18
18
  return this.client.http.request("GET", "/computer");
19
19
  }
20
- setRoutes(routes) {
21
- return this.client.http.request("PUT", "/computer/routes", { body: { routes } });
20
+ discovery() {
21
+ return this.client.http.request("GET", "/computer/discovery");
22
+ }
23
+ configureDiscovery(enabled, url) {
24
+ return this.client.http.request("PUT", "/computer/discovery", { body: { enabled, url } });
25
+ }
26
+ directoryEnrollment(url, challenge) {
27
+ return this.client.http.request("POST", "/computer/discovery/enrollment", { body: { url, challenge } });
28
+ }
29
+ setRoutes(routes, connection) {
30
+ return this.client.http.request("PUT", "/computer/routes", { body: { routes, connection } });
22
31
  }
23
32
  updateStatus() {
24
33
  return this.client.http.request("GET", "/computer/update");
@@ -79,7 +88,7 @@ function computerPairingURI(invitation) {
79
88
  }
80
89
 
81
90
  // src/version.ts
82
- var SDK_VERSION = "0.1.34" ;
91
+ var SDK_VERSION = "0.1.36" ;
83
92
  function versionAtLeast(actual, desired) {
84
93
  if (actual === desired) return true;
85
94
  if (!actual || !/^\d+\.\d+\.\d+$/.test(actual) || !/^\d+\.\d+\.\d+$/.test(desired)) return false;
@@ -117,12 +126,12 @@ async function ensureDaemonBinary(opts = {}) {
117
126
  if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
118
127
  throw new MindwireError(`mindwire: cannot download daemon for non-release SDK version ${version}`);
119
128
  }
120
- const path2 = await import('path');
121
- const { binaryCacheDirectory, cachedExecutable, cacheExecutable } = await import('./binary-cache-VM7VHCEU.js');
122
- const dir = path2.join(opts.cacheDir ?? await binaryCacheDirectory(daemonPlatform), version, `${daemonPlatform}-${daemonArch}`);
129
+ const path5 = await import('path');
130
+ const { binaryCacheDirectory: binaryCacheDirectory2, cachedExecutable: cachedExecutable2, cacheExecutable: cacheExecutable2 } = await import('./binary-cache-CG52TXCZ.js');
131
+ const dir = path5.join(opts.cacheDir ?? await binaryCacheDirectory2(daemonPlatform), version, `${daemonPlatform}-${daemonArch}`);
123
132
  const asset = assetName(version, daemonPlatform, daemonArch);
124
- const bin = path2.join(dir, asset);
125
- const cached = await cachedExecutable(bin);
133
+ const bin = path5.join(dir, asset);
134
+ const cached = await cachedExecutable2(bin);
126
135
  if (cached) return cached;
127
136
  const base = (opts.releaseBaseUrl ?? proc.process?.env?.MINDWIRE_RELEASE_BASE_URL ?? "https://github.com/oblien/mindwire/releases/download").replace(/\/$/, "");
128
137
  const release = `${base}/v${version}`;
@@ -145,7 +154,7 @@ async function ensureDaemonBinary(opts = {}) {
145
154
  const expected = checksumFor(await checksums.text(), asset);
146
155
  if (!expected) throw new MindwireError(`mindwire: release v${version} has no checksum for ${asset}`);
147
156
  const bytes = new Uint8Array(await binary.arrayBuffer());
148
- return cacheExecutable(bin, bytes, expected);
157
+ return cacheExecutable2(bin, bytes, expected);
149
158
  }
150
159
  var execute = promisify(execFile);
151
160
  function processAlive(pid) {
@@ -215,7 +224,7 @@ async function ownChild(child) {
215
224
  const alive = () => child.exitCode === null && child.signalCode === null;
216
225
  return { pid, identity, alive, async close() {
217
226
  if (!alive()) return;
218
- const exited = new Promise((resolve) => child.once("exit", () => resolve()));
227
+ const exited = new Promise((resolve3) => child.once("exit", () => resolve3()));
219
228
  child.kill("SIGTERM");
220
229
  const timer = setTimeout(() => {
221
230
  if (alive()) child.kill("SIGKILL");
@@ -240,6 +249,413 @@ async function adoptProcess(identity) {
240
249
  if (await matches()) process.kill(identity.pid, "SIGKILL");
241
250
  } };
242
251
  }
252
+ var RelayOutput = class _RelayOutput {
253
+ constructor(file, handle) {
254
+ this.file = file;
255
+ this.handle = handle;
256
+ }
257
+ file;
258
+ handle;
259
+ position = 0;
260
+ timer;
261
+ pending = Promise.resolve();
262
+ closing;
263
+ static async create(directory = tmpdir()) {
264
+ const file = path4.join(directory, `.relay-output-${randomUUID()}.log`);
265
+ return new _RelayOutput(file, await open(file, "ax+", 384));
266
+ }
267
+ static async adopt(directory, file) {
268
+ if (path4.dirname(file) !== path4.resolve(directory) || !/^\.relay-output-[a-f0-9-]{36}\.log$/.test(path4.basename(file))) return;
269
+ try {
270
+ const output = new _RelayOutput(file, await open(file, "r+"));
271
+ output.discard();
272
+ return output;
273
+ } catch (error) {
274
+ if (error.code !== "ENOENT") throw error;
275
+ }
276
+ }
277
+ get fd() {
278
+ return this.handle.fd;
279
+ }
280
+ async read() {
281
+ const { size } = await this.handle.stat();
282
+ if (size > 2 * 1024 * 1024) throw new Error("The tunnel helper produced too much startup output. Check its configuration.");
283
+ const data = Buffer.alloc(Math.min(65536, Math.max(0, size - this.position)));
284
+ const { bytesRead } = await this.handle.read(data, 0, data.length, this.position);
285
+ this.position += bytesRead;
286
+ return data.subarray(0, bytesRead);
287
+ }
288
+ discard() {
289
+ if (this.timer || this.closing) return;
290
+ const drain = () => {
291
+ this.pending = this.pending.then(() => this.handle.truncate(0)).catch(() => {
292
+ });
293
+ };
294
+ drain();
295
+ this.timer = setInterval(drain, 2e3);
296
+ this.timer.unref();
297
+ }
298
+ close() {
299
+ return this.closing ??= (async () => {
300
+ clearInterval(this.timer);
301
+ await this.pending;
302
+ await this.handle.close();
303
+ await rm(this.file, { force: true });
304
+ })();
305
+ }
306
+ };
307
+
308
+ // src/computer/provider-errors.ts
309
+ var ProviderConnectionError = class extends Error {
310
+ constructor(code, message) {
311
+ super(message);
312
+ this.code = code;
313
+ this.name = "ProviderConnectionError";
314
+ }
315
+ code;
316
+ };
317
+ function providerNeedsAction(code) {
318
+ return ["provider_sign_in", "provider_limit", "provider_missing", "provider_certificate", "provider_configuration"].includes(code ?? "");
319
+ }
320
+ function hasCertificateError(error) {
321
+ const seen = /* @__PURE__ */ new Set();
322
+ for (let current = error; current && typeof current === "object" && seen.size < 8 && !seen.has(current); ) {
323
+ seen.add(current);
324
+ const value = current;
325
+ if ([
326
+ "CERT_HAS_EXPIRED",
327
+ "CERT_NOT_YET_VALID",
328
+ "ERR_TLS_CERT_ALTNAME_INVALID",
329
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
330
+ "SELF_SIGNED_CERT_IN_CHAIN",
331
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
332
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
333
+ "CERT_REVOKED"
334
+ ].includes(value.code ?? "") || /certificate (?:has expired|expired|verify failed)|self[- ]signed certificate|CERT_HAS_EXPIRED/i.test(value.message ?? "")) return true;
335
+ current = value.cause;
336
+ }
337
+ return false;
338
+ }
339
+ function providerFailure(error, provider) {
340
+ if (error instanceof ProviderConnectionError) return error;
341
+ const value = error;
342
+ const setup = `mindwire connection ${provider.toLowerCase()}`;
343
+ if (hasCertificateError(error)) return new ProviderConnectionError(
344
+ "provider_certificate",
345
+ `${provider}'s TLS certificate could not be verified. The tunnel provider must fix its certificate; also check this computer's date and time. Your saved phone keys are unchanged.`
346
+ );
347
+ if (value?.code === "provider_sign_in" || [401, 403].includes(value?.status ?? 0) || /unexpected server response: (?:401|403)\b/i.test(value?.message ?? "")) {
348
+ return new ProviderConnectionError("provider_sign_in", `Sign in to ${provider} again with ${setup}. Your saved phone keys are unchanged.`);
349
+ }
350
+ if (value?.status === 402) return new ProviderConnectionError("provider_limit", `${provider}'s tunnel allowance was reached. Check your plan in its dashboard or choose another provider.`);
351
+ if (value?.status === 404) return new ProviderConnectionError("provider_missing", `This computer's ${provider} tunnel was removed. Run ${setup} to configure it again.`);
352
+ if (value?.status === 400 || value?.code === "provider_configuration") return new ProviderConnectionError("provider_configuration", `${provider}'s connection settings are incomplete or invalid. Run ${setup} to configure it again.`);
353
+ return new ProviderConnectionError("provider_connection", `${provider} couldn't connect. Check internet access and the provider account, then run ${setup}.`);
354
+ }
355
+ var CLOUDFLARED_VERSION = "2026.9.3";
356
+ var execute2 = promisify(execFile);
357
+ async function ensureCloudflareBinary(options = {}) {
358
+ const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
359
+ const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : "";
360
+ if (!cpu || !["darwin", "linux", "win32"].includes(platform) || platform === "win32" && cpu !== "amd64") {
361
+ throw new Error("Install cloudflared for this computer, or select another connection provider.");
362
+ }
363
+ const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
364
+ const bin = path4.join(
365
+ options.cacheDir ?? await binaryCacheDirectory(),
366
+ "cloudflared",
367
+ CLOUDFLARED_VERSION,
368
+ `${platform}-${arch}`,
369
+ platform === "win32" ? "cloudflared.exe" : "cloudflared"
370
+ );
371
+ const cached = await cachedExecutable(bin);
372
+ if (cached) return cached;
373
+ await options.onProgress?.("Downloading the secure connection helper\u2026");
374
+ const request = options.fetch ?? fetch;
375
+ const release = await request(
376
+ `https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${CLOUDFLARED_VERSION}`,
377
+ { headers: { Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(2e4) }
378
+ );
379
+ if (!release.ok) throw new Error("Couldn't download the connection helper. Retry, install cloudflared yourself, or choose another provider.");
380
+ const metadata = await release.json();
381
+ const item = metadata.assets?.find((value) => value.name === asset);
382
+ const checksum = item?.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1];
383
+ const expectedURL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset}`;
384
+ if (!checksum || item?.browser_download_url !== expectedURL) throw new Error("The connection helper has no verified release asset for this computer.");
385
+ const downloaded = await request(expectedURL, { signal: AbortSignal.timeout(12e4) });
386
+ if (!downloaded.ok) throw new Error("Couldn't download the connection helper. Check your internet connection and retry.");
387
+ const archive = new Uint8Array(await downloaded.arrayBuffer());
388
+ await verifyReleaseBytes(archive, checksum, asset);
389
+ let executable = archive;
390
+ if (platform === "darwin") {
391
+ const temporary = await mkdtemp(path4.join(tmpdir(), "mindwire-cloudflared-"));
392
+ try {
393
+ const archivePath = path4.join(temporary, asset);
394
+ await writeFile(archivePath, archive, { mode: 384 });
395
+ const result = await execute2(
396
+ "/usr/bin/tar",
397
+ ["-xzOf", archivePath, "cloudflared"],
398
+ { encoding: "buffer", maxBuffer: 128 * 1024 * 1024, timeout: 3e4 }
399
+ );
400
+ executable = new Uint8Array(result.stdout);
401
+ } finally {
402
+ await rm(temporary, { recursive: true, force: true });
403
+ }
404
+ }
405
+ return cacheExecutable(bin, executable, await executableChecksum(executable));
406
+ }
407
+ async function cloudflareCommand(options) {
408
+ try {
409
+ await execute2("cloudflared", ["--version"], { timeout: 3e3, windowsHide: true });
410
+ return "cloudflared";
411
+ } catch (error) {
412
+ if (error.code !== "ENOENT") throw new Error("The installed cloudflared could not start. Repair it or choose another connection provider.");
413
+ return ensureCloudflareBinary(options);
414
+ }
415
+ }
416
+
417
+ // src/computer/relay.ts
418
+ async function adoptRelay(owner, route, directory, outputFile, configFile) {
419
+ const output = outputFile ? await RelayOutput.adopt(directory, outputFile) : void 0;
420
+ const ownedConfig = configFile && path4.dirname(configFile) === path4.resolve(directory) && /^\.relay-config-[a-f0-9-]{36}\.json$/.test(path4.basename(configFile)) ? configFile : void 0;
421
+ return {
422
+ route,
423
+ owner,
424
+ outputFile: output?.file,
425
+ configFile: ownedConfig,
426
+ async close() {
427
+ await owner.close();
428
+ await output?.close();
429
+ if (ownedConfig) await fs2.rm(ownedConfig, { force: true });
430
+ }
431
+ };
432
+ }
433
+ function websocketURL(value) {
434
+ const url = new URL(value);
435
+ if (url.protocol === "https:") url.protocol = "wss:";
436
+ if (url.protocol !== "wss:" || !url.hostname || url.username || url.password || url.hash) {
437
+ throw new Error("Relay URLs must use wss:// with no embedded credentials or fragment.");
438
+ }
439
+ if (!url.pathname || url.pathname === "/") url.pathname = "/ssh";
440
+ return url.toString();
441
+ }
442
+ function validateRelayOptions(options) {
443
+ if (options.url) websocketURL(options.url);
444
+ if (options.kind === "custom" && !options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
445
+ if (options.kind === "oblien" && (!options.url || !options.oblienCredentialsFile || !Number.isSafeInteger(options.oblienTunnelId) || options.oblienTunnelId < 1)) {
446
+ throw new Error("Oblien setup is incomplete. Run mindwire connection oblien.");
447
+ }
448
+ if (options.kind === "cloudflare") {
449
+ const named = !!options.cloudflareTokenFile || !!options.cloudflareCredentialsFile && !!options.cloudflareTunnelId;
450
+ if (named && !options.url || options.url && !named) throw new Error("A named Cloudflare tunnel needs its hostname and credentials. Run mindwire connection cloudflare.");
451
+ if (options.cloudflareTokenFile && options.cloudflareCredentialsFile) throw new Error("Choose one Cloudflare tunnel credential source.");
452
+ }
453
+ if (options.kind === "ngrok" && options.ngrokTokenFile && !options.url) throw new Error("ngrok needs a reserved domain. Run mindwire connection ngrok.");
454
+ }
455
+ async function startRelay(options, port, runtime = {}) {
456
+ runtime.signal?.throwIfAborted();
457
+ validateRelayOptions(options);
458
+ if (options.kind === "none") return { close() {
459
+ } };
460
+ if (options.kind === "custom") {
461
+ if (!options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
462
+ return { route: { kind: "websocket", url: websocketURL(options.url) }, close() {
463
+ } };
464
+ }
465
+ const origin = `http://127.0.0.1:${port}`;
466
+ const env = { ...process.env };
467
+ let configFile;
468
+ const managed = options.kind === "oblien" || options.kind === "ngrok" && !!options.ngrokTokenFile;
469
+ let command, args;
470
+ if (managed) {
471
+ if (!runtime.cliPath || !runtime.outputDirectory) throw new Error("The managed relay needs its connection controller.");
472
+ configFile = path4.join(runtime.outputDirectory, `.relay-config-${randomUUID()}.json`);
473
+ await fs2.writeFile(configFile, JSON.stringify({ options, port }), { mode: 384 });
474
+ command = process.execPath;
475
+ args = [runtime.cliPath, "_relay", "--relay-config", configFile];
476
+ await runtime.onProgress?.(`Connecting through ${options.kind === "oblien" ? "Oblien" : "ngrok"}\u2026`);
477
+ } else if (options.kind === "cloudflare") {
478
+ command = await cloudflareCommand(runtime);
479
+ await runtime.onProgress?.("Connecting through Cloudflare\u2026");
480
+ if (options.cloudflareCredentialsFile && options.cloudflareTunnelId) {
481
+ if (!options.url || !runtime.outputDirectory) throw new Error("Named Cloudflare tunnel setup is incomplete.");
482
+ configFile = path4.join(runtime.outputDirectory, `.relay-config-${randomUUID()}.json`);
483
+ await fs2.writeFile(configFile, JSON.stringify({
484
+ tunnel: options.cloudflareTunnelId,
485
+ "credentials-file": options.cloudflareCredentialsFile,
486
+ ingress: [{ hostname: new URL(websocketURL(options.url)).hostname, service: origin }, { service: "http_status:404" }]
487
+ }), { mode: 384 });
488
+ args = ["tunnel", "--config", configFile, "--no-autoupdate", "run"];
489
+ } else if (options.cloudflareTokenFile) {
490
+ if (!options.url) throw new Error("A named Cloudflare tunnel needs --relay-url for its configured hostname.");
491
+ env.TUNNEL_TOKEN = (await readFile(options.cloudflareTokenFile, "utf8")).trim();
492
+ args = ["tunnel", "--no-autoupdate", "run"];
493
+ } else {
494
+ if (options.url) throw new Error("Use --cloudflare-token-file with a named tunnel, or --relay custom for an existing tunnel.");
495
+ args = ["tunnel", "--no-autoupdate", "--url", origin];
496
+ }
497
+ } else {
498
+ command = "ngrok";
499
+ args = ["http", origin, "--log", "stdout", "--log-format", "json"];
500
+ if (options.url) {
501
+ const url = new URL(websocketURL(options.url));
502
+ args.push("--url", `https://${url.host}`);
503
+ }
504
+ }
505
+ runtime.signal?.throwIfAborted();
506
+ const output = await RelayOutput.create(runtime.outputDirectory);
507
+ const child = spawn(command, args, { env, detached: true, stdio: ["ignore", output.fd, output.fd], windowsHide: true });
508
+ let failure;
509
+ child.on("error", (error) => {
510
+ failure = error;
511
+ });
512
+ let owner;
513
+ try {
514
+ await new Promise((resolve3, reject) => {
515
+ child.once("spawn", resolve3);
516
+ child.once("error", reject);
517
+ });
518
+ owner = await ownChild(child);
519
+ await runtime.onSpawn?.(owner, output.file, configFile);
520
+ const deadline = Date.now() + 45e3;
521
+ let buffer = "", cloudflareAddress = options.url, cloudflareConnected = false;
522
+ while (Date.now() < deadline) {
523
+ runtime.signal?.throwIfAborted();
524
+ if (failure) throw new Error(`Could not run ${options.kind}. Check its installation.`, { cause: failure });
525
+ const received = buffer + (await output.read()).toString();
526
+ buffer = received.slice(-16384);
527
+ let found;
528
+ if (!managed && options.kind === "cloudflare") {
529
+ cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
530
+ cloudflareConnected ||= /Registered tunnel connection/.test(received);
531
+ if (cloudflareConnected) found = cloudflareAddress;
532
+ } else {
533
+ for (const line of received.split(/\r?\n/)) {
534
+ try {
535
+ const event = JSON.parse(line);
536
+ if (managed && event.event === "relay_error") failure = new ProviderConnectionError(
537
+ providerNeedsAction(event.code) ? event.code : "provider_connection",
538
+ event.message ?? "The provider couldn't connect."
539
+ );
540
+ if (managed && event.event === "relay_ready") found = event.url;
541
+ if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
542
+ } catch {
543
+ }
544
+ }
545
+ }
546
+ if (found) {
547
+ const route = { kind: "websocket", url: websocketURL(found) };
548
+ output.discard();
549
+ const provider = owner;
550
+ return {
551
+ route,
552
+ process: child,
553
+ owner,
554
+ outputFile: output.file,
555
+ configFile,
556
+ async close() {
557
+ await provider.close();
558
+ await output.close();
559
+ if (configFile) await fs2.rm(configFile, { force: true });
560
+ }
561
+ };
562
+ }
563
+ if (failure) throw failure;
564
+ if (!owner.alive()) throw new Error(`${options.kind} exited before the tunnel connected. Check its account configuration.`);
565
+ await setTimeout$1(50, void 0, { signal: runtime.signal });
566
+ }
567
+ throw new Error(`${options.kind} did not establish its tunnel. Check your internet connection and its configuration.`);
568
+ } catch (error) {
569
+ if (owner) await owner.close();
570
+ else if (child.pid) await (await ownChild(child)).close();
571
+ await output.close();
572
+ if (configFile) await fs2.rm(configFile, { force: true });
573
+ throw error;
574
+ }
575
+ }
576
+ async function acquireProcessLock(file, options = {}) {
577
+ const owner = await currentProcessIdentity();
578
+ const queue = file + ".queue", id = randomUUID() + ".json", claimPath = queue + "/" + id;
579
+ await fs2.mkdir(queue, { recursive: true, mode: 448 });
580
+ let lock;
581
+ const release = async () => {
582
+ try {
583
+ if (lock) {
584
+ const held = lock;
585
+ lock = void 0;
586
+ const owned = await held.stat();
587
+ await held.close();
588
+ const current = await fs2.stat(file).catch(() => void 0);
589
+ if (current?.ino === owned.ino && current.dev === owned.dev) await fs2.rm(file, { force: true });
590
+ }
591
+ } finally {
592
+ await fs2.rm(claimPath, { force: true });
593
+ }
594
+ };
595
+ const publish = async (claim) => {
596
+ const temporary = claimPath + ".tmp";
597
+ try {
598
+ await fs2.writeFile(temporary, JSON.stringify(claim), { mode: 384 });
599
+ await fs2.rename(temporary, claimPath);
600
+ } finally {
601
+ await fs2.rm(temporary, { force: true });
602
+ }
603
+ };
604
+ const claims = async () => {
605
+ const entries = [];
606
+ for (const candidate of await fs2.readdir(queue)) {
607
+ if (!candidate.endsWith(".json")) continue;
608
+ const content = await fs2.readFile(queue + "/" + candidate, "utf8").catch((error) => {
609
+ if (error.code === "ENOENT") return void 0;
610
+ throw error;
611
+ });
612
+ if (content === void 0) continue;
613
+ const claim = JSON.parse(content);
614
+ if (!claim.owner || claim.ticket !== null && (!Number.isSafeInteger(claim.ticket) || claim.ticket < 1)) {
615
+ throw new Error("Invalid Mindwire lock ticket.");
616
+ }
617
+ if (await processStateAlive(claim.owner)) entries.push({ id: candidate, claim });
618
+ else await fs2.rm(queue + "/" + candidate, { force: true });
619
+ }
620
+ return entries;
621
+ };
622
+ try {
623
+ options.signal?.throwIfAborted();
624
+ await publish({ owner, ticket: null });
625
+ const ticket = Math.max(0, ...(await claims()).map((entry) => entry.claim.ticket ?? 0)) + 1;
626
+ if (!Number.isSafeInteger(ticket)) throw new Error("Mindwire's connection queue is full. Retry shortly.");
627
+ await publish({ owner, ticket });
628
+ const deadline = Date.now() + 3e4;
629
+ while (Date.now() < deadline) {
630
+ options.signal?.throwIfAborted();
631
+ if (await options.onWait?.()) {
632
+ await release();
633
+ return void 0;
634
+ }
635
+ const ahead = (await claims()).some((entry) => entry.id !== id && (entry.claim.ticket === null || entry.claim.ticket < ticket || entry.claim.ticket === ticket && entry.id < id));
636
+ if (!ahead) {
637
+ try {
638
+ lock = await fs2.open(file, "wx", 384);
639
+ await lock.writeFile(JSON.stringify(owner));
640
+ return { close: release };
641
+ } catch (error) {
642
+ if (error.code !== "EEXIST") throw error;
643
+ const holder = await readProcessState(file);
644
+ const age = Date.now() - (await fs2.stat(file).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
645
+ if (age > 5e3 && !await processStateAlive(holder)) {
646
+ await fs2.rm(file, { force: true });
647
+ continue;
648
+ }
649
+ }
650
+ }
651
+ await setTimeout$1(250, void 0, { signal: options.signal });
652
+ }
653
+ throw new Error("Another Mindwire operation is in progress. Try again shortly.");
654
+ } catch (error) {
655
+ await release();
656
+ throw error;
657
+ }
658
+ }
243
659
 
244
660
  // src/http.ts
245
661
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -287,13 +703,13 @@ var Http = class {
287
703
  * SDK error instead of a bare `TypeError: fetch failed`. An abort (the request's own signal, or
288
704
  * our timeout controller) is re-thrown untouched — `fetchWithTimeout` classifies it.
289
705
  */
290
- async fetchOnce(f, url, init, method, path2) {
706
+ async fetchOnce(f, url, init, method, path5) {
291
707
  try {
292
708
  return await f(url, init);
293
709
  } catch (e) {
294
710
  const aborted = init.signal?.aborted ?? false;
295
711
  if (aborted || isAbortError(e)) throw e;
296
- throw new MindwireError(`mindwire: ${method} ${path2} \u2014 network request failed`, { cause: e });
712
+ throw new MindwireError(`mindwire: ${method} ${path5} \u2014 network request failed`, { cause: e });
297
713
  }
298
714
  }
299
715
  /**
@@ -302,9 +718,9 @@ var Http = class {
302
718
  * `timeoutMs`; whichever aborts first wins. Timeout → {@link TimeoutError}; the caller's own abort
303
719
  * propagates as-is (its `reason`). `timeoutMs<=0` disables the deadline entirely.
304
720
  */
305
- async fetchWithTimeout(f, url, init, userSignal, method, path2) {
721
+ async fetchWithTimeout(f, url, init, userSignal, method, path5) {
306
722
  if (!this.timeoutMs || this.timeoutMs <= 0) {
307
- return this.fetchOnce(f, url, { ...init, ...userSignal ? { signal: userSignal } : {} }, method, path2);
723
+ return this.fetchOnce(f, url, { ...init, ...userSignal ? { signal: userSignal } : {} }, method, path5);
308
724
  }
309
725
  const ctrl = new AbortController();
310
726
  const onAbort = () => ctrl.abort(userSignal?.reason);
@@ -318,9 +734,9 @@ var Http = class {
318
734
  ctrl.abort();
319
735
  }, this.timeoutMs);
320
736
  try {
321
- return await this.fetchOnce(f, url, { ...init, signal: ctrl.signal }, method, path2);
737
+ return await this.fetchOnce(f, url, { ...init, signal: ctrl.signal }, method, path5);
322
738
  } catch (e) {
323
- if (timedOut) throw new TimeoutError(method, path2, this.timeoutMs);
739
+ if (timedOut) throw new TimeoutError(method, path5, this.timeoutMs);
324
740
  if (userSignal?.aborted) throw userSignal.reason ?? e;
325
741
  throw e;
326
742
  } finally {
@@ -359,8 +775,8 @@ var Http = class {
359
775
  async ready() {
360
776
  await this.base();
361
777
  }
362
- url(baseUrl, path2, query) {
363
- const u = new URL(baseUrl + (path2.startsWith("/") ? path2 : `/${path2}`));
778
+ url(baseUrl, path5, query) {
779
+ const u = new URL(baseUrl + (path5.startsWith("/") ? path5 : `/${path5}`));
364
780
  if (query) {
365
781
  for (const [k, v] of Object.entries(query)) {
366
782
  if (v !== void 0) u.searchParams.set(k, String(v));
@@ -374,10 +790,10 @@ var Http = class {
374
790
  if (hasBody) h["Content-Type"] = "application/json";
375
791
  return h;
376
792
  }
377
- async request(method, path2, init = {}) {
793
+ async request(method, path5, init = {}) {
378
794
  const base = await this.base();
379
795
  const hasBody = init.body !== void 0;
380
- const url = this.url(base.baseUrl, path2, init.query);
796
+ const url = this.url(base.baseUrl, path5, init.query);
381
797
  const extra = { ...base.headers, ...init.headers };
382
798
  const f = base.fetch ?? this.fetchImpl;
383
799
  const send = async (force) => this.fetchWithTimeout(
@@ -390,7 +806,7 @@ var Http = class {
390
806
  },
391
807
  init.signal,
392
808
  method,
393
- path2
809
+ path5
394
810
  );
395
811
  let res = await send(false);
396
812
  if (res.status === 401 && base.getToken) res = await send(true);
@@ -401,13 +817,13 @@ var Http = class {
401
817
  try {
402
818
  return JSON.parse(text);
403
819
  } catch (e) {
404
- throw new MindwireError(`mindwire: ${method} ${path2} returned a non-JSON body`, { cause: e });
820
+ throw new MindwireError(`mindwire: ${method} ${path5} returned a non-JSON body`, { cause: e });
405
821
  }
406
822
  }
407
823
  /** Open a streaming response (SSE). Caller owns the body. */
408
- async open(method, path2, init = {}) {
824
+ async open(method, path5, init = {}) {
409
825
  const base = await this.base();
410
- const url = this.url(base.baseUrl, path2, init.query);
826
+ const url = this.url(base.baseUrl, path5, init.query);
411
827
  const extra = { Accept: "text/event-stream", ...base.headers, ...init.headers };
412
828
  const f = base.fetch ?? this.fetchImpl;
413
829
  const send = async (force) => this.fetchOnce(
@@ -420,13 +836,13 @@ var Http = class {
420
836
  ...init.signal ? { signal: init.signal } : {}
421
837
  },
422
838
  method,
423
- path2
839
+ path5
424
840
  );
425
841
  let res = await send(false);
426
842
  if (res.status === 401 && base.getToken) res = await send(true);
427
843
  if (!res.ok) throw await this.toApiError(method, res);
428
844
  if (!res.body)
429
- throw new MindwireError(`mindwire: ${method} ${path2} returned no response body to stream`);
845
+ throw new MindwireError(`mindwire: ${method} ${path5} returned no response body to stream`);
430
846
  return res;
431
847
  }
432
848
  async toApiError(method, res) {
@@ -676,12 +1092,12 @@ var ProjectSyncError = class extends MindwireError {
676
1092
  };
677
1093
  var active = (o) => ["queued", "exporting", "preparing", "applying"].includes(o.status);
678
1094
  var prefix = "/workspace/sync";
679
- function delay2(ms, signal) {
1095
+ function delay4(ms, signal) {
680
1096
  signal?.throwIfAborted();
681
- return new Promise((resolve, reject) => {
1097
+ return new Promise((resolve3, reject) => {
682
1098
  const done = () => {
683
1099
  signal?.removeEventListener("abort", abort);
684
- resolve();
1100
+ resolve3();
685
1101
  };
686
1102
  const timer = setTimeout(done, ms);
687
1103
  const abort = () => {
@@ -743,7 +1159,7 @@ var ProjectSyncApi = class {
743
1159
  const o = await this.get(id, { signal: options.signal });
744
1160
  options.onUpdate?.(o);
745
1161
  if (!active(o)) return o;
746
- await delay2(500, options.signal);
1162
+ await delay4(500, options.signal);
747
1163
  }
748
1164
  }
749
1165
  /** Repeating this relay skips acknowledged checkpoints and verified chunks.
@@ -919,8 +1335,8 @@ var WorkspaceApi = class {
919
1335
  return this.mw.http.request("GET", "/workspace", { query: options });
920
1336
  }
921
1337
  /** Read the saved icon, or preview a candidate relative path within the project. Requires projectIconsVersion >= 1. */
922
- projectIcon(projectId, path2) {
923
- return this.mw.http.request("GET", `/workspace/projects/${encodeURIComponent(projectId)}/icon`, { query: { path: path2 } });
1338
+ projectIcon(projectId, path5) {
1339
+ return this.mw.http.request("GET", `/workspace/projects/${encodeURIComponent(projectId)}/icon`, { query: { path: path5 } });
924
1340
  }
925
1341
  /** Start an operation owned by the daemon. The same ID/payload returns the existing operation. */
926
1342
  createProject(request) {
@@ -1117,17 +1533,17 @@ var ExecutionApi = class {
1117
1533
  resources() {
1118
1534
  return this.client.http.request("GET", "/workspace/resources");
1119
1535
  }
1120
- files(path2 = "", query) {
1121
- return this.client.http.request("GET", "/workspace/files", { query: { path: path2, query } });
1536
+ files(path5 = "", query) {
1537
+ return this.client.http.request("GET", "/workspace/files", { query: { path: path5, query } });
1122
1538
  }
1123
- read(path2) {
1124
- return this.client.http.request("GET", "/workspace/file", { query: { path: path2 } });
1539
+ read(path5) {
1540
+ return this.client.http.request("GET", "/workspace/file", { query: { path: path5 } });
1125
1541
  }
1126
- write(path2, content) {
1127
- return this.client.http.request("PUT", "/workspace/file", { body: { path: path2, content } });
1542
+ write(path5, content) {
1543
+ return this.client.http.request("PUT", "/workspace/file", { body: { path: path5, content } });
1128
1544
  }
1129
- remove(path2) {
1130
- return this.client.http.request("DELETE", "/workspace/file", { query: { path: path2 } });
1545
+ remove(path5) {
1546
+ return this.client.http.request("DELETE", "/workspace/file", { query: { path: path5 } });
1131
1547
  }
1132
1548
  exec(command, signal) {
1133
1549
  return this.client.http.request("POST", "/workspace/exec", { body: command, signal });
@@ -1201,7 +1617,7 @@ async function spawnDaemon(opts) {
1201
1617
  "MindWire embedded mode needs a server runtime (Node/Bun/Deno). In the browser or an edge runtime, pass { baseUrl } to connect to a running daemon."
1202
1618
  );
1203
1619
  }
1204
- const { spawn: spawn2 } = await import('child_process');
1620
+ const { spawn: spawn3 } = await import('child_process');
1205
1621
  const net = await import('net');
1206
1622
  const proc = serverRuntime().process;
1207
1623
  const { randomBytes } = await import('crypto');
@@ -1210,7 +1626,7 @@ async function spawnDaemon(opts) {
1210
1626
  const port = await freePort(net);
1211
1627
  const baseUrl = `http://127.0.0.1:${port}`;
1212
1628
  const token = randomBytes(32).toString("hex");
1213
- const child = spawn2(bin, [], {
1629
+ const child = spawn3(bin, [], {
1214
1630
  env: {
1215
1631
  ...proc.env,
1216
1632
  ADDR: `127.0.0.1:${port}`,
@@ -1239,8 +1655,8 @@ async function spawnDaemon(opts) {
1239
1655
  return { baseUrl, token, stop };
1240
1656
  }
1241
1657
  async function resolveBinary(explicit) {
1242
- const fs3 = await import('fs');
1243
- const path2 = await import('path');
1658
+ const fs4 = await import('fs');
1659
+ const path5 = await import('path');
1244
1660
  const proc = serverRuntime().process;
1245
1661
  const ext = proc.platform === "win32" ? ".exe" : "";
1246
1662
  const candidates = [];
@@ -1248,14 +1664,14 @@ async function resolveBinary(explicit) {
1248
1664
  if (proc.env?.MINDWIRE_DAEMON) candidates.push(proc.env.MINDWIRE_DAEMON);
1249
1665
  try {
1250
1666
  const url = await import('url');
1251
- const here = path2.dirname(url.fileURLToPath(import.meta.url));
1667
+ const here = path5.dirname(url.fileURLToPath(import.meta.url));
1252
1668
  const named = `mindwired-${proc.platform}-${proc.arch}${ext}`;
1253
- candidates.push(path2.join(here, "bin", named), path2.join(here, "..", "bin", named));
1669
+ candidates.push(path5.join(here, "bin", named), path5.join(here, "..", "bin", named));
1254
1670
  } catch {
1255
1671
  }
1256
1672
  for (const c of candidates) {
1257
1673
  try {
1258
- if (fs3.existsSync(c)) return { bin: c, found: true, checked: candidates };
1674
+ if (fs4.existsSync(c)) return { bin: c, found: true, checked: candidates };
1259
1675
  } catch {
1260
1676
  }
1261
1677
  }
@@ -1278,13 +1694,13 @@ function daemonStartError(resolved, cause) {
1278
1694
  return `Couldn't start the mindwire daemon (${resolved.bin}): ${cause.message}. Set MINDWIRE_DAEMON to the binary path, or pass { baseUrl } to use a running daemon.`;
1279
1695
  }
1280
1696
  function freePort(net) {
1281
- return new Promise((resolve, reject) => {
1697
+ return new Promise((resolve3, reject) => {
1282
1698
  const srv = net.createServer();
1283
1699
  srv.on("error", reject);
1284
1700
  srv.listen(0, "127.0.0.1", () => {
1285
1701
  const addr = srv.address();
1286
1702
  const port = addr && typeof addr === "object" ? addr.port : 0;
1287
- srv.close(() => resolve(port));
1703
+ srv.close(() => resolve3(port));
1288
1704
  });
1289
1705
  });
1290
1706
  }
@@ -1688,8 +2104,8 @@ var Mindwire = class _Mindwire {
1688
2104
  * agent type. Frames are live snapshots (no replay); an empty `samples` array is a valid keep-alive.
1689
2105
  */
1690
2106
  async *processes(opts = {}) {
1691
- const path2 = "/processes/stream" + (opts.agent ? `?agent=${encodeURIComponent(opts.agent)}` : "");
1692
- const res = await this.http.open("GET", path2, {
2107
+ const path5 = "/processes/stream" + (opts.agent ? `?agent=${encodeURIComponent(opts.agent)}` : "");
2108
+ const res = await this.http.open("GET", path5, {
1693
2109
  ...opts.signal ? { signal: opts.signal } : {}
1694
2110
  });
1695
2111
  yield* readSSE(res.body, opts.signal);
@@ -1979,97 +2395,14 @@ var NotifyApi = class {
1979
2395
  await this.mw.http.request("DELETE", `/notify/rules/${encodeURIComponent(id)}`);
1980
2396
  }
1981
2397
  };
1982
- async function acquireProcessLock(file, options = {}) {
1983
- const owner = await currentProcessIdentity();
1984
- const queue = file + ".queue", id = randomUUID() + ".json", claimPath = queue + "/" + id;
1985
- await fs.mkdir(queue, { recursive: true, mode: 448 });
1986
- let lock;
1987
- const release = async () => {
1988
- try {
1989
- if (lock) {
1990
- const held = lock;
1991
- lock = void 0;
1992
- const owned = await held.stat();
1993
- await held.close();
1994
- const current = await fs.stat(file).catch(() => void 0);
1995
- if (current?.ino === owned.ino && current.dev === owned.dev) await fs.rm(file, { force: true });
1996
- }
1997
- } finally {
1998
- await fs.rm(claimPath, { force: true });
1999
- }
2000
- };
2001
- const publish = async (claim) => {
2002
- const temporary = claimPath + ".tmp";
2003
- try {
2004
- await fs.writeFile(temporary, JSON.stringify(claim), { mode: 384 });
2005
- await fs.rename(temporary, claimPath);
2006
- } finally {
2007
- await fs.rm(temporary, { force: true });
2008
- }
2009
- };
2010
- const claims = async () => {
2011
- const entries = [];
2012
- for (const candidate of await fs.readdir(queue)) {
2013
- if (!candidate.endsWith(".json")) continue;
2014
- const content = await fs.readFile(queue + "/" + candidate, "utf8").catch((error) => {
2015
- if (error.code === "ENOENT") return void 0;
2016
- throw error;
2017
- });
2018
- if (content === void 0) continue;
2019
- const claim = JSON.parse(content);
2020
- if (!claim.owner || claim.ticket !== null && (!Number.isSafeInteger(claim.ticket) || claim.ticket < 1)) {
2021
- throw new Error("Invalid Mindwire lock ticket.");
2022
- }
2023
- if (await processStateAlive(claim.owner)) entries.push({ id: candidate, claim });
2024
- else await fs.rm(queue + "/" + candidate, { force: true });
2025
- }
2026
- return entries;
2027
- };
2028
- try {
2029
- options.signal?.throwIfAborted();
2030
- await publish({ owner, ticket: null });
2031
- const ticket = Math.max(0, ...(await claims()).map((entry) => entry.claim.ticket ?? 0)) + 1;
2032
- if (!Number.isSafeInteger(ticket)) throw new Error("Mindwire's connection queue is full. Retry shortly.");
2033
- await publish({ owner, ticket });
2034
- const deadline = Date.now() + 3e4;
2035
- while (Date.now() < deadline) {
2036
- options.signal?.throwIfAborted();
2037
- if (await options.onWait?.()) {
2038
- await release();
2039
- return void 0;
2040
- }
2041
- const ahead = (await claims()).some((entry) => entry.id !== id && (entry.claim.ticket === null || entry.claim.ticket < ticket || entry.claim.ticket === ticket && entry.id < id));
2042
- if (!ahead) {
2043
- try {
2044
- lock = await fs.open(file, "wx", 384);
2045
- await lock.writeFile(JSON.stringify(owner));
2046
- return { close: release };
2047
- } catch (error) {
2048
- if (error.code !== "EEXIST") throw error;
2049
- const holder = await readProcessState(file);
2050
- const age = Date.now() - (await fs.stat(file).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
2051
- if (age > 5e3 && !await processStateAlive(holder)) {
2052
- await fs.rm(file, { force: true });
2053
- continue;
2054
- }
2055
- }
2056
- }
2057
- await setTimeout$1(250, void 0, { signal: options.signal });
2058
- }
2059
- throw new Error("Another Mindwire operation is in progress. Try again shortly.");
2060
- } catch (error) {
2061
- await release();
2062
- throw error;
2063
- }
2064
- }
2065
2398
 
2066
2399
  // src/computer/lifecycle.ts
2067
- var CONTROLLER_PROTOCOL = 2;
2068
- var defaultStateDirectory = () => path.join(homedir(), ".mindwire", "computer");
2400
+ var CONTROLLER_PROTOCOL = 3;
2401
+ var defaultStateDirectory = () => path4.join(homedir(), ".mindwire", "computer");
2069
2402
  var defaultComputerConfig = () => ({ directory: homedir(), bind: "0.0.0.0", sshPort: 8791, websocketPort: 8792, relay: { kind: "cloudflare" } });
2070
2403
  async function readJSON(file) {
2071
2404
  try {
2072
- return JSON.parse(await fs.readFile(file, "utf8"));
2405
+ return JSON.parse(await fs2.readFile(file, "utf8"));
2073
2406
  } catch (error) {
2074
2407
  if (error.code === "ENOENT") return void 0;
2075
2408
  throw error;
@@ -2078,18 +2411,18 @@ async function readJSON(file) {
2078
2411
  async function writeJSON(file, value) {
2079
2412
  const temp = `${file}.${randomUUID()}.tmp`;
2080
2413
  try {
2081
- await fs.writeFile(temp, JSON.stringify(value) + "\n", { mode: 384 });
2082
- await fs.rename(temp, file);
2414
+ await fs2.writeFile(temp, JSON.stringify(value) + "\n", { mode: 384 });
2415
+ await fs2.rename(temp, file);
2083
2416
  } finally {
2084
- await fs.rm(temp, { force: true });
2417
+ await fs2.rm(temp, { force: true });
2085
2418
  }
2086
2419
  }
2087
2420
  async function computerClient(directory) {
2088
- const runtime = await readJSON(path.join(directory, "computer-runtime.json"));
2421
+ const runtime = await readJSON(path4.join(directory, "computer-runtime.json"));
2089
2422
  if (!runtime) throw new Error("Mindwire is not running. Run mindwire connect on the computer.");
2090
2423
  const url = new URL(`http://${runtime.apiAddress}`);
2091
2424
  if (!["127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password) throw new Error("Invalid local Mindwire address.");
2092
- const token = (await fs.readFile(path.join(directory, "daemon.token"), "utf8")).trim();
2425
+ const token = (await fs2.readFile(path4.join(directory, "daemon.token"), "utf8")).trim();
2093
2426
  const client = new Mindwire({ target: remote(url.origin, { token }), requestTimeoutMs: 3e3 });
2094
2427
  const info = await client.computer.info();
2095
2428
  if (info.computerId !== runtime.computerId || info.pid !== runtime.pid) throw new Error("The local Mindwire process changed. Retry the command.");
@@ -2107,20 +2440,21 @@ function directRoutes(config, port) {
2107
2440
  async function ensureComputer(directory, cliPath, patch = {}, onProgress, options = {}) {
2108
2441
  const resume = options.resume !== false;
2109
2442
  options.signal?.throwIfAborted();
2110
- await fs.mkdir(directory, { recursive: true, mode: 448 });
2111
- const stoppedPath = path.join(directory, "computer-stopped.json");
2112
- if (resume) await fs.rm(stoppedPath, { force: true });
2443
+ await fs2.mkdir(directory, { recursive: true, mode: 448 });
2444
+ const stoppedPath = path4.join(directory, "computer-stopped.json");
2445
+ if (resume) await fs2.rm(stoppedPath, { force: true });
2113
2446
  else if ((await readJSON(stoppedPath))?.stopped) throw new Error("Mindwire was stopped by its owner.");
2114
2447
  let current;
2115
2448
  try {
2116
2449
  current = await computerClient(directory);
2117
2450
  } catch {
2118
2451
  }
2119
- const configPath = path.join(directory, "computer-config.json");
2452
+ const configPath = path4.join(directory, "computer-config.json");
2120
2453
  const previous = await readJSON(configPath);
2121
2454
  const config = { ...defaultComputerConfig(), ...previous, ...patch };
2455
+ validateRelayOptions(config.relay);
2122
2456
  const requestedAt = resume ? Date.now() : 0;
2123
- if (resume) await writeJSON(path.join(directory, "computer-check.json"), { requestedAt });
2457
+ if (resume) await writeJSON(path4.join(directory, "computer-check.json"), { requestedAt });
2124
2458
  const compatible = (controller) => (controller?.protocol ?? 0) >= CONTROLLER_PROTOCOL && versionAtLeast(controller?.cliVersion, SDK_VERSION);
2125
2459
  let launchedPID;
2126
2460
  let launchFailure;
@@ -2130,14 +2464,21 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2130
2464
  const deadline = Date.now() + (options.timeoutMs ?? 12e4);
2131
2465
  while (Date.now() < deadline) {
2132
2466
  options.signal?.throwIfAborted();
2133
- const controller = await readJSON(path.join(directory, "computer-controller.json"));
2467
+ const controller = await readJSON(path4.join(directory, "computer-controller.json"));
2134
2468
  lastState = controller;
2135
2469
  if (launchFailure) throw launchFailure;
2136
2470
  if (launchedPID && controller?.pid !== launchedPID) {
2137
2471
  await setTimeout$1(250, void 0, { signal: options.signal });
2138
2472
  continue;
2139
2473
  }
2474
+ if (options.waitForRoutes === false && compatible(controller) && await processStateAlive(controller)) {
2475
+ try {
2476
+ return await computerClient(directory);
2477
+ } catch {
2478
+ }
2479
+ }
2140
2480
  if (controller?.error && !controller.recovering) throw new Error(controller.error);
2481
+ if (controller?.error && providerNeedsAction(controller.errorCode) && (controller.checkedAt ?? 0) >= requestedAt) throw new Error(controller.error);
2141
2482
  if (controller?.ready && compatible(controller) && (controller.checkedAt ?? 0) >= requestedAt && await processStateAlive(controller)) return computerClient(directory);
2142
2483
  if (controller?.phase && controller.phase !== phase) {
2143
2484
  phase = controller.phase;
@@ -2150,17 +2491,27 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2150
2491
  throw new Error(`Mindwire's connection is not ready. ${lastStep}${recovering ? " Background recovery continues." : ""} Run mindwire reconnect to retry, or mindwire status for details.`);
2151
2492
  };
2152
2493
  if (current) {
2153
- if (Object.keys(patch).some((key) => JSON.stringify(config[key]) !== JSON.stringify(previous?.[key]))) {
2154
- throw new Error("Mindwire is running with different connection settings. Run mindwire stop when idle, then retry with the new settings.");
2494
+ const changed = Object.keys(patch).filter((key) => JSON.stringify(config[key]) !== JSON.stringify(previous?.[key]));
2495
+ if (changed.length) {
2496
+ if (changed.some((key) => !["relay", "host"].includes(key))) {
2497
+ throw new Error("Changing the service's directory or listening ports requires mindwire stop when idle. Connection providers can be changed live with mindwire connection.");
2498
+ }
2499
+ const controller2 = await readJSON(path4.join(directory, "computer-controller.json"));
2500
+ if (!compatible(controller2) || !await processStateAlive(controller2)) {
2501
+ await ensureComputer(directory, cliPath, {}, onProgress, { ...options, waitForRoutes: false });
2502
+ }
2503
+ const { changeComputerConnection } = await import('./connection-change-IBNL42BL.js');
2504
+ await changeComputerConnection(directory, Object.fromEntries(changed.map((key) => [key, patch[key]])), { signal: options.signal, onProgress });
2505
+ return computerClient(directory);
2155
2506
  }
2156
- const controller = await readJSON(path.join(directory, "computer-controller.json"));
2507
+ const controller = await readJSON(path4.join(directory, "computer-controller.json"));
2157
2508
  if (compatible(controller) && await processStateAlive(controller)) return waitUntilReady();
2158
2509
  }
2159
- const lockPath = path.join(directory, "launch.lock");
2510
+ const lockPath = path4.join(directory, "launch.lock");
2160
2511
  const lock = await acquireProcessLock(lockPath, {
2161
2512
  signal: options.signal,
2162
2513
  onWait: async () => {
2163
- const controller = await readJSON(path.join(directory, "computer-controller.json"));
2514
+ const controller = await readJSON(path4.join(directory, "computer-controller.json"));
2164
2515
  if (compatible(controller) && await processStateAlive(controller)) {
2165
2516
  const saved = await readJSON(configPath);
2166
2517
  if (Object.keys(patch).some((key) => JSON.stringify(patch[key]) !== JSON.stringify(saved?.[key]))) {
@@ -2184,13 +2535,13 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2184
2535
  if (Object.keys(patch).some((key) => JSON.stringify(patch[key]) !== JSON.stringify(saved?.[key]))) {
2185
2536
  throw new Error("Another start used different connection settings. Stop Mindwire when idle before changing them.");
2186
2537
  }
2187
- const controller2 = await readJSON(path.join(directory, "computer-controller.json"));
2538
+ const controller2 = await readJSON(path4.join(directory, "computer-controller.json"));
2188
2539
  if (compatible(controller2) && await processStateAlive(controller2)) return waitUntilReady();
2189
2540
  }
2190
- let controller = await readJSON(path.join(directory, "computer-controller.json"));
2541
+ let controller = await readJSON(path4.join(directory, "computer-controller.json"));
2191
2542
  if (existing && controller && !compatible(controller) && await processStateAlive(controller)) {
2192
2543
  onProgress?.("Refreshing the background connection service\u2026");
2193
- const { handoffController } = await import('./controller-handoff-3LAYBJUL.js');
2544
+ const { handoffController } = await import('./controller-handoff-RUUHODNC.js');
2194
2545
  await handoffController(directory, controller, config, await existing.computer.info());
2195
2546
  controller = void 0;
2196
2547
  }
@@ -2198,20 +2549,20 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2198
2549
  if (controller?.error && !controller.recovering) throw new Error(controller.error);
2199
2550
  } else {
2200
2551
  await writeJSON(configPath, config);
2201
- const log = await fs.open(path.join(directory, "computer.log"), "a", 384);
2552
+ const log = await fs2.open(path4.join(directory, "computer.log"), "a", 384);
2202
2553
  try {
2203
2554
  const child = spawn(process.execPath, [cliPath, "_serve", "--state-dir", directory], {
2204
2555
  detached: true,
2205
2556
  stdio: ["ignore", log.fd, log.fd],
2206
2557
  windowsHide: true
2207
2558
  });
2208
- await new Promise((resolve, reject) => {
2209
- child.once("spawn", resolve);
2559
+ await new Promise((resolve3, reject) => {
2560
+ child.once("spawn", resolve3);
2210
2561
  child.once("error", reject);
2211
2562
  });
2212
2563
  launchedPID = child.pid;
2213
2564
  child.on("exit", () => {
2214
- launchFailure = new Error(`The computer controller stopped. Inspect ${path.join(directory, "computer.log")}.`);
2565
+ launchFailure = new Error(`The computer controller stopped. Inspect ${path4.join(directory, "computer.log")}.`);
2215
2566
  });
2216
2567
  child.unref();
2217
2568
  } finally {
@@ -2224,20 +2575,20 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2224
2575
  }
2225
2576
  }
2226
2577
  async function superviseComputer(directory) {
2227
- const controller = await import('./controller-AGT6MCJG.js');
2578
+ const controller = await import('./controller-2SJLZMJZ.js');
2228
2579
  await controller.superviseComputer(directory);
2229
2580
  }
2230
2581
  async function stopComputer(directory, force = false) {
2231
2582
  const client = await computerClient(directory);
2232
- const controller = await readJSON(path.join(directory, "computer-controller.json"));
2583
+ const controller = await readJSON(path4.join(directory, "computer-controller.json"));
2233
2584
  if (!await processStateAlive(controller)) throw new Error("The Mindwire controller is not running.");
2234
2585
  const lease = force ? void 0 : await client.service.acquireUpdate();
2235
2586
  try {
2236
- await writeJSON(path.join(directory, "computer-stopped.json"), { stopped: true });
2587
+ await writeJSON(path4.join(directory, "computer-stopped.json"), { stopped: true });
2237
2588
  if (!await processStateAlive(controller)) throw new Error("The Mindwire controller changed. Retry the command.");
2238
2589
  process.kill(controller.pid, "SIGTERM");
2239
2590
  } catch (error) {
2240
- await fs.rm(path.join(directory, "computer-stopped.json"), { force: true });
2591
+ await fs2.rm(path4.join(directory, "computer-stopped.json"), { force: true });
2241
2592
  if (lease) await client.service.releaseUpdate(lease.id);
2242
2593
  throw error;
2243
2594
  }
@@ -2248,6 +2599,6 @@ async function stopComputer(directory, force = false) {
2248
2599
  throw new Error("Mindwire is still stopping. Check computer.log.");
2249
2600
  }
2250
2601
 
2251
- export { CONTROLLER_PROTOCOL, SDK_VERSION, acquireProcessLock, adoptProcess, computerClient, computerPairingQRData, computerPairingURI, currentProcessIdentity, defaultStateDirectory, directRoutes, ensureComputer, ensureDaemonBinary, ownChild, processIdentity, processStateAlive, readJSON, stopComputer, superviseComputer, writeJSON };
2252
- //# sourceMappingURL=chunk-3QFDSYKC.js.map
2253
- //# sourceMappingURL=chunk-3QFDSYKC.js.map
2602
+ export { CONTROLLER_PROTOCOL, ProviderConnectionError, RelayOutput, SDK_VERSION, acquireProcessLock, adoptProcess, adoptRelay, cloudflareCommand, computerClient, computerPairingQRData, computerPairingURI, currentProcessIdentity, defaultComputerConfig, defaultStateDirectory, directRoutes, ensureComputer, ensureDaemonBinary, hasCertificateError, ownChild, processIdentity, processStateAlive, providerFailure, providerNeedsAction, readJSON, startRelay, stopComputer, superviseComputer, websocketURL, writeJSON };
2603
+ //# sourceMappingURL=chunk-RTGZDLSG.js.map
2604
+ //# sourceMappingURL=chunk-RTGZDLSG.js.map