offgrid-ai 0.2.4 → 0.2.5

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.mjs +60 -40
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Privacy-first CLI for running local LLMs — discover, configure, run, benchmark",
5
5
  "author": "Eeshan Srivastava (https://eeshans.com)",
6
6
  "type": "module",
package/src/cli.mjs CHANGED
@@ -491,6 +491,18 @@ async function runningProfiles() {
491
491
  async function onboardFlow() {
492
492
  startInteractive("offgrid-ai setup");
493
493
  const prompt = createPrompt();
494
+
495
+ const { spawn } = await import("node:child_process");
496
+
497
+ /** Run a command, stream output to terminal, throw on failure. */
498
+ const run = (cmd, args, label) => new Promise((resolve, reject) => {
499
+ const child = spawn(cmd, args, { stdio: "inherit" });
500
+ child.on("close", (code) => {
501
+ if (code === 0) resolve();
502
+ else reject(new Error(`${label || cmd} exited with code ${code}`));
503
+ });
504
+ child.on("error", (err) => reject(err));
505
+ });
494
506
  try {
495
507
  console.log(pc.bold("Welcome to offgrid-ai!"));
496
508
  console.log(pc.dim("Let's make sure you have everything you need to run local models.\n"));
@@ -505,10 +517,8 @@ async function onboardFlow() {
505
517
  return;
506
518
  }
507
519
  console.log(pc.cyan("Installing Homebrew..."));
508
- const { execFile } = await import("node:child_process");
509
- const { promisify } = await import("node:util");
510
520
  try {
511
- await promisify(execFile)("/bin/bash", ["-c", "NONINTERACTIVE=1 /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""], { stdio: "inherit" });
521
+ await run("/bin/bash", ["-c", "NONINTERACTIVE=1 /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""], "Homebrew");
512
522
  // Add brew to PATH for this session
513
523
  const brewPaths = ["/opt/homebrew/bin", "/usr/local/bin"];
514
524
  for (const p of brewPaths) {
@@ -518,7 +528,7 @@ async function onboardFlow() {
518
528
  }
519
529
  }
520
530
  } catch (err) {
521
- console.log(pc.red(`Homebrew installation failed: ${err.message}`));
531
+ console.log(pc.red(`✗ Homebrew installation failed.`));
522
532
  console.log(pc.dim("Install it manually from https://brew.sh, then run offgrid-ai again."));
523
533
  return;
524
534
  }
@@ -539,13 +549,12 @@ async function onboardFlow() {
539
549
  return;
540
550
  }
541
551
  console.log(pc.cyan("Installing llama.cpp..."));
542
- const { execFile } = await import("node:child_process");
543
- const { promisify } = await import("node:util");
544
552
  try {
545
- await promisify(execFile)("brew", ["install", "llama.cpp"], { stdio: "inherit" });
553
+ await run("brew", ["install", "llama.cpp"], "llama.cpp");
546
554
  llamaBinary = await findLlamaServer();
547
- } catch (err) {
548
- console.log(pc.red(`Failed to install llama.cpp: ${err.message}`));
555
+ } catch {
556
+ console.log(pc.red("✗ Failed to install llama.cpp."));
557
+ console.log(pc.dim("Install it manually: brew install llama.cpp"));
549
558
  return;
550
559
  }
551
560
  if (!llamaBinary) {
@@ -584,9 +593,6 @@ async function onboardFlow() {
584
593
  { value: "skip", label: "Skip for now", hint: "I'll set up models myself" },
585
594
  ], "lmstudio");
586
595
 
587
- const { execFile } = await import("node:child_process");
588
- const { promisify } = await import("node:util");
589
-
590
596
  const lmsAppBundle = "/Applications/LM Studio.app/Contents/Resources/app/.webpack/lms";
591
597
  const lmsBin = join(homedir(), ".lmstudio", "bin");
592
598
 
@@ -597,8 +603,11 @@ async function onboardFlow() {
597
603
  mkdirSync(lmsBin, { recursive: true });
598
604
  copyFileSync(lmsAppBundle, lmsDest);
599
605
  }
600
- if (!existsSync(lmsDest)) return;
601
- if (process.env.PATH.split(":").includes(lmsBin)) return;
606
+ if (!existsSync(lmsDest)) {
607
+ console.log(pc.yellow(" Note: lms CLI will be available after opening LM Studio once."));
608
+ return false;
609
+ }
610
+ if (process.env.PATH.split(":").includes(lmsBin)) return true;
602
611
  process.env.PATH = `${lmsBin}:${process.env.PATH}`;
603
612
  const profileFiles = [join(homedir(), ".zshrc"), join(homedir(), ".bash_profile")];
604
613
  const line = `export PATH="$PATH:$HOME/.lmstudio/bin"`;
@@ -608,29 +617,34 @@ async function onboardFlow() {
608
617
  if (content.includes(".lmstudio/bin")) continue;
609
618
  appendFileSync(f, `\n${line}\n`);
610
619
  }
620
+ return true;
611
621
  };
612
622
 
613
623
  if (backendChoice === "lmstudio") {
614
624
  console.log(pc.cyan("Installing LM Studio via Homebrew..."));
615
625
  try {
616
- await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
617
- ensureLmsOnPath();
626
+ await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
627
+ const lmsReady = ensureLmsOnPath();
618
628
  console.log(pc.green("✓ LM Studio installed"));
619
629
  console.log(pc.yellow("\nDownload your first model:"));
620
- console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
630
+ if (lmsReady) {
631
+ console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
632
+ } else {
633
+ console.log(pc.bold(" Open LM Studio once, then: lms get qwen/qwen3.5-9b"));
634
+ }
621
635
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
622
636
  } catch (err) {
623
- console.log(pc.red(`Failed to install LM Studio: ${err.message}`));
637
+ console.log(pc.red(`✗ LM Studio installation failed.`));
624
638
  console.log(pc.dim("Download it manually from https://lmstudio.ai"));
625
639
  }
626
640
  } else if (backendChoice === "ollama") {
627
641
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
628
642
  try {
629
- await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
643
+ await run("brew", ["install", "ollama"], "Ollama");
630
644
  console.log(pc.green("✓ Ollama installed"));
631
645
  console.log(pc.cyan("\nStarting Ollama..."));
632
646
  try {
633
- await promisify(execFile)("ollama", ["serve"], { stdio: "ignore", detached: true });
647
+ await run("ollama", ["serve"], "Ollama serve");
634
648
  await new Promise((resolve) => setTimeout(resolve, 2000));
635
649
  } catch { /* may already be running */ }
636
650
  console.log(pc.green("Ollama is running."));
@@ -638,55 +652,57 @@ async function onboardFlow() {
638
652
  console.log(pc.bold(" ollama pull gemma3:4b"));
639
653
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
640
654
  } catch (err) {
641
- console.log(pc.red(`Failed to install Ollama: ${err.message}`));
655
+ console.log(pc.red(`✗ Ollama installation failed.`));
642
656
  console.log(pc.dim("Install it manually from https://ollama.com"));
643
657
  }
644
658
  } else if (backendChoice === "omlx") {
645
659
  console.log(pc.cyan("Installing oMLX via Homebrew..."));
646
660
  try {
647
- await promisify(execFile)("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], { stdio: "inherit" });
648
- await promisify(execFile)("brew", ["install", "omlx"], { stdio: "inherit" });
661
+ await run("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], "oMLX tap");
662
+ await run("brew", ["install", "omlx"], "oMLX");
649
663
  console.log(pc.green("✓ oMLX installed"));
650
664
  console.log(pc.yellow("\nStart oMLX server:"));
651
665
  console.log(pc.bold(" omlx start"));
652
666
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
653
667
  } catch (err) {
654
- console.log(pc.red(`Failed to install oMLX: ${err.message}`));
655
- console.log(pc.dim("Install it manually: brew tap jundot/omlx && brew install omlx"));
668
+ console.log(pc.red(`✗ oMLX installation failed.`));
669
+ console.log(pc.dim("Install manually: brew tap jundot/omlx && brew install omlx"));
656
670
  }
657
671
  } else if (backendChoice === "all") {
658
672
  let installed = [];
659
673
  // LM Studio
660
674
  console.log(pc.cyan("Installing LM Studio via Homebrew..."));
661
675
  try {
662
- await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
663
- ensureLmsOnPath();
664
- installed.push("LM Studio");
676
+ await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
677
+ const lmsReady = ensureLmsOnPath();
678
+ installed.push(lmsReady ? "LM Studio" : "LM Studio (open app once for lms CLI)");
665
679
  } catch {
666
- console.log(pc.yellow("LM Studio installation failed. Download from https://lmstudio.ai"));
680
+ console.log(pc.yellow("LM Studio installation failed. Download from https://lmstudio.ai"));
667
681
  }
668
682
  // Ollama
669
683
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
670
684
  try {
671
- await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
685
+ await run("brew", ["install", "ollama"], "Ollama");
672
686
  installed.push("Ollama");
673
687
  } catch {
674
- console.log(pc.yellow("Ollama installation failed. Install manually from https://ollama.com"));
688
+ console.log(pc.yellow("Ollama installation failed. Install manually from https://ollama.com"));
675
689
  }
676
690
  // oMLX
677
691
  console.log(pc.cyan("Installing oMLX via Homebrew..."));
678
692
  try {
679
- await promisify(execFile)("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], { stdio: "inherit" });
680
- await promisify(execFile)("brew", ["install", "omlx"], { stdio: "inherit" });
693
+ await run("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], "oMLX tap");
694
+ await run("brew", ["install", "omlx"], "oMLX");
681
695
  installed.push("oMLX");
682
696
  } catch {
683
- console.log(pc.yellow("oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
697
+ console.log(pc.yellow("oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
684
698
  }
685
699
  if (installed.length > 0) {
686
700
  console.log(pc.green(`\n✓ Installed: ${installed.join(", ")}`));
687
701
  console.log(pc.yellow("Next steps — download your first model:"));
688
- if (installed.includes("LM Studio")) {
689
- console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
702
+ const hasLms = installed.some(i => i.includes("LM Studio"));
703
+ if (hasLms) {
704
+ const lmsReady = installed.some(i => i === "LM Studio");
705
+ console.log(pc.bold(lmsReady ? " lms get qwen/qwen3.5-9b" : " Open LM Studio once, then: lms get qwen/qwen3.5-9b"));
690
706
  }
691
707
  if (installed.includes("Ollama")) {
692
708
  console.log(pc.bold(" ollama pull gemma3:4b"));
@@ -777,14 +793,18 @@ async function removeDataDir() {
777
793
 
778
794
  async function removeSelf() {
779
795
  console.log(pc.cyan("\nUninstalling offgrid-ai..."));
780
- const { execFile } = await import("node:child_process");
781
- const { promisify } = await import("node:util");
796
+ const { spawn: spawnUninstall } = await import("node:child_process");
797
+ const runCmd = (cmd, args, label) => new Promise((resolve, reject) => {
798
+ const child = spawnUninstall(cmd, args, { stdio: "inherit" });
799
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${label || cmd} exited with code ${code}`)));
800
+ child.on("error", (err) => reject(err));
801
+ });
782
802
  try {
783
- await promisify(execFile)("npm", ["uninstall", "-g", "offgrid-ai"], { stdio: "inherit" });
803
+ await runCmd("npm", ["uninstall", "-g", "offgrid-ai"], "npm uninstall");
784
804
  console.log(pc.green("\n✓ offgrid-ai has been uninstalled."));
785
805
  console.log(pc.dim("Reinstall anytime with: npm install -g offgrid-ai"));
786
806
  } catch {
787
- console.log(pc.red("\nCould not auto-uninstall. Run this manually:"));
807
+ console.log(pc.red("\n✗ Could not auto-uninstall. Run this manually:"));
788
808
  console.log(pc.bold(" npm uninstall -g offgrid-ai"));
789
809
  }
790
810
  }