mindwire 0.1.26 → 0.1.27
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/dist/{chunk-L3USUTZZ.js → chunk-QXHI4TMC.js} +38 -9
- package/dist/chunk-QXHI4TMC.js.map +1 -0
- package/dist/cli.js +145 -27
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +11 -1
- package/dist/computer/connection-flow.d.ts +22 -0
- package/dist/computer/pairing-display.d.ts +3 -3
- package/dist/computer.d.ts +23 -3
- package/dist/{controller-CGSJZZZD.js → controller-7YV6O7GT.js} +3 -3
- package/dist/{controller-CGSJZZZD.js.map → controller-7YV6O7GT.js.map} +1 -1
- package/dist/index.cjs +35 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +35 -6
- package/dist/index.js.map +1 -1
- package/dist/service.d.ts +5 -1
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
- package/dist/chunk-L3USUTZZ.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { defaultStateDirectory, superviseComputer, ensureComputer, computerPairingURI, stopComputer, computerClient, SDK_VERSION, readJSON, writeJSON, currentProcessIdentity, processStateAlive, acquireProcessLock, computerPairingQRData } from './chunk-
|
|
2
|
+
import { defaultStateDirectory, superviseComputer, ensureComputer, computerPairingURI, stopComputer, computerClient, SDK_VERSION, readJSON, writeJSON, currentProcessIdentity, processStateAlive, acquireProcessLock, computerPairingQRData } from './chunk-QXHI4TMC.js';
|
|
3
3
|
import './chunk-T6TD76UA.js';
|
|
4
4
|
import { promisify, parseArgs } from 'util';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
@@ -7,10 +7,10 @@ import { realpathSync, mkdtempSync, chmodSync, writeFileSync, rmSync } from 'fs'
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
import { createHash } from 'crypto';
|
|
10
|
+
import { hostname, homedir, tmpdir } from 'os';
|
|
10
11
|
import { createInterface } from 'readline/promises';
|
|
11
12
|
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
12
13
|
import { execFile, spawn } from 'child_process';
|
|
13
|
-
import { homedir, tmpdir } from 'os';
|
|
14
14
|
import * as fs from 'fs/promises';
|
|
15
15
|
|
|
16
16
|
var enterScreen = "\x1B[?1049h\x1B[?25l";
|
|
@@ -40,10 +40,12 @@ function escapeHTML(value) {
|
|
|
40
40
|
return value.replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]);
|
|
41
41
|
}
|
|
42
42
|
function browserPage(invitation, svg) {
|
|
43
|
+
const reconnect = "mode" in invitation && invitation.mode === "reconnect";
|
|
44
|
+
const title = reconnect ? "Reconnect your phone" : "Connect your phone";
|
|
43
45
|
return `<!doctype html>
|
|
44
46
|
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
45
47
|
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
|
|
46
|
-
<title
|
|
48
|
+
<title>${title} \xB7 Mindwire</title>
|
|
47
49
|
<style>
|
|
48
50
|
*{box-sizing:border-box}body{margin:0;background:#141416;color:#f5f5f7;font:16px/1.5 system-ui,sans-serif}
|
|
49
51
|
main{width:min(100% - 40px,520px);margin:32px auto;text-align:center}small{color:#a6a6af;letter-spacing:.16em}
|
|
@@ -55,18 +57,18 @@ button:disabled{opacity:.45;cursor:default}button:focus-visible{outline:3px soli
|
|
|
55
57
|
textarea{width:100%;margin-top:12px;padding:8px;color:inherit;background:#252528;border:1px solid #45454b;border-radius:8px}
|
|
56
58
|
[hidden]{display:none!important}#expiry{font-size:14px}footer{font-size:13px;color:#93939d;margin:18px 0}
|
|
57
59
|
</style></head><body><main>
|
|
58
|
-
<small>MINDWIRE</small><h1
|
|
59
|
-
<div class="code" id="code" role="img" aria-label="Mindwire
|
|
60
|
+
<small>MINDWIRE</small><h1>${title}</h1><p>${escapeHTML(invitation.name)}</p>
|
|
61
|
+
<div class="code" id="code" role="img" aria-label="Mindwire connection QR code">${svg}</div>
|
|
60
62
|
<p id="expiry" data-expires="${escapeHTML(invitation.expiresAt)}" aria-live="off"></p>
|
|
61
63
|
<div class="steps"><p>In Mindwire, open <strong>Add computer</strong> and scan.</p>
|
|
62
|
-
<p
|
|
63
|
-
<button type="button" id="copy">Copy pairing link</button>
|
|
64
|
+
<p>${reconnect ? "Your saved pairing and workspace stay the same. No new approval is needed." : "Then return to the terminal to approve your phone."}</p></div>
|
|
65
|
+
<button type="button" id="copy">Copy ${reconnect ? "reconnect" : "pairing"} link</button>
|
|
64
66
|
<textarea id="link" readonly hidden aria-label="Pairing link">${escapeHTML(computerPairingURI(invitation))}</textarea>
|
|
65
|
-
<footer>You can close this page after pairing.</footer>
|
|
67
|
+
<footer>You can close this page after ${reconnect ? "reconnecting" : "pairing"}.</footer>
|
|
66
68
|
</main><script>
|
|
67
69
|
const expiry=document.getElementById('expiry'),code=document.getElementById('code'),copy=document.getElementById('copy'),link=document.getElementById('link');
|
|
68
70
|
function tick(){const seconds=Math.max(0,Math.ceil((Date.parse(expiry.dataset.expires)-Date.now())/1000));
|
|
69
|
-
if(!seconds){code.remove();copy.disabled=true;link.value='';link.hidden=true;expiry.textContent='Code expired. Run mindwire connect for a new code.';return;}
|
|
71
|
+
if(!seconds){code.remove();copy.disabled=true;link.value='';link.hidden=true;expiry.textContent='Code expired. Run mindwire ${reconnect ? "reconnect" : "connect pair"} for a new code.';return;}
|
|
70
72
|
expiry.textContent='Expires in '+Math.floor(seconds/60)+':'+String(seconds%60).padStart(2,'0');}
|
|
71
73
|
tick();setInterval(tick,1000);
|
|
72
74
|
copy.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(link.value);copy.textContent='Copied';}
|
|
@@ -90,6 +92,10 @@ async function openBrowser(url) {
|
|
|
90
92
|
});
|
|
91
93
|
}
|
|
92
94
|
async function showPairingInvitation(invitation, options = {}) {
|
|
95
|
+
const reconnect = "mode" in invitation && invitation.mode === "reconnect";
|
|
96
|
+
const nextStep = reconnect ? "Scan with a saved phone, then press Enter here." : "Return here to approve your phone.";
|
|
97
|
+
const waiting = reconnect ? "Your saved pairing stays the same." : "Waiting for your phone\u2026";
|
|
98
|
+
const command = reconnect ? "mindwire reconnect" : "mindwire connect pair";
|
|
93
99
|
const output = options.output ?? process.stdout;
|
|
94
100
|
const environment = options.environment ?? process.env;
|
|
95
101
|
const mode = options.mode ?? "auto";
|
|
@@ -99,7 +105,7 @@ async function showPairingInvitation(invitation, options = {}) {
|
|
|
99
105
|
output.write(`Paste this link in Mindwire \u2192 Add computer (expires in 5 minutes):
|
|
100
106
|
${computerPairingURI(invitation)}
|
|
101
107
|
|
|
102
|
-
|
|
108
|
+
${waiting}
|
|
103
109
|
`);
|
|
104
110
|
return { close() {
|
|
105
111
|
} };
|
|
@@ -115,10 +121,10 @@ Waiting for your phone\u2026
|
|
|
115
121
|
if (closed || !useScreen) return;
|
|
116
122
|
let lines;
|
|
117
123
|
if (fits()) {
|
|
118
|
-
lines = [scanInstruction, "", ...qr.lines, "",
|
|
124
|
+
lines = [scanInstruction, "", ...qr.lines, "", waiting];
|
|
119
125
|
} else {
|
|
120
126
|
const size = `Terminal QR needs ${qr.columns} columns \xD7 ${qr.rows} rows.`;
|
|
121
|
-
lines = browser === "opened" ? ["QR opened in your browser.", scanInstruction,
|
|
127
|
+
lines = browser === "opened" ? ["QR opened in your browser.", scanInstruction, nextStep] : browser === "opening" ? ["Opening a resizable QR in your browser\u2026", nextStep] : browser === "failed" ? ["Couldn't open a browser here.", size, "Enlarge this terminal, or run:", `${command} --no-qr`] : [size, "Enlarge this terminal, or use --qr browser."];
|
|
122
128
|
lines = lines.map((line) => line.slice(0, Math.max(0, (output.columns ?? 80) - 1))).slice(0, Math.max(0, (output.rows ?? 24) - 1));
|
|
123
129
|
}
|
|
124
130
|
output.write(clearScreen + lines.join("\r\n"));
|
|
@@ -144,11 +150,11 @@ Waiting for your phone\u2026
|
|
|
144
150
|
if (useScreen) draw();
|
|
145
151
|
else output.write(browser === "opened" ? `QR opened in your browser.
|
|
146
152
|
${scanInstruction}
|
|
147
|
-
|
|
153
|
+
${nextStep}
|
|
148
154
|
` : `Open this local QR page in a browser:
|
|
149
|
-
${pageURL ??
|
|
155
|
+
${pageURL ?? `Page unavailable. Run ${command} --no-qr for a link.`}
|
|
150
156
|
|
|
151
|
-
|
|
157
|
+
${waiting}
|
|
152
158
|
`);
|
|
153
159
|
})();
|
|
154
160
|
return browserAttempt;
|
|
@@ -401,11 +407,74 @@ async function watchComputer(directory, cliPath) {
|
|
|
401
407
|
}
|
|
402
408
|
}
|
|
403
409
|
|
|
410
|
+
// src/computer/connection-flow.ts
|
|
411
|
+
function connectionAction(value) {
|
|
412
|
+
if (value === void 0) return void 0;
|
|
413
|
+
if (value === "resume" || value === "reconnect" || value === "pair") return value;
|
|
414
|
+
throw new Error("Use mindwire connect, mindwire connect resume, mindwire reconnect, or mindwire connect pair.");
|
|
415
|
+
}
|
|
416
|
+
async function chooseConnectionAction(options) {
|
|
417
|
+
const paired = options.devices.filter((device) => !device.revoked);
|
|
418
|
+
if (options.requested) {
|
|
419
|
+
if (options.requested !== "pair" && paired.length === 0) {
|
|
420
|
+
throw new Error("No saved phones yet. Run mindwire connect to pair one first.");
|
|
421
|
+
}
|
|
422
|
+
return options.requested;
|
|
423
|
+
}
|
|
424
|
+
if (paired.length === 0) return "pair";
|
|
425
|
+
if (!options.question) return "resume";
|
|
426
|
+
for (; ; ) {
|
|
427
|
+
const answer = (await options.question(
|
|
428
|
+
"\n 1 Resume saved connection\n 2 Refresh address on a saved phone (QR)\n 3 Pair another phone\n\nChoose [1]: "
|
|
429
|
+
)).trim().toLowerCase();
|
|
430
|
+
if (answer === "" || answer === "1" || answer === "resume") return "resume";
|
|
431
|
+
if (answer === "2" || answer === "reconnect") return "reconnect";
|
|
432
|
+
if (answer === "3" || answer === "pair") return "pair";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function chooseStartupAction(options) {
|
|
436
|
+
if (options.startup && options.noStartup) throw new Error("Choose either --startup or --no-startup.");
|
|
437
|
+
if (options.startup) return !options.state.enabled || !options.state.running ? true : void 0;
|
|
438
|
+
if (options.noStartup) return options.state.kind ? void 0 : false;
|
|
439
|
+
if (options.state.kind) return options.state.enabled && !options.state.running ? true : void 0;
|
|
440
|
+
if (!options.question) return void 0;
|
|
441
|
+
for (; ; ) {
|
|
442
|
+
const answer = (await options.question(
|
|
443
|
+
"\nStart Mindwire automatically after you sign in to this computer? [Y/n] "
|
|
444
|
+
)).trim().toLowerCase();
|
|
445
|
+
if (["", "y", "yes"].includes(answer)) return true;
|
|
446
|
+
if (["n", "no"].includes(answer)) return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function reconnectCode(info, name, now = /* @__PURE__ */ new Date()) {
|
|
450
|
+
if (info.routes.length === 0) throw new Error("No connection address is ready. Run mindwire status and retry once the tunnel is online.");
|
|
451
|
+
return {
|
|
452
|
+
version: 1,
|
|
453
|
+
mode: "reconnect",
|
|
454
|
+
computerId: info.computerId,
|
|
455
|
+
registryId: info.registryId,
|
|
456
|
+
fingerprint: info.fingerprint,
|
|
457
|
+
name,
|
|
458
|
+
routes: info.routes,
|
|
459
|
+
expiresAt: new Date(now.getTime() + 5 * 6e4).toISOString()
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function deviceSummary(devices) {
|
|
463
|
+
return devices.filter((device) => !device.revoked).sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id)).map((device) => {
|
|
464
|
+
const name = device.name.replace(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
465
|
+
const status = device.connected === true ? "connected" : "paired";
|
|
466
|
+
return ` ${name} \xB7 ${status}`;
|
|
467
|
+
}).join("\n");
|
|
468
|
+
}
|
|
469
|
+
|
|
404
470
|
// src/cli.ts
|
|
405
471
|
var help = `Mindwire \u2014 connect this computer to your phone
|
|
406
472
|
|
|
407
473
|
npm install -g mindwire
|
|
408
|
-
mindwire connect
|
|
474
|
+
mindwire connect Resume a saved connection, or pair your first phone
|
|
475
|
+
mindwire connect resume Resume without creating another pairing
|
|
476
|
+
mindwire reconnect Refresh a saved phone's address with a QR code
|
|
477
|
+
mindwire connect pair Pair another phone with this computer
|
|
409
478
|
mindwire connect --relay none --host laptop.tailnet Use your existing VPN
|
|
410
479
|
mindwire connect --relay none Direct SSH only (reachable network required)
|
|
411
480
|
mindwire connect --relay cloudflare Cloudflare quick tunnel
|
|
@@ -431,7 +500,8 @@ Options:
|
|
|
431
500
|
--json Structured output (QR invitation includes a secret)
|
|
432
501
|
--qr auto|terminal|browser Fit the QR to your terminal or open a local page
|
|
433
502
|
--no-qr Print only the pairing link
|
|
434
|
-
--
|
|
503
|
+
--startup Enable automatic startup without a prompt
|
|
504
|
+
--no-startup Skip automatic startup setup
|
|
435
505
|
|
|
436
506
|
The default works across Wi-Fi and mobile networks. Mindwire privately downloads
|
|
437
507
|
a verified Cloudflare helper when needed. Relays carry encrypted SSH.
|
|
@@ -460,6 +530,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
460
530
|
"websocket-port": { type: "string" },
|
|
461
531
|
version: { type: "string" },
|
|
462
532
|
force: { type: "boolean" },
|
|
533
|
+
startup: { type: "boolean" },
|
|
463
534
|
"no-startup": { type: "boolean" }
|
|
464
535
|
} });
|
|
465
536
|
const command = positionals[0] ?? "help";
|
|
@@ -468,6 +539,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
468
539
|
return;
|
|
469
540
|
}
|
|
470
541
|
if (values.qr && !["auto", "terminal", "browser"].includes(values.qr)) throw new Error("Choose --qr auto, terminal or browser.");
|
|
542
|
+
if (values.startup && values["no-startup"]) throw new Error("Choose either --startup or --no-startup.");
|
|
471
543
|
const directory = path.resolve(values["state-dir"] ?? defaultStateDirectory());
|
|
472
544
|
const emit = (event, data = {}, text) => {
|
|
473
545
|
if (values.json) process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
|
|
@@ -488,7 +560,8 @@ async function main(args = process.argv.slice(2)) {
|
|
|
488
560
|
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.");
|
|
489
561
|
return;
|
|
490
562
|
}
|
|
491
|
-
if (["connect", "start"].includes(command)) {
|
|
563
|
+
if (["connect", "start", "reconnect"].includes(command)) {
|
|
564
|
+
const requestedAction = command === "reconnect" ? "reconnect" : command === "connect" ? connectionAction(positionals[1]) : void 0;
|
|
492
565
|
const patch = {};
|
|
493
566
|
if (values.directory) patch.directory = path.resolve(values.directory);
|
|
494
567
|
if (values.host) patch.host = values.host;
|
|
@@ -521,22 +594,67 @@ async function main(args = process.argv.slice(2)) {
|
|
|
521
594
|
const info = await client2.computer.info();
|
|
522
595
|
emit("ready", { computerId: info.computerId, routes: info.routes }, "Mindwire is running in the background.");
|
|
523
596
|
if (command === "start") return;
|
|
597
|
+
if (info.routes.some((route) => route.kind === "websocket")) {
|
|
598
|
+
emit("secure_connection", {}, "Your phone connects over end-to-end encrypted SSH. The tunnel carries encrypted traffic; only phones you approve can connect.");
|
|
599
|
+
}
|
|
600
|
+
const devices = await client2.computer.devices();
|
|
601
|
+
const saved = devices.filter((device) => !device.revoked);
|
|
602
|
+
if (saved.length) emit("saved_devices", { devices: saved }, `
|
|
603
|
+
Saved phones
|
|
604
|
+
${deviceSummary(saved)}`);
|
|
605
|
+
const question = !values.json && process.stdin.isTTY ? async (prompt) => {
|
|
606
|
+
const input = createInterface({ input: process.stdin, output: process.stderr });
|
|
607
|
+
try {
|
|
608
|
+
return await input.question(prompt);
|
|
609
|
+
} finally {
|
|
610
|
+
input.close();
|
|
611
|
+
}
|
|
612
|
+
} : void 0;
|
|
613
|
+
const action = await chooseConnectionAction({ requested: requestedAction, devices, question });
|
|
524
614
|
const startupPreference = await startupStatus(directory);
|
|
525
|
-
|
|
615
|
+
const startupAction = await chooseStartupAction({
|
|
616
|
+
state: startupPreference,
|
|
617
|
+
startup: values.startup,
|
|
618
|
+
noStartup: values["no-startup"],
|
|
619
|
+
question
|
|
620
|
+
});
|
|
621
|
+
if (startupAction !== void 0) {
|
|
526
622
|
try {
|
|
527
|
-
const startup = await configureStartup(directory, fileURLToPath(import.meta.url),
|
|
528
|
-
emit("startup", startup, "Automatic startup enabled.
|
|
623
|
+
const startup = await configureStartup(directory, fileURLToPath(import.meta.url), startupAction);
|
|
624
|
+
emit("startup", startup, startup.enabled ? "Automatic startup enabled. Saved phones can reconnect after you sign in to this computer." : "Automatic startup is off. Run mindwire connect after restarting, or mindwire startup enable to change this.");
|
|
529
625
|
} catch {
|
|
530
|
-
emit("startup_unavailable", {}, "Mindwire is running, but
|
|
626
|
+
emit("startup_unavailable", {}, "Mindwire is running, but startup setup didn't complete. Run mindwire startup enable to check your system's startup service.");
|
|
531
627
|
}
|
|
628
|
+
} else {
|
|
629
|
+
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.");
|
|
532
630
|
}
|
|
533
|
-
if (
|
|
534
|
-
emit("
|
|
631
|
+
if (action === "resume") {
|
|
632
|
+
emit("resumed", { computerId: info.computerId, devices: saved }, "\nReady for your saved phone. Open this computer in Mindwire.");
|
|
633
|
+
if (info.routes.some((route) => route.kind === "websocket" && new URL(route.url).hostname.endsWith(".trycloudflare.com"))) {
|
|
634
|
+
emit("reconnect_hint", {}, "If its temporary tunnel address changed, run mindwire reconnect to refresh your phone.");
|
|
635
|
+
}
|
|
636
|
+
return;
|
|
535
637
|
}
|
|
536
|
-
if (
|
|
537
|
-
|
|
638
|
+
if (action === "reconnect") {
|
|
639
|
+
const code = reconnectCode(await client2.computer.info(), hostname());
|
|
640
|
+
emit("reconnect_code", { code, uri: computerPairingURI(code) });
|
|
641
|
+
if (values.json) return;
|
|
642
|
+
const display2 = await showPairingInvitation(code, {
|
|
643
|
+
mode: values["no-qr"] ? "none" : values.qr ?? "auto"
|
|
644
|
+
});
|
|
645
|
+
try {
|
|
646
|
+
if (question) await question("\nScan with your saved phone, then press Enter to close this code. ");
|
|
647
|
+
else if (values.qr === "browser") {
|
|
648
|
+
emit("waiting", {}, "This code stays open until it expires. Press Ctrl+C to close it; Mindwire keeps running.");
|
|
649
|
+
await setTimeout$1(Math.max(0, Date.parse(code.expiresAt) - Date.now()));
|
|
650
|
+
}
|
|
651
|
+
} finally {
|
|
652
|
+
display2.close();
|
|
653
|
+
}
|
|
654
|
+
emit("resumed", { computerId: info.computerId }, "Saved pairing kept. Mindwire continues running in the background.");
|
|
655
|
+
return;
|
|
538
656
|
}
|
|
539
|
-
const invitation = await client2.computer.invite(info.routes);
|
|
657
|
+
const invitation = await client2.computer.invite((await client2.computer.info()).routes);
|
|
540
658
|
const uri = computerPairingURI(invitation);
|
|
541
659
|
emit("invitation", { invitation, uri });
|
|
542
660
|
const display = values.json ? void 0 : await showPairingInvitation(invitation, {
|