mindwire 0.1.38 → 0.1.40

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/README.md +16 -0
  2. package/dist/{chunk-CKTVMUS4.js → chunk-BJL5K6M7.js} +50 -16
  3. package/dist/chunk-BJL5K6M7.js.map +1 -0
  4. package/dist/chunk-ZP7F7LP7.js +306 -0
  5. package/dist/chunk-ZP7F7LP7.js.map +1 -0
  6. package/dist/cli.js +499 -250
  7. package/dist/cli.js.map +1 -1
  8. package/dist/computer/dashboard.d.ts +1 -0
  9. package/dist/computer/desktop.d.ts +17 -0
  10. package/dist/computer/directory-http.d.ts +1 -1
  11. package/dist/computer/discovery.d.ts +22 -0
  12. package/dist/computer/inspection.d.ts +30 -0
  13. package/dist/computer/lifecycle.d.ts +4 -2
  14. package/dist/computer/runtime-version.d.ts +6 -0
  15. package/dist/computer/service-update.d.ts +26 -0
  16. package/dist/computer/setup-prompt.d.ts +2 -0
  17. package/dist/computer/startup.d.ts +7 -0
  18. package/dist/computer/terminal-ui.d.ts +9 -0
  19. package/dist/computer.d.ts +2 -0
  20. package/dist/{connection-change-5F4WNCYV.js → connection-change-7ZV6O75Q.js} +3 -3
  21. package/dist/{connection-change-5F4WNCYV.js.map → connection-change-7ZV6O75Q.js.map} +1 -1
  22. package/dist/{controller-3SZI7TBW.js → controller-FBMGMSLM.js} +67 -9
  23. package/dist/controller-FBMGMSLM.js.map +1 -0
  24. package/dist/{controller-handoff-KV7WRDAI.js → controller-handoff-CJKXUFQI.js} +3 -3
  25. package/dist/{controller-handoff-KV7WRDAI.js.map → controller-handoff-CJKXUFQI.js.map} +1 -1
  26. package/dist/daemon-binary.d.ts +2 -0
  27. package/dist/index.cjs +41 -7
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +41 -7
  31. package/dist/index.js.map +1 -1
  32. package/dist/project-sync.d.ts +29 -0
  33. package/dist/surfaces.d.ts +20 -1
  34. package/dist/types.d.ts +2 -0
  35. package/package.json +1 -1
  36. package/dist/chunk-CKTVMUS4.js.map +0 -1
  37. package/dist/chunk-QODJKRFK.js +0 -20
  38. package/dist/chunk-QODJKRFK.js.map +0 -1
  39. package/dist/controller-3SZI7TBW.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { connectionAddress, connectionInfo, connectionDescription } from './chunk-QODJKRFK.js';
3
- import { defaultStateDirectory, superviseComputer, computerClient, ensureComputer, readJSON, defaultComputerConfig, computerPairingURI, stopComputer, SDK_VERSION, providerFailure, writeJSON, currentProcessIdentity, processStateAlive, acquireProcessLock, websocketURL, providerNeedsAction, cloudflareCommand, computerPairingQRData, ProviderConnectionError } from './chunk-CKTVMUS4.js';
2
+ import { enableAddressDiscovery, disableAddressDiscovery, discoveryDescription, connectionAddress, saveDiscoveryPreference, discoveryPreference, reconcileAddressDiscovery, connectionInfo, connectionDescription, updateComputer, updateProgress, activeUpdate } from './chunk-ZP7F7LP7.js';
3
+ import { SDK_VERSION, defaultStateDirectory, computerClient, superviseComputer, ensureComputer, readJSON, defaultComputerConfig, computerPairingURI, stopComputer, processStateAlive, providerFailure, acquireProcessLock, writeJSON, currentProcessIdentity, CONTROLLER_PROTOCOL, versionAtLeast, websocketURL, providerNeedsAction, cloudflareCommand, computerPairingQRData, ProviderConnectionError } from './chunk-BJL5K6M7.js';
4
4
  import './chunk-PFYKJ6LX.js';
5
5
  import { promisify, parseArgs } from 'util';
6
6
  import { fileURLToPath, pathToFileURL } from 'url';
@@ -11,10 +11,10 @@ import { createHash, randomUUID } from 'crypto';
11
11
  import { createInterface } from 'readline/promises';
12
12
  import { setTimeout as setTimeout$1 } from 'timers/promises';
13
13
  import { execFile, spawn } from 'child_process';
14
- import { homedir, hostname, tmpdir } from 'os';
14
+ import { hostname, homedir, tmpdir } from 'os';
15
15
  import * as fs from 'fs/promises';
16
- import { rm } from 'fs/promises';
17
- import { cursorTo, clearLine, createInterface as createInterface$1 } from 'readline';
16
+ import { readFile, rm } from 'fs/promises';
17
+ import { emitKeypressEvents, cursorTo, clearLine, createInterface as createInterface$1 } from 'readline';
18
18
  import { TunnelClient, Oblien } from 'oblien';
19
19
  import { Writable } from 'stream';
20
20
 
@@ -221,6 +221,18 @@ var execute = promisify(execFile);
221
221
  var xml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
222
222
  var systemd = (value, expandEnvironment = false) => '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\$/g, () => expandEnvironment ? "$$" : "$") + '"';
223
223
  var windowsArg = (value) => '"' + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1") + '"';
