forge-memory 0.3.2 → 0.3.4
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/bin/forge-memory.mjs +194 -50
- package/package.json +1 -1
package/bin/forge-memory.mjs
CHANGED
|
@@ -427,7 +427,9 @@ function tailscaleInstallPlan() {
|
|
|
427
427
|
|
|
428
428
|
function tailscaleAutodetectDisabled() {
|
|
429
429
|
return ["1", "true", "yes"].includes(
|
|
430
|
-
String(
|
|
430
|
+
String(
|
|
431
|
+
process.env.FORGE_MEMORY_SKIP_TAILSCALE_AUTODETECT ?? ""
|
|
432
|
+
).toLowerCase()
|
|
431
433
|
);
|
|
432
434
|
}
|
|
433
435
|
|
|
@@ -439,7 +441,9 @@ function parseTailscaleStatus(raw) {
|
|
|
439
441
|
const dnsName = String(self?.DNSName ?? self?.dnsName ?? "")
|
|
440
442
|
.trim()
|
|
441
443
|
.replace(/\.$/, "");
|
|
442
|
-
const backendState = String(
|
|
444
|
+
const backendState = String(
|
|
445
|
+
payload.BackendState ?? payload.backendState ?? ""
|
|
446
|
+
);
|
|
443
447
|
const running = backendState.toLowerCase() === "running";
|
|
444
448
|
return {
|
|
445
449
|
running,
|
|
@@ -456,7 +460,11 @@ function normalizeForgePublicUiUrl(value) {
|
|
|
456
460
|
const normalized = normalizePublicPairingUrl(value);
|
|
457
461
|
if (!normalized) return null;
|
|
458
462
|
const url = new URL(normalized);
|
|
459
|
-
if (
|
|
463
|
+
if (
|
|
464
|
+
!url.pathname ||
|
|
465
|
+
url.pathname === "/" ||
|
|
466
|
+
url.pathname.startsWith("/api/")
|
|
467
|
+
) {
|
|
460
468
|
url.pathname = "/forge/";
|
|
461
469
|
}
|
|
462
470
|
if (!url.pathname.endsWith("/")) url.pathname = `${url.pathname}/`;
|
|
@@ -501,8 +509,12 @@ function detectTailscaleState() {
|
|
|
501
509
|
installPlan: tailscaleInstallPlan()
|
|
502
510
|
};
|
|
503
511
|
}
|
|
504
|
-
const status = parseTailscaleStatus(
|
|
505
|
-
|
|
512
|
+
const status = parseTailscaleStatus(
|
|
513
|
+
runCapture("tailscale", ["status", "--json"], 4_000)
|
|
514
|
+
);
|
|
515
|
+
const envPublicUrl = normalizeForgePublicUiUrl(
|
|
516
|
+
process.env.FORGE_MEMORY_TAILSCALE_PUBLIC_URL
|
|
517
|
+
);
|
|
506
518
|
const publicUrl =
|
|
507
519
|
envPublicUrl ??
|
|
508
520
|
(status.dnsName ? `https://${status.dnsName}/forge/` : null);
|
|
@@ -529,14 +541,22 @@ async function probePublicForgeUrl(publicUrl, timeoutMs = 4_000) {
|
|
|
529
541
|
.map((entry) => entry.trim())
|
|
530
542
|
.filter(Boolean);
|
|
531
543
|
const current = entries.shift() ?? "fail";
|
|
532
|
-
fs.writeFileSync(
|
|
544
|
+
fs.writeFileSync(
|
|
545
|
+
fakeProbeSequencePath,
|
|
546
|
+
entries.length ? `${entries.join("\n")}\n` : ""
|
|
547
|
+
);
|
|
533
548
|
return current === "ok"
|
|
534
549
|
? { ok: true, fake: true }
|
|
535
|
-
: {
|
|
550
|
+
: {
|
|
551
|
+
ok: false,
|
|
552
|
+
error: current === "fail" ? "fake probe failure" : current
|
|
553
|
+
};
|
|
536
554
|
}
|
|
537
555
|
if (
|
|
538
556
|
["1", "true", "yes"].includes(
|
|
539
|
-
String(
|
|
557
|
+
String(
|
|
558
|
+
process.env.FORGE_MEMORY_SKIP_TAILSCALE_PUBLIC_PROBE ?? ""
|
|
559
|
+
).toLowerCase()
|
|
540
560
|
)
|
|
541
561
|
) {
|
|
542
562
|
return { ok: true, skipped: true };
|
|
@@ -551,7 +571,11 @@ async function probePublicForgeUrl(publicUrl, timeoutMs = 4_000) {
|
|
|
551
571
|
if (!response.ok) return { ok: false, status: response.status };
|
|
552
572
|
const payload = await response.json().catch(() => null);
|
|
553
573
|
if (!isForgeHealthPayload(payload)) {
|
|
554
|
-
return {
|
|
574
|
+
return {
|
|
575
|
+
ok: false,
|
|
576
|
+
status: response.status,
|
|
577
|
+
error: "non-Forge health payload"
|
|
578
|
+
};
|
|
555
579
|
}
|
|
556
580
|
return { ok: true, status: response.status };
|
|
557
581
|
} catch (error) {
|
|
@@ -854,7 +878,9 @@ async function withProgress(title, detail, options, task) {
|
|
|
854
878
|
|
|
855
879
|
function printStep(title, detail, options = {}) {
|
|
856
880
|
if (!progressEnabled(options)) return;
|
|
857
|
-
console.log(
|
|
881
|
+
console.log(
|
|
882
|
+
`${color.cyan("->")} ${title}${detail ? color.dim(` ${detail}`) : ""}`
|
|
883
|
+
);
|
|
858
884
|
}
|
|
859
885
|
|
|
860
886
|
async function promptLine(question, defaultValue) {
|
|
@@ -1128,7 +1154,10 @@ async function buildInstallConfig(parsed, currentConfig, discovery, command) {
|
|
|
1128
1154
|
const origin = parsed.values.origin ?? currentConfig.origin ?? DEFAULT_ORIGIN;
|
|
1129
1155
|
const runtimeTarget = await resolveInstallRuntimeTarget({
|
|
1130
1156
|
origin,
|
|
1131
|
-
requestedPort: normalizePort(
|
|
1157
|
+
requestedPort: normalizePort(
|
|
1158
|
+
parsed.values.port ?? currentConfig.port,
|
|
1159
|
+
DEFAULT_PORT
|
|
1160
|
+
),
|
|
1132
1161
|
requestedWebPort: normalizePort(
|
|
1133
1162
|
parsed.values.webPort ?? currentConfig.webPort,
|
|
1134
1163
|
DEFAULT_WEB_PORT
|
|
@@ -1269,7 +1298,8 @@ function stripCodexForgeMcpConfig(source) {
|
|
|
1269
1298
|
const tableMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
1270
1299
|
if (tableMatch) {
|
|
1271
1300
|
currentTable = tableMatch[1];
|
|
1272
|
-
skippingForgeTable =
|
|
1301
|
+
skippingForgeTable =
|
|
1302
|
+
currentTable === "mcp_servers.forge" ||
|
|
1273
1303
|
currentTable.startsWith("mcp_servers.forge.");
|
|
1274
1304
|
if (skippingForgeTable) continue;
|
|
1275
1305
|
}
|
|
@@ -1285,7 +1315,10 @@ function stripCodexForgeMcpConfig(source) {
|
|
|
1285
1315
|
}
|
|
1286
1316
|
kept.push(line);
|
|
1287
1317
|
}
|
|
1288
|
-
return kept
|
|
1318
|
+
return kept
|
|
1319
|
+
.join("\n")
|
|
1320
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
1321
|
+
.trimEnd();
|
|
1289
1322
|
}
|
|
1290
1323
|
|
|
1291
1324
|
async function runCommand(
|
|
@@ -1478,7 +1511,8 @@ function describeNetworkError(error) {
|
|
|
1478
1511
|
}
|
|
1479
1512
|
|
|
1480
1513
|
function describeHealthResult(result) {
|
|
1481
|
-
if (result.ok && result.forge === false)
|
|
1514
|
+
if (result.ok && result.forge === false)
|
|
1515
|
+
return "HTTP 200 from a non-Forge service";
|
|
1482
1516
|
if (result.ok) return "healthy";
|
|
1483
1517
|
if (result.status) return `HTTP ${result.status}`;
|
|
1484
1518
|
if (result.error) return result.error;
|
|
@@ -1502,6 +1536,45 @@ function processExists(pid) {
|
|
|
1502
1536
|
}
|
|
1503
1537
|
}
|
|
1504
1538
|
|
|
1539
|
+
function signalProcess(pid, signal = "SIGTERM") {
|
|
1540
|
+
try {
|
|
1541
|
+
process.kill(pid, signal);
|
|
1542
|
+
return true;
|
|
1543
|
+
} catch {
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function signalDetachedProcessGroup(pid, signal = "SIGTERM") {
|
|
1549
|
+
if (process.platform === "win32") return false;
|
|
1550
|
+
try {
|
|
1551
|
+
process.kill(-pid, signal);
|
|
1552
|
+
return true;
|
|
1553
|
+
} catch {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
async function waitForProcessExit(pid, timeoutMs = 1_500) {
|
|
1559
|
+
const deadline = Date.now() + timeoutMs;
|
|
1560
|
+
while (Date.now() < deadline) {
|
|
1561
|
+
if (!processExists(pid)) return true;
|
|
1562
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1563
|
+
}
|
|
1564
|
+
return !processExists(pid);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
async function stopRecordedRuntimeProcess(pid) {
|
|
1568
|
+
if (!Number.isInteger(pid) || pid <= 0 || !processExists(pid)) return false;
|
|
1569
|
+
const signaled =
|
|
1570
|
+
signalDetachedProcessGroup(pid, "SIGTERM") || signalProcess(pid, "SIGTERM");
|
|
1571
|
+
if (!signaled) return false;
|
|
1572
|
+
if (await waitForProcessExit(pid)) return true;
|
|
1573
|
+
signalDetachedProcessGroup(pid, "SIGKILL") || signalProcess(pid, "SIGKILL");
|
|
1574
|
+
await waitForProcessExit(pid, 500);
|
|
1575
|
+
return true;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1505
1578
|
async function waitForHealth(config, timeoutMs = 30_000) {
|
|
1506
1579
|
const deadline = Date.now() + timeoutMs;
|
|
1507
1580
|
while (Date.now() < deadline) {
|
|
@@ -1540,9 +1613,7 @@ function resolveOpenClawPluginRoot(options = {}) {
|
|
|
1540
1613
|
}
|
|
1541
1614
|
|
|
1542
1615
|
async function ensurePackagedRuntimeInstalled(options = {}) {
|
|
1543
|
-
const existing = options.forceInstall
|
|
1544
|
-
? null
|
|
1545
|
-
: resolveOpenClawPluginRoot();
|
|
1616
|
+
const existing = options.forceInstall ? null : resolveOpenClawPluginRoot();
|
|
1546
1617
|
if (existing) return existing;
|
|
1547
1618
|
const installRoot = runtimeInstallRoot();
|
|
1548
1619
|
await fsp.mkdir(installRoot, { recursive: true });
|
|
@@ -1616,7 +1687,9 @@ async function repairPackagedRuntimeCache(config) {
|
|
|
1616
1687
|
const rotatedLogPath = await rotateRuntimeLog("repair");
|
|
1617
1688
|
await fsp.rm(runtimeStatePath(), { force: true });
|
|
1618
1689
|
await fsp.rm(runtimeInstallRoot(), { recursive: true, force: true });
|
|
1619
|
-
const pluginRoot = await ensurePackagedRuntimeInstalled({
|
|
1690
|
+
const pluginRoot = await ensurePackagedRuntimeInstalled({
|
|
1691
|
+
forceInstall: true
|
|
1692
|
+
});
|
|
1620
1693
|
const result = await startRuntime(config);
|
|
1621
1694
|
const record = {
|
|
1622
1695
|
repairedAt: new Date().toISOString(),
|
|
@@ -1631,9 +1704,17 @@ async function repairPackagedRuntimeCache(config) {
|
|
|
1631
1704
|
health: result.health ?? { ok: result.ok }
|
|
1632
1705
|
};
|
|
1633
1706
|
const stamp = record.repairedAt.replace(/[:.]/g, "-");
|
|
1634
|
-
const repairRecordPath = path.join(
|
|
1707
|
+
const repairRecordPath = path.join(
|
|
1708
|
+
forgeHome(),
|
|
1709
|
+
"run",
|
|
1710
|
+
`runtime-repair-${stamp}.json`
|
|
1711
|
+
);
|
|
1635
1712
|
await fsp.mkdir(path.dirname(repairRecordPath), { recursive: true });
|
|
1636
|
-
await fsp.writeFile(
|
|
1713
|
+
await fsp.writeFile(
|
|
1714
|
+
repairRecordPath,
|
|
1715
|
+
`${JSON.stringify(record, null, 2)}\n`,
|
|
1716
|
+
"utf8"
|
|
1717
|
+
);
|
|
1637
1718
|
return { ...record, repairRecordPath };
|
|
1638
1719
|
}
|
|
1639
1720
|
|
|
@@ -1805,8 +1886,7 @@ async function stopRuntime(config = null) {
|
|
|
1805
1886
|
const stopped = [];
|
|
1806
1887
|
for (const child of state?.children ?? []) {
|
|
1807
1888
|
if (!child?.pid || !processExists(child.pid)) continue;
|
|
1808
|
-
|
|
1809
|
-
stopped.push(child.pid);
|
|
1889
|
+
if (await stopRecordedRuntimeProcess(child.pid)) stopped.push(child.pid);
|
|
1810
1890
|
}
|
|
1811
1891
|
const current = await health(effectiveConfig);
|
|
1812
1892
|
const runtimePid = Number(current?.payload?.runtime?.pid);
|
|
@@ -1817,8 +1897,7 @@ async function stopRuntime(config = null) {
|
|
|
1817
1897
|
!stopped.includes(runtimePid) &&
|
|
1818
1898
|
processExists(runtimePid)
|
|
1819
1899
|
) {
|
|
1820
|
-
|
|
1821
|
-
stopped.push(runtimePid);
|
|
1900
|
+
if (await stopRecordedRuntimeProcess(runtimePid)) stopped.push(runtimePid);
|
|
1822
1901
|
}
|
|
1823
1902
|
await fsp.rm(runtimeStatePath(), { force: true });
|
|
1824
1903
|
if (stopped.length === 0) {
|
|
@@ -1953,7 +2032,9 @@ async function removeHermesAdapterConfig() {
|
|
|
1953
2032
|
}
|
|
1954
2033
|
const forgeConfigDir = path.dirname(forgeConfigPath);
|
|
1955
2034
|
if (fs.existsSync(forgeConfigDir)) {
|
|
1956
|
-
await fsp
|
|
2035
|
+
await fsp
|
|
2036
|
+
.rm(forgeConfigDir, { recursive: false, force: true })
|
|
2037
|
+
.catch(() => {});
|
|
1957
2038
|
}
|
|
1958
2039
|
|
|
1959
2040
|
const hermesYamlPath = path.join(homeDir(), ".hermes", "config.yaml");
|
|
@@ -2169,7 +2250,10 @@ async function bootstrapLocalOperatorSession(config) {
|
|
|
2169
2250
|
|
|
2170
2251
|
async function createPairing(config, options = {}) {
|
|
2171
2252
|
const transportMode = options.transportMode ?? "iroh";
|
|
2172
|
-
const publicUrl = validatePairingOptions({
|
|
2253
|
+
const publicUrl = validatePairingOptions({
|
|
2254
|
+
transportMode,
|
|
2255
|
+
publicUrl: options.publicUrl
|
|
2256
|
+
});
|
|
2173
2257
|
const pairingUrl = forgeApiUrl(config, "/api/v1/health/pairing-sessions");
|
|
2174
2258
|
const operatorCookie = await bootstrapLocalOperatorSession(config);
|
|
2175
2259
|
let response;
|
|
@@ -2221,7 +2305,9 @@ async function createPairing(config, options = {}) {
|
|
|
2221
2305
|
);
|
|
2222
2306
|
}
|
|
2223
2307
|
const pairing = await response.json();
|
|
2224
|
-
assertPairingTransportUsable(pairing, {
|
|
2308
|
+
assertPairingTransportUsable(pairing, {
|
|
2309
|
+
requestedTransportMode: transportMode
|
|
2310
|
+
});
|
|
2225
2311
|
return pairing;
|
|
2226
2312
|
}
|
|
2227
2313
|
|
|
@@ -2242,7 +2328,10 @@ async function maybeInstallTailscaleForPairing(parsed) {
|
|
|
2242
2328
|
true
|
|
2243
2329
|
);
|
|
2244
2330
|
if (!shouldInstall) return { installed: false, guidance: plan.guidance };
|
|
2245
|
-
const result = await runCommand(
|
|
2331
|
+
const result = await runCommand(
|
|
2332
|
+
plan.autoInstallCommand.command,
|
|
2333
|
+
plan.autoInstallCommand.args
|
|
2334
|
+
);
|
|
2246
2335
|
return { installed: result.ok, guidance: plan.guidance, result };
|
|
2247
2336
|
}
|
|
2248
2337
|
|
|
@@ -2253,7 +2342,11 @@ async function resolveTailscalePairingOptions(parsed, config) {
|
|
|
2253
2342
|
const installAttempt = await maybeInstallTailscaleForPairing(parsed);
|
|
2254
2343
|
if (installAttempt.installed) {
|
|
2255
2344
|
const installedState = detectTailscaleState();
|
|
2256
|
-
if (
|
|
2345
|
+
if (
|
|
2346
|
+
installedState.installed &&
|
|
2347
|
+
installedState.running &&
|
|
2348
|
+
installedState.authenticated
|
|
2349
|
+
) {
|
|
2257
2350
|
return resolveTailscalePairingOptions(parsed, config);
|
|
2258
2351
|
}
|
|
2259
2352
|
}
|
|
@@ -2268,7 +2361,11 @@ async function resolveTailscalePairingOptions(parsed, config) {
|
|
|
2268
2361
|
"Tailscale is installed, but it is not running/authenticated or does not expose a MagicDNS name."
|
|
2269
2362
|
)
|
|
2270
2363
|
);
|
|
2271
|
-
console.log(
|
|
2364
|
+
console.log(
|
|
2365
|
+
color.dim(
|
|
2366
|
+
"Open Tailscale, sign in, then rerun npx forge-memory pair-ios. Falling back to Iroh for this pairing."
|
|
2367
|
+
)
|
|
2368
|
+
);
|
|
2272
2369
|
}
|
|
2273
2370
|
return null;
|
|
2274
2371
|
}
|
|
@@ -2276,7 +2373,9 @@ async function resolveTailscalePairingOptions(parsed, config) {
|
|
|
2276
2373
|
const firstProbe = await probePublicForgeUrl(state.publicUrl);
|
|
2277
2374
|
if (firstProbe.ok) {
|
|
2278
2375
|
if (!parsed.flags.json) {
|
|
2279
|
-
console.log(
|
|
2376
|
+
console.log(
|
|
2377
|
+
color.green(`Using Tailscale for iOS pairing: ${state.publicUrl}`)
|
|
2378
|
+
);
|
|
2280
2379
|
}
|
|
2281
2380
|
return {
|
|
2282
2381
|
transportMode: "manual-http",
|
|
@@ -2334,7 +2433,9 @@ async function resolveTailscalePairingOptions(parsed, config) {
|
|
|
2334
2433
|
return null;
|
|
2335
2434
|
}
|
|
2336
2435
|
if (!parsed.flags.json) {
|
|
2337
|
-
console.log(
|
|
2436
|
+
console.log(
|
|
2437
|
+
color.green(`Using Tailscale for iOS pairing: ${state.publicUrl}`)
|
|
2438
|
+
);
|
|
2338
2439
|
}
|
|
2339
2440
|
return {
|
|
2340
2441
|
transportMode: "manual-http",
|
|
@@ -2367,7 +2468,10 @@ async function resolveIosPairingOptions(parsed, config = null) {
|
|
|
2367
2468
|
};
|
|
2368
2469
|
}
|
|
2369
2470
|
if (config) {
|
|
2370
|
-
const tailscalePairing = await resolveTailscalePairingOptions(
|
|
2471
|
+
const tailscalePairing = await resolveTailscalePairingOptions(
|
|
2472
|
+
parsed,
|
|
2473
|
+
config
|
|
2474
|
+
);
|
|
2371
2475
|
if (tailscalePairing) return tailscalePairing;
|
|
2372
2476
|
}
|
|
2373
2477
|
if (parsed.flags.yes || parsed.flags.json || !process.stdin.isTTY) {
|
|
@@ -2381,7 +2485,9 @@ async function resolveIosPairingOptions(parsed, config = null) {
|
|
|
2381
2485
|
}
|
|
2382
2486
|
console.log(color.bold("iOS companion connection"));
|
|
2383
2487
|
console.log(color.dim(tailscalePreferredMessage()));
|
|
2384
|
-
console.log(
|
|
2488
|
+
console.log(
|
|
2489
|
+
color.dim("Choose Iroh or a fixed/private IP fallback for this pairing.")
|
|
2490
|
+
);
|
|
2385
2491
|
const choice = (
|
|
2386
2492
|
await promptLine("Connection [iroh/ip]", "iroh")
|
|
2387
2493
|
).toLowerCase();
|
|
@@ -2412,7 +2518,8 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
|
|
|
2412
2518
|
if (!payload || requestedTransportMode !== "iroh") {
|
|
2413
2519
|
return;
|
|
2414
2520
|
}
|
|
2415
|
-
const resolvedTransportMode =
|
|
2521
|
+
const resolvedTransportMode =
|
|
2522
|
+
payload.transportMode ?? payload.transport?.protocol;
|
|
2416
2523
|
const resolvedProtocol = payload.transport?.protocol;
|
|
2417
2524
|
if (resolvedTransportMode === "iroh" || resolvedProtocol === "iroh") {
|
|
2418
2525
|
const phoneFacingUrls = [
|
|
@@ -2420,7 +2527,9 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
|
|
|
2420
2527
|
payload.uiBaseUrl,
|
|
2421
2528
|
payload.transport?.publicBaseUrl
|
|
2422
2529
|
].filter(Boolean);
|
|
2423
|
-
const loopbackUrl = phoneFacingUrls.find((url) =>
|
|
2530
|
+
const loopbackUrl = phoneFacingUrls.find((url) =>
|
|
2531
|
+
isLoopbackPairingUrl(url)
|
|
2532
|
+
);
|
|
2424
2533
|
if (loopbackUrl) {
|
|
2425
2534
|
throw new PairingTransportUnavailableError(
|
|
2426
2535
|
[
|
|
@@ -2429,14 +2538,20 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
|
|
|
2429
2538
|
"A physical iPhone cannot reach localhost on this Mac.",
|
|
2430
2539
|
"Use Iroh logical URLs, a selected Tailscale URL, or a selected private/fixed IP URL."
|
|
2431
2540
|
].join(" "),
|
|
2432
|
-
{
|
|
2541
|
+
{
|
|
2542
|
+
apiBaseUrl: payload.apiBaseUrl,
|
|
2543
|
+
transportMode: resolvedTransportMode,
|
|
2544
|
+
protocol: resolvedProtocol
|
|
2545
|
+
}
|
|
2433
2546
|
);
|
|
2434
2547
|
}
|
|
2435
2548
|
return;
|
|
2436
2549
|
}
|
|
2437
2550
|
const apiBaseUrl = payload.apiBaseUrl ?? "";
|
|
2438
2551
|
const lastError = payload.transport?.lastError;
|
|
2439
|
-
const notes = Array.isArray(payload.transport?.notes)
|
|
2552
|
+
const notes = Array.isArray(payload.transport?.notes)
|
|
2553
|
+
? payload.transport.notes
|
|
2554
|
+
: [];
|
|
2440
2555
|
throw new PairingTransportUnavailableError(
|
|
2441
2556
|
[
|
|
2442
2557
|
"Forge created a direct HTTP pairing while default iOS pairing requested Iroh.",
|
|
@@ -2450,7 +2565,11 @@ function assertPairingTransportUsable(pairing, { requestedTransportMode }) {
|
|
|
2450
2565
|
]
|
|
2451
2566
|
.filter(Boolean)
|
|
2452
2567
|
.join(" "),
|
|
2453
|
-
{
|
|
2568
|
+
{
|
|
2569
|
+
apiBaseUrl,
|
|
2570
|
+
transportMode: resolvedTransportMode,
|
|
2571
|
+
protocol: resolvedProtocol
|
|
2572
|
+
}
|
|
2454
2573
|
);
|
|
2455
2574
|
}
|
|
2456
2575
|
|
|
@@ -2555,7 +2674,9 @@ function compactQrPairingPayload(payload) {
|
|
|
2555
2674
|
|
|
2556
2675
|
function compactObject(value) {
|
|
2557
2676
|
if (Array.isArray(value)) {
|
|
2558
|
-
const compacted = value
|
|
2677
|
+
const compacted = value
|
|
2678
|
+
.map((entry) => compactObject(entry))
|
|
2679
|
+
.filter((entry) => entry !== undefined);
|
|
2559
2680
|
return compacted.length ? compacted : undefined;
|
|
2560
2681
|
}
|
|
2561
2682
|
if (!value || typeof value !== "object") {
|
|
@@ -2564,7 +2685,10 @@ function compactObject(value) {
|
|
|
2564
2685
|
const output = {};
|
|
2565
2686
|
for (const [key, entry] of Object.entries(value)) {
|
|
2566
2687
|
const compacted = compactObject(entry);
|
|
2567
|
-
if (
|
|
2688
|
+
if (
|
|
2689
|
+
compacted !== undefined &&
|
|
2690
|
+
!(Array.isArray(compacted) && compacted.length === 0)
|
|
2691
|
+
) {
|
|
2568
2692
|
output[key] = compacted;
|
|
2569
2693
|
}
|
|
2570
2694
|
}
|
|
@@ -2578,7 +2702,11 @@ async function writePairingPayloadFile(payload) {
|
|
|
2578
2702
|
pairingDir,
|
|
2579
2703
|
`forge-companion-${payload.sessionId}.json`
|
|
2580
2704
|
);
|
|
2581
|
-
await fsp.writeFile(
|
|
2705
|
+
await fsp.writeFile(
|
|
2706
|
+
filePath,
|
|
2707
|
+
`${JSON.stringify(payload, null, 2)}\n`,
|
|
2708
|
+
"utf8"
|
|
2709
|
+
);
|
|
2582
2710
|
return filePath;
|
|
2583
2711
|
}
|
|
2584
2712
|
|
|
@@ -2601,8 +2729,14 @@ async function printPairing(pairing) {
|
|
|
2601
2729
|
qrcode.generate(qrPayloadText, { small: true });
|
|
2602
2730
|
} else {
|
|
2603
2731
|
console.log("");
|
|
2604
|
-
console.log(
|
|
2605
|
-
|
|
2732
|
+
console.log(
|
|
2733
|
+
color.yellow(
|
|
2734
|
+
"QR skipped because the terminal is too narrow or the payload is too large to scan reliably."
|
|
2735
|
+
)
|
|
2736
|
+
);
|
|
2737
|
+
console.log(
|
|
2738
|
+
"Use Manual connection in the iPhone app and paste the saved payload below."
|
|
2739
|
+
);
|
|
2606
2740
|
}
|
|
2607
2741
|
const transport = manualPayload.transport;
|
|
2608
2742
|
if (transport?.provider) {
|
|
@@ -2613,7 +2747,10 @@ async function printPairing(pairing) {
|
|
|
2613
2747
|
? "Iroh"
|
|
2614
2748
|
: "Manual HTTP";
|
|
2615
2749
|
console.log(`${color.cyan(label)}: ${manualPayload.apiBaseUrl}`);
|
|
2616
|
-
if (
|
|
2750
|
+
if (
|
|
2751
|
+
label === "Manual HTTP" &&
|
|
2752
|
+
isLoopbackPairingUrl(manualPayload.apiBaseUrl)
|
|
2753
|
+
) {
|
|
2617
2754
|
console.log(
|
|
2618
2755
|
color.yellow(
|
|
2619
2756
|
"Manual HTTP points at this machine's loopback address. That is only useful for the iOS Simulator; a real iPhone needs Iroh, Tailscale, or a LAN URL passed with --public-url."
|
|
@@ -2627,7 +2764,9 @@ async function printPairing(pairing) {
|
|
|
2627
2764
|
try {
|
|
2628
2765
|
const filePath = await writePairingPayloadFile(manualPayload);
|
|
2629
2766
|
console.log("");
|
|
2630
|
-
console.log(
|
|
2767
|
+
console.log(
|
|
2768
|
+
color.bold("If the QR is too large or the camera will not scan:")
|
|
2769
|
+
);
|
|
2631
2770
|
console.log("1. Open Manual connection in the iPhone app.");
|
|
2632
2771
|
console.log("2. Tap Paste pairing payload.");
|
|
2633
2772
|
console.log(`3. Paste the payload saved at: ${filePath}`);
|
|
@@ -2773,7 +2912,9 @@ async function runInstall(parsed, command) {
|
|
|
2773
2912
|
assertRuntimeStartedForPairing(runtimeResult, config);
|
|
2774
2913
|
pairing = await withProgress(
|
|
2775
2914
|
"Creating iOS companion pairing",
|
|
2776
|
-
pairingOptions.transportMode === "manual-http"
|
|
2915
|
+
pairingOptions.transportMode === "manual-http"
|
|
2916
|
+
? "phone-reachable HTTP"
|
|
2917
|
+
: "Iroh QR",
|
|
2777
2918
|
parsed.flags,
|
|
2778
2919
|
() =>
|
|
2779
2920
|
createPairing(config, {
|
|
@@ -2894,7 +3035,7 @@ async function doctorCheckRuntime(config, options) {
|
|
|
2894
3035
|
? "Forge API is reachable."
|
|
2895
3036
|
: result.ok && result.forge === false
|
|
2896
3037
|
? `Port ${config.port || DEFAULT_PORT} responded, but not with Forge runtime health. Stop the conflicting process or choose another --port.`
|
|
2897
|
-
|
|
3038
|
+
: `Run npx forge-memory doctor --repair, then inspect ${logPath()} if the runtime still does not start. Repair reinstalls only the owned runtime cache and preserves the data folder.`
|
|
2898
3039
|
};
|
|
2899
3040
|
}
|
|
2900
3041
|
|
|
@@ -3038,10 +3179,13 @@ async function runPairIos(parsed) {
|
|
|
3038
3179
|
const pairingOptions =
|
|
3039
3180
|
explicitPairingOptions ??
|
|
3040
3181
|
noStartPairingOptions ??
|
|
3041
|
-
await resolveIosPairingOptions(parsed, config);
|
|
3182
|
+
(await resolveIosPairingOptions(parsed, config));
|
|
3042
3183
|
const transportMode = pairingOptions.transportMode;
|
|
3043
3184
|
const publicUrl = pairingOptions.publicUrl;
|
|
3044
|
-
if (
|
|
3185
|
+
if (
|
|
3186
|
+
transportMode === "iroh" &&
|
|
3187
|
+
noStartPairingOptions?.transportMode !== "iroh"
|
|
3188
|
+
) {
|
|
3045
3189
|
await withProgress(
|
|
3046
3190
|
"Preparing Forge Companion Iroh transport",
|
|
3047
3191
|
"checking Rust/Cargo and building the local host",
|
package/package.json
CHANGED