forge-memory 0.2.115 → 0.2.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -57,10 +57,12 @@ the iOS Simulator but not for a physical phone.
57
57
  The base install stays one command on purpose. The detailed companion transport
58
58
  reference lives in the Forge repo at `docs/companion-iroh.md` and in the published
59
59
  docs at `https://albertbuchard.github.io/forge/companion-transport.html`. Forge
60
- Memory ships prebuilt Iroh host binaries for common desktop platforms and a bundled
61
- Rust source fallback for other machines. If neither a prebuilt host nor Cargo is
62
- available, `pair-ios` stops with transport-specific repair guidance instead of
63
- printing a localhost QR that a physical iPhone cannot use.
60
+ Memory ships Forge's Iroh host source and lockfile, not native desktop binaries. When
61
+ the default iOS pairing flow is selected, the installer checks for Cargo, offers to
62
+ install the minimal Rust toolchain when the platform supports it, builds the local
63
+ host from the bundled source, then creates the QR. If Cargo cannot be installed
64
+ automatically, `install`, `configure`, and `pair-ios` stop with platform-specific
65
+ steps instead of printing a localhost QR that a physical iPhone cannot use.
64
66
 
65
67
  `configure` reruns the full guided flow using the current config as defaults.
66
68
  Install and configure run Forge doctor before finishing. `doctor --repair` creates
@@ -277,6 +277,231 @@ function runCapture(command, args, timeoutMs = 2_000) {
277
277
  return `${result.stdout}${result.stderr}`.trim();
278
278
  }
279
279
 
