nebula-ai 0.0.15 → 0.0.17
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/bin/nebula-ai.js +32 -68
- package/bin/update-check-lib.js +223 -0
- package/package.json +1 -1
package/bin/nebula-ai.js
CHANGED
|
@@ -46,6 +46,10 @@ import { homedir } from "node:os";
|
|
|
46
46
|
import { dirname, join } from "node:path";
|
|
47
47
|
import { pipeline } from "node:stream/promises";
|
|
48
48
|
import { fileURLToPath } from "node:url";
|
|
49
|
+
import { detectInstallMethod, getChannel, getUpdateCheckCachePath, getUpgradeCommand, isNewerVersion, isNpx, isUpdateCheckDisabled, UPDATE_CHECK_TTL_MS, } from "./update-check-lib.js";
|
|
50
|
+
// Re-export the pure update-check helpers so older tests / downstream
|
|
51
|
+
// importers that still reach for `./launcher.js` keep working.
|
|
52
|
+
export { detectInstallMethod, getChannel, getUpdateCheckCachePath, getUpgradeCommand, isNewerVersion, isNpx, isUpdateCheckDisabled, } from "./update-check-lib.js";
|
|
49
53
|
// The public half of the ed25519 key that signs `checksums.json`. The
|
|
50
54
|
// private half lives only in the `NEBULA_AI_SIGNING_PRIVATE_KEY` GitHub
|
|
51
55
|
// Actions secret.
|
|
@@ -66,68 +70,11 @@ const SUPPORTED_PLATFORMS = new Set([
|
|
|
66
70
|
"linux-arm64",
|
|
67
71
|
"win32-x64",
|
|
68
72
|
]);
|
|
69
|
-
// ─── update-check constants ────────────────────────────────────────────────
|
|
70
|
-
const UPDATE_CHECK_TTL = 4 * 60 * 60 * 1000; // 4 hours
|
|
71
73
|
// ─── pure helpers (exported for tests) ─────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
*/
|
|
77
|
-
export function getUpdateCheckCachePath() {
|
|
78
|
-
const base = process.env.NEBULA_AI_CACHE_DIR ||
|
|
79
|
-
(process.platform === "win32"
|
|
80
|
-
? process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local")
|
|
81
|
-
: process.env.XDG_CACHE_HOME || join(homedir(), ".cache"));
|
|
82
|
-
return join(base, "nebula-ai", "update-check.json");
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Determine the release channel from a version string.
|
|
86
|
-
* Dev versions look like `x.y.z-dev.YYYYMMDDHHMMSS.sha`.
|
|
87
|
-
*/
|
|
88
|
-
export function getChannel(version) {
|
|
89
|
-
return version.includes("-dev.") ? "dev" : "prod";
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Return true if `latest` is a newer semver than `current` (prod), or simply
|
|
93
|
-
* different (dev — timestamps are monotonically increasing).
|
|
94
|
-
*/
|
|
95
|
-
export function isNewerVersion(latest, current) {
|
|
96
|
-
if (current.includes("-dev.")) {
|
|
97
|
-
return latest !== current;
|
|
98
|
-
}
|
|
99
|
-
// Strip any prerelease suffix (e.g., "-beta.1") before parsing.
|
|
100
|
-
const clean = (v) => v.replace(/-.*$/, "");
|
|
101
|
-
const [a1, a2, a3] = clean(latest).split(".").map(Number);
|
|
102
|
-
const [b1, b2, b3] = clean(current).split(".").map(Number);
|
|
103
|
-
if (a1 !== b1)
|
|
104
|
-
return a1 > b1;
|
|
105
|
-
if (a2 !== b2)
|
|
106
|
-
return a2 > b2;
|
|
107
|
-
if (a3 !== b3)
|
|
108
|
-
return a3 > b3;
|
|
109
|
-
// Same numeric version: a stable release is newer than a prerelease
|
|
110
|
-
// (e.g. 1.0.0 is newer than 1.0.0-beta.1).
|
|
111
|
-
if (current.includes("-") && !latest.includes("-"))
|
|
112
|
-
return true;
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Return true when the update check should be suppressed entirely.
|
|
117
|
-
*/
|
|
118
|
-
export function isUpdateCheckDisabled() {
|
|
119
|
-
return (process.env.NEBULA_AI_NO_UPDATE_CHECK === "1" ||
|
|
120
|
-
process.env.NEBULA_AI_NO_UPDATE_CHECK === "true" ||
|
|
121
|
-
process.env.CI === "true" ||
|
|
122
|
-
!process.stderr.isTTY);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Return true when running under `npx` (which always fetches the latest
|
|
126
|
-
* wrapper, making the check redundant).
|
|
127
|
-
*/
|
|
128
|
-
export function isNpx() {
|
|
129
|
-
return process.env.npm_command === "exec";
|
|
130
|
-
}
|
|
74
|
+
//
|
|
75
|
+
// Version/update-check helpers live in ./update-check-lib.ts and are
|
|
76
|
+
// re-exported above. Supply-chain helpers remain here because they import
|
|
77
|
+
// node:crypto and aren't used outside the launcher.
|
|
131
78
|
export function sha256HexSync(buffer) {
|
|
132
79
|
return createHash("sha256").update(buffer).digest("hex");
|
|
133
80
|
}
|
|
@@ -288,7 +235,7 @@ function maybeSpawnUpdateCheck(currentVersion) {
|
|
|
288
235
|
const cachePath = getUpdateCheckCachePath();
|
|
289
236
|
// Skip if the cache is still fresh (stat is cheaper than read + parse).
|
|
290
237
|
try {
|
|
291
|
-
if (Date.now() - statSync(cachePath).mtimeMs <
|
|
238
|
+
if (Date.now() - statSync(cachePath).mtimeMs < UPDATE_CHECK_TTL_MS)
|
|
292
239
|
return;
|
|
293
240
|
}
|
|
294
241
|
catch {
|
|
@@ -331,16 +278,22 @@ function maybeSpawnUpdateCheck(currentVersion) {
|
|
|
331
278
|
/**
|
|
332
279
|
* Read the cached check result (written by a *previous* invocation's
|
|
333
280
|
* background process) and print a one-line update notification to stderr
|
|
334
|
-
* if a newer version is available.
|
|
281
|
+
* if a newer version is available. The suggested upgrade command is
|
|
282
|
+
* chosen based on the detected install method so pnpm/yarn/bun/brew users
|
|
283
|
+
* see an actually-applicable command.
|
|
335
284
|
*/
|
|
336
|
-
function maybeShowUpdateNotification(currentVersion) {
|
|
285
|
+
function maybeShowUpdateNotification(currentVersion, installMethod) {
|
|
337
286
|
try {
|
|
338
287
|
const raw = readFileSync(getUpdateCheckCachePath(), "utf8");
|
|
339
288
|
const cached = JSON.parse(raw);
|
|
340
289
|
if (cached.latest && isNewerVersion(cached.latest, currentVersion)) {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
290
|
+
const channel = getChannel(currentVersion);
|
|
291
|
+
// Fall back to the canonical npm command when the install method
|
|
292
|
+
// is unknown — matches the wrapper's install-time docs.
|
|
293
|
+
const cmd = getUpgradeCommand(installMethod, channel) ||
|
|
294
|
+
(channel === "dev"
|
|
295
|
+
? "npm install -g nebula-ai@dev"
|
|
296
|
+
: "npm install -g nebula-ai");
|
|
344
297
|
process.stderr.write(`\nnebula-ai: update available ${currentVersion} \u2192 ${cached.latest}\n` +
|
|
345
298
|
` Run: ${cmd}\n`);
|
|
346
299
|
}
|
|
@@ -404,12 +357,23 @@ async function main() {
|
|
|
404
357
|
if (!isUpdateCheckDisabled() && !isNpx()) {
|
|
405
358
|
maybeSpawnUpdateCheck(version);
|
|
406
359
|
}
|
|
360
|
+
// Figure out how the user installed us (npm/pnpm/yarn/bun/npx/...) from
|
|
361
|
+
// the wrapper's own launch path + env. We forward the result to the Bun
|
|
362
|
+
// binary via NEBULA_AI_INSTALL_METHOD so the in-TUI "What's new" banner
|
|
363
|
+
// can surface the right upgrade command without redoing the detection.
|
|
364
|
+
const installMethod = detectInstallMethod({
|
|
365
|
+
execPath: fileURLToPath(import.meta.url),
|
|
366
|
+
argv0: process.argv[0],
|
|
367
|
+
npmCommand: process.env.npm_command,
|
|
368
|
+
npmUserAgent: process.env.npm_config_user_agent,
|
|
369
|
+
});
|
|
407
370
|
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
408
371
|
stdio: "inherit",
|
|
372
|
+
env: { ...process.env, NEBULA_AI_INSTALL_METHOD: installMethod },
|
|
409
373
|
});
|
|
410
374
|
// Show update notification (reads PREVIOUS check's cached result).
|
|
411
375
|
if (!isUpdateCheckDisabled() && !isNpx()) {
|
|
412
|
-
maybeShowUpdateNotification(version);
|
|
376
|
+
maybeShowUpdateNotification(version, installMethod);
|
|
413
377
|
}
|
|
414
378
|
if (result.error) {
|
|
415
379
|
console.error(`nebula-ai: unable to start the application.\n` +
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Shared, pure helpers for the nebula-ai update-check flow.
|
|
2
|
+
//
|
|
3
|
+
// This module is imported by two very different consumers:
|
|
4
|
+
//
|
|
5
|
+
// 1. `scripts/publish/launcher.ts` — the Node launcher shipped as the
|
|
6
|
+
// npm wrapper. Uses these helpers to decide whether to fire the
|
|
7
|
+
// background check and to print the post-exit stderr notification.
|
|
8
|
+
//
|
|
9
|
+
// 2. `src/update-check/lib.ts` — a byte-for-byte mirror living inside
|
|
10
|
+
// the Bun SEA binary. Kept in sync via a parity test rather than a
|
|
11
|
+
// cross-tree import because the main tsconfig's `rootDir: src`
|
|
12
|
+
// forbids reaching outside `src/`. If you edit this file, edit the
|
|
13
|
+
// mirror as well (see the parity test in
|
|
14
|
+
// `src/update-check/lib.test.ts`).
|
|
15
|
+
//
|
|
16
|
+
// Everything exported here MUST be pure (no fs, no network, no process
|
|
17
|
+
// exits). Side-effecting code belongs in the callers.
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
// ─── update-check constants ────────────────────────────────────────────────
|
|
21
|
+
/** How long a cache entry counts as fresh (4 hours). */
|
|
22
|
+
export const UPDATE_CHECK_TTL_MS = 4 * 60 * 60 * 1000;
|
|
23
|
+
// ─── cache-path resolution ─────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Return the path to the update-check cache file. Lives at the top of the
|
|
26
|
+
* cache directory (NOT under a version-specific subdirectory) so it persists
|
|
27
|
+
* across version upgrades.
|
|
28
|
+
*/
|
|
29
|
+
export function getUpdateCheckCachePath() {
|
|
30
|
+
const base = process.env.NEBULA_AI_CACHE_DIR ||
|
|
31
|
+
(process.platform === "win32"
|
|
32
|
+
? process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local")
|
|
33
|
+
: process.env.XDG_CACHE_HOME || join(homedir(), ".cache"));
|
|
34
|
+
return join(base, "nebula-ai", "update-check.json");
|
|
35
|
+
}
|
|
36
|
+
// ─── version helpers ───────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Determine the release channel from a version string.
|
|
39
|
+
* Dev versions look like `x.y.z-dev.YYYYMMDDHHMMSS.sha`.
|
|
40
|
+
*/
|
|
41
|
+
export function getChannel(version) {
|
|
42
|
+
return version.includes("-dev.") ? "dev" : "prod";
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Return true if `latest` is a newer semver than `current` (prod), or a
|
|
46
|
+
* strictly newer build timestamp (dev).
|
|
47
|
+
*
|
|
48
|
+
* Dev versions look like `x.y.z-dev.YYYYMMDDHHMMSS.sha`, produced by CI on
|
|
49
|
+
* every merge. Comparing with `!==` would falsely flag any mismatch as an
|
|
50
|
+
* "update available" — including the common case where a contributor runs a
|
|
51
|
+
* locally-built dev binary that's ahead of the registry's `dev` dist-tag
|
|
52
|
+
* (e.g. a PR branch that's ahead of main). Parse the 14-digit stamp out of
|
|
53
|
+
* both sides and compare those instead; fall back to `!==` only if either
|
|
54
|
+
* string doesn't match the expected shape.
|
|
55
|
+
*/
|
|
56
|
+
export function isNewerVersion(latest, current) {
|
|
57
|
+
if (current.includes("-dev.")) {
|
|
58
|
+
const devStamp = /-dev\.(\d{14})\./;
|
|
59
|
+
const latestStamp = latest.match(devStamp)?.[1];
|
|
60
|
+
const currentStamp = current.match(devStamp)?.[1];
|
|
61
|
+
if (latestStamp && currentStamp)
|
|
62
|
+
return latestStamp > currentStamp;
|
|
63
|
+
return latest !== current;
|
|
64
|
+
}
|
|
65
|
+
// Strip any prerelease suffix (e.g., "-beta.1") before parsing.
|
|
66
|
+
const clean = (v) => v.replace(/-.*$/, "");
|
|
67
|
+
const [a1, a2, a3] = clean(latest).split(".").map(Number);
|
|
68
|
+
const [b1, b2, b3] = clean(current).split(".").map(Number);
|
|
69
|
+
if (a1 !== b1)
|
|
70
|
+
return a1 > b1;
|
|
71
|
+
if (a2 !== b2)
|
|
72
|
+
return a2 > b2;
|
|
73
|
+
if (a3 !== b3)
|
|
74
|
+
return a3 > b3;
|
|
75
|
+
// Same numeric version: a stable release is newer than a prerelease
|
|
76
|
+
// (e.g. 1.0.0 is newer than 1.0.0-beta.1).
|
|
77
|
+
if (current.includes("-") && !latest.includes("-"))
|
|
78
|
+
return true;
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
// ─── environment gates ─────────────────────────────────────────────────────
|
|
82
|
+
/**
|
|
83
|
+
* Return true when the update check should be suppressed entirely.
|
|
84
|
+
*
|
|
85
|
+
* The TTY check targets the stream the caller is about to write to — callers
|
|
86
|
+
* pass the stream they'd render on:
|
|
87
|
+
*
|
|
88
|
+
* - Launcher context → `process.stderr` (the stderr one-liner).
|
|
89
|
+
* - Binary / TUI context → `process.stdout` (the in-TUI banner).
|
|
90
|
+
*
|
|
91
|
+
* Without this distinction, a user running `nebula-ai 2>/dev/null` would
|
|
92
|
+
* silence the in-TUI banner even though the TUI itself is still rendering
|
|
93
|
+
* on stdout. Defaults to `process.stderr` to preserve the launcher's
|
|
94
|
+
* historical behaviour for any caller that doesn't pass one.
|
|
95
|
+
*/
|
|
96
|
+
export function isUpdateCheckDisabled(stream = process.stderr) {
|
|
97
|
+
// Canonical `CI=true` is what GitHub Actions / CircleCI / GitLab CI set,
|
|
98
|
+
// but the long tail of CI runners uses `CI=1`, `CI=yes`, etc. Treat any
|
|
99
|
+
// non-empty value as "in CI" unless it's explicitly opted out
|
|
100
|
+
// (`CI=false` / `CI=0`), matching npm's is-ci heuristic.
|
|
101
|
+
const ci = process.env.CI?.toLowerCase();
|
|
102
|
+
const inCI = !!ci && ci !== "false" && ci !== "0";
|
|
103
|
+
return (process.env.NEBULA_AI_NO_UPDATE_CHECK === "1" ||
|
|
104
|
+
process.env.NEBULA_AI_NO_UPDATE_CHECK === "true" ||
|
|
105
|
+
inCI ||
|
|
106
|
+
!stream.isTTY);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Return true when running under a transient package runner (npx, pnpm dlx,
|
|
110
|
+
* yarn dlx, bunx). Those flows fetch a fresh copy of the wrapper every
|
|
111
|
+
* invocation, so an "update available" notice would be misleading — there's
|
|
112
|
+
* nothing persistent to upgrade.
|
|
113
|
+
*
|
|
114
|
+
* Detection is a two-step heuristic because no single signal catches every
|
|
115
|
+
* runner:
|
|
116
|
+
*
|
|
117
|
+
* - `npm_command` is set to `"exec"` by npm and `"dlx"` by pnpm/yarn.
|
|
118
|
+
* - bunx doesn't set `npm_command` at all, but bun-originated invocations
|
|
119
|
+
* set `npm_config_user_agent` to `bun/...`. If we see that UA *and* the
|
|
120
|
+
* binary isn't living under a persistent bun globals path (that check
|
|
121
|
+
* belongs in `detectInstallMethod`), we treat it as transient.
|
|
122
|
+
*
|
|
123
|
+
* `isNpx()` deliberately stays conservative — callers that also have a
|
|
124
|
+
* path-based signal (like `detectInstallMethod`) layer that check on top.
|
|
125
|
+
*/
|
|
126
|
+
export function isNpx() {
|
|
127
|
+
const cmd = process.env.npm_command;
|
|
128
|
+
return cmd === "exec" || cmd === "dlx";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Pure detection: given a resolved launch path + a few env vars, return
|
|
132
|
+
* the best-guess install method. No fs / no process inspection — all
|
|
133
|
+
* inputs are passed in by the caller so this function stays trivial to
|
|
134
|
+
* test.
|
|
135
|
+
*/
|
|
136
|
+
export function detectInstallMethod(input) {
|
|
137
|
+
// 1. Transient-runner env vars win unconditionally. `exec` is set by npx;
|
|
138
|
+
// `dlx` by pnpm/yarn's dlx commands. Each invocation re-fetches the
|
|
139
|
+
// wrapper, so there's nothing persistent to upgrade and the banner
|
|
140
|
+
// should be suppressed.
|
|
141
|
+
if (input.npmCommand === "exec" || input.npmCommand === "dlx")
|
|
142
|
+
return "npx";
|
|
143
|
+
// 2. Path fingerprinting precedes user-agent matching because the path
|
|
144
|
+
// tells us WHERE the binary lives (persistent vs. transient cache),
|
|
145
|
+
// whereas the UA only tells us WHO invoked it. A `bunx nebula-ai`
|
|
146
|
+
// invocation sets the UA to `bun/...` but the binary lives in bun's
|
|
147
|
+
// run-cache, not its globals dir — trusting UA there would flash an
|
|
148
|
+
// `bun add -g` banner even though bunx already fetches fresh. Check
|
|
149
|
+
// both execPath and argv0 because on some shims they diverge.
|
|
150
|
+
const paths = [input.execPath, input.argv0 ?? ""].map((p) =>
|
|
151
|
+
// Normalize Windows separators so our substring checks work uniformly.
|
|
152
|
+
p.replace(/\\/g, "/"));
|
|
153
|
+
const anyMatch = (...needles) => paths.some((p) => needles.some((n) => p.includes(n)));
|
|
154
|
+
// Homebrew formula name is `nebula-ai`, so a brew-installed binary lives
|
|
155
|
+
// under `.../Cellar/nebula-ai/<version>/bin/nebula-ai`. Matching the
|
|
156
|
+
// formula segment specifically avoids misdetecting an npm global under a
|
|
157
|
+
// Homebrew-installed Node (which lives at
|
|
158
|
+
// `/opt/homebrew/lib/node_modules/...`, no `/Cellar/nebula-ai/`).
|
|
159
|
+
if (anyMatch("/Cellar/nebula-ai/", "/linuxbrew/Cellar/nebula-ai/")) {
|
|
160
|
+
return "brew";
|
|
161
|
+
}
|
|
162
|
+
if (anyMatch("/.bun/install/global/", "/bun/install/global/")) {
|
|
163
|
+
return "bun";
|
|
164
|
+
}
|
|
165
|
+
if (anyMatch("/pnpm/global/", "/.pnpm/global/", "/Library/pnpm/", "/.local/share/pnpm/", "/pnpm-global/")) {
|
|
166
|
+
return "pnpm";
|
|
167
|
+
}
|
|
168
|
+
if (anyMatch("/.yarn/global/", "/yarn/global/")) {
|
|
169
|
+
return "yarn";
|
|
170
|
+
}
|
|
171
|
+
// Any `/node_modules/` fingerprint that fell through is most likely an
|
|
172
|
+
// npm global (npm installs the wrapper as a symlink inside
|
|
173
|
+
// `<prefix>/lib/node_modules/nebula-ai/`).
|
|
174
|
+
if (anyMatch("/node_modules/")) {
|
|
175
|
+
return "npm";
|
|
176
|
+
}
|
|
177
|
+
// 3. User-agent fallthrough. If we get here, none of the persistent
|
|
178
|
+
// install paths matched, which means a UA match most likely indicates
|
|
179
|
+
// a transient runner (e.g. `bunx nebula-ai`, or a project-local
|
|
180
|
+
// `pnpm exec` that didn't set `npm_command=exec`). Treat these as npx
|
|
181
|
+
// — no persistent install to upgrade.
|
|
182
|
+
const ua = (input.npmUserAgent ?? "").toLowerCase();
|
|
183
|
+
if (ua.startsWith("pnpm") ||
|
|
184
|
+
ua.startsWith("yarn") ||
|
|
185
|
+
ua.startsWith("bun") ||
|
|
186
|
+
ua.startsWith("npm")) {
|
|
187
|
+
return "npx";
|
|
188
|
+
}
|
|
189
|
+
return "unknown";
|
|
190
|
+
}
|
|
191
|
+
// ─── upgrade-command mapping ───────────────────────────────────────────────
|
|
192
|
+
/**
|
|
193
|
+
* Map an install method + release channel to the shell command a user
|
|
194
|
+
* would run to upgrade. Returns an empty string for cases where no
|
|
195
|
+
* single-shot upgrade makes sense (npx / unknown).
|
|
196
|
+
*/
|
|
197
|
+
export function getUpgradeCommand(method, channel) {
|
|
198
|
+
const pkg = channel === "dev" ? "nebula-ai@dev" : "nebula-ai";
|
|
199
|
+
switch (method) {
|
|
200
|
+
case "npm":
|
|
201
|
+
return `npm install -g ${pkg}`;
|
|
202
|
+
case "pnpm":
|
|
203
|
+
return `pnpm add -g ${pkg}`;
|
|
204
|
+
case "yarn":
|
|
205
|
+
// `yarn global add` only exists on classic yarn; modern yarn (berry)
|
|
206
|
+
// drops the global registry in favour of dlx, but there's no better
|
|
207
|
+
// single-command upgrade for a classic-yarn user — this mirrors what
|
|
208
|
+
// their install command would have been.
|
|
209
|
+
return `yarn global add ${pkg}`;
|
|
210
|
+
case "bun":
|
|
211
|
+
return `bun add -g ${pkg}`;
|
|
212
|
+
case "brew":
|
|
213
|
+
// The brew tap is prod-only (dev versions use `-dev.<stamp>.<sha>`
|
|
214
|
+
// suffixes Homebrew mishandles). If someone somehow ended up on dev
|
|
215
|
+
// via brew, redirect them to npm for the dev channel.
|
|
216
|
+
return channel === "dev"
|
|
217
|
+
? `npm install -g nebula-ai@dev`
|
|
218
|
+
: `brew upgrade nebula-ai`;
|
|
219
|
+
case "npx":
|
|
220
|
+
case "unknown":
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
}
|