offgrid-ai 0.8.13 → 0.8.15
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 +3 -1
- package/package.json +1 -2
- package/src/benchmark.mjs +171 -63
- package/src/updates.mjs +3 -28
- package/src/postinstall.mjs +0 -106
package/README.md
CHANGED
|
@@ -41,12 +41,14 @@ This installs offgrid-ai and anything else it needs. Then open a new terminal wi
|
|
|
41
41
|
offgrid-ai
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
If you already have Node.js installed, you can also
|
|
44
|
+
If you already have Node.js installed, you can also install with npm:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
npm install -g offgrid-ai@latest --prefer-online
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
The curl installer is recommended for first-time setup because it also verifies the global npm bin directory is on your PATH. The npm package itself does not run install scripts or mutate shell config during `npm install`.
|
|
51
|
+
|
|
50
52
|
### 2. Pick a model
|
|
51
53
|
|
|
52
54
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "offgrid-ai",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.15",
|
|
4
4
|
"description": "Privacy-first CLI for running local LLMs — discover, configure, run, benchmark",
|
|
5
5
|
"author": "Eeshan Srivastava (https://eeshans.com)",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
"release:check": "bash scripts/release-check.sh",
|
|
37
37
|
"release:check:fast": "bash scripts/release-check.sh --skip-install --skip-manual",
|
|
38
38
|
"prepack": "npm run check:privacy",
|
|
39
|
-
"postinstall": "node src/postinstall.mjs",
|
|
40
39
|
"pretest": "npm run lint"
|
|
41
40
|
},
|
|
42
41
|
"dependencies": {
|
package/src/benchmark.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile, readFile, readdir } from "node:fs/promises";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { spawn, execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
@@ -250,6 +250,8 @@ const BENCH_COLORS = {
|
|
|
250
250
|
thinking: pc.magenta,
|
|
251
251
|
text: pc.green,
|
|
252
252
|
tool: pc.yellow,
|
|
253
|
+
success: pc.green,
|
|
254
|
+
warning: pc.yellow,
|
|
253
255
|
toolOutput: pc.dim,
|
|
254
256
|
error: pc.red,
|
|
255
257
|
info: pc.cyan,
|
|
@@ -268,9 +270,9 @@ function formatTokens(n) {
|
|
|
268
270
|
return String(Math.round(n));
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
function
|
|
272
|
-
// Simple heuristic: ~4
|
|
273
|
-
return Math.max(1, Math.ceil(
|
|
273
|
+
function estimatedTokensFromBytes(bytes) {
|
|
274
|
+
// Simple heuristic: ~4 bytes per token for code/English.
|
|
275
|
+
return Math.max(1, Math.ceil(bytes / 4));
|
|
274
276
|
}
|
|
275
277
|
|
|
276
278
|
function clearStatusLine() {
|
|
@@ -296,24 +298,24 @@ function renderStreamEvent(parsed, state, opts = {}) {
|
|
|
296
298
|
|
|
297
299
|
switch (type) {
|
|
298
300
|
case "session":
|
|
299
|
-
|
|
301
|
+
printFinalLine(BENCH_COLORS.info("Pi benchmark started"));
|
|
302
|
+
if (parsed.id) printFinalLine(BENCH_COLORS.dim(` Session ${parsed.id}`));
|
|
300
303
|
break;
|
|
301
304
|
case "agent_start":
|
|
302
|
-
console.log(BENCH_COLORS.dim("[agent_start]"));
|
|
303
305
|
break;
|
|
304
306
|
case "turn_start": {
|
|
305
307
|
state.turn += 1;
|
|
306
|
-
state.
|
|
307
|
-
state
|
|
308
|
-
|
|
309
|
-
state.
|
|
310
|
-
printFinalLine(BENCH_COLORS.info(`[turn ${state.turn}]`));
|
|
308
|
+
state.turnHadToolError = false;
|
|
309
|
+
resetStatus(state, "thinking");
|
|
310
|
+
printFinalLine("");
|
|
311
|
+
printFinalLine(BENCH_COLORS.info(`Turn ${state.turn}`));
|
|
311
312
|
break;
|
|
312
313
|
}
|
|
313
314
|
case "message_start": {
|
|
314
315
|
const msg = parsed.message;
|
|
315
|
-
if (msg?.role === "assistant" && msg.provider && msg.model) {
|
|
316
|
-
|
|
316
|
+
if (!state.modelPrinted && msg?.role === "assistant" && msg.provider && msg.model) {
|
|
317
|
+
state.modelPrinted = true;
|
|
318
|
+
printFinalLine(BENCH_COLORS.dim(` Model ${msg.provider}/${msg.model}`));
|
|
317
319
|
}
|
|
318
320
|
break;
|
|
319
321
|
}
|
|
@@ -321,90 +323,189 @@ function renderStreamEvent(parsed, state, opts = {}) {
|
|
|
321
323
|
const evt = parsed.assistantMessageEvent;
|
|
322
324
|
if (!evt) return;
|
|
323
325
|
const subtype = String(evt.type ?? "").replace(/_/gu, "");
|
|
324
|
-
if (subtype === "thinkingstart"
|
|
326
|
+
if (subtype === "thinkingstart") {
|
|
327
|
+
resetStatus(state, "thinking");
|
|
328
|
+
} else if (subtype === "thinkingdelta") {
|
|
325
329
|
if (verbose) process.stdout.write(BENCH_COLORS.thinking(evt.delta || ""));
|
|
326
|
-
state.
|
|
327
|
-
|
|
328
|
-
|
|
330
|
+
updateStatusFromDelta(state, evt.delta, "thinking");
|
|
331
|
+
} else if (subtype === "textstart") {
|
|
332
|
+
resetStatus(state, "text");
|
|
333
|
+
} else if (subtype === "textdelta") {
|
|
329
334
|
if (verbose) process.stdout.write(BENCH_COLORS.text(evt.delta || ""));
|
|
330
|
-
state.
|
|
331
|
-
updateStatusFromDelta(state, evt.delta);
|
|
335
|
+
updateStatusFromDelta(state, evt.delta, "text");
|
|
332
336
|
} else if (subtype === "toolcallstart") {
|
|
333
|
-
|
|
337
|
+
resetStatus(state, "tool");
|
|
334
338
|
} else if (subtype === "toolcalldelta") {
|
|
335
339
|
if (verbose) process.stdout.write(BENCH_COLORS.tool(evt.delta || ""));
|
|
336
|
-
state.
|
|
337
|
-
updateStatusFromDelta(state, evt.delta);
|
|
338
|
-
} else if (subtype === "toolcallend") {
|
|
339
|
-
if (!verbose) printFinalLine(BENCH_COLORS.tool("[tool_call_end]"));
|
|
340
|
+
updateStatusFromDelta(state, evt.delta, "tool");
|
|
340
341
|
}
|
|
341
342
|
break;
|
|
342
343
|
}
|
|
343
|
-
case "message_end":
|
|
344
|
-
const msg = parsed.message;
|
|
345
|
-
if (msg?.role === "assistant" && Array.isArray(msg.content)) {
|
|
346
|
-
for (const item of msg.content) {
|
|
347
|
-
if (item.type === "toolCall") {
|
|
348
|
-
const toolLine = formatToolCall(item);
|
|
349
|
-
state.status.toolName = item.name;
|
|
350
|
-
if (!verbose) printFinalLine(BENCH_COLORS.tool(toolLine));
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
344
|
+
case "message_end":
|
|
354
345
|
break;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
346
|
+
case "tool_execution_start": {
|
|
347
|
+
state.activeTool = {
|
|
348
|
+
name: parsed.toolName,
|
|
349
|
+
args: parsed.args ?? {},
|
|
350
|
+
outputText: "",
|
|
351
|
+
};
|
|
352
|
+
resetStatus(state, "exec", parsed.toolName);
|
|
353
|
+
printFinalLine(BENCH_COLORS.tool(formatToolStart(parsed.toolName, parsed.args ?? {}, state)));
|
|
362
354
|
break;
|
|
355
|
+
}
|
|
363
356
|
case "tool_execution_update": {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
357
|
+
const text = toolResultText(parsed.partialResult ?? parsed.result ?? parsed);
|
|
358
|
+
if (text) {
|
|
359
|
+
if (verbose) process.stdout.write(BENCH_COLORS.toolOutput(text));
|
|
360
|
+
if (state.activeTool) state.activeTool.outputText = text;
|
|
361
|
+
updateStatusFromDelta(state, text, "exec");
|
|
368
362
|
}
|
|
369
363
|
break;
|
|
370
364
|
}
|
|
371
|
-
case "tool_execution_end":
|
|
372
|
-
|
|
365
|
+
case "tool_execution_end": {
|
|
366
|
+
const lines = formatToolEnd(parsed, state);
|
|
367
|
+
if (parsed.isError) state.turnHadToolError = true;
|
|
368
|
+
for (const line of lines) printFinalLine(line);
|
|
369
|
+
state.activeTool = null;
|
|
370
|
+
resetStatus(state, "idle");
|
|
373
371
|
break;
|
|
372
|
+
}
|
|
374
373
|
case "toolResult": {
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
if (parsed.isError) state.turnHadToolError = true;
|
|
375
|
+
const status = parsed.isError ? BENCH_COLORS.error("✗") : BENCH_COLORS.success("✓");
|
|
376
|
+
printFinalLine(`${status} ${parsed.toolName ?? "tool"}`);
|
|
377
377
|
break;
|
|
378
378
|
}
|
|
379
379
|
case "turn_end": {
|
|
380
380
|
const usage = parsed.message?.usage;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
printFinalLine(BENCH_COLORS.info(`[turn ${state.turn}] completed`));
|
|
386
|
-
}
|
|
381
|
+
const tokenPart = usage ? ` · ${formatTokens(usage.output ?? usage.totalTokens ?? 0)} tokens` : "";
|
|
382
|
+
const marker = state.turnHadToolError ? BENCH_COLORS.warning("⚠") : BENCH_COLORS.success("✓");
|
|
383
|
+
const suffix = state.turnHadToolError ? " · tool issue" : "";
|
|
384
|
+
printFinalLine(`${marker} turn ${state.turn}${tokenPart}${suffix}`);
|
|
387
385
|
break;
|
|
388
386
|
}
|
|
389
387
|
case "agent_end":
|
|
390
388
|
clearStatusLine();
|
|
391
|
-
|
|
389
|
+
printFinalLine(BENCH_COLORS.info("Pi benchmark finished"));
|
|
392
390
|
break;
|
|
393
391
|
default:
|
|
394
392
|
break;
|
|
395
393
|
}
|
|
396
394
|
}
|
|
397
395
|
|
|
398
|
-
function
|
|
396
|
+
function resetStatus(state, mode, toolName = null) {
|
|
397
|
+
state.status.mode = mode;
|
|
398
|
+
state.status.toolName = toolName;
|
|
399
|
+
state.status.bytes = 0;
|
|
400
|
+
state.status.tokens = 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function updateStatusFromDelta(state, delta, mode = state.status.mode) {
|
|
399
404
|
if (!delta) return;
|
|
405
|
+
state.status.mode = mode;
|
|
400
406
|
state.status.bytes += Buffer.byteLength(delta, "utf8");
|
|
401
|
-
state.status.
|
|
402
|
-
state.status.tokens = estimatedTokensFromText(state.status.text);
|
|
407
|
+
state.status.tokens = estimatedTokensFromBytes(state.status.bytes);
|
|
403
408
|
const label = state.status.toolName ? ` · ${state.status.toolName}` : "";
|
|
404
|
-
const modeLabel =
|
|
409
|
+
const modeLabel = {
|
|
410
|
+
thinking: "thinking…",
|
|
411
|
+
text: "drafting response…",
|
|
412
|
+
tool: "preparing tool…",
|
|
413
|
+
exec: "running tool…",
|
|
414
|
+
}[state.status.mode] ?? "working…";
|
|
405
415
|
const bytes = formatBytes(state.status.bytes);
|
|
406
416
|
const tokens = formatTokens(state.status.tokens);
|
|
407
|
-
printStatusLine(BENCH_COLORS.dim(`
|
|
417
|
+
printStatusLine(BENCH_COLORS.dim(`Turn ${state.turn} ${modeLabel}${label} · ${bytes} (~${tokens} tokens)`));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function formatToolStart(toolName, args, state) {
|
|
421
|
+
if (toolName === "read") return `→ read ${displayPath(args.path, state)}`;
|
|
422
|
+
if (toolName === "write") {
|
|
423
|
+
const size = args.content ? ` · ${formatBytes(Buffer.byteLength(String(args.content), "utf8"))}` : "";
|
|
424
|
+
return `→ write ${displayPath(args.path, state)}${size}`;
|
|
425
|
+
}
|
|
426
|
+
if (toolName === "edit") {
|
|
427
|
+
const count = Array.isArray(args.edits) ? args.edits.length : 0;
|
|
428
|
+
const suffix = count > 0 ? ` · ${count} replacement${count === 1 ? "" : "s"}` : "";
|
|
429
|
+
return `→ edit ${displayPath(args.path, state)}${suffix}`;
|
|
430
|
+
}
|
|
431
|
+
if (toolName === "bash") return `→ run ${truncateOneLine(args.command ?? "")}`;
|
|
432
|
+
return `→ ${toolName}${compactArgs(args)}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function formatToolEnd(parsed, state) {
|
|
436
|
+
const toolName = parsed.toolName ?? state.activeTool?.name ?? "tool";
|
|
437
|
+
const args = parsed.args ?? state.activeTool?.args ?? {};
|
|
438
|
+
const text = toolResultText(parsed.result) || state.activeTool?.outputText || "";
|
|
439
|
+
const marker = parsed.isError ? BENCH_COLORS.error("✗") : BENCH_COLORS.success("✓");
|
|
440
|
+
|
|
441
|
+
if (parsed.isError) {
|
|
442
|
+
return [`${marker} ${toolName} failed · ${firstUsefulLine(text)}`];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (toolName === "write") return [`${marker} wrote ${displayPath(args.path, state)}${parsedWriteSize(text)}`];
|
|
446
|
+
if (toolName === "read") return [`${marker} read ${displayPath(args.path, state)}${text ? ` · ${formatBytes(Buffer.byteLength(text, "utf8"))}` : ""}`];
|
|
447
|
+
if (toolName === "edit") return [`${marker} edited ${displayPath(args.path, state)}`];
|
|
448
|
+
if (toolName === "bash") return formatBashResult(marker, text);
|
|
449
|
+
|
|
450
|
+
const summary = firstUsefulLine(text);
|
|
451
|
+
return [`${marker} ${toolName}${summary ? ` · ${summary}` : ""}`];
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function formatBashResult(marker, text) {
|
|
455
|
+
const lines = meaningfulLines(text).slice(0, 2);
|
|
456
|
+
if (lines.length === 0) return [`${marker} command completed`];
|
|
457
|
+
return [`${marker} ${lines[0]}`, ...lines.slice(1).map((line) => BENCH_COLORS.dim(` ${line}`))];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function parsedWriteSize(text) {
|
|
461
|
+
const match = String(text).match(/Successfully wrote\s+([0-9,]+)\s+bytes/iu);
|
|
462
|
+
if (!match) return "";
|
|
463
|
+
const bytes = Number(match[1].replace(/,/gu, ""));
|
|
464
|
+
return Number.isFinite(bytes) ? ` · ${formatBytes(bytes)}` : "";
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function toolResultText(result) {
|
|
468
|
+
const content = result?.content;
|
|
469
|
+
if (typeof content === "string") return content;
|
|
470
|
+
if (!Array.isArray(content)) return "";
|
|
471
|
+
return content
|
|
472
|
+
.map((item) => typeof item?.text === "string" ? item.text : "")
|
|
473
|
+
.filter(Boolean)
|
|
474
|
+
.join("\n");
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function firstUsefulLine(text) {
|
|
478
|
+
return meaningfulLines(text)[0] ?? "no details";
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function meaningfulLines(text) {
|
|
482
|
+
const lines = String(text ?? "")
|
|
483
|
+
.split(/\r?\n/u)
|
|
484
|
+
.map((line) => line.trim())
|
|
485
|
+
.filter(Boolean)
|
|
486
|
+
.filter((line) => !/^\^+$/u.test(line));
|
|
487
|
+
const errorLine = lines.find((line) => /(?:error|exception|failed|not found|command exited with code|validation failed)/iu.test(line));
|
|
488
|
+
if (errorLine) return [errorLine, ...lines.filter((line) => line !== errorLine)];
|
|
489
|
+
return lines;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function displayPath(value, state) {
|
|
493
|
+
if (!value) return "unknown";
|
|
494
|
+
const path = String(value);
|
|
495
|
+
const rel = state.cwd ? relative(state.cwd, path) : path;
|
|
496
|
+
if (rel && !rel.startsWith("..") && rel !== ".") return rel;
|
|
497
|
+
return basename(path) || path;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function compactArgs(args) {
|
|
501
|
+
const entries = Object.entries(args ?? {}).filter(([, value]) => value !== undefined && value !== null && value !== "");
|
|
502
|
+
if (entries.length === 0) return "";
|
|
503
|
+
return ` · ${truncateOneLine(entries.map(([key, value]) => `${key}=${String(value)}`).join(" "))}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function truncateOneLine(value, max = Math.max(60, Math.min(process.stdout.columns ?? 100, 140) - 12)) {
|
|
507
|
+
const text = String(value ?? "").replace(/\s+/gu, " ").trim();
|
|
508
|
+
return text.length > max ? `${text.slice(0, Math.max(1, max - 1))}…` : text;
|
|
408
509
|
}
|
|
409
510
|
|
|
410
511
|
function formatBytes(bytes) {
|
|
@@ -463,7 +564,14 @@ export async function runBenchmarkInPi(profile, runDirectory, { signal } = {}) {
|
|
|
463
564
|
const stderrHandle = await openFileHandle(stderrPath, "w");
|
|
464
565
|
|
|
465
566
|
const verbose = Boolean(process.env.OFFGRID_BENCHMARK_VERBOSE);
|
|
466
|
-
const renderState = {
|
|
567
|
+
const renderState = {
|
|
568
|
+
cwd: runDirectory,
|
|
569
|
+
turn: 0,
|
|
570
|
+
turnHadToolError: false,
|
|
571
|
+
modelPrinted: false,
|
|
572
|
+
activeTool: null,
|
|
573
|
+
status: { mode: "idle", toolName: null, bytes: 0, tokens: 0 },
|
|
574
|
+
};
|
|
467
575
|
|
|
468
576
|
function appendResponse(text) {
|
|
469
577
|
responseBuffer += text;
|
package/src/updates.mjs
CHANGED
|
@@ -1,27 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
2
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
3
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { DATA_DIR } from "./config.mjs";
|
|
7
4
|
|
|
8
5
|
const PACKAGE_NAME = "offgrid-ai";
|
|
9
|
-
const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
|
10
6
|
|
|
11
|
-
export async function checkForUpdate({
|
|
7
|
+
export async function checkForUpdate({ fetchImpl = globalThis.fetch } = {}) {
|
|
12
8
|
if (process.env.OFFGRID_NO_UPDATE_CHECK) return null;
|
|
13
9
|
|
|
14
10
|
const currentVersion = currentPackageVersion();
|
|
15
|
-
const cacheFile = join(DATA_DIR, "update-cache.json");
|
|
16
|
-
const cached = await readUpdateCache(cacheFile);
|
|
17
|
-
|
|
18
|
-
// Use cache if fresh (within 24h) and still applies to current version
|
|
19
|
-
if (!force && cached?.lastChecked && now - cached.lastChecked < UPDATE_CHECK_INTERVAL) {
|
|
20
|
-
if (cached.currentVersion === currentVersion) {
|
|
21
|
-
return updateResult(currentVersion, cached.latestVersion);
|
|
22
|
-
}
|
|
23
|
-
// Cache is from a different installed version — invalidate and refetch
|
|
24
|
-
}
|
|
25
11
|
|
|
26
12
|
try {
|
|
27
13
|
const response = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
@@ -32,9 +18,6 @@ export async function checkForUpdate({ now = Date.now(), fetchImpl = globalThis.
|
|
|
32
18
|
const latestVersion = typeof body?.version === "string" ? body.version : null;
|
|
33
19
|
if (!latestVersion) return null;
|
|
34
20
|
|
|
35
|
-
await mkdir(DATA_DIR, { recursive: true });
|
|
36
|
-
await writeFile(cacheFile, JSON.stringify({ lastChecked: now, currentVersion, latestVersion }, null, 2) + "\n", "utf8");
|
|
37
|
-
|
|
38
21
|
return updateResult(currentVersion, latestVersion);
|
|
39
22
|
} catch {
|
|
40
23
|
return null;
|
|
@@ -42,8 +25,8 @@ export async function checkForUpdate({ now = Date.now(), fetchImpl = globalThis.
|
|
|
42
25
|
}
|
|
43
26
|
|
|
44
27
|
export function currentPackageVersion() {
|
|
45
|
-
const
|
|
46
|
-
const pkg = JSON.parse(readFileSync(
|
|
28
|
+
const pkgPath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
29
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
47
30
|
return pkg.version;
|
|
48
31
|
}
|
|
49
32
|
|
|
@@ -97,14 +80,6 @@ export function runUpdateCommand(plan) {
|
|
|
97
80
|
});
|
|
98
81
|
}
|
|
99
82
|
|
|
100
|
-
async function readUpdateCache(cacheFile) {
|
|
101
|
-
try {
|
|
102
|
-
return JSON.parse(await readFile(cacheFile, "utf8"));
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
83
|
function updateResult(currentVersion, latestVersion) {
|
|
109
84
|
return isNewerVersion(latestVersion, currentVersion)
|
|
110
85
|
? { current: currentVersion, latest: latestVersion }
|
package/src/postinstall.mjs
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, appendFileSync } from "node:fs";
|
|
3
|
-
import { mkdirSync } from "node:fs";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
if (process.env.CI || process.env.OFFGRID_SKIP_POSTINSTALL) process.exit(0);
|
|
8
|
-
|
|
9
|
-
// npm v9+ sets npm_config_global="true" for global installs. Some contexts (Hermes,
|
|
10
|
-
// older npm, or wrapped installs) may not set it, so also check the prefix.
|
|
11
|
-
const isGlobalInstall = process.env.npm_config_global === "true" || isLikelyGlobalPrefix(process.env.npm_config_prefix, process.env.HOME);
|
|
12
|
-
if (!isGlobalInstall) process.exit(0);
|
|
13
|
-
|
|
14
|
-
const prefix = process.env.npm_config_prefix;
|
|
15
|
-
if (!prefix) {
|
|
16
|
-
console.log("offgrid-ai postinstall: npm global prefix not detected; skipping PATH update.");
|
|
17
|
-
process.exit(0);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const npmBin = join(prefix, "bin");
|
|
21
|
-
const marker = "# Added by offgrid-ai installer";
|
|
22
|
-
const pathLine = `export PATH="${npmBin}:$PATH"`;
|
|
23
|
-
|
|
24
|
-
const home = process.env.HOME;
|
|
25
|
-
if (!home) {
|
|
26
|
-
console.log("offgrid-ai postinstall: HOME not set; cannot update shell PATH.");
|
|
27
|
-
process.exit(0);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Detect target shell config file. Prefer the file matching the user's shell.
|
|
31
|
-
const shell = process.env.SHELL ?? "";
|
|
32
|
-
const isZsh = /\bzsh$/u.test(shell);
|
|
33
|
-
const isBash = /\bbash$/u.test(shell);
|
|
34
|
-
|
|
35
|
-
let rcCandidates;
|
|
36
|
-
if (isZsh) {
|
|
37
|
-
rcCandidates = [".zshrc", ".zprofile", ".profile"];
|
|
38
|
-
} else if (isBash) {
|
|
39
|
-
rcCandidates = [".bashrc", ".bash_profile", ".profile"];
|
|
40
|
-
} else {
|
|
41
|
-
// Fallback: try common files, prefer existing ones
|
|
42
|
-
rcCandidates = [".bashrc", ".bash_profile", ".zshrc", ".zprofile", ".profile"];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const rcPaths = rcCandidates.map((name) => join(home, name));
|
|
46
|
-
let rcFile = rcPaths.find((file) => existsSync(file));
|
|
47
|
-
if (!rcFile) {
|
|
48
|
-
// No existing rc file: create the one matching the user's shell.
|
|
49
|
-
const defaultName = isZsh ? ".zshrc" : isBash ? ".bashrc" : ".bashrc";
|
|
50
|
-
rcFile = join(home, defaultName);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let content = "";
|
|
54
|
-
try {
|
|
55
|
-
content = existsSync(rcFile) ? readFileSync(rcFile, "utf8") : "";
|
|
56
|
-
} catch (err) {
|
|
57
|
-
console.log(`offgrid-ai postinstall: could not read ${rcFile}: ${err.message}`);
|
|
58
|
-
process.exit(0);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!content.includes(npmBin)) {
|
|
62
|
-
try {
|
|
63
|
-
mkdirSync(dirname(rcFile), { recursive: true });
|
|
64
|
-
appendFileSync(rcFile, `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}\n${marker}\n${pathLine}\n`, "utf8");
|
|
65
|
-
const version = currentPackageVersion();
|
|
66
|
-
console.log("");
|
|
67
|
-
console.log(`offgrid-ai v${version} installed and added to PATH`);
|
|
68
|
-
console.log(` Config file: ${rcFile}`);
|
|
69
|
-
console.log(` Bin path: ${npmBin}`);
|
|
70
|
-
console.log("");
|
|
71
|
-
console.log("To use it right now in this terminal, run:");
|
|
72
|
-
console.log(` source ${rcFile}`);
|
|
73
|
-
console.log("");
|
|
74
|
-
console.log("Or open a new terminal window/tab.");
|
|
75
|
-
} catch (err) {
|
|
76
|
-
console.log(`offgrid-ai postinstall: could not write to ${rcFile}: ${err.message}`);
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
console.log(`offgrid-ai is installed in ${npmBin}`);
|
|
80
|
-
console.log(`Open a new terminal if the command is not found yet.`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (process.getuid?.() === 0) {
|
|
84
|
-
console.log("Warning: offgrid-ai was installed as root. PATH was added to root's shell config, not the regular user's.");
|
|
85
|
-
console.log("If you installed with sudo, run the installer as your normal user instead, or manually add the line above to your user's shell config.");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function currentPackageVersion() {
|
|
89
|
-
try {
|
|
90
|
-
const pkg = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"));
|
|
91
|
-
return pkg.version;
|
|
92
|
-
} catch {
|
|
93
|
-
return "";
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function isLikelyGlobalPrefix(prefix, home) {
|
|
98
|
-
if (!prefix || !home) return false;
|
|
99
|
-
const normalized = prefix.replace(/\\/gu, "/");
|
|
100
|
-
const homeNormalized = home.replace(/\\/gu, "/");
|
|
101
|
-
// Common global prefixes: /usr/local, /opt/homebrew, nvm paths, ~/.npm-global, ~/.local
|
|
102
|
-
return normalized.startsWith("/usr/") ||
|
|
103
|
-
normalized.startsWith("/opt/") ||
|
|
104
|
-
normalized.startsWith(homeNormalized) ||
|
|
105
|
-
/\/versions\/node\/v?\d/u.test(normalized);
|
|
106
|
-
}
|