280
+ function binaryNameForPlatform() {
281
+ return process.platform === "win32"
282
+ ? "forge-companion-iroh.exe"
283
+ : "forge-companion-iroh";
284
+ }
285
+
286
+ function candidateIrohRoots(config) {
287
+ const roots = [];
288
+ if (config.mode === "dev" && config.repo) {
289
+ roots.push(config.repo);
290
+ roots.push(path.join(config.repo, "openclaw-plugin", "dist"));
291
+ }
292
+ const pluginRoot = resolveOpenClawPluginRoot();
293
+ if (pluginRoot) {
294
+ roots.push(pluginRoot);
295
+ roots.push(path.join(pluginRoot, "dist"));
296
+ }
297
+ return [...new Set(roots.map((entry) => path.resolve(entry)))];
298
+ }
299
+
300
+ function candidateIrohBinariesForInstall(config) {
301
+ const binaryName = binaryNameForPlatform();
302
+ const platformKey = `${process.platform}-${process.arch}`;
303
+ const explicitBin = process.env.FORGE_COMPANION_IROH_BIN?.trim();
304
+ return [
305
+ ...(explicitBin ? [explicitBin] : []),
306
+ ...candidateIrohRoots(config).flatMap((root) => [
307
+ path.join(root, "companion-iroh", "target", "release", binaryName),
308
+ path.join(root, "companion-iroh", "target", "debug", binaryName),
309
+ path.join(root, "companion-iroh-src", "target", "release", binaryName),
310
+ path.join(root, "companion-iroh-src", "target", "debug", binaryName),
311
+ path.join(root, "companion-iroh", platformKey, binaryName),
312
+ path.join(root, "companion-iroh", binaryName)
313
+ ])
314
+ ];
315
+ }
316
+
317
+ function findIrohBinaryForInstall(config) {
318
+ return candidateIrohBinariesForInstall(config).find((candidate) =>
319
+ fs.existsSync(candidate)
320
+ );
321
+ }
322
+
323
+ function candidateIrohManifestsForInstall(config) {
324
+ return candidateIrohRoots(config).flatMap((root) => [
325
+ path.join(root, "companion-iroh", "Cargo.toml"),
326
+ path.join(root, "companion-iroh-src", "Cargo.toml")
327
+ ]);
328
+ }
329
+
330
+ function findIrohManifestForInstall(config) {
331
+ return candidateIrohManifestsForInstall(config).find((candidate) =>
332
+ fs.existsSync(candidate)
333
+ );
334
+ }
335
+
336
+ function rustInstallGuidance() {
337
+ if (process.platform === "darwin") {
338
+ return [
339
+ "Install Apple's command line tools first if prompted: xcode-select --install",
340
+ "Then install Rust with the official minimal installer:",
341
+ "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal",
342
+ "Restart the terminal or run: source ~/.cargo/env"
343
+ ];
344
+ }
345
+ if (process.platform === "linux") {
346
+ return [
347
+ "Install build tools with your system package manager, for example: sudo apt-get install -y build-essential pkg-config libssl-dev",
348
+ "Then install Rust with the official minimal installer:",
349
+ "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal",
350
+ "Restart the terminal or run: source ~/.cargo/env"
351
+ ];
352
+ }
353
+ if (process.platform === "win32") {
354
+ return [
355
+ "Install Rustup for Windows:",
356
+ "winget install Rustlang.Rustup",
357
+ "Then reopen PowerShell and rerun: npx forge-memory install"
358
+ ];
359
+ }
360
+ return [
361
+ "Install Rust/Cargo from https://rustup.rs, then rerun: npx forge-memory install"
362
+ ];
363
+ }
364
+
365
+ function refreshCargoPath() {
366
+ const cargoBin = path.join(homeDir(), ".cargo", "bin");
367
+ if (fs.existsSync(cargoBin)) {
368
+ const current = process.env.PATH ?? "";
369
+ if (!current.split(path.delimiter).includes(cargoBin)) {
370
+ process.env.PATH = `${cargoBin}${path.delimiter}${current}`;
371
+ }
372
+ }
373
+ }
374
+
375
+ async function maybeInstallRustToolchain(flags) {
376
+ refreshCargoPath();
377
+ if (commandExists("cargo")) {
378
+ return { ok: true, installed: false };
379
+ }
380
+ const guidance = rustInstallGuidance();
381
+ const canUseRustupScript =
382
+ (process.platform === "darwin" || process.platform === "linux") &&
383
+ commandExists("curl");
384
+ const canUseWinget = process.platform === "win32" && commandExists("winget");
385
+ if (!canUseRustupScript && !canUseWinget) {
386
+ return {
387
+ ok: false,
388
+ installed: false,
389
+ guidance:
390
+ process.platform === "darwin" || process.platform === "linux"
391
+ ? ["Install curl first, then install Rust/Cargo.", ...guidance]
392
+ : guidance
393
+ };
394
+ }
395
+ if (flags?.json || flags?.dryRun) {
396
+ return { ok: false, installed: false, guidance };
397
+ }
398
+ const shouldInstall = flags?.yes
399
+ ? true
400
+ : await promptYesNo(
401
+ "Forge Companion Iroh needs Rust/Cargo to build the local transport host. Install the minimal Rust toolchain now?",
402
+ true
403
+ );
404
+ if (!shouldInstall) {
405
+ return { ok: false, installed: false, guidance };
406
+ }
407
+ console.log(color.cyan("Installing minimal Rust toolchain..."));
408
+ const result = canUseWinget
409
+ ? await runCommand("winget", [
410
+ "install",
411
+ "--id",
412
+ "Rustlang.Rustup",
413
+ "-e",
414
+ "--source",
415
+ "winget",
416
+ "--accept-package-agreements",
417
+ "--accept-source-agreements"
418
+ ])
419
+ : await runCommand("sh", [
420
+ "-c",
421
+ "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal"
422
+ ]);
423
+ refreshCargoPath();
424
+ return {
425
+ ok: result.ok && commandExists("cargo"),
426
+ installed: result.ok,
427
+ guidance
428
+ };
429
+ }
430
+
431
+ async function ensureIrohTransportPrepared(config, flags = {}) {
432
+ const existingBinary = findIrohBinaryForInstall(config);
433
+ if (existingBinary) {
434
+ return { ok: true, built: false, binary: existingBinary };
435
+ }
436
+
437
+ if (config.mode !== "dev") {
438
+ await ensurePackagedRuntimeInstalled();
439
+ }
440
+
441
+ const manifestPath = findIrohManifestForInstall(config);
442
+ if (!manifestPath) {
443
+ throw new Error(
444
+ [
445
+ "Forge could not find the bundled companion Iroh source.",
446
+ "Run npx forge-memory doctor --repair so Forge Memory refreshes the packaged runtime.",
447
+ `Runtime log: ${logPath()}.`
448
+ ].join(" ")
449
+ );
450
+ }
451
+
452
+ const rust = await maybeInstallRustToolchain(flags);
453
+ if (!rust.ok) {
454
+ throw new Error(
455
+ [
456
+ "Forge Companion Iroh is source-built on this machine, but Rust/Cargo is not installed yet.",
457
+ "Install steps:",
458
+ ...rustInstallGuidance().map((entry) => `- ${entry}`),
459
+ "Then rerun: npx forge-memory install",
460
+ "For a temporary direct network fallback, use: npx forge-memory pair-ios --manual-http --public-url <phone-reachable Forge URL>"
461
+ ].join("\n")
462
+ );
463
+ }
464
+
465
+ const result = await runLoggedCommand(
466
+ "cargo",
467
+ [
468
+ "build",
469
+ "--release",
470
+ "--manifest-path",
471
+ manifestPath,
472
+ "--bin",
473
+ "forge-companion-iroh"
474
+ ],
475
+ {
476
+ cwd: path.dirname(manifestPath),
477
+ dryRun: flags.dryRun,
478
+ env: process.env,
479
+ logFile: logPath()
480
+ }
481
+ );
482
+ if (!result.ok) {
483
+ throw new Error(
484
+ [
485
+ "Forge could not build the local companion Iroh transport host from source.",
486
+ `Manifest: ${manifestPath}`,
487
+ `Log: ${logPath()}`,
488
+ "Install or repair Rust/Cargo, then rerun: npx forge-memory install"
489
+ ].join(" ")
490
+ );
491
+ }
492
+ const binary = findIrohBinaryForInstall(config);
493
+ if (!binary && !flags.dryRun) {
494
+ throw new Error(
495
+ [
496
+ "Cargo finished, but Forge could not find the built companion Iroh binary.",
497
+ `Manifest: ${manifestPath}`,
498
+ `Expected one of: ${candidateIrohBinariesForInstall(config).join(", ")}`
499
+ ].join(" ")
500
+ );
501
+ }
502
+ return { ok: true, built: true, binary: binary ?? null, manifestPath };
503
+ }
504
+
280
505
  function detectOpenClaw() {
281
506
  const installed =
282
507
  commandExists("openclaw") ||
@@ -1679,9 +1904,8 @@ class PairingTransportUnavailableError extends Error {
1679
1904
  this.code = "pairing_transport_unavailable";
1680
1905
  this.detail = detail;
1681
1906
  this.guidance = [
1682
- "Run npx forge-memory doctor --repair so Forge Memory refreshes the packaged runtime and companion transport files.",
1683
- "Then rerun npx forge-memory pair-ios.",
1684
- "On unsupported platforms, install Rust/Cargo so Forge can build the bundled companion Iroh source fallback.",
1907
+ "Run npx forge-memory install or npx forge-memory configure so the installer can prepare the Iroh transport.",
1908
+ "If Rust/Cargo is missing, the installer will guide you through installing the minimal Rust toolchain and building Forge's bundled Iroh host source.",
1685
1909
  "For an explicit Tailscale or LAN fallback, rerun with npx forge-memory pair-ios --manual-http --public-url <phone-reachable Forge URL>.",
1686
1910
  "Do not scan a QR whose API URL is 127.0.0.1 on a physical iPhone; that address only works in the iOS Simulator."
1687
1911
  ];
@@ -2060,6 +2284,25 @@ async function runInstall(parsed, command) {
2060
2284
  dryRun: parsed.flags.dryRun
2061
2285
  })
2062
2286
  );