224
+ async function startupExecutable(executable = process.execPath, environmentPath = process.env.PATH ?? "") {
225
+ const homebrew = executable.match(/^(.*)\/Cellar\/([^/]+)\/[^/]+\/(.+)$/);
226
+ const candidates = [
227
+ ...homebrew ? [`${homebrew[1]}/opt/${homebrew[2]}/${homebrew[3]}`] : [],
228
+ ...environmentPath.split(path.delimiter).filter((part) => path.isAbsolute(part)).map((part) => path.join(part, path.basename(executable)))
229
+ ];
230
+ const target = await fs.realpath(executable);
231
+ for (const candidate of candidates) {
232
+ if (candidate !== executable && await fs.realpath(candidate).catch(() => void 0) === target) return candidate;
233
+ }
234
+ return executable;
235
+ }
224
236
  function startupPlan(options) {
225
237
  const { directory, cliPath } = options;
226
238
  const executable = options.executable ?? process.execPath, home = options.home ?? homedir();
@@ -310,7 +322,16 @@ var run = async ([command, ...args]) => {
310
322
  async function startupStatus(directory) {
311
323
  const state = await readJSON(path.join(directory, "computer-startup.json")) ?? { enabled: false };
312
324
  const guardian = await readJSON(path.join(directory, "computer-guardian.json"));
313
- return { ...state, running: state.enabled && await processStateAlive(guardian) };
325
+ let registered = false;
326
+ if (state.enabled && state.name && state.kind) {
327
+ const query = state.kind === "launchd" ? ["launchctl", "print", `gui/${process.getuid?.()}/${state.name}`] : state.kind === "systemd" ? ["systemctl", "--user", "is-enabled", state.name + ".service"] : ["schtasks.exe", "/Query", "/TN", state.name];
328
+ try {
329
+ await run(query);
330
+ registered = true;
331
+ } catch {
332
+ }
333
+ }
334
+ return { ...state, registered, running: state.enabled && registered && await processStateAlive(guardian) };
314
335
  }
315
336
  async function configureStartup(directory, cliPath, enabled, runtime = {}) {
316
337
  await fs.mkdir(directory, { recursive: true, mode: 448 });
@@ -319,7 +340,7 @@ async function configureStartup(directory, cliPath, enabled, runtime = {}) {
319
340
  if (!lock) throw new Error("Startup is already being configured. Try again shortly.");
320
341
  try {
321
342
  const user = process.platform === "win32" && !runtime.plan ? (await execute("whoami.exe", [], { timeout: 3e3, windowsHide: true })).stdout.trim() : void 0;
322
- const plan = runtime.plan ?? startupPlan({ directory, cliPath, user });
343
+ const plan = runtime.plan ?? startupPlan({ directory, cliPath, user, executable: await startupExecutable() });
323
344
  const command = runtime.run ?? run;
324
345
  let loaded = false;
325
346
  try {
@@ -340,8 +361,28 @@ async function configureStartup(directory, cliPath, enabled, runtime = {}) {
340
361
  await fs.rm(temporary, { force: true });
341
362
  }
342
363
  }
343
- if (!loaded || changed && plan.kind !== "launchd") {
344
- for (const action of plan.enable) await command(action);
364
+ const guardianRunning = loaded && (runtime.isRunning ? await runtime.isRunning() : await processStateAlive(await readJSON(path.join(directory, "computer-guardian.json"))));
365
+ const reload = loaded && (runtime.repair || changed || !guardianRunning);
366
+ if (reload && plan.kind === "launchd") await command(plan.disable[0]);
367
+ if (reload && plan.kind === "task-scheduler") {
368
+ try {
369
+ await command(plan.disable[0]);
370
+ } catch {
371
+ }
372
+ }
373
+ if (!loaded || reload) {
374
+ for (const action of plan.enable) {
375
+ for (let attempt = 0; ; attempt++) {
376
+ try {
377
+ await command(action);
378
+ break;
379
+ } catch (error) {
380
+ if (plan.kind !== "launchd" || !action.includes("bootstrap") || error.code !== 5 || attempt >= 7) throw error;
381
+ await setTimeout$1(100 * (attempt + 1));
382
+ }
383
+ }
384
+ }
385
+ if (reload && plan.kind === "systemd") await command(["systemctl", "--user", "restart", plan.name + ".service"]);
345
386
  }
346
387
  await command(plan.query);
347
388
  if (runtime.verify) await runtime.verify();
@@ -372,7 +413,7 @@ async function configureStartup(directory, cliPath, enabled, runtime = {}) {
372
413
  }
373
414
  const state = { enabled, kind: plan.kind, name: plan.name, file: plan.file };
374
415
  await writeJSON(path.join(directory, "computer-startup.json"), state);
375
- return { ...state, running: enabled };
416
+ return { ...state, registered: enabled, running: enabled };
376
417
  } finally {
377
418
  await lock.close();
378
419
  }
@@ -388,12 +429,18 @@ async function watchComputer(directory, cliPath) {
388
429
  process.on("SIGINT", stop);
389
430
  let reportedError = "";
390
431
  const guardianPath = path.join(directory, "computer-guardian.json");
432
+ const lock = await acquireProcessLock(path.join(directory, "guardian.lock"), { signal: abort.signal });
433
+ if (!lock) {
434
+ process.off("SIGTERM", stop);
435
+ process.off("SIGINT", stop);
436
+ return;
437
+ }
391
438
  try {
392
439
  await writeJSON(guardianPath, await currentProcessIdentity());
393
440
  while (!stopping) {
394
441
  const paused = await readJSON(path.join(directory, "computer-stopped.json"));
395
442
  const controller = await readJSON(path.join(directory, "computer-controller.json"));
396
- if (!paused?.stopped && !await processStateAlive(controller)) {
443
+ if (!paused?.stopped && (!await processStateAlive(controller) || (controller?.protocol ?? 0) < CONTROLLER_PROTOCOL || !versionAtLeast(controller?.cliVersion, SDK_VERSION))) {
397
444
  try {
398
445
  await ensureComputer(directory, cliPath, {}, void 0, { resume: false, signal: abort.signal });
399
446
  reportedError = "";
@@ -413,6 +460,7 @@ async function watchComputer(directory, cliPath) {
413
460
  if ((await readJSON(guardianPath))?.pid === process.pid) await writeJSON(guardianPath, { pid: 0 });
414
461
  process.off("SIGTERM", stop);
415
462
  process.off("SIGINT", stop);
463
+ await lock.close();
416
464
  }
417
465
  }
418
466
 
@@ -513,174 +561,6 @@ var ConnectionProgress = class {
513
561
  this.stream.write(text + elapsed);
514
562
  }
515
563
  };
516
-
517
- // src/computer/directory-http.ts
518
- var DirectoryRequestError = class extends Error {
519
- constructor(status, code, retryAfter = 0) {
520
- super(
521
- status === 0 ? "Couldn't reach the address directory securely. Check its HTTPS address and retry." : `The address directory rejected the request (${code}).`
522
- );
523
- this.status = status;
524
- this.code = code;
525
- this.retryAfter = retryAfter;
526
- }
527
- status;
528
- code;
529
- retryAfter;
530
- };
531
- async function directoryRequest(url, path5, body) {
532
- let response;
533
- try {
534
- response = await fetch(url + path5, {
535
- method: "POST",
536
- redirect: "error",
537
- signal: AbortSignal.timeout(1e4),
538
- headers: {
539
- "Content-Type": "application/json",
540
- Accept: "application/json"
541
- },
542
- body: JSON.stringify(body)
543
- });
544
- } catch {
545
- throw new DirectoryRequestError(0, "unreachable");
546
- }
547
- const reader = response.body?.getReader();
548
- const chunks = [];
549
- let length = 0;
550
- try {
551
- if (reader)
552
- for (; ; ) {
553
- const { done, value } = await reader.read();
554
- if (done) break;
555
- length += value.byteLength;
556
- if (length > 64 * 1024) throw new Error("The address directory returned an oversized response.");
557
- chunks.push(value);
558
- }
559
- } finally {
560
- void reader?.cancel().catch(() => {
561
- });
562
- }
563
- let data;
564
- try {
565
- data = JSON.parse(Buffer.concat(chunks).toString("utf8"));
566
- } catch {
567
- throw new DirectoryRequestError(response.status, "invalid_response");
568
- }
569
- if (!response.ok) {
570
- const error = data?.error;
571
- const code = typeof error === "string" ? error : error?.code;
572
- const retryAfter = Number(response.headers.get("Retry-After"));
573
- throw new DirectoryRequestError(
574
- response.status,
575
- typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? code : "directory_error",
576
- Number.isFinite(retryAfter) ? Math.max(0, Math.min(60, retryAfter)) : 0
577
- );
578
- }
579
- return data;
580
- }
581
-
582
- // src/computer/discovery.ts
583
- var DIRECTORY_API_PATH = "/api/computer-directory/v1";
584
- var DIRECTORY_URL = `https://console.mindwire.sh${DIRECTORY_API_PATH}`;
585
- function directoryURL(value) {
586
- let url;
587
- try {
588
- url = new URL(value);
589
- } catch {
590
- throw new Error("Enter the directory's HTTPS API URL.");
591
- }
592
- if (value.length > 2048 || /[\s\x00]/.test(value) || url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))) {
593
- throw new Error("Use an HTTPS directory URL without credentials, a query, or a fragment.");
594
- }
595
- if (url.pathname === "/") url.pathname = DIRECTORY_API_PATH;
596
- if (url.pathname.endsWith("/") || url.pathname.includes("//") || url.pathname.includes("%")) {
597
- throw new Error("Use the exact directory API URL without a trailing slash.");
598
- }
599
- return url.href;
600
- }
601
- async function enableAddressDiscovery(options) {
602
- const lock = await acquireProcessLock(join(options.directory, "discovery-setup.lock"));
603
- try {
604
- let status;
605
- try {
606
- status = await options.computer.computer.discovery();
607
- } catch (error) {
608
- if (error.status === 404)
609
- throw new Error("Update the Mindwire service before enabling automatic address recovery.");
610
- throw error;
611
- }
612
- const url = directoryURL(
613
- options.url ?? process.env.MINDWIRE_DIRECTORY_URL ?? status.url ?? DIRECTORY_URL
614
- );
615
- if (status.url && status.url !== url && (status.enabled || (status.sequence ?? 0) !== (status.publishedSequence ?? 0))) {
616
- throw new Error(
617
- "Disable the current directory with mindwire discovery disable and wait for its withdrawal before changing the directory URL."
618
- );
619
- }
620
- if (!status.enabled || status.url !== url || status.error?.includes("enrollment")) {
621
- options.prompt.print(`Enabling encrypted address recovery with ${new URL(url).host}\u2026`);
622
- try {
623
- const challenge = await directoryRequest(url, "/registrations/challenge", {
624
- directoryId: status.directoryId
625
- });
626
- if (typeof challenge?.challenge !== "string" || challenge.challenge.length < 16 || challenge.challenge.length > 2048) {
627
- throw new Error("The address directory returned an invalid enrollment challenge.");
628
- }
629
- const proof = await options.computer.computer.directoryEnrollment(url, challenge.challenge);
630
- const enrolled = await directoryRequest(url, "/registrations", proof);
631
- if (enrolled?.directoryId !== status.directoryId)
632
- throw new Error("The directory acknowledged a different computer.");
633
- } catch (error) {
634
- if (error instanceof DirectoryRequestError) {
635
- if (error.status === 404 || error.status === 501)
636
- throw new Error(
637
- "The address directory isn't available at this URL. Your existing connection still works; retry after the backend is deployed."
638
- );
639
- if (error.status === 409)
640
- throw new Error(
641
- "This computer's directory registration is revoked or conflicts with its saved identity. Contact the directory operator."
642
- );
643
- if (error.code === "account_required")
644
- throw new Error(
645
- "This directory still requires an account. Update its Mindwire backend to enable key-based registration, or choose another directory with --directory-url."
646
- );
647
- if (error.status === 401 || error.status === 403)
648
- throw new Error(
649
- "The directory couldn't verify this computer's key or challenge. Retry enrollment."
650
- );
651
- if (error.status === 429)
652
- throw new Error(
653
- "The directory's registration limit was reached. Retry later or use another directory."
654
- );
655
- if (error.status === 503)
656
- throw new Error(
657
- "The directory is temporarily unavailable or at capacity. Your existing connection still works."
658
- );
659
- }
660
- throw error;
661
- }
662
- status = await options.computer.computer.configureDiscovery(true, url);
663
- }
664
- const deadline = Date.now() + 15e3;
665
- while (Date.now() < deadline) {
666
- status = await options.computer.computer.discovery();
667
- if (status.enabled && status.sequence && status.publishedSequence === status.sequence) return status;
668
- if (status.error?.includes("enrollment") || status.error?.includes("older revision"))
669
- throw new Error(status.error);
670
- await setTimeout$1(250);
671
- }
672
- throw new Error(
673
- "Address recovery is configured and will retry in the background. Run mindwire discovery status to check publication."
674
- );
675
- } finally {
676
- await lock?.close();
677
- }
678
- }
679
- function discoveryDescription(status) {
680
- if (!status?.enabled) return "Automatic address recovery is off. Enable it with mindwire discovery enable.";
681
- if (status.error) return status.error;
682
- return status.sequence && status.publishedSequence === status.sequence ? "Encrypted address recovery is on. Saved phones can find this computer when its tunnel address changes." : "Publishing the computer's encrypted address\u2026";
683
- }
684
564
  var BASE = "https://api.oblien.com";
685
565
  var RENEW_BEFORE_MS = 24 * 60 * 6e4;
686
566
  var ProviderSignInRequired = class extends Error {
@@ -812,6 +692,19 @@ Waiting for sign-in\u2026`);
812
692
  // src/computer/provider-setup.ts
813
693
  var execute2 = promisify(execFile);
814
694
  async function chooseProvider(prompt) {
695
+ if (prompt.select) {
696
+ const selected = await prompt.select("Connection provider", [
697
+ { value: "automatic", label: "Automatic \xB7 Cloudflare + Mindwire recovery, no account" },
698
+ { value: "oblien", label: "Oblien \xB7 persistent address" },
699
+ { value: "cloudflare", label: "Cloudflare \xB7 your account and domain" },
700
+ { value: "ngrok", label: "ngrok \xB7 your account and reserved domain" },
701
+ { value: "vpn", label: "Your VPN \xB7 direct SSH" },
702
+ { value: "custom", label: "Existing secure tunnel" },
703
+ { value: "temporary", label: "Temporary Cloudflare \xB7 no address recovery" }
704
+ ]);
705
+ if (!selected) throw new Error("Connection setup cancelled.");
706
+ return selected;
707
+ }
815
708
  prompt.print("\nConnect this computer\n\n 1 Automatic \u2014 free Cloudflare tunnel + Mindwire address recovery, no account\n 2 Oblien \u2014 persistent address, browser sign-in\n 3 Cloudflare \u2014 your account and domain\n 4 ngrok \u2014 your account and reserved domain\n 5 Your VPN \u2014 direct SSH\n 6 Existing secure tunnel\n 7 Temporary Cloudflare connection \u2014 no address recovery\n");
816
709
  for (; ; ) {
817
710
  const answer = (await prompt.question("Connection [1]: ")).trim();
@@ -962,6 +855,81 @@ The token stays on this computer and is never sent to your phone.`);
962
855
  await lock?.close();
963
856
  }
964
857
  }
858
+ var terminalText = (value) => value.replace(/[\x00-\x1f\x7f-\x9f]/g, "");
859
+ function accent(value) {
860
+ return process.stderr.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb" ? `\x1B[38;5;208m${value}\x1B[0m` : value;
861
+ }
862
+ async function chooseTerminal(title, choices) {
863
+ if (!choices.length) return;
864
+ if (!process.stdin.isTTY || !process.stderr.isTTY) throw new Error("Open mindwire in an interactive terminal, or use a command from mindwire --help.");
865
+ const input = process.stdin, output = process.stderr;
866
+ const wasRaw = !!input.isRaw, wasFlowing = input.readableFlowing === true;
867
+ emitKeypressEvents(input);
868
+ let selected = 0, lines = 0, finished = false;
869
+ const clear = () => {
870
+ if (lines) output.write(`\x1B[${lines}A\r\x1B[J`);
871
+ lines = 0;
872
+ };
873
+ const draw = () => {
874
+ clear();
875
+ const width = Math.max(8, (output.columns || 80) - 4);
876
+ const headings = typeof title === "string" ? [title] : title;
877
+ const visible = Math.max(1, Math.min(choices.length, (output.rows || 24) - headings.length - 4));
878
+ const first = Math.max(0, Math.min(selected - visible + 1, choices.length - visible));
879
+ const clip = (text) => {
880
+ const chars = [...terminalText(text)];
881
+ return chars.length > width ? chars.slice(0, width - 1).join("") + "\u2026" : chars.join("");
882
+ };
883
+ const rows = [
884
+ ...headings.map(clip),
885
+ ...choices.slice(first, first + visible).map((choice, index) => index + first === selected ? accent(`\u203A ${clip(choice.label)}`) : ` ${clip(choice.label)}`),
886
+ "",
887
+ clip("\u2191/\u2193 choose \xB7 Enter select \xB7 Esc back")
888
+ ];
889
+ output.write(rows.join("\n") + "\n");
890
+ lines = rows.length;
891
+ };
892
+ return await new Promise((resolve2, reject) => {
893
+ const cleanup = () => {
894
+ if (finished) return;
895
+ finished = true;
896
+ input.off("keypress", onKey);
897
+ input.off("end", onEnd);
898
+ output.off("resize", draw);
899
+ clear();
900
+ output.write("\x1B[?25h");
901
+ input.setRawMode(wasRaw);
902
+ if (!wasFlowing) input.pause();
903
+ };
904
+ const finish = (value) => {
905
+ cleanup();
906
+ resolve2(value);
907
+ };
908
+ const onEnd = () => finish();
909
+ const onKey = (_text, key) => {
910
+ if (key.name === "escape" || key.ctrl && ["c", "d"].includes(key.name ?? "")) return finish();
911
+ if (key.name === "return") return finish(choices[selected]?.value);
912
+ if (key.name === "up" || key.name === "k") selected = (selected + choices.length - 1) % choices.length;
913
+ else if (key.name === "down" || key.name === "j") selected = (selected + 1) % choices.length;
914
+ else return;
915
+ draw();
916
+ };
917
+ try {
918
+ input.setRawMode(true);
919
+ input.resume();
920
+ output.write("\x1B[?25l");
921
+ input.on("keypress", onKey);
922
+ input.once("end", onEnd);
923
+ output.on("resize", draw);
924
+ draw();
925
+ } catch (error) {
926
+ cleanup();
927
+ reject(error);
928
+ }
929
+ });
930
+ }
931
+
932
+ // src/computer/setup-prompt.ts
965
933
  function openSetupURL(value) {
966
934
  const url = new URL(value);
967
935
  if (url.protocol !== "https:" || url.username || url.password) throw new Error("Invalid provider login URL.");
@@ -990,7 +958,7 @@ function question(text, hidden) {
990
958
  answered = true;
991
959
  input.close();
992
960
  if (hidden) process.stderr.write("\n");
993
- resolve2(value.trim());
961
+ resolve2(hidden ? value : value.trim());
994
962
  });
995
963
  });
996
964
  }
@@ -1000,7 +968,8 @@ var terminalSetupPrompt = {
1000
968
  print: (text) => {
1001
969
  process.stderr.write(text + "\n");
1002
970
  },
1003
- open: openSetupURL
971
+ open: openSetupURL,
972
+ select: chooseTerminal
1004
973
  };
1005
974
  var OblienTunnelLease = class {
1006
975
  constructor(tunnelId, client, now = Date.now) {
@@ -1156,11 +1125,277 @@ async function runManagedRelay(file) {
1156
1125
  process.off("SIGINT", stop);
1157
1126
  }
1158
1127
  }
1128
+ async function inspectComputer(directory) {
1129
+ const [config, controller, startup, preference, stopped, update, change] = await Promise.all([
1130
+ readJSON(join(directory, "computer-config.json")),
1131
+ readJSON(join(directory, "computer-controller.json")),
1132
+ startupStatus(directory),
1133
+ discoveryPreference(directory),
1134
+ readJSON(join(directory, "computer-stopped.json")),
1135
+ readJSON(join(directory, "computer-update.json")),
1136
+ readJSON(join(directory, "computer-connection-change.json"))
1137
+ ]);
1138
+ const alive = await processStateAlive(controller).catch(() => false);
1139
+ const result = {
1140
+ name: hostname(),
1141
+ cliVersion: SDK_VERSION,
1142
+ service: !config ? "not-configured" : stopped?.stopped ? "stopped" : alive ? "starting" : "unavailable",
1143
+ online: false,
1144
+ startup,
1145
+ discoveryPreference: preference,
1146
+ devices: [],
1147
+ update,
1148
+ connection: config && connectionInfo(config.relay, config.host),
1149
+ problem: controller?.error,
1150
+ recoveryProblem: controller?.discoveryError,
1151
+ recovering: controller?.recovering,
1152
+ relayError: controller?.error,
1153
+ relayErrorCode: controller?.errorCode,
1154
+ connectionChange: change && { status: change.status, message: change.message }
1155
+ };
1156
+ if (!config || stopped?.stopped) return result;
1157
+ try {
1158
+ const client = await computerClient(directory);
1159
+ const [info, health, work, devices] = await Promise.all([
1160
+ client.computer.info(),
1161
+ client.health(),
1162
+ client.service.updateStatus(),
1163
+ client.computer.devices()
1164
+ ]);
1165
+ return {
1166
+ ...result,
1167
+ service: "running",
1168
+ online: alive && !!controller?.ready,
1169
+ info,
1170
+ daemonVersion: health.version,
1171
+ activeOperations: work.activeOperations,
1172
+ connection: info.connection ?? result.connection,
1173
+ discovery: info.discovery,
1174
+ devices
1175
+ };
1176
+ } catch {
1177
+ return { ...result, problem: result.problem ?? (alive ? "The service is starting." : "The background service is not running.") };
1178
+ }
1179
+ }
1180
+ function recoverySummary(state) {
1181
+ if (state.discoveryPreference?.enabled === false) return "Off";
1182
+ if (state.discovery?.enabled) {
1183
+ if (state.discovery.error) return "Needs attention";
1184
+ if (!state.discovery.sequence || state.discovery.sequence !== state.discovery.publishedSequence) return "Publishing address";
1185
+ const saved = state.devices.filter((device) => !device.revoked);
1186
+ if (saved.length && saved.some((device) => !device.addressRecovery)) return "Finish setup on your phone";
1187
+ return "Ready";
1188
+ }
1189
+ if (state.connection?.address === "persistent") return "Persistent address";
1190
+ if (state.connection?.provider === "direct") return "Direct / VPN";
1191
+ if (state.recoveryProblem) return "Needs attention";
1192
+ return state.discoveryPreference?.enabled ? "Setting up" : "Not set up";
1193
+ }
1194
+ function inspectionText(state, detailed = false) {
1195
+ const devices = state.devices.filter((device) => !device.revoked);
1196
+ const connected = devices.filter((device) => device.connected).length;
1197
+ const running = state.service === "running";
1198
+ const startup = state.startup.enabled ? state.startup.running ? "On" : "Needs repair" : "Off";
1199
+ const lines = [
1200
+ `Mindwire ${state.cliVersion} \xB7 ${state.name}`,
1201
+ "",
1202
+ ` Service ${running ? `${state.daemonVersion} \xB7 running` : state.service.replaceAll("-", " ")}`,
1203
+ ` Connection ${state.online ? connected ? `${connected} phone${connected === 1 ? "" : "s"} connected` : "Ready for your phone" : "Offline"}`,
1204
+ ` Auto reconnect ${recoverySummary(state)}`,
1205
+ ` Start at login ${startup}`
1206
+ ];
1207
+ if (state.update && ["queued", "downloading", "waiting", "restarting"].includes(state.update.status)) {
1208
+ lines.push(` Update ${state.update.version} \xB7 ${state.update.status === "waiting" ? `waiting for ${state.activeOperations ?? "active"} operation(s)` : state.update.status}`);
1209
+ }
1210
+ if (detailed) {
1211
+ if (state.connection) lines.push(` Carrier ${state.connection.provider}`);
1212
+ if (state.discovery?.url) lines.push(` Directory ${state.discovery.url}`);
1213
+ if (state.info) {
1214
+ lines.push(` Service PID ${state.info.pid}`, ` Computer ${state.info.computerId}`, ` Host key ${state.info.fingerprint}`);
1215
+ for (const route of state.info.routes) lines.push(` Address ${route.kind === "ssh" ? `${route.host}:${route.port}` : route.url}`);
1216
+ }
1217
+ if (state.problem) lines.push("", state.problem);
1218
+ if (state.recoveryProblem) lines.push("", state.recoveryProblem);
1219
+ if (state.discovery?.error) lines.push("", state.discovery.error);
1220
+ if (state.connectionChange?.status === "failed") lines.push("", state.connectionChange.message ?? "Connection provider change failed.");
1221
+ }
1222
+ return lines.map(terminalText).join("\n");
1223
+ }
1224
+
1225
+ // src/computer/dashboard.ts
1226
+ async function computerDashboard(directory, run2) {
1227
+ for (; ; ) {
1228
+ const state = await inspectComputer(directory);
1229
+ const saved = state.devices.filter((device) => !device.revoked);
1230
+ const running = state.service === "running";
1231
+ const action = await chooseTerminal([...inspectionText(state).split("\n"), "", "Manage this computer"], [
1232
+ { value: "reconnect", label: running ? "Reconnect" : "Start connection" },
1233
+ { value: "pair", label: "Pair or repair a phone" },
1234
+ { value: "devices", label: `Phones${saved.length ? ` \xB7 ${saved.length} paired` : ""}` },
1235
+ { value: "startup", label: `Start at login \xB7 ${state.startup.enabled ? state.startup.running ? "On" : "Needs repair" : "Off"}` },
1236
+ { value: "connection", label: "Connection provider" },
1237
+ ...process.platform === "darwin" ? [{ value: "desktop", label: "Desktop access" }] : [],
1238
+ { value: "recovery", label: `Automatic reconnection \xB7 ${recoverySummary(state)}` },
1239
+ { value: "update", label: activeUpdate(state.update) ? `Service update \xB7 ${state.update.status}` : "Update service" },
1240
+ { value: "inspect", label: "Connection details" },
1241
+ ...running ? [{ value: "stop", label: "Stop Mindwire" }] : [],
1242
+ { value: "exit", label: "Exit" }
1243
+ ]);
1244
+ if (!action || action === "exit") return;
1245
+ try {
1246
+ switch (action) {
1247
+ case "reconnect":
1248
+ await run2(["reconnect"]);
1249
+ break;
1250
+ case "pair":
1251
+ await run2(["connect", "pair"]);
1252
+ break;
1253
+ case "connection":
1254
+ await run2(["connection"]);
1255
+ break;
1256
+ case "desktop":
1257
+ await run2(["desktop"]);
1258
+ break;
1259
+ case "inspect":
1260
+ await run2(["inspect"]);
1261
+ break;
1262
+ case "devices": {
1263
+ if (!saved.length) {
1264
+ process.stdout.write("No paired phones. Choose Pair or repair a phone to connect one.\n");
1265
+ break;
1266
+ }
1267
+ const id = await chooseTerminal("Paired phones", saved.map((device2) => ({ value: device2.id, label: deviceSummary([device2], true).trim() })));
1268
+ const device = saved.find((value) => value.id === id);
1269
+ if (!device) break;
1270
+ const decision = await chooseTerminal([
1271
+ terminalText(device.name),
1272
+ `Key ${terminalText(device.id)}`,
1273
+ `Address recovery: ${device.addressRecovery ? "Ready" : "Open this phone to finish setup"}`,
1274
+ ""
1275
+ ], [{ value: "back", label: "Back" }, { value: "revoke", label: "Revoke access and disconnect this phone" }]);
1276
+ if (decision === "revoke") await run2(["revoke", "--", id]);
1277
+ break;
1278
+ }
1279
+ case "startup": {
1280
+ const options = state.startup.enabled ? [{ value: "repair", label: "Repair automatic startup" }, { value: "disable", label: "Turn off automatic startup" }] : [{ value: "enable", label: "Start Mindwire when I sign in" }];
1281
+ const choice = await chooseTerminal("Start at login", [...options, { value: "back", label: "Back" }]);
1282
+ if (choice && choice !== "back") await run2(["startup", choice]);
1283
+ break;
1284
+ }
1285
+ case "recovery": {
1286
+ const enabled = state.discovery?.enabled;
1287
+ const choice = await chooseTerminal([
1288
+ "Automatic reconnection",
1289
+ "Encrypted addresses only. No account needed.",
1290
+ "Files, commands and chats stay inside your SSH connection.",
1291
+ ""
1292
+ ], [
1293
+ { value: "enable", label: enabled ? "Check and repair address recovery" : "Enable address recovery" },
1294
+ { value: "directory", label: "Use a self-hosted directory" },
1295
+ ...enabled ? [{ value: "disable", label: "Turn off address recovery" }] : [],
1296
+ { value: "back", label: "Back" }
1297
+ ]);
1298
+ if (choice === "directory") {
1299
+ const url = await terminalSetupPrompt.question("Directory HTTPS URL: ");
1300
+ if (url) await run2(["discovery", "enable", "--directory-url", url]);
1301
+ } else if (choice === "enable") await run2(["discovery", "enable"]);
1302
+ else if (choice === "disable") {
1303
+ const decision = await chooseTerminal("A temporary address may need another scan after a restart.", [
1304
+ { value: "back", label: "Keep automatic recovery" },
1305
+ { value: "disable", label: "Turn off recovery" }
1306
+ ]);
1307
+ if (decision === "disable") await run2(["discovery", "disable"]);
1308
+ }
1309
+ break;
1310
+ }
1311
+ case "update":
1312
+ await run2(["update"]);
1313
+ break;
1314
+ case "stop": {
1315
+ const decision = await chooseTerminal(state.activeOperations ? "Stopping Mindwire closes its terminals and active sessions." : "Stop Mindwire until you choose Start again?", [
1316
+ { value: "back", label: "Keep running" },
1317
+ { value: "stop", label: "Stop Mindwire" }
1318
+ ]);
1319
+ if (decision === "stop") await run2(["stop", ...state.activeOperations ? ["--force"] : []]);
1320
+ break;
1321
+ }
1322
+ }
1323
+ } catch (error) {
1324
+ process.stderr.write(`Mindwire: ${terminalText(error instanceof Error ? error.message : String(error))}
1325
+ `);
1326
+ }
1327
+ if (!await chooseTerminal("", [{ value: "continue", label: "Back to Mindwire" }])) return;
1328
+ }
1329
+ }
1330
+ function desktopSummary(info) {
1331
+ if (!info.supported) return "Desktop control is available on macOS.";
1332
+ if (!info.enabled) return "Desktop access is off. Enable it with mindwire desktop enable.";
1333
+ if (!info.screenSharing) return "Desktop access needs Screen Sharing. Run mindwire desktop enable to finish setup.";
1334
+ return "Desktop access is on \xB7 paired SSH connection";
1335
+ }
1336
+ function openScreenSharingSettings() {
1337
+ const child = spawn("/usr/bin/open", ["x-apple.systempreferences:com.apple.Sharing-Settings.extension?ScreenSharing"], { stdio: "ignore" });
1338
+ child.on("error", () => {
1339
+ });
1340
+ child.unref();
1341
+ }
1342
+ async function manageDesktop(options) {
1343
+ const { client, directory, interactive } = options;
1344
+ const prompt = options.prompt ?? terminalSetupPrompt;
1345
+ let action = options.action ?? (interactive ? "menu" : "status");
1346
+ if (!["menu", "status", "enable", "disable"].includes(action)) throw new Error("Use mindwire desktop, or mindwire desktop enable, disable or status.");
1347
+ let info = await client.surfaces.localStatus();
1348
+ if (!info.supported) throw new Error("Desktop control is currently available on paired Macs.");
1349
+ if (action === "menu") {
1350
+ const choices = [
1351
+ { value: "enable", label: info.enabled ? "Check setup or update Mac login" : "Enable desktop access" },
1352
+ ...info.enabled ? [{ value: "disable", label: "Disable desktop access" }] : [],
1353
+ { value: "status", label: "Back" }
1354
+ ];
1355
+ action = await prompt.select?.(desktopSummary(info), choices) ?? "status";
1356
+ }
1357
+ if (action === "status") return info;
1358
+ if (action === "enable" && !interactive) throw new Error("Run mindwire desktop enable in an interactive terminal on your Mac. The password is entered privately there.");
1359
+ const localToken = (await (options.readControlToken?.() ?? readFile(path.join(directory, "desktop-control.token"), "utf8"))).trim();
1360
+ if (!/^[a-f0-9]{64}$/.test(localToken)) throw new Error("Update and restart Mindwire on this Mac before setting up desktop access.");
1361
+ if (action === "disable") {
1362
+ await client.surfaces.configureLocal({ enabled: false }, localToken);
1363
+ if (!options.json) prompt.print("Desktop access is off. Its active viewers and controls were closed.\nApple Screen Sharing is unchanged; you can turn it off in Mac settings too.");
1364
+ return client.surfaces.localStatus();
1365
+ }
1366
+ prompt.print("Desktop access\nYour paired phones can view and control this Mac. Agents still need desktop approval.\nThe display uses your existing encrypted SSH connection. The Mac login stays in Mindwire\u2019s private credentials on this Mac.");
1367
+ const decision = prompt.select ? await prompt.select("Allow desktop access?", [
1368
+ { value: "cancel", label: "Keep current settings" },
1369
+ { value: "allow", label: "Allow desktop access" }
1370
+ ]) : (await prompt.question("Allow desktop access? [y/N]: ")).toLowerCase() === "y" ? "allow" : "cancel";
1371
+ if (decision !== "allow") return info;
1372
+ while (!info.screenSharing) {
1373
+ prompt.print(`In System Settings \u2192 General \u2192 Sharing, enable Screen Sharing.
1374
+ Allow only ${terminalText(info.username)}. Leave password-only VNC access off.`);
1375
+ const choice = await prompt.select?.("Finish the Mac permission step", [
1376
+ { value: "open", label: "Open Screen Sharing settings" },
1377
+ { value: "check", label: "I enabled it \u2014 check again" },
1378
+ { value: "cancel", label: "Cancel setup" }
1379
+ ]) ?? "cancel";
1380
+ if (choice === "cancel") return info;
1381
+ if (choice === "open") {
1382
+ (options.openSettings ?? openScreenSharingSettings)();
1383
+ await prompt.question("Enable Screen Sharing, then press Return to check: ");
1384
+ }
1385
+ info = await client.surfaces.localStatus();
1386
+ }
1387
+ const password = await prompt.secret(`Mac password for ${terminalText(info.username)} (hidden): `);
1388
+ if (!password || Buffer.byteLength(password) > 63 || password.includes("\0")) throw new Error("Enter your Mac password (up to 63 UTF-8 bytes). Desktop settings were not changed.");
1389
+ await client.surfaces.configureLocal({ enabled: true, username: info.username, password }, localToken);
1390
+ prompt.print("Desktop access is on. Open Desktop in Mindwire on your phone.\nTurn it off anytime with mindwire desktop disable.");
1391
+ return client.surfaces.localStatus();
1392
+ }
1159
1393
 
1160
1394
  // src/cli.ts
1161
1395
  var help = `Mindwire \u2014 connect this computer to your phone
1162
1396
 
1163
1397
  npm install -g mindwire
1398
+ mindwire Interactive status and connection controls
1164
1399
  mindwire connect Connect your phone; saved keys reconnect automatically
1165
1400
  mindwire reconnect Retry the saved connection
1166
1401
  mindwire connect pair Pair a phone, or repair a lost phone key/address
@@ -1179,12 +1414,17 @@ var help = `Mindwire \u2014 connect this computer to your phone
1179
1414
  mindwire connect --relay custom --relay-url wss://computer.example/ssh
1180
1415
 
1181
1416
  mindwire start Start the saved connection in the background
1182
- mindwire startup enable|disable Manage automatic startup after computer login
1417
+ mindwire startup enable|disable|repair Manage automatic startup after computer login
1183
1418
  mindwire startup status Check automatic startup
1184
1419
  mindwire status Show connection and running service
1420
+ mindwire inspect Connection details, recovery and diagnostics
1185
1421
  mindwire devices List paired phones
1422
+ mindwire desktop Desktop status and Mac permission setup
1423
+ mindwire desktop enable|disable Allow or stop desktop access from this Mac
1424
+ mindwire desktop status Check desktop setup
1186
1425
  mindwire revoke DEVICE_ID Disconnect and revoke one phone
1187
1426
  mindwire update [--version VERSION] Download a verified release; wait until idle
1427
+ mindwire update --force Restart now, closing active terminals and sessions
1188
1428
  mindwire stop Stop when no work or terminals are running
1189
1429
 
1190
1430
  Options:
@@ -1215,6 +1455,10 @@ function safeName(text) {
1215
1455
  return text.replace(/[\x00-\x1f\x7f-\x9f]/g, "");
1216
1456
  }
1217
1457
  async function main(args = process.argv.slice(2)) {
1458
+ if (args.some((value) => ["--version", "-v", "version"].includes(value)) && args.every((value) => ["--version", "-v", "version", "--json"].includes(value))) {
1459
+ process.stdout.write(args.includes("--json") ? JSON.stringify({ version: SDK_VERSION }) + "\n" : SDK_VERSION + "\n");
1460
+ return;
1461
+ }
1218
1462
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
1219
1463
  help: { type: "boolean", short: "h" },
1220
1464
  json: { type: "boolean" },
@@ -1241,11 +1485,14 @@ async function main(args = process.argv.slice(2)) {
1241
1485
  "no-startup": { type: "boolean" },
1242
1486
  "replace-device": { type: "string", multiple: true }
1243
1487
  } });
1244
- const command = positionals[0] ?? "help";
1488
+ const interactive = !values.json && !!process.stdin.isTTY && !!process.stderr.isTTY && process.env.TERM !== "dumb";
1489
+ const command = positionals[0] ?? (interactive ? "dashboard" : "help");
1245
1490
  if (values.help || command === "help") {
1246
1491
  process.stdout.write(help);
1247
1492
  return;
1248
1493
  }
1494
+ if (!["dashboard", "connect", "start", "reconnect", "connection", "startup", "discovery", "desktop", "status", "inspect", "devices", "revoke", "approve", "reject", "update", "stop", "_serve", "_watch", "_relay"].includes(command))
1495
+ throw new Error(`Unknown command ${safeName(command)}. Run mindwire --help.`);
1249
1496
  if (values.qr && !["auto", "terminal", "browser"].includes(values.qr)) throw new Error("Choose --qr auto, terminal or browser.");
1250
1497
  if (values.startup && values["no-startup"]) throw new Error("Choose either --startup or --no-startup.");
1251
1498
  const directory = path.resolve(values["state-dir"] ?? defaultStateDirectory());
@@ -1254,6 +1501,29 @@ async function main(args = process.argv.slice(2)) {
1254
1501
  if (values.json) process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
1255
1502
  else if (text) process.stdout.write(text + "\n");
1256
1503
  };
1504
+ if (command === "desktop") {
1505
+ if (process.platform !== "darwin") throw new Error("Desktop setup is currently available on macOS. Your other Mindwire features are unchanged.");
1506
+ const client2 = await computerClient(directory, 3e4);
1507
+ const health = await client2.health();
1508
+ if ((health.localDesktopVersion ?? 0) < 1) throw new Error("Update the Mindwire service on this Mac before setting up desktop access: mindwire update");
1509
+ const info = await manageDesktop({ client: client2, directory, action: positionals[1], interactive, json: values.json });
1510
+ emit("desktop", info, desktopSummary(info));
1511
+ return;
1512
+ }
1513
+ if (command === "dashboard") {
1514
+ await computerDashboard(directory, (next) => main([
1515
+ "--state-dir",
1516
+ directory,
1517
+ ...values["directory-url"] && !next.includes("--directory-url") ? ["--directory-url", values["directory-url"]] : [],
1518
+ ...next
1519
+ ]));
1520
+ return;
1521
+ }
1522
+ if (command === "status" || command === "inspect") {
1523
+ const state = await inspectComputer(directory);
1524
+ emit(command, { ...state.info, ...state, idle: state.activeOperations === 0 }, inspectionText(state, command === "inspect"));
1525
+ return;
1526
+ }
1257
1527
  if (command === "_serve") {
1258
1528
  await superviseComputer(directory);
1259
1529
  return;
@@ -1269,23 +1539,23 @@ async function main(args = process.argv.slice(2)) {
1269
1539
  }
1270
1540
  if (command === "startup") {
1271
1541
  const action = positionals[1] ?? "status";
1272
- if (!["enable", "disable", "status"].includes(action)) throw new Error("Use mindwire startup enable, disable or status.");
1273
- const state = action === "status" ? await startupStatus(directory) : await configureStartup(directory, fileURLToPath(import.meta.url), action === "enable");
1274
- emit("startup", state, state.enabled ? "Mindwire starts after you sign in to this computer and recovers automatically." : "Automatic startup is off. Use mindwire start to run the service.");
1542
+ if (!["enable", "disable", "repair", "status"].includes(action)) throw new Error("Use mindwire startup enable, disable, repair or status.");
1543
+ const state = action === "status" ? await startupStatus(directory) : await configureStartup(directory, fileURLToPath(import.meta.url), action !== "disable", { repair: action === "repair" });
1544
+ emit("startup", state, state.enabled ? state.running ? "Mindwire starts after you sign in to this computer." : "Automatic startup needs repair. Run mindwire startup repair." : "Automatic startup is off. Use mindwire start to run the service.");
1275
1545
  return;
1276
1546
  }
1277
1547
  if (command === "discovery") {
1278
1548
  const action = positionals[1] ?? "status";
1279
1549
  if (!["enable", "disable", "status"].includes(action)) throw new Error("Use mindwire discovery enable, disable or status.");
1280
1550
  const client2 = action === "status" ? await computerClient(directory) : await ensureComputer(directory, fileURLToPath(import.meta.url), {}, void 0, { waitForRoutes: false });
1281
- const status = action === "enable" ? await enableAddressDiscovery({ directory, computer: client2, prompt: terminalSetupPrompt, ...discoveryOptions }) : action === "disable" ? await client2.computer.configureDiscovery(false) : await client2.computer.discovery();
1551
+ const status = action === "enable" ? await enableAddressDiscovery({ directory, computer: client2, prompt: terminalSetupPrompt, ...discoveryOptions }) : action === "disable" ? await disableAddressDiscovery(directory, client2) : await client2.computer.discovery();
1282
1552
  emit("discovery", status, discoveryDescription(status));
1283
1553
  if (action === "enable") emit("discovery_hint", {}, "Open each saved phone once while its current address works. Pair new phones with mindwire connect pair.");
1284
1554
  return;
1285
1555
  }
1286
1556
  if (["connect", "start", "reconnect", "connection"].includes(command)) {
1287
1557
  let requestedAction = command === "reconnect" ? "reconnect" : command === "connect" ? connectionAction(positionals[1]) : void 0;
1288
- let automaticDiscovery = false, addressChanged = false;
1558
+ let automaticDiscovery = false, temporaryConnection = false, addressChanged = false;
1289
1559
  const patch = {};
1290
1560
  const previous = await readJSON(path.join(directory, "computer-config.json"));
1291
1561
  if (values.directory) patch.directory = path.resolve(values.directory);
@@ -1299,7 +1569,10 @@ async function main(args = process.argv.slice(2)) {
1299
1569
  }
1300
1570
  }
1301
1571
  if (values["daemon-bin"] || process.env.MINDWIRE_DAEMON) patch.daemonBin = path.resolve(values["daemon-bin"] ?? process.env.MINDWIRE_DAEMON);
1302
- if (values.version) patch.version = values.version.replace(/^v/, "");
1572
+ if (values.version) {
1573
+ patch.version = values.version.replace(/^v/, "");
1574
+ if (!previous) patch.versionPinned = true;
1575
+ }
1303
1576
  if (values.relay || values["relay-url"] || values["cloudflare-token-file"] || values["ngrok-token-file"] || values["oblien-credentials-file"]) {
1304
1577
  const kind = values.relay ?? "custom";
1305
1578
  if (!["none", "oblien", "cloudflare", "ngrok", "custom"].includes(kind)) throw new Error("Choose relay none, oblien, cloudflare, ngrok or custom.");
@@ -1314,7 +1587,6 @@ async function main(args = process.argv.slice(2)) {
1314
1587
  oblienTunnelId: tunnelId
1315
1588
  };
1316
1589
  }
1317
- const interactive = !values.json && !!process.stdin.isTTY;
1318
1590
  const needsSetup = command === "connection" || command === "connect" && !previous && !patch.relay && interactive || patch.relay?.kind === "oblien" && !patch.relay.oblienCredentialsFile;
1319
1591
  if (needsSetup) {
1320
1592
  const selected = command === "connection" && positionals[1] ? connectionProvider(positionals[1]) : void 0;
@@ -1322,6 +1594,7 @@ async function main(args = process.argv.slice(2)) {
1322
1594
  throw new Error("Run mindwire connection in an interactive terminal, or supply the provider's saved URL and credential-file options.");
1323
1595
  const provider = selected ?? (patch.relay?.kind === "oblien" ? "oblien" : await chooseProvider(terminalSetupPrompt));
1324
1596
  automaticDiscovery = provider === "automatic";
1597
+ temporaryConnection = provider === "temporary";
1325
1598
  Object.assign(patch, await setupProvider({
1326
1599
  directory,
1327
1600
  provider,
@@ -1330,6 +1603,11 @@ async function main(args = process.argv.slice(2)) {
1330
1603
  }));
1331
1604
  if (!previous || connectionAddress(patch.relay ?? previous.relay, patch.host ?? previous.host) !== connectionAddress(previous.relay, previous.host)) addressChanged = true;
1332
1605
  }
1606
+ if (temporaryConnection) await saveDiscoveryPreference(directory, { enabled: false });
1607
+ if (automaticDiscovery) await saveDiscoveryPreference(directory, {
1608
+ enabled: true,
1609
+ url: values["directory-url"] ?? (await discoveryPreference(directory))?.url
1610
+ });
1333
1611
  if (automaticDiscovery && previous) {
1334
1612
  const running = await ensureComputer(directory, fileURLToPath(import.meta.url), {}, void 0, { waitForRoutes: false });
1335
1613
  await enableAddressDiscovery({ directory, computer: running, prompt: terminalSetupPrompt, ...discoveryOptions });
@@ -1340,12 +1618,17 @@ async function main(args = process.argv.slice(2)) {
1340
1618
  if (progress) progress.update(phase);
1341
1619
  else emit("progress", { message: phase }, phase);
1342
1620
  }).finally(() => progress?.close());
1343
- if (automaticDiscovery && !previous) await enableAddressDiscovery({ directory, computer: client2, prompt: terminalSetupPrompt, ...discoveryOptions });
1344
- const info = await client2.computer.info();
1345
1621
  const savedConfig = await readJSON(path.join(directory, "computer-config.json"));
1622
+ try {
1623
+ if (temporaryConnection) await disableAddressDiscovery(directory, client2);
1624
+ else if (savedConfig) await reconcileAddressDiscovery({ directory, config: savedConfig, computer: client2 });
1625
+ } catch (error) {
1626
+ emit("recovery_pending", {}, `Automatic reconnection: ${safeName(error instanceof Error ? error.message : "Setup will retry in the background.")}`);
1627
+ }
1628
+ const info = await client2.computer.info();
1346
1629
  const connection = connectionInfo(savedConfig?.relay ?? { kind: "cloudflare" }, savedConfig?.host);
1347
1630
  emit("ready", { computerId: info.computerId, routes: info.routes, connection }, "Mindwire is running in the background.");
1348
- emit("connection", connection, connectionDescription(connection));
1631
+ emit("connection", connection, info.discovery?.enabled && connection.address === "temporary" ? "Cloudflare \xB7 encrypted Mindwire address recovery" : connectionDescription(connection));
1349
1632
  if (command === "start") return;
1350
1633
  if (info.routes.some((route) => route.kind === "websocket")) {
1351
1634
  emit("secure_connection", {}, "Your phone connects over end-to-end encrypted SSH. The tunnel carries encrypted traffic; only phones you approve can connect.");
@@ -1381,7 +1664,7 @@ ${deviceSummary(saved)}`);
1381
1664
  emit("startup_unavailable", {}, "Mindwire is running, but startup setup didn't complete. Run mindwire startup enable to check your system's startup service.");
1382
1665
  }
1383
1666
  } else {
1384
- emit("startup", startupPreference, startupPreference.enabled ? "Automatic startup is on. Change it with mindwire startup disable." : "Automatic startup is off. Enable it with mindwire startup enable.");
1667
+ emit("startup", startupPreference, startupPreference.enabled ? startupPreference.running ? "Automatic startup is on. Change it with mindwire startup disable." : "Automatic startup needs repair. Run mindwire startup repair." : "Automatic startup is off. Enable it with mindwire startup enable.");
1385
1668
  }
1386
1669
  if (action === "resume") {
1387
1670
  const connected = saved.filter((device) => device.connected);
@@ -1473,42 +1756,30 @@ ${command2}`);
1473
1756
  emit("stopped", {}, "Mindwire stopped.");
1474
1757
  return;
1475
1758
  }
1476
- const client = await computerClient(directory);
1477
- switch (command) {
1478
- case "status": {
1479
- const [info, health, work, controller, startup, config, change] = await Promise.all([
1480
- client.computer.info(),
1481
- client.health(),
1482
- client.service.updateStatus(),
1483
- readJSON(path.join(directory, "computer-controller.json")),
1484
- startupStatus(directory),
1485
- readJSON(path.join(directory, "computer-config.json")),
1486
- readJSON(path.join(directory, "computer-connection-change.json"))
1759
+ if (command === "update") {
1760
+ const client2 = await ensureComputer(directory, fileURLToPath(import.meta.url), {}, void 0, { waitForRoutes: false });
1761
+ let force = values.force === true;
1762
+ if (interactive && !force && !(await client2.service.updateStatus()).idle) {
1763
+ const choice = await chooseTerminal("An update can restart Mindwire while work is open.", [
1764
+ { value: "wait", label: "Update when idle" },
1765
+ { value: "restart", label: "Restart now \u2014 close terminals and active sessions" },
1766
+ { value: "cancel", label: "Cancel" }
1487
1767
  ]);
1488
- const connection = info.connection ?? (config && connectionInfo(config.relay, config.host));
1489
- const connectionChange = change && { status: change.status, message: change.message };
1490
- emit(
1491
- "status",
1492
- {
1493
- ...info,
1494
- connection,
1495
- daemonVersion: health.version,
1496
- ...work,
1497
- startup,
1498
- connectionChange,
1499
- recovering: controller?.recovering,
1500
- relayError: controller?.error,
1501
- relayErrorCode: controller?.errorCode
1502
- },
1503
- `Mindwire ${health.version} \xB7 ${work.idle ? "idle" : "working"}
1504
- Startup: ${startup.enabled ? "on" : "off"}
1505
- ${connection ? connectionDescription(connection) + "\n" : ""}${info.routes.map((route) => route.kind === "ssh" ? `SSH ${route.host}:${route.port}` : route.url).join("\n")}${controller?.error ? `
1506
- ${controller.error}` : ""}${change && change.status !== "complete" ? `
1507
- Connection change: ${change.status}${change.message ? " \xB7 " + change.message : ""}` : ""}`
1508
- );
1509
- if (info.discovery?.enabled && !values.json) emit("discovery", info.discovery, discoveryDescription(info.discovery));
1510
- break;
1768
+ if (!choice || choice === "cancel") return;
1769
+ force = choice === "restart";
1511
1770
  }
1771
+ const result = await updateComputer({
1772
+ directory,
1773
+ version: values.version ?? SDK_VERSION,
1774
+ force,
1775
+ allowDowngrade: !!values.version,
1776
+ onProgress: (update) => emit("update", update, updateProgress(update))
1777
+ });
1778
+ emit(result.complete ? "updated" : "update_pending", result, result.complete ? `Mindwire service ${result.version} is installed.` : `${result.update?.version} is ${result.update?.status === "waiting" ? "waiting for active work to finish" : "updating in the background"}.${result.update?.version !== result.version ? ` Run mindwire update again after it completes to check ${result.version}.` : " You can close this terminal."} Run mindwire inspect for status.`);
1779
+ return;
1780
+ }
1781
+ const client = await computerClient(directory);
1782
+ switch (command) {
1512
1783
  case "devices": {
1513
1784
  const devices = await client.computer.devices();
1514
1785
  emit("devices", { devices }, devices.length ? devices.map((d) => `${safeName(d.name)} ${d.id}${d.revoked ? " (revoked)" : ""}`).join("\n") : "No paired phones yet. Run mindwire connect.");
@@ -1532,28 +1803,6 @@ Connection change: ${change.status}${change.message ? " \xB7 " + change.message
1532
1803
  emit(command, {}, command === "approve" ? "Phone approved. Waiting for it to save the connection." : "Phone declined.");
1533
1804
  break;
1534
1805
  }
1535
- case "update": {
1536
- const version = (values.version ?? SDK_VERSION).replace(/^v/, "");
1537
- const health = await client.health();
1538
- if (health.version.replace(/^v/, "") === version) {
1539
- emit("updated", { version }, `Mindwire ${version} is already installed.`);
1540
- return;
1541
- }
1542
- const requested = await client.computer.requestUpdate(version);
1543
- emit("update_queued", requested, "Update queued. It will wait until all work and terminals are finished.");
1544
- let last = "";
1545
- for (; ; ) {
1546
- const update = await readJSON(path.join(directory, "computer-update.json"));
1547
- if (update?.id !== requested.id) throw new Error("The update request changed. Run mindwire status.");
1548
- if (update.status !== last) {
1549
- last = update.status;
1550
- emit("update", update, `Update: ${update.status}`);
1551
- }
1552
- if (update.status === "complete") return;
1553
- if (update.status === "failed") throw new Error(update.error ?? "Update failed.");
1554
- await setTimeout$1(750);
1555
- }
1556
- }
1557
1806
  default:
1558
1807
  throw new Error(`Unknown command ${command}. Run mindwire --help.`);
1559
1808
  }