offgrid-ai 0.2.3 → 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 +69 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.2.3",
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
@@ -1,5 +1,5 @@
1
1
  import { homedir } from "node:os";
2
- import { existsSync, statSync, rmSync, readFileSync, appendFileSync } from "node:fs";
2
+ import { existsSync, statSync, rmSync, readFileSync, appendFileSync, mkdirSync, copyFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { ensureDirs, findLlamaServer, hasHomebrew, DATA_DIR } from "./config.mjs";
5
5
  import { scanGgufModels } from "./scan.mjs";
@@ -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,13 +593,21 @@ 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");
596
+ const lmsAppBundle = "/Applications/LM Studio.app/Contents/Resources/app/.webpack/lms";
597
+ const lmsBin = join(homedir(), ".lmstudio", "bin");
589
598
 
590
599
  const ensureLmsOnPath = () => {
591
- const lmsBin = join(homedir(), ".lmstudio", "bin");
592
- if (!existsSync(join(lmsBin, "lms"))) return;
593
- if (process.env.PATH.split(":").includes(lmsBin)) return;
600
+ const lmsDest = join(lmsBin, "lms");
601
+ // Bootstrap lms from app bundle if not yet in ~/.lmstudio/bin
602
+ if (!existsSync(lmsDest) && existsSync(lmsAppBundle)) {
603
+ mkdirSync(lmsBin, { recursive: true });
604
+ copyFileSync(lmsAppBundle, lmsDest);
605
+ }
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;
594
611
  process.env.PATH = `${lmsBin}:${process.env.PATH}`;
595
612
  const profileFiles = [join(homedir(), ".zshrc"), join(homedir(), ".bash_profile")];
596
613
  const line = `export PATH="$PATH:$HOME/.lmstudio/bin"`;
@@ -600,29 +617,34 @@ async function onboardFlow() {
600
617
  if (content.includes(".lmstudio/bin")) continue;
601
618
  appendFileSync(f, `\n${line}\n`);
602
619
  }
620
+ return true;
603
621
  };
604
622
 
605
623
  if (backendChoice === "lmstudio") {
606
624
  console.log(pc.cyan("Installing LM Studio via Homebrew..."));
607
625
  try {
608
- await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
609
- ensureLmsOnPath();
626
+ await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
627
+ const lmsReady = ensureLmsOnPath();
610
628
  console.log(pc.green("✓ LM Studio installed"));
611
629
  console.log(pc.yellow("\nDownload your first model:"));
612
- 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
+ }
613
635
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
614
636
  } catch (err) {
615
- console.log(pc.red(`Failed to install LM Studio: ${err.message}`));
637
+ console.log(pc.red(`✗ LM Studio installation failed.`));
616
638
  console.log(pc.dim("Download it manually from https://lmstudio.ai"));
617
639
  }
618
640
  } else if (backendChoice === "ollama") {
619
641
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
620
642
  try {
621
- await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
643
+ await run("brew", ["install", "ollama"], "Ollama");
622
644
  console.log(pc.green("✓ Ollama installed"));
623
645
  console.log(pc.cyan("\nStarting Ollama..."));
624
646
  try {
625
- await promisify(execFile)("ollama", ["serve"], { stdio: "ignore", detached: true });
647
+ await run("ollama", ["serve"], "Ollama serve");
626
648
  await new Promise((resolve) => setTimeout(resolve, 2000));
627
649
  } catch { /* may already be running */ }
628
650
  console.log(pc.green("Ollama is running."));
@@ -630,55 +652,57 @@ async function onboardFlow() {
630
652
  console.log(pc.bold(" ollama pull gemma3:4b"));
631
653
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
632
654
  } catch (err) {
633
- console.log(pc.red(`Failed to install Ollama: ${err.message}`));
655
+ console.log(pc.red(`✗ Ollama installation failed.`));
634
656
  console.log(pc.dim("Install it manually from https://ollama.com"));
635
657
  }
636
658
  } else if (backendChoice === "omlx") {
637
659
  console.log(pc.cyan("Installing oMLX via Homebrew..."));
638
660
  try {
639
- await promisify(execFile)("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], { stdio: "inherit" });
640
- 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");
641
663
  console.log(pc.green("✓ oMLX installed"));
642
664
  console.log(pc.yellow("\nStart oMLX server:"));
643
665
  console.log(pc.bold(" omlx start"));
644
666
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
645
667
  } catch (err) {
646
- console.log(pc.red(`Failed to install oMLX: ${err.message}`));
647
- 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"));
648
670
  }
649
671
  } else if (backendChoice === "all") {
650
672
  let installed = [];
651
673
  // LM Studio
652
674
  console.log(pc.cyan("Installing LM Studio via Homebrew..."));
653
675
  try {
654
- await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
655
- ensureLmsOnPath();
656
- 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)");
657
679
  } catch {
658
- 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"));
659
681
  }
660
682
  // Ollama
661
683
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
662
684
  try {
663
- await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
685
+ await run("brew", ["install", "ollama"], "Ollama");
664
686
  installed.push("Ollama");
665
687
  } catch {
666
- 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"));
667
689
  }
668
690
  // oMLX
669
691
  console.log(pc.cyan("Installing oMLX via Homebrew..."));
670
692
  try {
671
- await promisify(execFile)("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], { stdio: "inherit" });
672
- 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");
673
695
  installed.push("oMLX");
674
696
  } catch {
675
- 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"));
676
698
  }
677
699
  if (installed.length > 0) {
678
700
  console.log(pc.green(`\n✓ Installed: ${installed.join(", ")}`));
679
701
  console.log(pc.yellow("Next steps — download your first model:"));
680
- if (installed.includes("LM Studio")) {
681
- 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"));
682
706
  }
683
707
  if (installed.includes("Ollama")) {
684
708
  console.log(pc.bold(" ollama pull gemma3:4b"));
@@ -769,14 +793,18 @@ async function removeDataDir() {
769
793
 
770
794
  async function removeSelf() {
771
795
  console.log(pc.cyan("\nUninstalling offgrid-ai..."));
772
- const { execFile } = await import("node:child_process");
773
- 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
+ });
774
802
  try {
775
- await promisify(execFile)("npm", ["uninstall", "-g", "offgrid-ai"], { stdio: "inherit" });
803
+ await runCmd("npm", ["uninstall", "-g", "offgrid-ai"], "npm uninstall");
776
804
  console.log(pc.green("\n✓ offgrid-ai has been uninstalled."));
777
805
  console.log(pc.dim("Reinstall anytime with: npm install -g offgrid-ai"));
778
806
  } catch {
779
- console.log(pc.red("\nCould not auto-uninstall. Run this manually:"));
807
+ console.log(pc.red("\n✗ Could not auto-uninstall. Run this manually:"));
780
808
  console.log(pc.bold(" npm uninstall -g offgrid-ai"));
781
809
  }
782
810
  }