offgrid-ai 0.1.2 → 0.2.0
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 +194 -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,176 @@ 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: "Easiest way — models download on demand" },
|
|
593
|
+
{ value: "lmstudio", label: "LM Studio", hint: "Visual model browser (opens download page)" },
|
|
594
|
+
{ value: "omlx", label: "oMLX", hint: "Apple Silicon optimized" },
|
|
595
|
+
{ value: "skip", label: "Skip for now", hint: "I'll set up models myself" },
|
|
596
|
+
], "ollama");
|
|
597
|
+
|
|
598
|
+
if (backendChoice === "ollama") {
|
|
599
|
+
console.log(pc.cyan("Installing Ollama via Homebrew..."));
|
|
600
|
+
const { execFile } = await import("node:child_process");
|
|
601
|
+
const { promisify } = await import("node:util");
|
|
602
|
+
try {
|
|
603
|
+
await promisify(execFile)("brew", ["install", "ollama"], { stdio: "inherit" });
|
|
604
|
+
console.log(pc.green("✓ Ollama installed"));
|
|
605
|
+
console.log(pc.cyan("\nStarting Ollama..."));
|
|
606
|
+
try {
|
|
607
|
+
await promisify(execFile)("ollama", ["serve"], { stdio: "ignore", detached: true });
|
|
608
|
+
// Give it a moment to start
|
|
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("LM Studio needs to be installed manually."));
|
|
621
|
+
console.log(pc.bold("\n Download LM Studio: https://lmstudio.ai"));
|
|
622
|
+
console.log(pc.dim("Then browse and download models inside LM Studio, and run offgrid-ai again."));
|
|
623
|
+
} else if (backendChoice === "omlx") {
|
|
624
|
+
console.log(pc.cyan("Installing oMLX via pip..."));
|
|
625
|
+
const { execFile } = await import("node:child_process");
|
|
626
|
+
const { promisify } = await import("node:util");
|
|
627
|
+
try {
|
|
628
|
+
await promisify(execFile)("pip3", ["install", "omlx"], { stdio: "inherit" });
|
|
629
|
+
console.log(pc.green("✓ oMLX installed"));
|
|
630
|
+
console.log(pc.yellow("\nStart oMLX server:"));
|
|
631
|
+
console.log(pc.bold(" omlx serve"));
|
|
632
|
+
console.log(pc.dim("Then run offgrid-ai again to pick and run a model."));
|
|
633
|
+
} catch (err) {
|
|
634
|
+
console.log(pc.red(`Failed to install oMLX: ${err.message}`));
|
|
635
|
+
console.log(pc.dim("Install it manually: pip3 install omlx"));
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
console.log(pc.dim("Run offgrid-ai again when you've set up a model backend."));
|
|
639
|
+
}
|
|
640
|
+
return;
|
|
544
641
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
642
|
+
|
|
643
|
+
console.log(pc.green("\n✓ Setup complete! Run offgrid-ai to pick and run a model."));
|
|
644
|
+
} finally {
|
|
645
|
+
prompt.close();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ── Uninstall ───────────────────────────────────────────────────────────────
|
|
650
|
+
|
|
651
|
+
async function uninstallCommand(argv) {
|
|
652
|
+
if (!process.stdin.isTTY) {
|
|
653
|
+
// Non-interactive: remove everything
|
|
654
|
+
await removeDataDir();
|
|
655
|
+
removeSelf();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
startInteractive("offgrid-ai uninstall");
|
|
660
|
+
const prompt = createPrompt();
|
|
661
|
+
try {
|
|
662
|
+
console.log(pc.bold("offgrid-ai uninstall\n"));
|
|
663
|
+
|
|
664
|
+
// Stop any running servers first
|
|
665
|
+
const running = await runningProfiles();
|
|
666
|
+
if (running.length > 0) {
|
|
667
|
+
console.log(pc.yellow(`${running.length} server(s) still running. Stopping...`));
|
|
668
|
+
for (const { profile } of running) {
|
|
669
|
+
await stopProfile(profile);
|
|
548
670
|
}
|
|
671
|
+
console.log(pc.green("All servers stopped."));
|
|
549
672
|
}
|
|
550
673
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
674
|
+
// Ask about data
|
|
675
|
+
const dataDir = DATA_DIR;
|
|
676
|
+
const keepData = await prompt.yesNo("Keep your profiles and model configurations? (Recommended if you plan to reinstall)", true);
|
|
677
|
+
|
|
678
|
+
if (!keepData) {
|
|
679
|
+
const confirmDelete = await prompt.yesNo(`Delete ${dataDir}? This removes all profiles and settings.`, false);
|
|
680
|
+
if (confirmDelete) {
|
|
681
|
+
await removeDataDir();
|
|
682
|
+
} else {
|
|
683
|
+
console.log(pc.dim("Keeping data directory."));
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
console.log(pc.dim(`Keeping ${dataDir} for when you reinstall.`));
|
|
556
687
|
}
|
|
557
688
|
|
|
558
|
-
|
|
689
|
+
// Remove the npm package
|
|
690
|
+
const confirmUninstall = await prompt.yesNo("Uninstall offgrid-ai npm package?", true);
|
|
691
|
+
if (confirmUninstall) {
|
|
692
|
+
removeSelf();
|
|
693
|
+
} else {
|
|
694
|
+
console.log(pc.dim("Cancelled."));
|
|
695
|
+
}
|
|
559
696
|
} finally {
|
|
560
697
|
prompt.close();
|
|
561
698
|
}
|
|
562
699
|
}
|
|
563
700
|
|
|
701
|
+
async function removeDataDir() {
|
|
702
|
+
const dataDir = DATA_DIR;
|
|
703
|
+
if (existsSync(dataDir)) {
|
|
704
|
+
try {
|
|
705
|
+
rmSync(dataDir, { recursive: true, force: true });
|
|
706
|
+
console.log(pc.green(`✓ Removed ${dataDir}`));
|
|
707
|
+
} catch (err) {
|
|
708
|
+
console.log(pc.red(`Failed to remove ${dataDir}: ${err.message}`));
|
|
709
|
+
console.log(pc.dim(`Remove it manually: rm -rf ${dataDir}`));
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
console.log(pc.dim(`${dataDir} doesn't exist — already clean.`));
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function removeSelf() {
|
|
717
|
+
console.log(pc.cyan("\nUninstalling offgrid-ai..."));
|
|
718
|
+
const { execFile } = await import("node:child_process");
|
|
719
|
+
const { promisify } = await import("node:util");
|
|
720
|
+
try {
|
|
721
|
+
await promisify(execFile)("npm", ["uninstall", "-g", "offgrid-ai"], { stdio: "inherit" });
|
|
722
|
+
console.log(pc.green("\n✓ offgrid-ai has been uninstalled."));
|
|
723
|
+
console.log(pc.dim("Reinstall anytime with: npm install -g offgrid-ai"));
|
|
724
|
+
} catch {
|
|
725
|
+
console.log(pc.red("\nCould not auto-uninstall. Run this manually:"));
|
|
726
|
+
console.log(pc.bold(" npm uninstall -g offgrid-ai"));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
564
730
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
565
731
|
|
|
566
732
|
async function scanManagedModels() {
|
|
@@ -592,12 +758,13 @@ function printHelp() {
|
|
|
592
758
|
console.log(`${pc.bold("offgrid-ai")} — privacy-first local LLM runner
|
|
593
759
|
|
|
594
760
|
Usage:
|
|
595
|
-
offgrid-ai
|
|
596
|
-
offgrid-ai status
|
|
597
|
-
offgrid-ai stop
|
|
598
|
-
offgrid-ai
|
|
599
|
-
offgrid-ai
|
|
761
|
+
offgrid-ai Pick a model and run it
|
|
762
|
+
offgrid-ai status Show running local models
|
|
763
|
+
offgrid-ai stop Stop a running server (or: offgrid-ai stop <id>)
|
|
764
|
+
offgrid-ai uninstall Remove offgrid-ai (optionally keep profiles)
|
|
765
|
+
offgrid-ai help Show this help
|
|
766
|
+
offgrid-ai version Show version
|
|
600
767
|
|
|
601
768
|
First run? offgrid-ai walks you through installing everything you need.
|
|
602
769
|
After that, just run it — it finds your models, auto-configures, and launches Pi.`);
|
|
603
|
-
}
|
|
770
|
+
}
|