mindwire 0.1.36 → 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.
- package/README.md +16 -0
- package/dist/{chunk-RTGZDLSG.js → chunk-BJL5K6M7.js} +74 -21
- package/dist/chunk-BJL5K6M7.js.map +1 -0
- package/dist/chunk-ZP7F7LP7.js +306 -0
- package/dist/chunk-ZP7F7LP7.js.map +1 -0
- package/dist/cli.js +499 -250
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +6 -8
- package/dist/computer/dashboard.d.ts +1 -0
- package/dist/computer/desktop.d.ts +17 -0
- package/dist/computer/directory-http.d.ts +1 -1
- package/dist/computer/discovery.d.ts +22 -0
- package/dist/computer/inspection.d.ts +30 -0
- package/dist/computer/lifecycle.d.ts +4 -2
- package/dist/computer/runtime-version.d.ts +6 -0
- package/dist/computer/service-update.d.ts +26 -0
- package/dist/computer/setup-prompt.d.ts +2 -0
- package/dist/computer/startup.d.ts +7 -0
- package/dist/computer/terminal-ui.d.ts +9 -0
- package/dist/computer.d.ts +2 -0
- package/dist/{connection-change-IBNL42BL.js → connection-change-7ZV6O75Q.js} +3 -3
- package/dist/{connection-change-IBNL42BL.js.map → connection-change-7ZV6O75Q.js.map} +1 -1
- package/dist/{controller-2SJLZMJZ.js → controller-FBMGMSLM.js} +67 -9
- package/dist/controller-FBMGMSLM.js.map +1 -0
- package/dist/{controller-handoff-RUUHODNC.js → controller-handoff-CJKXUFQI.js} +3 -3
- package/dist/{controller-handoff-RUUHODNC.js.map → controller-handoff-CJKXUFQI.js.map} +1 -1
- package/dist/conversations.d.ts +57 -0
- package/dist/daemon-binary.d.ts +2 -0
- package/dist/index.cjs +66 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +66 -13
- package/dist/index.js.map +1 -1
- package/dist/project-sync.d.ts +29 -0
- package/dist/surfaces.d.ts +20 -1
- package/dist/types.d.ts +18 -0
- package/dist/workspace.d.ts +2 -0
- package/package.json +1 -1
- package/dist/chunk-67BNDGRK.js +0 -20
- package/dist/chunk-67BNDGRK.js.map +0 -1
- package/dist/chunk-RTGZDLSG.js.map +0 -1
- package/dist/controller-2SJLZMJZ.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-
|
|
3
|
-
import {
|
|
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 {
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
|
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
|
|
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 === "
|
|
1274
|
-
emit("startup", state, state.enabled ? "Mindwire starts after you sign in to this computer
|
|
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
|
|
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)
|
|
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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
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
|
}
|