offgrid-ai 0.1.2 → 0.2.1
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 +199 -27
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, rmSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { ensureDirs, findLlamaServer, hasHomebrew } from "./config.mjs";
|
|
4
|
+
import { ensureDirs, findLlamaServer, hasHomebrew, DATA_DIR } from "./config.mjs";
|
|
5
5
|
import { scanGgufModels } from "./scan.mjs";
|
|
6
6
|
import { createProfileFromModel, normalizeProfile } from "./profiles.mjs";
|
|
7
7
|
import { readProfile, saveProfile, deleteProfile, loadProfiles } from "./profiles.mjs";
|
|
@@ -22,6 +22,7 @@ export async function run(argv) {
|
|
|
22
22
|
if (command === "version" || command === "--version" || command === "-v") return printVersion();
|
|
23
23
|
if (command === "status") return statusCommand();
|
|
24
24
|
if (command === "stop") return stopCommand(argv.slice(1));
|
|
25
|
+
if (command === "uninstall" || command === "--uninstall") return uninstallCommand(argv.slice(1));
|
|
25
26
|
|
|
26
27
|
throw new Error(`Unknown command: ${command}. Run offgrid-ai help`);
|
|
27
28
|
}
|
|
@@ -509,18 +510,46 @@ async function onboardFlow() {
|
|
|
509
510
|
// 1. Homebrew
|
|
510
511
|
const hasBrew = await hasHomebrew();
|
|
511
512
|
if (!hasBrew) {
|
|
512
|
-
const install = await prompt.yesNo("Homebrew is required. Install it?", true);
|
|
513
|
-
if (!install) {
|
|
514
|
-
|
|
515
|
-
|
|
513
|
+
const install = await prompt.yesNo("Homebrew is required. Install it now?", true);
|
|
514
|
+
if (!install) {
|
|
515
|
+
console.log(pc.red("offgrid-ai needs Homebrew to install dependencies."));
|
|
516
|
+
console.log(pc.dim("Install it from https://brew.sh, then run offgrid-ai again."));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
console.log(pc.cyan("Installing Homebrew..."));
|
|
520
|
+
const { execFile } = await import("node:child_process");
|
|
521
|
+
const { promisify } = await import("node:util");
|
|
522
|
+
try {
|
|
523
|
+
await promisify(execFile)("/bin/bash", ["-c", "NONINTERACTIVE=1 /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""], { stdio: "inherit" });
|
|
524
|
+
// Add brew to PATH for this session
|
|
525
|
+
const brewPaths = ["/opt/homebrew/bin", "/usr/local/bin"];
|
|
526
|
+
for (const p of brewPaths) {
|
|
527
|
+
if (existsSync(p)) {
|
|
528
|
+
process.env.PATH = `${p}:${process.env.PATH}`;
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch (err) {
|
|
533
|
+
console.log(pc.red(`Homebrew installation failed: ${err.message}`));
|
|
534
|
+
console.log(pc.dim("Install it manually from https://brew.sh, then run offgrid-ai again."));
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!(await hasHomebrew())) {
|
|
538
|
+
console.log(pc.red("Homebrew was installed but not found on PATH. Restart your terminal and run offgrid-ai again."));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
516
541
|
}
|
|
517
542
|
console.log(pc.green("✓ Homebrew found"));
|
|
518
543
|
|
|
519
544
|
// 2. llama-server
|
|
520
545
|
let llamaBinary = await findLlamaServer();
|
|
521
546
|
if (!llamaBinary) {
|
|
522
|
-
const install = await prompt.yesNo("llama-server is required. Install via Homebrew?", true);
|
|
523
|
-
if (!install) {
|
|
547
|
+
const install = await prompt.yesNo("llama-server is required to run local models. Install via Homebrew?", true);
|
|
548
|
+
if (!install) {
|
|
549
|
+
console.log(pc.red("offgrid-ai needs llama-server to run local models."));
|
|
550
|
+
console.log(pc.dim("Install it manually: brew install llama.cpp"));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
524
553
|
console.log(pc.cyan("Installing llama.cpp..."));
|
|
525
554
|
const { execFile } = await import("node:child_process");
|
|
526
555
|
const { promisify } = await import("node:util");
|
|
@@ -528,39 +557,181 @@ async function onboardFlow() {
|
|
|
528
557
|
await promisify(execFile)("brew", ["install", "llama.cpp"], { stdio: "inherit" });
|
|
529
558
|
llamaBinary = await findLlamaServer();
|
|
530
559
|
} catch (err) {
|
|
531
|
-
console.log(pc.red(`Failed: ${err.message}`));
|
|
560
|
+
console.log(pc.red(`Failed to install llama.cpp: ${err.message}`));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (!llamaBinary) {
|
|
564
|
+
console.log(pc.yellow("llama.cpp installed but llama-server not found. You may need to restart your terminal."));
|
|
532
565
|
return;
|
|
533
566
|
}
|
|
534
567
|
}
|
|
535
568
|
console.log(pc.green(`✓ llama-server: ${llamaBinary}`));
|
|
536
569
|
|
|
537
|
-
// 3.
|
|
570
|
+
// 3. Model backends — at least one is mandatory
|
|
538
571
|
const ggufModels = await scanGgufModels();
|
|
539
572
|
const managedModels = await scanManagedModels();
|
|
540
573
|
const totalManaged = managedModels.reduce((sum, m) => sum + m.models.length, 0);
|
|
574
|
+
const hasModels = ggufModels.length > 0 || totalManaged > 0;
|
|
541
575
|
|
|
542
|
-
if (
|
|
543
|
-
|
|
576
|
+
if (hasModels) {
|
|
577
|
+
// They already have models — show what was found
|
|
578
|
+
if (ggufModels.length > 0) {
|
|
579
|
+
console.log(pc.green(`✓ Found ${ggufModels.length} GGUF model${ggufModels.length === 1 ? "" : "s"}`));
|
|
580
|
+
}
|
|
581
|
+
for (const { backendId, models } of managedModels) {
|
|
582
|
+
if (models.length > 0) {
|
|
583
|
+
console.log(pc.green(`✓ ${BACKENDS[backendId].label}: ${models.length} model${models.length === 1 ? "" : "s"}`));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
// No models found — offer to install backends that come with models
|
|
588
|
+
console.log(pc.yellow("\nNo models found."));
|
|
589
|
+
console.log(pc.dim("You need at least one model backend to use offgrid-ai.\n"));
|
|
590
|
+
|
|
591
|
+
const backendChoice = await prompt.choice("Install a model backend?", [
|
|
592
|
+
{ value: "ollama", label: "Ollama", hint: "brew install ollama — models download on demand" },
|
|
593
|
+
{ value: "lmstudio", label: "LM Studio", hint: "brew install --cask lm-studio — visual model browser" },
|
|
594
|
+
{ value: "omlx", label: "oMLX", hint: "brew tap jundot/omlx && brew install omlx — Apple Silicon optimized" },
|
|
595
|
+
{ value: "skip", label: "Skip for now", hint: "I'll set up models myself" },
|
|
596
|
+
], "ollama");
|
|
597
|
+
|
|
598
|
+
const { execFile } = await import("node:child_process");
|
|
599
|
+
const { promisify } = await import("node:util");
|
|
600
|
+
|
|
601
|
+
if (backendChoice === "ollama") {
|
|
602
|
+
console.log(pc.cyan("Installing Ollama via Homebrew..."));
|
|
603
|
+
try {
|
|
604
|
+
await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
|
|
605
|
+
console.log(pc.green("✓ Ollama installed"));
|
|
606
|
+
console.log(pc.cyan("\nStarting Ollama..."));
|
|
607
|
+
try {
|
|
608
|
+
await promisify(execFile)("ollama", ["serve"], { stdio: "ignore", detached: true });
|
|
609
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
610
|
+
} catch { /* may already be running */ }
|
|
611
|
+
console.log(pc.green("Ollama is running."));
|
|
612
|
+
console.log(pc.yellow("\nPull your first model:"));
|
|
613
|
+
console.log(pc.bold(" ollama pull gemma3:4b"));
|
|
614
|
+
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
615
|
+
} catch (err) {
|
|
616
|
+
console.log(pc.red(`Failed to install Ollama: ${err.message}`));
|
|
617
|
+
console.log(pc.dim("Install it manually from https://ollama.com"));
|
|
618
|
+
}
|
|
619
|
+
} else if (backendChoice === "lmstudio") {
|
|
620
|
+
console.log(pc.cyan("Installing LM Studio via Homebrew..."));
|
|
621
|
+
try {
|
|
622
|
+
await promisify(execFile)("brew", ["install", "--cask", "lm-studio"], { stdio: "inherit" });
|
|
623
|
+
console.log(pc.green("✓ LM Studio installed"));
|
|
624
|
+
console.log(pc.yellow("\nOpen LM Studio to browse and download models, then run offgrid-ai again."));
|
|
625
|
+
} catch (err) {
|
|
626
|
+
console.log(pc.red(`Failed to install LM Studio: ${err.message}`));
|
|
627
|
+
console.log(pc.dim("Download it manually from https://lmstudio.ai"));
|
|
628
|
+
}
|
|
629
|
+
} else if (backendChoice === "omlx") {
|
|
630
|
+
console.log(pc.cyan("Installing oMLX via Homebrew..."));
|
|
631
|
+
try {
|
|
632
|
+
await promisify(execFile)("brew", ["tap", "jundot/omlx", "https://github.com/jundot/omlx"], { stdio: "inherit" });
|
|
633
|
+
await promisify(execFile)("brew", ["install", "omlx"], { stdio: "inherit" });
|
|
634
|
+
console.log(pc.green("✓ oMLX installed"));
|
|
635
|
+
console.log(pc.yellow("\nStart oMLX server:"));
|
|
636
|
+
console.log(pc.bold(" omlx start"));
|
|
637
|
+
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
638
|
+
} catch (err) {
|
|
639
|
+
console.log(pc.red(`Failed to install oMLX: ${err.message}`));
|
|
640
|
+
console.log(pc.dim("Install it manually: brew tap jundot/omlx && brew install omlx"));
|
|
641
|
+
}
|
|
642
|
+
} else {
|
|
643
|
+
console.log(pc.dim("Run offgrid-ai again when you've set up a model backend."));
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
544
646
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
647
|
+
|
|
648
|
+
console.log(pc.green("\n✓ Setup complete! Run offgrid-ai to pick and run a model."));
|
|
649
|
+
} finally {
|
|
650
|
+
prompt.close();
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ── Uninstall ───────────────────────────────────────────────────────────────
|
|
655
|
+
|
|
656
|
+
async function uninstallCommand(argv) {
|
|
657
|
+
if (!process.stdin.isTTY) {
|
|
658
|
+
// Non-interactive: remove everything
|
|
659
|
+
await removeDataDir();
|
|
660
|
+
removeSelf();
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
startInteractive("offgrid-ai uninstall");
|
|
665
|
+
const prompt = createPrompt();
|
|
666
|
+
try {
|
|
667
|
+
console.log(pc.bold("offgrid-ai uninstall\n"));
|
|
668
|
+
|
|
669
|
+
// Stop any running servers first
|
|
670
|
+
const running = await runningProfiles();
|
|
671
|
+
if (running.length > 0) {
|
|
672
|
+
console.log(pc.yellow(`${running.length} server(s) still running. Stopping...`));
|
|
673
|
+
for (const { profile } of running) {
|
|
674
|
+
await stopProfile(profile);
|
|
548
675
|
}
|
|
676
|
+
console.log(pc.green("All servers stopped."));
|
|
549
677
|
}
|
|
550
678
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
679
|
+
// Ask about data
|
|
680
|
+
const dataDir = DATA_DIR;
|
|
681
|
+
const keepData = await prompt.yesNo("Keep your profiles and model configurations? (Recommended if you plan to reinstall)", true);
|
|
682
|
+
|
|
683
|
+
if (!keepData) {
|
|
684
|
+
const confirmDelete = await prompt.yesNo(`Delete ${dataDir}? This removes all profiles and settings.`, false);
|
|
685
|
+
if (confirmDelete) {
|
|
686
|
+
await removeDataDir();
|
|
687
|
+
} else {
|
|
688
|
+
console.log(pc.dim("Keeping data directory."));
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
console.log(pc.dim(`Keeping ${dataDir} for when you reinstall.`));
|
|
556
692
|
}
|
|
557
693
|
|
|
558
|
-
|
|
694
|
+
// Remove the npm package
|
|
695
|
+
const confirmUninstall = await prompt.yesNo("Uninstall offgrid-ai npm package?", true);
|
|
696
|
+
if (confirmUninstall) {
|
|
697
|
+
removeSelf();
|
|
698
|
+
} else {
|
|
699
|
+
console.log(pc.dim("Cancelled."));
|
|
700
|
+
}
|
|
559
701
|
} finally {
|
|
560
702
|
prompt.close();
|
|
561
703
|
}
|
|
562
704
|
}
|
|
563
705
|
|
|
706
|
+
async function removeDataDir() {
|
|
707
|
+
const dataDir = DATA_DIR;
|
|
708
|
+
if (existsSync(dataDir)) {
|
|
709
|
+
try {
|
|
710
|
+
rmSync(dataDir, { recursive: true, force: true });
|
|
711
|
+
console.log(pc.green(`✓ Removed ${dataDir}`));
|
|
712
|
+
} catch (err) {
|
|
713
|
+
console.log(pc.red(`Failed to remove ${dataDir}: ${err.message}`));
|
|
714
|
+
console.log(pc.dim(`Remove it manually: rm -rf ${dataDir}`));
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
console.log(pc.dim(`${dataDir} doesn't exist — already clean.`));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function removeSelf() {
|
|
722
|
+
console.log(pc.cyan("\nUninstalling offgrid-ai..."));
|
|
723
|
+
const { execFile } = await import("node:child_process");
|
|
724
|
+
const { promisify } = await import("node:util");
|
|
725
|
+
try {
|
|
726
|
+
await promisify(execFile)("npm", ["uninstall", "-g", "offgrid-ai"], { stdio: "inherit" });
|
|
727
|
+
console.log(pc.green("\n✓ offgrid-ai has been uninstalled."));
|
|
728
|
+
console.log(pc.dim("Reinstall anytime with: npm install -g offgrid-ai"));
|
|
729
|
+
} catch {
|
|
730
|
+
console.log(pc.red("\nCould not auto-uninstall. Run this manually:"));
|
|
731
|
+
console.log(pc.bold(" npm uninstall -g offgrid-ai"));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
564
735
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
565
736
|
|
|
566
737
|
async function scanManagedModels() {
|
|
@@ -592,12 +763,13 @@ function printHelp() {
|
|
|
592
763
|
console.log(`${pc.bold("offgrid-ai")} — privacy-first local LLM runner
|
|
593
764
|
|
|
594
765
|
Usage:
|
|
595
|
-
offgrid-ai
|
|
596
|
-
offgrid-ai status
|
|
597
|
-
offgrid-ai stop
|
|
598
|
-
offgrid-ai
|
|
599
|
-
offgrid-ai
|
|
766
|
+
offgrid-ai Pick a model and run it
|
|
767
|
+
offgrid-ai status Show running local models
|
|
768
|
+
offgrid-ai stop Stop a running server (or: offgrid-ai stop <id>)
|
|
769
|
+
offgrid-ai uninstall Remove offgrid-ai (optionally keep profiles)
|
|
770
|
+
offgrid-ai help Show this help
|
|
771
|
+
offgrid-ai version Show version
|
|
600
772
|
|
|
601
773
|
First run? offgrid-ai walks you through installing everything you need.
|
|
602
774
|
After that, just run it — it finds your models, auto-configures, and launches Pi.`);
|
|
603
|
-
}
|
|
775
|
+
}
|