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.
- package/package.json +1 -1
- package/src/cli.mjs +69 -41
package/package.json
CHANGED
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
|
|
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(
|
|
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
|
|
553
|
+
await run("brew", ["install", "llama.cpp"], "llama.cpp");
|
|
546
554
|
llamaBinary = await findLlamaServer();
|
|
547
|
-
} catch
|
|
548
|
-
console.log(pc.red(
|
|
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
|
|
588
|
-
const
|
|
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
|
|
592
|
-
|
|
593
|
-
if (
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
640
|
-
await
|
|
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(
|
|
647
|
-
console.log(pc.dim("Install
|
|
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
|
|
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
|
|
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
|
|
672
|
-
await
|
|
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
|
-
|
|
681
|
-
|
|
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 {
|
|
773
|
-
const
|
|
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
|
|
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("\
|
|
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
|
}
|