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.
- package/package.json +1 -1
- package/src/cli.mjs +150 -51
package/package.json
CHANGED
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
|
|
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(
|
|
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
|
|
566
|
+
await run("brew", ["install", "llama.cpp"], "llama.cpp");
|
|
546
567
|
llamaBinary = await findLlamaServer();
|
|
547
|
-
} catch
|
|
548
|
-
console.log(pc.red(
|
|
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))
|
|
601
|
-
|
|
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
|
|
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
|
-
|
|
620
|
-
|
|
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
|
|
623
|
-
console.log(pc.red(
|
|
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
|
|
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
|
|
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
|
-
|
|
638
|
-
|
|
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
|
|
641
|
-
console.log(pc.red(
|
|
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
|
|
648
|
-
await
|
|
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(
|
|
655
|
-
console.log(pc.dim("Install
|
|
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
|
|
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
|
|
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
|
|
680
|
-
await
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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 {
|
|
781
|
-
const
|
|
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
|
|
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("\
|
|
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
|
}
|