offgrid-ai 0.3.9 → 0.3.11
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/install.sh +3 -3
- package/package.json +1 -1
- package/src/autodetect.mjs +11 -2
- package/src/cli.mjs +68 -59
- package/src/shell-path.mjs +58 -0
- package/src/ui.mjs +11 -3
- package/src/updates.mjs +125 -0
package/install.sh
CHANGED
|
@@ -144,8 +144,8 @@ fi
|
|
|
144
144
|
INSTALLED_VERSION=""
|
|
145
145
|
|
|
146
146
|
if [[ -n "$NPM_BIN" && -x "$NPM_BIN/offgrid-ai" ]]; then
|
|
147
|
-
# Get version
|
|
148
|
-
INSTALLED_VERSION="$(OFFGRID_NO_UPDATE_CHECK=1 "$NPM_BIN/offgrid-ai" version 2>/dev/null || echo "")"
|
|
147
|
+
# Get plain version from `offgrid-ai version` output (`offgrid-ai vX.Y.Z`).
|
|
148
|
+
INSTALLED_VERSION="$(OFFGRID_NO_UPDATE_CHECK=1 "$NPM_BIN/offgrid-ai" version 2>/dev/null | sed -E 's/^offgrid-ai v//' || echo "")"
|
|
149
149
|
|
|
150
150
|
if command -v offgrid-ai &>/dev/null; then
|
|
151
151
|
# Already on PATH — nothing to do
|
|
@@ -179,7 +179,7 @@ if [[ -n "$NPM_BIN" && -x "$NPM_BIN/offgrid-ai" ]]; then
|
|
|
179
179
|
else
|
|
180
180
|
# Fallback: try to find it anywhere on PATH after install
|
|
181
181
|
if command -v offgrid-ai &>/dev/null; then
|
|
182
|
-
INSTALLED_VERSION="$(offgrid-ai version 2>/dev/null || echo "")"
|
|
182
|
+
INSTALLED_VERSION="$(OFFGRID_NO_UPDATE_CHECK=1 offgrid-ai version 2>/dev/null | sed -E 's/^offgrid-ai v//' || echo "")"
|
|
183
183
|
ok "offgrid-ai ${INSTALLED_VERSION:+v${INSTALLED_VERSION} }installed at $(command -v offgrid-ai)"
|
|
184
184
|
else
|
|
185
185
|
echo ""
|
package/package.json
CHANGED
package/src/autodetect.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { readGgufMetadata } from "./gguf.mjs";
|
|
|
5
5
|
// ── Detect model capabilities from GGUF metadata ──────────────────────────
|
|
6
6
|
|
|
7
7
|
export function detectCapabilities(modelPath, mmprojPath) {
|
|
8
|
-
const meta =
|
|
8
|
+
const meta = safeReadGgufMetadata(modelPath);
|
|
9
9
|
const name = basename(modelPath).toLowerCase();
|
|
10
10
|
|
|
11
11
|
// Architecture
|
|
@@ -17,7 +17,7 @@ export function detectCapabilities(modelPath, mmprojPath) {
|
|
|
17
17
|
const thinking = hasThinkingKwargs || nameHintsThinking;
|
|
18
18
|
|
|
19
19
|
// Vision — mmproj present
|
|
20
|
-
const vision = mmprojPath && existsSync(mmprojPath);
|
|
20
|
+
const vision = Boolean(mmprojPath && existsSync(mmprojPath));
|
|
21
21
|
|
|
22
22
|
// MTP (multi-token prediction) — detect speculative decoding
|
|
23
23
|
const mtp = /mtp/i.test(name) || architecture === "qwen3";
|
|
@@ -107,6 +107,15 @@ export function computeFlags(capabilities, modelPath, mmprojPath, draftModelPath
|
|
|
107
107
|
|
|
108
108
|
// ── Internal helper ─────────────────────────────────────────────────────
|
|
109
109
|
|
|
110
|
+
function safeReadGgufMetadata(modelPath) {
|
|
111
|
+
if (!existsSync(modelPath)) return {};
|
|
112
|
+
try {
|
|
113
|
+
return readGgufMetadata(modelPath);
|
|
114
|
+
} catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
function numberMeta(meta, key) {
|
|
111
120
|
const value = key ? meta[key] : undefined;
|
|
112
121
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
package/src/cli.mjs
CHANGED
|
@@ -10,11 +10,42 @@ import { syncPiConfig, removeFromPiConfig, hasPiModel, launchPi, hasPi } from ".
|
|
|
10
10
|
import { tailFriendly } from "./logs.mjs";
|
|
11
11
|
import { estimateMemory } from "./estimate.mjs";
|
|
12
12
|
import { pc, formatBytes, renderRows, renderSection, startInteractive, createPrompt, parseOptions } from "./ui.mjs";
|
|
13
|
+
import { checkForUpdate, currentPackageVersion, detectInvocation, updateCommand, runUpdateCommand } from "./updates.mjs";
|
|
14
|
+
import { removeInstallerPathEntries } from "./shell-path.mjs";
|
|
13
15
|
|
|
14
16
|
// ── Entry point ────────────────────────────────────────────────────────────
|
|
15
17
|
|
|
18
|
+
async function offerUpdate(argv) {
|
|
19
|
+
const invocation = detectInvocation();
|
|
20
|
+
const update = await checkForUpdate({ force: invocation === "npx" });
|
|
21
|
+
if (!update) return false;
|
|
22
|
+
|
|
23
|
+
const plan = updateCommand(invocation, argv);
|
|
24
|
+
console.log(pc.yellow(`\nUpdate available: v${update.latest}. You have v${update.current}.`));
|
|
25
|
+
console.log(pc.dim(`Run: ${plan.display}`));
|
|
26
|
+
console.log();
|
|
27
|
+
|
|
28
|
+
if (!process.stdin.isTTY) return false;
|
|
29
|
+
|
|
30
|
+
const prompt = createPrompt();
|
|
31
|
+
try {
|
|
32
|
+
const shouldUpdate = await prompt.yesNo("Update now?", false);
|
|
33
|
+
if (!shouldUpdate) return false;
|
|
34
|
+
await runUpdateCommand(plan);
|
|
35
|
+
if (plan.mode === "install-global") {
|
|
36
|
+
console.log(pc.green("Updated. Run offgrid-ai again to use the new version."));
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
} finally {
|
|
40
|
+
prompt.close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
export async function run(argv) {
|
|
17
|
-
if (argv.length === 0)
|
|
45
|
+
if (argv.length === 0) {
|
|
46
|
+
if (await offerUpdate(argv)) return;
|
|
47
|
+
return mainFlow();
|
|
48
|
+
}
|
|
18
49
|
const [command] = argv;
|
|
19
50
|
|
|
20
51
|
if (command === "help" || command === "--help" || command === "-h") return printHelp();
|
|
@@ -246,7 +277,10 @@ async function pickAndRun(prompt, profiles, newModels, managedItems) {
|
|
|
246
277
|
}
|
|
247
278
|
|
|
248
279
|
if (selected.startsWith("managed:")) {
|
|
249
|
-
const
|
|
280
|
+
const managedSelection = selected.slice("managed:".length);
|
|
281
|
+
const separator = managedSelection.indexOf(":");
|
|
282
|
+
const backendId = separator === -1 ? managedSelection : managedSelection.slice(0, separator);
|
|
283
|
+
const modelId = separator === -1 ? "" : managedSelection.slice(separator + 1);
|
|
250
284
|
const model = managedItems.find((m) => m.model.id === modelId && m.backendId === backendId)?.model;
|
|
251
285
|
if (!model) throw new Error("Model not found.");
|
|
252
286
|
const profile = normalizeProfile({
|
|
@@ -287,10 +321,9 @@ async function runProfile(profile, options = {}) {
|
|
|
287
321
|
console.log(pc.green(`[ready] ${backend.label} at ${profile.baseUrl}`));
|
|
288
322
|
} else {
|
|
289
323
|
const ready = await serverReady(profile.baseUrl);
|
|
290
|
-
if (ready
|
|
291
|
-
console.log(pc.green(`[ready]
|
|
292
|
-
|
|
293
|
-
} else if (!ready) {
|
|
324
|
+
if (ready) {
|
|
325
|
+
console.log(pc.green(`[ready] Reusing server at ${profile.baseUrl}`));
|
|
326
|
+
} else {
|
|
294
327
|
console.log(pc.dim(`Starting ${backend.label} for ${profile.label}...`));
|
|
295
328
|
let state;
|
|
296
329
|
try {
|
|
@@ -785,11 +818,15 @@ async function uninstallCommand(argv) {
|
|
|
785
818
|
const { options } = parseOptions(argv);
|
|
786
819
|
const force = options.force || options.f;
|
|
787
820
|
|
|
788
|
-
if (!process.stdin.isTTY
|
|
789
|
-
|
|
821
|
+
if (!process.stdin.isTTY && !force) {
|
|
822
|
+
throw new Error("Non-interactive uninstall requires --force to avoid accidental data loss.");
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (force) {
|
|
826
|
+
await stopTrackedServers();
|
|
790
827
|
await removeDataDir();
|
|
791
|
-
await removeSelf();
|
|
792
828
|
await removeShellPath();
|
|
829
|
+
await removeSelf();
|
|
793
830
|
return;
|
|
794
831
|
}
|
|
795
832
|
|
|
@@ -826,8 +863,8 @@ async function uninstallCommand(argv) {
|
|
|
826
863
|
// Remove the npm package
|
|
827
864
|
const confirmUninstall = await prompt.yesNo("Uninstall offgrid-ai npm package?", true);
|
|
828
865
|
if (confirmUninstall) {
|
|
829
|
-
await removeSelf();
|
|
830
866
|
await removeShellPath();
|
|
867
|
+
await removeSelf();
|
|
831
868
|
} else {
|
|
832
869
|
console.log(pc.dim("Cancelled."));
|
|
833
870
|
}
|
|
@@ -836,6 +873,14 @@ async function uninstallCommand(argv) {
|
|
|
836
873
|
}
|
|
837
874
|
}
|
|
838
875
|
|
|
876
|
+
async function stopTrackedServers() {
|
|
877
|
+
const running = await runningProfiles();
|
|
878
|
+
for (const { profile } of running) {
|
|
879
|
+
const result = await stopProfile(profile);
|
|
880
|
+
console.log(result.stopped ? pc.green(`✓ ${result.message}`) : pc.dim(result.message));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
839
884
|
async function removeDataDir() {
|
|
840
885
|
const dataDir = DATA_DIR;
|
|
841
886
|
if (existsSync(dataDir)) {
|
|
@@ -852,47 +897,13 @@ async function removeDataDir() {
|
|
|
852
897
|
}
|
|
853
898
|
|
|
854
899
|
async function removeShellPath() {
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
const { promisify } = await import("node:util");
|
|
858
|
-
const { execFile } = await import("node:child_process");
|
|
859
|
-
let npmBin;
|
|
860
|
-
try {
|
|
861
|
-
const { stdout } = await promisify(execFile)("npm", ["bin", "-g"]);
|
|
862
|
-
npmBin = stdout.trim();
|
|
863
|
-
} catch { return; }
|
|
864
|
-
if (!npmBin) return;
|
|
865
|
-
|
|
866
|
-
const marker = "# Added by offgrid-ai installer";
|
|
867
|
-
const pathLine = `export PATH="${npmBin}:$PATH"`;
|
|
868
|
-
const rcFiles = [`${homedir()}/.zshrc`, `${homedir()}/.bashrc`, `${homedir()}/.bash_profile`];
|
|
869
|
-
let cleaned = false;
|
|
870
|
-
|
|
871
|
-
for (const rcFile of rcFiles) {
|
|
872
|
-
if (!existsSync(rcFile)) continue;
|
|
873
|
-
let content;
|
|
874
|
-
try { content = await readFile(rcFile, "utf8"); } catch { continue; }
|
|
875
|
-
if (!content.includes(npmBin) && !content.includes(marker)) continue;
|
|
876
|
-
|
|
877
|
-
const lines = content.split("\n");
|
|
878
|
-
const filtered = lines.filter((line, i) => {
|
|
879
|
-
// Remove the marker comment line
|
|
880
|
-
if (line.trim() === marker) return false;
|
|
881
|
-
// Remove the PATH export line added by the installer
|
|
882
|
-
if (line.trim() === pathLine) return false;
|
|
883
|
-
// Also remove a blank line that immediately precedes the marker (formatter)
|
|
884
|
-
if (i + 1 < lines.length && lines[i + 1].trim() === marker && line.trim() === "") return false;
|
|
885
|
-
return true;
|
|
886
|
-
});
|
|
887
|
-
|
|
888
|
-
if (filtered.length !== lines.length) {
|
|
889
|
-
await writeFile(rcFile, filtered.join("\n").replace(/\n{3,}/g, "\n\n") + "\n", "utf8");
|
|
890
|
-
console.log(pc.green(`✓ Cleaned PATH from ${rcFile}`));
|
|
891
|
-
cleaned = true;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
if (!cleaned) {
|
|
900
|
+
const cleaned = await removeInstallerPathEntries();
|
|
901
|
+
if (cleaned.length === 0) {
|
|
895
902
|
console.log(pc.dim("No offgrid-ai PATH entries found in shell configs."));
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
for (const rcFile of cleaned) {
|
|
906
|
+
console.log(pc.green(`✓ Cleaned PATH from ${rcFile}`));
|
|
896
907
|
}
|
|
897
908
|
}
|
|
898
909
|
|
|
@@ -960,15 +971,13 @@ async function scanManagedModels() {
|
|
|
960
971
|
}
|
|
961
972
|
|
|
962
973
|
async function printVersion() {
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
const
|
|
969
|
-
console.log(`
|
|
970
|
-
} catch {
|
|
971
|
-
console.log("offgrid-ai v0.1.0");
|
|
974
|
+
const version = currentPackageVersion();
|
|
975
|
+
console.log(`offgrid-ai v${version}`);
|
|
976
|
+
const invocation = detectInvocation();
|
|
977
|
+
const update = await checkForUpdate({ force: invocation === "npx" });
|
|
978
|
+
if (update) {
|
|
979
|
+
const plan = updateCommand(invocation, ["version"]);
|
|
980
|
+
console.log(pc.yellow(`Update available: v${update.latest}. Run: ${plan.display}`));
|
|
972
981
|
}
|
|
973
982
|
}
|
|
974
983
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
export const INSTALLER_PATH_MARKER = "# Added by offgrid-ai installer";
|
|
6
|
+
|
|
7
|
+
export function defaultShellConfigFiles(home = homedir()) {
|
|
8
|
+
return [`${home}/.zshrc`, `${home}/.bashrc`, `${home}/.bash_profile`];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function removeInstallerPathBlock(content) {
|
|
12
|
+
const lines = content.split("\n");
|
|
13
|
+
const kept = [];
|
|
14
|
+
let changed = false;
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < lines.length; i++) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
const next = lines[i + 1];
|
|
19
|
+
|
|
20
|
+
if (line.trim() === "" && next?.trim() === INSTALLER_PATH_MARKER) {
|
|
21
|
+
changed = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (line.trim() === INSTALLER_PATH_MARKER) {
|
|
26
|
+
changed = true;
|
|
27
|
+
const following = lines[i + 1]?.trim() ?? "";
|
|
28
|
+
if (/^export\s+PATH=".+:\$PATH"$/u.test(following)) i += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
kept.push(line);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
changed,
|
|
37
|
+
content: kept.join("\n").replace(/\n{3,}/gu, "\n\n").replace(/\s+$/u, "") + "\n",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function removeInstallerPathEntries(rcFiles = defaultShellConfigFiles()) {
|
|
42
|
+
const cleaned = [];
|
|
43
|
+
for (const rcFile of rcFiles) {
|
|
44
|
+
if (!existsSync(rcFile)) continue;
|
|
45
|
+
let content;
|
|
46
|
+
try {
|
|
47
|
+
content = await readFile(rcFile, "utf8");
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = removeInstallerPathBlock(content);
|
|
53
|
+
if (!result.changed) continue;
|
|
54
|
+
await writeFile(rcFile, result.content, "utf8");
|
|
55
|
+
cleaned.push(rcFile);
|
|
56
|
+
}
|
|
57
|
+
return cleaned;
|
|
58
|
+
}
|
package/src/ui.mjs
CHANGED
|
@@ -51,6 +51,7 @@ function handleCancel(value) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
export function renderRows(rows) {
|
|
54
|
+
if (rows.length === 0) return "";
|
|
54
55
|
const width = Math.max(...rows.map(([key]) => stripVTControlCharacters(String(key)).length));
|
|
55
56
|
return rows.map(([key, value]) => {
|
|
56
57
|
const visible = stripVTControlCharacters(String(key)).length;
|
|
@@ -67,14 +68,21 @@ export function parseOptions(argv) {
|
|
|
67
68
|
const options = {};
|
|
68
69
|
for (let i = 0; i < argv.length; i++) {
|
|
69
70
|
const item = argv[i];
|
|
71
|
+
if (item === "--") {
|
|
72
|
+
positional.push(...argv.slice(i + 1));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
70
75
|
if (item.startsWith("--")) {
|
|
71
|
-
const key = item.slice(2);
|
|
76
|
+
const [key, inlineValue] = item.slice(2).split(/=(.*)/u, 2);
|
|
72
77
|
const next = argv[i + 1];
|
|
73
|
-
if (
|
|
78
|
+
if (inlineValue !== undefined) options[key] = inlineValue;
|
|
79
|
+
else if (next && !next.startsWith("-")) { options[key] = next; i += 1; }
|
|
74
80
|
else options[key] = true;
|
|
81
|
+
} else if (/^-[A-Za-z]+$/u.test(item)) {
|
|
82
|
+
for (const key of item.slice(1)) options[key] = true;
|
|
75
83
|
} else {
|
|
76
84
|
positional.push(item);
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
return { positional, options };
|
|
80
|
-
}
|
|
88
|
+
}
|
package/src/updates.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { DATA_DIR } from "./config.mjs";
|
|
7
|
+
|
|
8
|
+
const PACKAGE_NAME = "offgrid-ai";
|
|
9
|
+
const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
export async function checkForUpdate({ now = Date.now(), fetchImpl = globalThis.fetch, force = false } = {}) {
|
|
12
|
+
if (process.env.OFFGRID_NO_UPDATE_CHECK) return null;
|
|
13
|
+
|
|
14
|
+
const currentVersion = currentPackageVersion();
|
|
15
|
+
const cacheFile = join(DATA_DIR, "update-cache.json");
|
|
16
|
+
const cached = await readUpdateCache(cacheFile);
|
|
17
|
+
|
|
18
|
+
if (!force && cached?.currentVersion === currentVersion && cached?.lastChecked && now - cached.lastChecked < UPDATE_CHECK_INTERVAL) {
|
|
19
|
+
return updateResult(currentVersion, cached.latestVersion);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetchImpl(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
|
|
24
|
+
signal: AbortSignal.timeout(3000),
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) return null;
|
|
27
|
+
const body = await response.json();
|
|
28
|
+
const latestVersion = typeof body?.version === "string" ? body.version : null;
|
|
29
|
+
if (!latestVersion) return null;
|
|
30
|
+
|
|
31
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
32
|
+
await writeFile(cacheFile, JSON.stringify({ lastChecked: now, currentVersion, latestVersion }, null, 2) + "\n", "utf8");
|
|
33
|
+
|
|
34
|
+
return updateResult(currentVersion, latestVersion);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function currentPackageVersion() {
|
|
41
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
43
|
+
return pkg.version;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isNewerVersion(candidate, current) {
|
|
47
|
+
return compareVersions(candidate, current) > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function compareVersions(a, b) {
|
|
51
|
+
const left = versionParts(a);
|
|
52
|
+
const right = versionParts(b);
|
|
53
|
+
const len = Math.max(left.length, right.length);
|
|
54
|
+
for (let i = 0; i < len; i++) {
|
|
55
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
56
|
+
if (diff !== 0) return diff;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function detectInvocation(env = process.env) {
|
|
62
|
+
const execPath = env.npm_execpath ?? "";
|
|
63
|
+
if (/(^|[\\/])npx-cli\.js$/u.test(execPath)) return "npx";
|
|
64
|
+
if (env.npm_command === "exec") return "npx";
|
|
65
|
+
return "global";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function updateCommand(invocation = detectInvocation(), argv = []) {
|
|
69
|
+
if (invocation === "npx") {
|
|
70
|
+
const args = ["exec", "--yes", "--", `${PACKAGE_NAME}@latest`, ...argv];
|
|
71
|
+
return {
|
|
72
|
+
cmd: "npm",
|
|
73
|
+
args,
|
|
74
|
+
display: shellCommand("npm", args),
|
|
75
|
+
mode: "run-latest",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const args = ["install", "-g", `${PACKAGE_NAME}@latest`];
|
|
80
|
+
return {
|
|
81
|
+
cmd: "npm",
|
|
82
|
+
args,
|
|
83
|
+
display: shellCommand("npm", args),
|
|
84
|
+
mode: "install-global",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function runUpdateCommand(plan) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const child = spawn(plan.cmd, plan.args, { stdio: "inherit" });
|
|
91
|
+
child.on("error", reject);
|
|
92
|
+
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${plan.cmd} exited with code ${code}`)));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readUpdateCache(cacheFile) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(await readFile(cacheFile, "utf8"));
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function updateResult(currentVersion, latestVersion) {
|
|
105
|
+
return isNewerVersion(latestVersion, currentVersion)
|
|
106
|
+
? { current: currentVersion, latest: latestVersion }
|
|
107
|
+
: null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function versionParts(value) {
|
|
111
|
+
return String(value)
|
|
112
|
+
.replace(/^v/u, "")
|
|
113
|
+
.split(/[.-]/u)
|
|
114
|
+
.map((part) => Number.parseInt(part, 10))
|
|
115
|
+
.filter((part) => Number.isFinite(part));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function shellCommand(cmd, args) {
|
|
119
|
+
return [cmd, ...args.map(shellQuote)].join(" ");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function shellQuote(value) {
|
|
123
|
+
const text = String(value);
|
|
124
|
+
return /^[A-Za-z0-9_./:@=-]+$/u.test(text) ? text : JSON.stringify(text);
|
|
125
|
+
}
|