2287
+ const shouldPair =
2288
+ parsed.flags.pairIos ||
2289
+ (!parsed.flags.skipPairIos &&
2290
+ (parsed.flags.yes
2291
+ ? true
2292
+ : await promptYesNo("Pair the iOS companion now?", true)));
2293
+ let irohTransportResult = null;
2294
+ if (
2295
+ shouldPair &&
2296
+ !parsed.flags.manualHttp &&
2297
+ !parsed.flags.dryRun
2298
+ ) {
2299
+ irohTransportResult = await withProgress(
2300
+ "Preparing Forge Companion Iroh transport",
2301
+ "checking Rust/Cargo and building the local host",
2302
+ parsed.flags,
2303
+ () => ensureIrohTransportPrepared(config, parsed.flags)
2304
+ );
2305
+ }
2063
2306
  let runtimeResult = null;
2064
2307
  if (!parsed.flags.noStart && !parsed.flags.dryRun) {
2065
2308
  runtimeResult = await withProgress(
@@ -2097,12 +2340,6 @@ async function runInstall(parsed, command) {
2097
2340
  }
2098
2341
  }
2099
2342
  }
2100
- const shouldPair =
2101
- parsed.flags.pairIos ||
2102
- (!parsed.flags.skipPairIos &&
2103
- (parsed.flags.yes
2104
- ? true
2105
- : await promptYesNo("Pair the iOS companion now?", true)));
2106
2343
  let pairing = null;
2107
2344
  if (shouldPair && !parsed.flags.dryRun) {
2108
2345
  if (!runtimeResult) {
@@ -2141,6 +2378,7 @@ async function runInstall(parsed, command) {
2141
2378
  adapterResults,
2142
2379
  runtimeResult,
2143
2380
  doctorResult,
2381
+ irohTransportResult,
2144
2382
  pairing: Boolean(pairing)
2145
2383
  };
2146
2384
  if (parsed.flags.json) console.log(JSON.stringify(summary, null, 2));
@@ -2351,6 +2589,14 @@ async function runPairIos(parsed) {
2351
2589
  transportMode,
2352
2590
  publicUrl: parsed.values.publicUrl
2353
2591
  });
2592
+ if (transportMode === "iroh") {
2593
+ await withProgress(
2594
+ "Preparing Forge Companion Iroh transport",
2595
+ "checking Rust/Cargo and building the local host",
2596
+ parsed.flags,
2597
+ () => ensureIrohTransportPrepared(config, parsed.flags)
2598
+ );
2599
+ }
2354
2600
  if (!parsed.flags.noStart) {
2355
2601
  const runtimeResult = await withProgress(
2356
2602
  "Starting Forge runtime for iOS pairing",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-memory",
3
- "version": "0.2.115",
3
+ "version": "0.2.116",
4
4
  "description": "Guided Forge installer and local runtime manager for the Forge UI, OpenClaw, Hermes, Codex, and iOS pairing.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",