offgrid-ai 0.8.10 → 0.8.12
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/README.md +47 -57
- package/package.json +1 -1
- package/src/benchmark.mjs +46 -7
- package/src/commands/models.mjs +42 -5
- package/src/model-catalog.mjs +15 -1
- package/src/model-presenters.mjs +12 -5
- package/src/process.mjs +12 -1
package/README.md
CHANGED
|
@@ -2,105 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
# offgrid-ai
|
|
4
4
|
|
|
5
|
-
**Privacy-first CLI for running local
|
|
5
|
+
**Privacy-first CLI for running local AI models on your own machine.**
|
|
6
6
|
|
|
7
7
|
[](package.json)
|
|
8
8
|
[]()
|
|
9
9
|
|
|
10
|
-
Install •
|
|
11
|
-
|
|
10
|
+
Install • Pick a model • Start chatting
|
|
12
11
|
```bash
|
|
13
12
|
curl -fsSL https://raw.githubusercontent.com/eeshansrivastava89/offgrid-ai/main/install.sh | bash
|
|
14
13
|
```
|
|
15
14
|
|
|
16
15
|
</div>
|
|
17
16
|
|
|
18
|
-
## What
|
|
17
|
+
## What is offgrid-ai?
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
offgrid-ai is a command-line tool that lets you run AI models locally. Everything stays on your computer. No API keys, no remote servers, no data leaving your machine.
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
It works with:
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
23
|
+
- Models from **LM Studio**
|
|
24
|
+
- **Ollama** models
|
|
25
|
+
- **oMLX** models on Apple Silicon
|
|
26
|
+
- GGUF models from **Hugging Face** or other sources
|
|
29
27
|
|
|
30
|
-
##
|
|
28
|
+
## Quick start
|
|
31
29
|
|
|
32
|
-
###
|
|
30
|
+
### 1. Install
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
Open your terminal and run:
|
|
35
33
|
|
|
36
34
|
```bash
|
|
37
35
|
curl -fsSL https://raw.githubusercontent.com/eeshansrivastava89/offgrid-ai/main/install.sh | bash
|
|
38
36
|
```
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
This installs offgrid-ai and anything else it needs. Then open a new terminal window and run:
|
|
41
39
|
|
|
42
40
|
```bash
|
|
43
|
-
|
|
41
|
+
offgrid-ai
|
|
44
42
|
```
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
If you already have Node.js installed, you can also use:
|
|
47
45
|
|
|
48
46
|
```bash
|
|
49
47
|
npm install -g offgrid-ai@latest --prefer-online
|
|
50
48
|
```
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
### 2. Pick a model
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
The first time you run offgrid-ai, it looks for models already on your machine. If it does not find any, it tells you how to get one.
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
Supported ways to get models:
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
| Source | Example command |
|
|
57
|
+
|---|---|
|
|
58
|
+
| LM Studio | `lms get qwen/qwen3.5-9b` |
|
|
59
|
+
| Ollama | `ollama pull gemma3:4b` |
|
|
60
|
+
| oMLX | Use `omlx start` |
|
|
61
|
+
| Hugging Face | Download a GGUF file |
|
|
59
62
|
|
|
60
|
-
3.
|
|
63
|
+
### 3. Start chatting
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|---|---|---|
|
|
66
|
-
| **LM Studio** | Visual model browser + CLI (`lms`) | ✓ models in `~/.lmstudio/models/` |
|
|
67
|
-
| **llama.cpp** | Managed local runtime | ✓ GGUF models in `~/.lmstudio/models/` and Hugging Face cache |
|
|
68
|
-
| **llama.cpp MTP** | Managed local runtime (speculative decoding) | ✓ MTP detected from model metadata |
|
|
69
|
-
| **Ollama** | Managed server | ✓ via `localhost:11434` |
|
|
70
|
-
| **oMLX** | Managed server | ✓ via `127.0.0.1:8000` |
|
|
65
|
+
```bash
|
|
66
|
+
offgrid-ai
|
|
67
|
+
```
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
Pick a model from the list and press Enter. offgrid-ai configures the rest and opens the Pi coding agent.
|
|
73
70
|
|
|
74
|
-
|
|
71
|
+
## Everyday commands
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
```bash
|
|
74
|
+
offgrid-ai # start a model
|
|
75
|
+
offgrid-ai status # see what's running
|
|
76
|
+
offgrid-ai stop # stop the running model
|
|
77
|
+
offgrid-ai benchmark # run a benchmark
|
|
78
|
+
offgrid-ai uninstall # remove offgrid-ai
|
|
79
|
+
```
|
|
83
80
|
|
|
84
|
-
|
|
81
|
+
## What can I do with it?
|
|
85
82
|
|
|
86
|
-
|
|
83
|
+
- **Chat with local models** — no internet required after setup.
|
|
84
|
+
- **Run benchmarks** — compare how different models perform on creative or data-science tasks.
|
|
85
|
+
- **Keep data private** — everything happens on your machine.
|
|
87
86
|
|
|
88
|
-
|
|
89
|
-
~/.offgrid-ai/
|
|
90
|
-
config.json # auto-detected paths, editable for overrides
|
|
91
|
-
profiles/ # one per model, auto-created on first run
|
|
92
|
-
<id>/
|
|
93
|
-
profile.json # model metadata + auto-detected settings
|
|
94
|
-
command.json # llama-server flags (auto-generated, hand-editable)
|
|
95
|
-
notes.md # scratch notes
|
|
96
|
-
logs/
|
|
97
|
-
run/ # PID state files
|
|
98
|
-
runtime/ # managed llama.cpp binaries
|
|
99
|
-
```
|
|
87
|
+
## Need help?
|
|
100
88
|
|
|
101
|
-
|
|
89
|
+
Run any command with `--help`:
|
|
102
90
|
|
|
103
|
-
|
|
91
|
+
```bash
|
|
92
|
+
offgrid-ai --help
|
|
93
|
+
```
|
|
104
94
|
|
|
105
95
|
## Development
|
|
106
96
|
|
|
@@ -113,4 +103,4 @@ node bin/offgrid-ai.mjs
|
|
|
113
103
|
|
|
114
104
|
## License
|
|
115
105
|
|
|
116
|
-
Personal project by [Eeshan Srivastava](https://eeshans.com).
|
|
106
|
+
Personal project by [Eeshan Srivastava](https://eeshans.com).
|
package/package.json
CHANGED
package/src/benchmark.mjs
CHANGED
|
@@ -306,7 +306,7 @@ function renderStreamEvent(parsed, state, opts = {}) {
|
|
|
306
306
|
state.status.mode = "thinking";
|
|
307
307
|
state.status.toolName = null;
|
|
308
308
|
state.status.bytes = 0;
|
|
309
|
-
state.status.
|
|
309
|
+
state.status.text = "";
|
|
310
310
|
printFinalLine(BENCH_COLORS.info(`[turn ${state.turn}]`));
|
|
311
311
|
break;
|
|
312
312
|
}
|
|
@@ -357,7 +357,7 @@ function renderStreamEvent(parsed, state, opts = {}) {
|
|
|
357
357
|
state.status.mode = "exec";
|
|
358
358
|
state.status.toolName = parsed.toolName;
|
|
359
359
|
state.status.bytes = 0;
|
|
360
|
-
state.status.
|
|
360
|
+
state.status.text = "";
|
|
361
361
|
printFinalLine(BENCH_COLORS.tool(`[exec] ${parsed.toolName}`));
|
|
362
362
|
break;
|
|
363
363
|
case "tool_execution_update": {
|
|
@@ -398,7 +398,8 @@ function renderStreamEvent(parsed, state, opts = {}) {
|
|
|
398
398
|
function updateStatusFromDelta(state, delta) {
|
|
399
399
|
if (!delta) return;
|
|
400
400
|
state.status.bytes += Buffer.byteLength(delta, "utf8");
|
|
401
|
-
state.status.
|
|
401
|
+
state.status.text = (state.status.text || "") + delta;
|
|
402
|
+
state.status.tokens = estimatedTokensFromText(state.status.text);
|
|
402
403
|
const label = state.status.toolName ? ` · ${state.status.toolName}` : "";
|
|
403
404
|
const modeLabel = state.status.mode === "thinking" ? "thinking" : state.status.mode === "text" ? "text" : state.status.mode === "tool" ? "tool" : "exec";
|
|
404
405
|
const bytes = formatBytes(state.status.bytes);
|
|
@@ -462,7 +463,7 @@ export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
|
|
|
462
463
|
const stderrHandle = await openFileHandle(stderrPath, "w");
|
|
463
464
|
|
|
464
465
|
const verbose = Boolean(process.env.OFFGRID_BENCHMARK_VERBOSE);
|
|
465
|
-
const renderState = { turn: 0, status: { mode: "idle", toolName: null, bytes: 0, tokens: 0 } };
|
|
466
|
+
const renderState = { turn: 0, status: { mode: "idle", toolName: null, bytes: 0, text: "", tokens: 0 } };
|
|
466
467
|
|
|
467
468
|
function appendResponse(text) {
|
|
468
469
|
responseBuffer += text;
|
|
@@ -866,7 +867,21 @@ export async function finalizeBenchmarkRun(runDirectory, runResult, speedMetrics
|
|
|
866
867
|
|
|
867
868
|
const success = existsSync(requiredPath) && (await readFile(requiredPath, "utf8")).trim().length > 0;
|
|
868
869
|
const hasTurns = runResult.agentTurns > 0;
|
|
869
|
-
|
|
870
|
+
|
|
871
|
+
let failureReason = null;
|
|
872
|
+
if (runResult.error) {
|
|
873
|
+
failureReason = typeof runResult.error === "string" ? runResult.error : (runResult.error.message ?? "Unknown error");
|
|
874
|
+
} else if (!hasTurns) {
|
|
875
|
+
failureReason = "The model did not produce any response turns.";
|
|
876
|
+
} else if (!success) {
|
|
877
|
+
if (runResult.toolCalls === 0) {
|
|
878
|
+
failureReason = `The model finished without writing the required output file (${requiredFile}). It may have returned the response as chat text instead of using the write tool.`;
|
|
879
|
+
} else {
|
|
880
|
+
failureReason = `The required output file (${requiredFile}) was missing or empty after the run.`;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const failed = failureReason !== null;
|
|
870
885
|
|
|
871
886
|
metadata.status = failed ? "failed" : "completed";
|
|
872
887
|
metadata.updatedAt = timestamp;
|
|
@@ -898,7 +913,9 @@ export async function finalizeBenchmarkRun(runDirectory, runResult, speedMetrics
|
|
|
898
913
|
perTurn: runResult.perTurn,
|
|
899
914
|
};
|
|
900
915
|
|
|
901
|
-
if (
|
|
916
|
+
if (failureReason) {
|
|
917
|
+
metadata.error = { message: failureReason, ...(typeof runResult.error === "object" && runResult.error?.stack ? { stack: runResult.error.stack } : {}) };
|
|
918
|
+
} else if (runResult.error) {
|
|
902
919
|
metadata.error = typeof runResult.error === "string"
|
|
903
920
|
? { message: runResult.error }
|
|
904
921
|
: { message: runResult.error.message ?? "Unknown error", ...(runResult.error.stack ? { stack: runResult.error.stack } : {}) };
|
|
@@ -1048,8 +1065,30 @@ export function renderBenchmarkSummary(metadata) {
|
|
|
1048
1065
|
];
|
|
1049
1066
|
console.log(renderSection("Speed Metrics", renderRows(speedRows)));
|
|
1050
1067
|
} else if (error) {
|
|
1051
|
-
|
|
1068
|
+
const wrappedError = wrapText(error.message ?? "Unknown error");
|
|
1069
|
+
console.log(renderSection("Error", pc.red(wrappedError)));
|
|
1070
|
+
if (error.message?.includes("write tool") || error.message?.includes("required output file")) {
|
|
1071
|
+
const tip = wrapText("Tip: This usually means the model returned the answer as chat text instead of writing the file. Try a model with stronger tool-use support, or run the prompt manually.", 64);
|
|
1072
|
+
console.log(pc.dim("\n" + tip));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function wrapText(text, width = 64) {
|
|
1078
|
+
if (!text) return "";
|
|
1079
|
+
const words = text.split(/\s+/);
|
|
1080
|
+
const lines = [];
|
|
1081
|
+
let current = "";
|
|
1082
|
+
for (const word of words) {
|
|
1083
|
+
if ((current + " " + word).trim().length > width) {
|
|
1084
|
+
if (current) lines.push(current.trim());
|
|
1085
|
+
current = word;
|
|
1086
|
+
} else {
|
|
1087
|
+
current = current ? `${current} ${word}` : word;
|
|
1088
|
+
}
|
|
1052
1089
|
}
|
|
1090
|
+
if (current) lines.push(current.trim());
|
|
1091
|
+
return lines.join("\n");
|
|
1053
1092
|
}
|
|
1054
1093
|
|
|
1055
1094
|
function benchmarkModelSource(profile) {
|
package/src/commands/models.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ensureDirs } from "../config.mjs";
|
|
2
2
|
import { backendFor, BACKENDS } from "../backends.mjs";
|
|
3
3
|
import { createProfileFromModel, readProfile, saveProfile, deleteProfile, profileJsonPath } from "../profiles.mjs";
|
|
4
|
-
import { isProfileRunning, stopProfile } from "../process.mjs";
|
|
4
|
+
import { isProfileRunning, isProfileServerUp, stopProfile } from "../process.mjs";
|
|
5
5
|
import { syncPiConfig, removeFromPiConfig } from "../harness-pi.mjs";
|
|
6
6
|
import { configureLocalProfile } from "../profile-setup.mjs";
|
|
7
7
|
import { pc, startInteractive, createPrompt } from "../ui.mjs";
|
|
@@ -40,18 +40,55 @@ export async function modelCommandCenter(initialCatalog) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const runningProfilesNow = [];
|
|
43
|
+
const serverUpIds = new Set();
|
|
43
44
|
for (const profile of normalized.profiles) {
|
|
44
|
-
if (await isProfileRunning(profile))
|
|
45
|
+
if (await isProfileRunning(profile)) {
|
|
46
|
+
runningProfilesNow.push(profile);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (await isProfileServerUp(profile)) serverUpIds.add(profile.id);
|
|
45
50
|
}
|
|
46
|
-
printWorkspaceHeader(normalized, runningProfilesNow);
|
|
51
|
+
printWorkspaceHeader(normalized, runningProfilesNow, serverUpIds);
|
|
47
52
|
await printBenchmarkLine();
|
|
48
53
|
|
|
49
54
|
const nameWidth = modelNameWidth(allItems);
|
|
50
55
|
|
|
56
|
+
const statusFor = (item) => {
|
|
57
|
+
if (item.type === "profile") {
|
|
58
|
+
if (item.fileMissing) return "missing";
|
|
59
|
+
if (runningProfilesNow.some((profile) => profile.id === item.profile.id)) return "running";
|
|
60
|
+
if (serverUpIds.has(item.profile.id)) return "serverup";
|
|
61
|
+
return "ready";
|
|
62
|
+
}
|
|
63
|
+
return "setup";
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const groupOrder = [
|
|
67
|
+
{ key: "running", label: pc.green(" Running") },
|
|
68
|
+
{ key: "serverup", label: pc.yellow(" Server up · model not loaded") },
|
|
69
|
+
{ key: "ready", label: pc.blue(" Ready to chat") },
|
|
70
|
+
{ key: "setup", label: pc.yellow(" Need setup") },
|
|
71
|
+
{ key: "missing", label: pc.red(" File missing") },
|
|
72
|
+
];
|
|
73
|
+
const grouped = new Map(groupOrder.map((g) => [g.key, []]));
|
|
74
|
+
for (const item of allItems) grouped.get(statusFor(item)).push(item);
|
|
75
|
+
|
|
76
|
+
const sectionSentinel = "__section__";
|
|
77
|
+
const choices = [];
|
|
78
|
+
for (const group of groupOrder) {
|
|
79
|
+
const bucket = grouped.get(group.key);
|
|
80
|
+
if (!bucket || bucket.length === 0) continue;
|
|
81
|
+
choices.push({ value: `${sectionSentinel}:${group.key}`, label: `── ${group.label} (${bucket.length}) ──`, disabled: true });
|
|
82
|
+
for (const item of bucket) {
|
|
83
|
+
const opt = modelSelectOption(item, { runningProfilesNow, serverUpIds, nameWidth });
|
|
84
|
+
choices.push({ value: opt.value, label: opt.label });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
51
88
|
const prompt = createPrompt();
|
|
52
89
|
try {
|
|
53
|
-
const selected = await prompt.choice("Select a model",
|
|
54
|
-
if (!selected) return;
|
|
90
|
+
const selected = await prompt.choice("Select a model", choices);
|
|
91
|
+
if (!selected || selected.startsWith(`${sectionSentinel}:`)) return;
|
|
55
92
|
const item = allItems.find((candidate) => itemKey(candidate) === selected);
|
|
56
93
|
if (!item) return;
|
|
57
94
|
|
package/src/model-catalog.mjs
CHANGED
|
@@ -37,10 +37,24 @@ export function itemKey(item) {
|
|
|
37
37
|
return `managed:${item.backendId}:${item.model.id}`;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function profileRecency(item) {
|
|
41
|
+
const updated = item.profile?.updatedAt ?? item.profile?.createdAt;
|
|
42
|
+
const ts = updated ? Date.parse(updated) : NaN;
|
|
43
|
+
return Number.isFinite(ts) ? ts : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compareRecency(a, b) {
|
|
47
|
+
const diff = profileRecency(b) - profileRecency(a);
|
|
48
|
+
if (diff !== 0) return diff;
|
|
49
|
+
return String(a.label ?? "").localeCompare(String(b.label ?? ""));
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
export function buildCatalogItems(normalized) {
|
|
41
53
|
const { profiles, newModels, managedItems, drafters } = normalized;
|
|
54
|
+
const profileItems = profiles.map((profile) => ({ type: "profile", profile, label: profile.label, fileMissing: isProfileFileMissing(profile) }));
|
|
55
|
+
profileItems.sort(compareRecency);
|
|
42
56
|
return [
|
|
43
|
-
...
|
|
57
|
+
...profileItems,
|
|
44
58
|
...newModels.map((model) => ({ type: "new", model, label: model.label, drafter: matchDrafter(model.path, drafters) })),
|
|
45
59
|
...managedItems.map(({ model, backendId }) => ({ type: "managed", model, backendId, label: model.label })),
|
|
46
60
|
];
|
package/src/model-presenters.mjs
CHANGED
|
@@ -25,6 +25,7 @@ function optionPad(text, color, width) {
|
|
|
25
25
|
function optionStatusTag(kind) {
|
|
26
26
|
const statuses = {
|
|
27
27
|
running: ["RUNNING", pc.green],
|
|
28
|
+
serverup: ["SERVER UP", pc.yellow],
|
|
28
29
|
ready: ["READY", pc.blue],
|
|
29
30
|
missing: ["MISSING", pc.red],
|
|
30
31
|
setup: ["SETUP", pc.yellow],
|
|
@@ -79,14 +80,16 @@ function optionLabel({ status, source, name, ctx, size, nameWidth }) {
|
|
|
79
80
|
return [status, source, pc.bold(optionPad(name, null, nameWidth)), ctx, pc.dim(size)].join(OPTION_SEPARATOR);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
export function modelSelectOption(item, { runningProfilesNow, nameWidth }) {
|
|
83
|
+
export function modelSelectOption(item, { runningProfilesNow, serverUpIds, nameWidth }) {
|
|
83
84
|
if (item.type === "profile") {
|
|
84
85
|
const backend = backendFor(item.profile.backend);
|
|
85
86
|
const running = runningProfilesNow.some((profile) => profile.id === item.profile.id);
|
|
87
|
+
const serverUp = !running && !item.fileMissing && serverUpIds?.has(item.profile.id);
|
|
88
|
+
const status = item.fileMissing ? "missing" : running ? "running" : serverUp ? "serverup" : "ready";
|
|
86
89
|
return {
|
|
87
90
|
value: itemKey(item),
|
|
88
91
|
label: optionLabel({
|
|
89
|
-
status: optionStatusTag(
|
|
92
|
+
status: optionStatusTag(status),
|
|
90
93
|
source: optionSourceTag(item.profile.backend, backend.label),
|
|
91
94
|
name: item.profile.label,
|
|
92
95
|
nameWidth,
|
|
@@ -122,15 +125,19 @@ export function modelSelectOption(item, { runningProfilesNow, nameWidth }) {
|
|
|
122
125
|
};
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
export function printWorkspaceHeader(normalized, runningProfilesNow) {
|
|
128
|
+
export function printWorkspaceHeader(normalized, runningProfilesNow, serverUpIds = new Set()) {
|
|
126
129
|
const profiles = normalized.profiles;
|
|
127
|
-
const
|
|
130
|
+
const isRunning = (p) => runningProfilesNow.some((r) => r.id === p.id);
|
|
131
|
+
const isMissing = (p) => isProfileFileMissing(p);
|
|
132
|
+
const readyCount = profiles.filter((p) => !isMissing(p) && !isRunning(p) && !serverUpIds.has(p.id)).length;
|
|
128
133
|
const runningCount = runningProfilesNow.length;
|
|
129
|
-
const
|
|
134
|
+
const serverUpCount = profiles.filter((p) => !isMissing(p) && serverUpIds.has(p.id) && !isRunning(p)).length;
|
|
135
|
+
const missingCount = profiles.filter(isMissing).length;
|
|
130
136
|
const setupCount = normalized.newModels.length + normalized.managedItems.length;
|
|
131
137
|
|
|
132
138
|
const countParts = [];
|
|
133
139
|
if (runningCount > 0) countParts.push(pc.green(`${runningCount} running`));
|
|
140
|
+
if (serverUpCount > 0) countParts.push(pc.yellow(`${serverUpCount} server up, model not loaded`));
|
|
134
141
|
if (readyCount > 0) countParts.push(pc.blue(`${readyCount} model${readyCount === 1 ? "" : "s"} ready`));
|
|
135
142
|
if (missingCount > 0) countParts.push(pc.red(`${missingCount} model${missingCount === 1 ? "" : "s"} missing`));
|
|
136
143
|
if (setupCount > 0) countParts.push(pc.yellow(`${setupCount} model${setupCount === 1 ? "" : "s"} need${setupCount === 1 ? "s" : ""} setup`));
|
package/src/process.mjs
CHANGED
|
@@ -113,11 +113,22 @@ export async function stopProfile(profile) {
|
|
|
113
113
|
|
|
114
114
|
export async function isProfileRunning(profile) {
|
|
115
115
|
const backend = backendFor(profile.backend);
|
|
116
|
-
if (backend.type === "managed-server")
|
|
116
|
+
if (backend.type === "managed-server") {
|
|
117
|
+
return await serverReady(profile.baseUrl) && (await modelLoadedOnServer(profile));
|
|
118
|
+
}
|
|
117
119
|
const state = await readState(profile.id);
|
|
118
120
|
return Boolean(state?.pid && pidAlive(state.pid));
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
export async function isProfileServerUp(profile) {
|
|
124
|
+
return await serverReady(profile.baseUrl);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function modelLoadedOnServer(profile) {
|
|
128
|
+
const { matches } = await serverMatchesProfile(profile);
|
|
129
|
+
return matches;
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
export async function profileRuntimeStatus(profile) {
|
|
122
133
|
const backend = backendFor(profile.backend);
|
|
123
134
|
if (backend.type === "managed-server") {
|