offgrid-ai 0.2.4 → 0.2.6

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 +150 -51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "offgrid-ai",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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
@@ -23,6 +23,7 @@ export async function run(argv) {
23
23
  if (command === "status") return statusCommand();
24
24
  if (command === "stop") return stopCommand(argv.slice(1));
25
25
  if (command === "uninstall" || command === "--uninstall") return uninstallCommand(argv.slice(1));
26
+ if (command === "--verbose") return mainFlow(); // verbose flag handled inside onboardFlow
26
27
 
27
28
  throw new Error(`Unknown command: ${command}. Run offgrid-ai help`);
28
29
  }
@@ -491,6 +492,30 @@ async function runningProfiles() {
491
492
  async function onboardFlow() {
492
493
  startInteractive("offgrid-ai setup");
493
494
  const prompt = createPrompt();
495
+ const verbose = process.argv.includes("--verbose");
496
+
497
+ const { spawn } = await import("node:child_process");
498
+
499
+ /** Run a command. Verbose: stream output. Quiet: show only label + result. */
500
+ const run = (cmd, args, label) => new Promise((resolve, reject) => {
501
+ if (verbose) {
502
+ const child = spawn(cmd, args, { stdio: "inherit" });
503
+ child.on("close", (code) => {
504
+ if (code === 0) resolve();
505
+ else reject(new Error(`${label || cmd} exited with code ${code}`));
506
+ });
507
+ child.on("error", (err) => reject(err));
508
+ } else {
509
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
510
+ let stderr = "";
511
+ child.stderr.on("data", (d) => { stderr += d; });
512
+ child.on("close", (code) => {
513
+ if (code === 0) resolve();
514
+ else reject(new Error(stderr.split("\n").filter(l => l.trim()).slice(-3).join("\n") || `${label || cmd} exited with code ${code}`));
515
+ });
516
+ child.on("error", (err) => reject(err));
517
+ }
518
+ });
494
519
  try {
495
520
  console.log(pc.bold("Welcome to offgrid-ai!"));
496
521
  console.log(pc.dim("Let's make sure you have everything you need to run local models.\n"));
@@ -505,10 +530,8 @@ async function onboardFlow() {
505
530
  return;
506
531
  }
507
532
  console.log(pc.cyan("Installing Homebrew..."));
508
- const { execFile } = await import("node:child_process");
509
- const { promisify } = await import("node:util");
510
533
  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" });
534
+ await run("/bin/bash", ["-c", "NONINTERACTIVE=1 /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""], "Homebrew");
512
535
  // Add brew to PATH for this session
513
536
  const brewPaths = ["/opt/homebrew/bin", "/usr/local/bin"];
514
537
  for (const p of brewPaths) {
@@ -518,7 +541,7 @@ async function onboardFlow() {
518
541
  }
519
542
  }
520
543
  } catch (err) {
521
- console.log(pc.red(`Homebrew installation failed: ${err.message}`));
544
+ console.log(pc.red(`✗ Homebrew installation failed.`));
522
545
  console.log(pc.dim("Install it manually from https://brew.sh, then run offgrid-ai again."));
523
546
  return;
524
547
  }
@@ -539,13 +562,12 @@ async function onboardFlow() {
539
562
  return;
540
563
  }
541
564
  console.log(pc.cyan("Installing llama.cpp..."));
542
- const { execFile } = await import("node:child_process");
543
- const { promisify } = await import("node:util");
544
565
  try {
545
- await promisify(execFile)("brew", ["install", "llama.cpp"], { stdio: "inherit" });
566
+ await run("brew", ["install", "llama.cpp"], "llama.cpp");
546
567
  llamaBinary = await findLlamaServer();
547
- } catch (err) {
548
- console.log(pc.red(`Failed to install llama.cpp: ${err.message}`));
568
+ } catch {
569
+ console.log(pc.red("✗ Failed to install llama.cpp."));
570
+ console.log(pc.dim("Install it manually: brew install llama.cpp"));
549
571
  return;
550
572
  }
551
573
  if (!llamaBinary) {
@@ -584,9 +606,6 @@ async function onboardFlow() {
584
606
  { value: "skip", label: "Skip for now", hint: "I'll set up models myself" },
585
607
  ], "lmstudio");
586
608
 
587
- const { execFile } = await import("node:child_process");
588
- const { promisify } = await import("node:util");
589
-
590
609
  const lmsAppBundle = "/Applications/LM Studio.app/Contents/Resources/app/.webpack/lms";
591
610
  const lmsBin = join(homedir(), ".lmstudio", "bin");
592
611
 
@@ -597,8 +616,11 @@ async function onboardFlow() {
597
616
  mkdirSync(lmsBin, { recursive: true });
598
617
  copyFileSync(lmsAppBundle, lmsDest);
599
618
  }
600
- if (!existsSync(lmsDest)) return;
601
- if (process.env.PATH.split(":").includes(lmsBin)) return;
619
+ if (!existsSync(lmsDest)) {
620
+ console.log(pc.yellow(" Note: lms CLI will be available after opening LM Studio once."));
621
+ return false;
622
+ }
623
+ if (process.env.PATH.split(":").includes(lmsBin)) return true;
602
624
  process.env.PATH = `${lmsBin}:${process.env.PATH}`;
603
625
  const profileFiles = [join(homedir(), ".zshrc"), join(homedir(), ".bash_profile")];
604
626
  const line = `export PATH="$PATH:$HOME/.lmstudio/bin"`;
@@ -608,91 +630,150 @@ async function onboardFlow() {
608
630
  if (content.includes(".lmstudio/bin")) continue;
609
631
  appendFileSync(f, `\n${line}\n`);
610
632
  }
633
+ return true;
611
634
  };
612
635
 
613
636
  if (backendChoice === "lmstudio") {
614
637
  console.log(pc.cyan("Installing LM Studio via Homebrew..."));
615
638
  try {
616
- await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
617
- ensureLmsOnPath();
639
+ await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
640
+ const lmsReady = ensureLmsOnPath();
618
641
  console.log(pc.green("✓ LM Studio installed"));
619
- console.log(pc.yellow("\nDownload your first model:"));
620
- console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
642
+ if (lmsReady) {
643
+ const download = await prompt.yesNo("Download a model now? (qwen3.5-9b)", true);
644
+ if (download) {
645
+ console.log(pc.cyan("Downloading qwen/qwen3.5-9b via lms..."));
646
+ try {
647
+ await run("lms", ["get", "qwen/qwen3.5-9b", "-y"], "lms get qwen/qwen3.5-9b");
648
+ console.log(pc.green("✓ Model downloaded."));
649
+ } catch {
650
+ console.log(pc.yellow("Model download failed. Run manually: lms get qwen/qwen3.5-9b"));
651
+ }
652
+ } else {
653
+ console.log(pc.yellow("Download a model later:"));
654
+ console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
655
+ }
656
+ } else {
657
+ console.log(pc.yellow("\nOpen LM Studio once to finish setup, then download a model:"));
658
+ console.log(pc.bold(" lms get qwen/qwen3.5-9b"));
659
+ }
621
660
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
622
- } catch (err) {
623
- console.log(pc.red(`Failed to install LM Studio: ${err.message}`));
661
+ } catch {
662
+ console.log(pc.red("✗ LM Studio installation failed."));
624
663
  console.log(pc.dim("Download it manually from https://lmstudio.ai"));
625
664
  }
626
665
  } else if (backendChoice === "ollama") {
627
666
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
628
667
  try {
629
- await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
668
+ await run("brew", ["install", "ollama"], "Ollama");
630
669
  console.log(pc.green("✓ Ollama installed"));
631
670
  console.log(pc.cyan("\nStarting Ollama..."));
632
671
  try {
633
- await promisify(execFile)("ollama", ["serve"], { stdio: "ignore", detached: true });
672
+ await run("ollama", ["serve"], "Ollama serve");
634
673
  await new Promise((resolve) => setTimeout(resolve, 2000));
635
674
  } catch { /* may already be running */ }
636
675
  console.log(pc.green("Ollama is running."));
637
- console.log(pc.yellow("\nPull your first model:"));
638
- console.log(pc.bold(" ollama pull gemma3:4b"));
676
+ const download = await prompt.yesNo("Pull a model now? (gemma3:4b)", true);
677
+ if (download) {
678
+ console.log(pc.cyan("Pulling gemma3:4b via Ollama..."));
679
+ try {
680
+ await run("ollama", ["pull", "gemma3:4b"], "ollama pull gemma3:4b");
681
+ console.log(pc.green("✓ Model pulled."));
682
+ } catch {
683
+ console.log(pc.yellow("Model pull failed. Run manually: ollama pull gemma3:4b"));
684
+ }
685
+ } else {
686
+ console.log(pc.yellow("Pull a model later:"));
687
+ console.log(pc.bold(" ollama pull gemma3:4b"));
688
+ }
639
689
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
640
- } catch (err) {
641
- console.log(pc.red(`Failed to install Ollama: ${err.message}`));
690
+ } catch {
691
+ console.log(pc.red("✗ Ollama installation failed."));
642
692
  console.log(pc.dim("Install it manually from https://ollama.com"));
643
693
  }
644
694
  } else if (backendChoice === "omlx") {
645
695
  console.log(pc.cyan("Installing oMLX via Homebrew..."));
646
696
  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" });
697
+ await run("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], "oMLX tap");
698
+ await run("brew", ["install", "omlx"], "oMLX");
649
699
  console.log(pc.green("✓ oMLX installed"));
650
700
  console.log(pc.yellow("\nStart oMLX server:"));
651
701
  console.log(pc.bold(" omlx start"));
652
702
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
653
703
  } 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"));
704
+ console.log(pc.red(`✗ oMLX installation failed.`));
705
+ console.log(pc.dim("Install manually: brew tap jundot/omlx && brew install omlx"));
656
706
  }
657
707
  } else if (backendChoice === "all") {
658
708
  let installed = [];
659
709
  // LM Studio
660
710
  console.log(pc.cyan("Installing LM Studio via Homebrew..."));
711
+ let lmsReady = false;
661
712
  try {
662
- await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
663
- ensureLmsOnPath();
713
+ await run("brew", ["install", "--cask", "lm-studio"], "LM Studio");
714
+ lmsReady = ensureLmsOnPath();
664
715
  installed.push("LM Studio");
665
716
  } catch {
666
- console.log(pc.yellow("LM Studio installation failed. Download from https://lmstudio.ai"));
717
+ console.log(pc.yellow("LM Studio installation failed. Download from https://lmstudio.ai"));
667
718
  }
668
719
  // Ollama
669
720
  console.log(pc.cyan("Installing Ollama via Homebrew..."));
721
+ let ollamaInstalled = false;
670
722
  try {
671
- await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
723
+ await run("brew", ["install", "ollama"], "Ollama");
672
724
  installed.push("Ollama");
725
+ ollamaInstalled = true;
673
726
  } catch {
674
- console.log(pc.yellow("Ollama installation failed. Install manually from https://ollama.com"));
727
+ console.log(pc.yellow("Ollama installation failed. Install manually from https://ollama.com"));
675
728
  }
676
729
  // oMLX
677
730
  console.log(pc.cyan("Installing oMLX via Homebrew..."));
678
731
  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" });
732
+ await run("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], "oMLX tap");
733
+ await run("brew", ["install", "omlx"], "oMLX");
681
734
  installed.push("oMLX");
682
735
  } catch {
683
- console.log(pc.yellow("oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
736
+ console.log(pc.yellow("oMLX installation failed. Install manually: brew tap jundot/omlx && brew install omlx"));
684
737
  }
738
+ // Auto-download model with best available backend
685
739
  if (installed.length > 0) {
686
740
  console.log(pc.green(`\n✓ Installed: ${installed.join(", ")}`));
687
- 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"));
690
- }
691
- if (installed.includes("Ollama")) {
692
- console.log(pc.bold(" ollama pull gemma3:4b"));
693
- }
694
- if (installed.includes("oMLX")) {
695
- console.log(pc.bold(" omlx start"));
741
+ if (lmsReady) {
742
+ const download = await prompt.yesNo("Download a model now? (qwen3.5-9b)", true);
743
+ if (download) {
744
+ console.log(pc.cyan("Downloading qwen/qwen3.5-9b via lms..."));
745
+ try {
746
+ await run("lms", ["get", "qwen/qwen3.5-9b", "-y"], "lms get qwen/qwen3.5-9b");
747
+ console.log(pc.green("✓ Model downloaded."));
748
+ } catch {
749
+ console.log(pc.yellow("Model download failed. Run manually: lms get qwen/qwen3.5-9b"));
750
+ }
751
+ }
752
+ } else if (ollamaInstalled) {
753
+ // Start Ollama and offer to pull
754
+ console.log(pc.cyan("Starting Ollama..."));
755
+ try { await run("ollama", ["serve"], "Ollama serve"); await new Promise(r => setTimeout(r, 2000)); } catch { /* may already be running */ }
756
+ const download = await prompt.yesNo("Pull a model now? (gemma3:4b)", true);
757
+ if (download) {
758
+ console.log(pc.cyan("Pulling gemma3:4b via Ollama..."));
759
+ try {
760
+ await run("ollama", ["pull", "gemma3:4b"], "ollama pull gemma3:4b");
761
+ console.log(pc.green("✓ Model pulled."));
762
+ } catch {
763
+ console.log(pc.yellow("Model pull failed. Run manually: ollama pull gemma3:4b"));
764
+ }
765
+ }
766
+ } else {
767
+ console.log(pc.yellow("Next steps — download your first model:"));
768
+ if (installed.some(i => i.includes("LM Studio"))) {
769
+ console.log(pc.bold(lmsReady ? " lms get qwen/qwen3.5-9b" : " Open LM Studio once, then: lms get qwen/qwen3.5-9b"));
770
+ }
771
+ if (installed.includes("Ollama")) {
772
+ console.log(pc.bold(" ollama pull gemma3:4b"));
773
+ }
774
+ if (installed.includes("oMLX")) {
775
+ console.log(pc.bold(" omlx start"));
776
+ }
696
777
  }
697
778
  console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
698
779
  }
@@ -777,14 +858,29 @@ async function removeDataDir() {
777
858
 
778
859
  async function removeSelf() {
779
860
  console.log(pc.cyan("\nUninstalling offgrid-ai..."));
780
- const { execFile } = await import("node:child_process");
781
- const { promisify } = await import("node:util");
861
+ const { spawn: spawnUninstall } = await import("node:child_process");
862
+ const verbose = process.argv.includes("--verbose");
863
+ const runCmd = (cmd, args, label) => new Promise((resolve, reject) => {
864
+ const stdio = verbose ? "inherit" : ["ignore", "pipe", "pipe"];
865
+ const child = spawnUninstall(cmd, args, { stdio });
866
+ if (!verbose) {
867
+ let stderr = "";
868
+ child.stderr?.on("data", (d) => { stderr += d; });
869
+ child.on("close", (code) => {
870
+ if (code === 0) resolve();
871
+ else reject(new Error(stderr.split("\n").filter(l => l.trim()).slice(-3).join("\n") || `${label || cmd} exited with code ${code}`));
872
+ });
873
+ } else {
874
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`${label || cmd} exited with code ${code}`)));
875
+ }
876
+ child.on("error", (err) => reject(err));
877
+ });
782
878
  try {
783
- await promisify(execFile)("npm", ["uninstall", "-g", "offgrid-ai"], { stdio: "inherit" });
879
+ await runCmd("npm", ["uninstall", "-g", "offgrid-ai"], "npm uninstall");
784
880
  console.log(pc.green("\n✓ offgrid-ai has been uninstalled."));
785
881
  console.log(pc.dim("Reinstall anytime with: npm install -g offgrid-ai"));
786
882
  } catch {
787
- console.log(pc.red("\nCould not auto-uninstall. Run this manually:"));
883
+ console.log(pc.red("\n✗ Could not auto-uninstall. Run this manually:"));
788
884
  console.log(pc.bold(" npm uninstall -g offgrid-ai"));
789
885
  }
790
886
  }
@@ -827,6 +923,9 @@ Usage:
827
923
  offgrid-ai help Show this help
828
924
  offgrid-ai version Show version
829
925
 
926
+ Flags:
927
+ --verbose Show install output (brew, lms, ollama, etc.)
928
+
830
929
  First run? offgrid-ai walks you through installing everything you need.
831
930
  After that, just run it — it finds your models, auto-configures, and launches Pi.`);
832
931
  }