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