nebula-ai 0.0.7 → 0.0.8
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 +194 -13
- package/package.json +1 -1
package/bin/nebula-ai.js
CHANGED
|
@@ -39,16 +39,18 @@
|
|
|
39
39
|
// long enough for the dist-tag to catch up, then delete the old
|
|
40
40
|
// key material.
|
|
41
41
|
|
|
42
|
-
import { spawnSync } from "node:child_process";
|
|
42
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
43
43
|
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
44
44
|
import {
|
|
45
45
|
chmodSync,
|
|
46
46
|
createWriteStream,
|
|
47
47
|
existsSync,
|
|
48
48
|
mkdirSync,
|
|
49
|
+
readFileSync,
|
|
49
50
|
realpathSync,
|
|
50
51
|
renameSync,
|
|
51
52
|
rmSync,
|
|
53
|
+
statSync,
|
|
52
54
|
} from "node:fs";
|
|
53
55
|
import { readFile } from "node:fs/promises";
|
|
54
56
|
import { homedir } from "node:os";
|
|
@@ -80,8 +82,75 @@ const SUPPORTED_PLATFORMS = new Set([
|
|
|
80
82
|
"win32-x64",
|
|
81
83
|
]);
|
|
82
84
|
|
|
85
|
+
// ─── update-check constants ────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
const UPDATE_CHECK_TTL = 4 * 60 * 60 * 1000; // 4 hours
|
|
88
|
+
|
|
83
89
|
// ─── pure helpers (exported for tests) ─────────────────────────────────────
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Return the path to the update-check cache file. Lives at the top of the
|
|
93
|
+
* cache directory (NOT under a version-specific subdirectory) so it persists
|
|
94
|
+
* across version upgrades.
|
|
95
|
+
*/
|
|
96
|
+
export function getUpdateCheckCachePath() {
|
|
97
|
+
const base =
|
|
98
|
+
process.env.NEBULA_AI_CACHE_DIR ||
|
|
99
|
+
(process.platform === "win32"
|
|
100
|
+
? process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local")
|
|
101
|
+
: process.env.XDG_CACHE_HOME || join(homedir(), ".cache"));
|
|
102
|
+
return join(base, "nebula-ai", "update-check.json");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Determine the release channel from a version string.
|
|
107
|
+
* Dev versions look like `x.y.z-dev.YYYYMMDDHHMMSS.sha`.
|
|
108
|
+
*/
|
|
109
|
+
export function getChannel(version) {
|
|
110
|
+
return version.includes("-dev.") ? "dev" : "prod";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Return true if `latest` is a newer semver than `current` (prod), or simply
|
|
115
|
+
* different (dev — timestamps are monotonically increasing).
|
|
116
|
+
*/
|
|
117
|
+
export function isNewerVersion(latest, current) {
|
|
118
|
+
if (current.includes("-dev.")) {
|
|
119
|
+
return latest !== current;
|
|
120
|
+
}
|
|
121
|
+
// Strip any prerelease suffix (e.g., "-beta.1") before parsing.
|
|
122
|
+
const clean = (v) => v.replace(/-.*$/, "");
|
|
123
|
+
const [a1, a2, a3] = clean(latest).split(".").map(Number);
|
|
124
|
+
const [b1, b2, b3] = clean(current).split(".").map(Number);
|
|
125
|
+
if (a1 !== b1) return a1 > b1;
|
|
126
|
+
if (a2 !== b2) return a2 > b2;
|
|
127
|
+
if (a3 !== b3) return a3 > b3;
|
|
128
|
+
// Same numeric version: a stable release is newer than a prerelease
|
|
129
|
+
// (e.g. 1.0.0 is newer than 1.0.0-beta.1).
|
|
130
|
+
if (current.includes("-") && !latest.includes("-")) return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Return true when the update check should be suppressed entirely.
|
|
136
|
+
*/
|
|
137
|
+
export function isUpdateCheckDisabled() {
|
|
138
|
+
return (
|
|
139
|
+
process.env.NEBULA_AI_NO_UPDATE_CHECK === "1" ||
|
|
140
|
+
process.env.NEBULA_AI_NO_UPDATE_CHECK === "true" ||
|
|
141
|
+
process.env.CI === "true" ||
|
|
142
|
+
!process.stderr.isTTY
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Return true when running under `npx` (which always fetches the latest
|
|
148
|
+
* wrapper, making the check redundant).
|
|
149
|
+
*/
|
|
150
|
+
export function isNpx() {
|
|
151
|
+
return process.env.npm_command === "exec";
|
|
152
|
+
}
|
|
153
|
+
|
|
85
154
|
export function sha256HexSync(buffer) {
|
|
86
155
|
return createHash("sha256").update(buffer).digest("hex");
|
|
87
156
|
}
|
|
@@ -189,7 +258,7 @@ async function downloadBinary(version, destPath) {
|
|
|
189
258
|
const url = `${DOWNLOAD_BASE_URL}/v${version}/${platformKey}/${binName}`;
|
|
190
259
|
|
|
191
260
|
process.stderr.write(
|
|
192
|
-
`nebula-ai:
|
|
261
|
+
`nebula-ai: downloading runtime for your platform (one-time setup)…\n`,
|
|
193
262
|
);
|
|
194
263
|
|
|
195
264
|
const manifest = await fetchManifest(version);
|
|
@@ -227,21 +296,105 @@ async function downloadBinary(version, destPath) {
|
|
|
227
296
|
}
|
|
228
297
|
}
|
|
229
298
|
|
|
299
|
+
// ─── update-check runtime ───────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* If the cached check result is stale (or missing), spawn a detached
|
|
303
|
+
* background process to fetch the latest version from the npm registry and
|
|
304
|
+
* write it to the cache file. The process is unref'd so it does not keep the
|
|
305
|
+
* launcher alive.
|
|
306
|
+
*/
|
|
307
|
+
function maybeSpawnUpdateCheck(currentVersion) {
|
|
308
|
+
const cachePath = getUpdateCheckCachePath();
|
|
309
|
+
|
|
310
|
+
// Skip if the cache is still fresh (stat is cheaper than read + parse).
|
|
311
|
+
try {
|
|
312
|
+
if (Date.now() - statSync(cachePath).mtimeMs < UPDATE_CHECK_TTL) return;
|
|
313
|
+
} catch {
|
|
314
|
+
// File missing — fall through to spawn.
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const channel = getChannel(currentVersion);
|
|
318
|
+
const distTag = channel === "dev" ? "dev" : "latest";
|
|
319
|
+
|
|
320
|
+
const script = `
|
|
321
|
+
const fs = require("node:fs");
|
|
322
|
+
const path = require("node:path");
|
|
323
|
+
const cachePath = ${JSON.stringify(cachePath)};
|
|
324
|
+
fetch("https://registry.npmjs.org/nebula-ai/${distTag}", {
|
|
325
|
+
headers: { "Accept": "application/vnd.npm.install-v1+json" },
|
|
326
|
+
signal: AbortSignal.timeout(10000),
|
|
327
|
+
})
|
|
328
|
+
.then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
|
|
329
|
+
.then(data => {
|
|
330
|
+
if (typeof data.version === "string") {
|
|
331
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
332
|
+
fs.writeFileSync(cachePath, JSON.stringify({
|
|
333
|
+
latest: data.version,
|
|
334
|
+
channel: "${distTag}",
|
|
335
|
+
checkedAt: Date.now(),
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
.catch(() => {});
|
|
340
|
+
`;
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const child = spawn(process.execPath, ["-e", script], {
|
|
344
|
+
detached: true,
|
|
345
|
+
stdio: "ignore",
|
|
346
|
+
});
|
|
347
|
+
child.unref();
|
|
348
|
+
} catch {
|
|
349
|
+
// If we cannot spawn, silently skip.
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Read the cached check result (written by a *previous* invocation's
|
|
355
|
+
* background process) and print a one-line update notification to stderr
|
|
356
|
+
* if a newer version is available.
|
|
357
|
+
*/
|
|
358
|
+
function maybeShowUpdateNotification(currentVersion) {
|
|
359
|
+
try {
|
|
360
|
+
const raw = readFileSync(getUpdateCheckCachePath(), "utf8");
|
|
361
|
+
const cached = JSON.parse(raw);
|
|
362
|
+
if (cached.latest && isNewerVersion(cached.latest, currentVersion)) {
|
|
363
|
+
const cmd = currentVersion.includes("-dev.")
|
|
364
|
+
? "npm install -g nebula-ai@dev"
|
|
365
|
+
: "npm install -g nebula-ai";
|
|
366
|
+
process.stderr.write(
|
|
367
|
+
`\nnebula-ai: update available ${currentVersion} \u2192 ${cached.latest}\n` +
|
|
368
|
+
` Run: ${cmd}\n`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
// Cache missing or corrupt — do nothing.
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
230
376
|
async function main() {
|
|
231
377
|
if (DOWNLOAD_BASE_URL.includes("__NEBULA_AI_DOWNLOAD_BASE__")) {
|
|
232
378
|
console.error(
|
|
233
|
-
"nebula-ai:
|
|
234
|
-
"
|
|
235
|
-
"
|
|
379
|
+
"nebula-ai: unable to download — this installation appears to be incomplete.\n" +
|
|
380
|
+
"\n" +
|
|
381
|
+
"To fix this, reinstall nebula-ai:\n" +
|
|
382
|
+
" npm install -g nebula-ai\n" +
|
|
383
|
+
"\n" +
|
|
384
|
+
"If the problem persists, please open an issue:\n" +
|
|
385
|
+
" https://github.com/thirdweb-dev/nebula-cli/issues",
|
|
236
386
|
);
|
|
237
387
|
process.exit(1);
|
|
238
388
|
}
|
|
239
389
|
|
|
240
390
|
if (!SUPPORTED_PLATFORMS.has(platformKey)) {
|
|
241
391
|
console.error(
|
|
242
|
-
`nebula-ai:
|
|
392
|
+
`nebula-ai: your platform (${platformKey}) is not yet supported.\n` +
|
|
393
|
+
`\n` +
|
|
243
394
|
`Supported platforms: ${[...SUPPORTED_PLATFORMS].join(", ")}.\n` +
|
|
244
|
-
`
|
|
395
|
+
`\n` +
|
|
396
|
+
`Please open an issue to request support:\n` +
|
|
397
|
+
` https://github.com/thirdweb-dev/nebula-cli/issues`,
|
|
245
398
|
);
|
|
246
399
|
process.exit(1);
|
|
247
400
|
}
|
|
@@ -250,7 +403,12 @@ async function main() {
|
|
|
250
403
|
try {
|
|
251
404
|
version = await readVersion();
|
|
252
405
|
} catch (err) {
|
|
253
|
-
console.error(
|
|
406
|
+
console.error(
|
|
407
|
+
`nebula-ai: unable to determine the installed version.\n` +
|
|
408
|
+
`\n` +
|
|
409
|
+
`This usually means the installation is corrupted. To fix this, reinstall:\n` +
|
|
410
|
+
` npm install -g nebula-ai`,
|
|
411
|
+
);
|
|
254
412
|
process.exit(1);
|
|
255
413
|
}
|
|
256
414
|
|
|
@@ -261,21 +419,44 @@ async function main() {
|
|
|
261
419
|
try {
|
|
262
420
|
await downloadBinary(version, binaryPath);
|
|
263
421
|
} catch (err) {
|
|
264
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
265
422
|
console.error(
|
|
266
|
-
`nebula-ai:
|
|
267
|
-
`\
|
|
268
|
-
`
|
|
423
|
+
`nebula-ai: could not download the runtime.\n` +
|
|
424
|
+
`\n` +
|
|
425
|
+
`This is usually a network issue. Please check your internet connection and try again.\n` +
|
|
426
|
+
`\n` +
|
|
427
|
+
`If the problem persists:\n` +
|
|
428
|
+
` • Clear the download cache: rm -rf ${getCacheDir(version)}\n` +
|
|
429
|
+
` • Reinstall: npm install -g nebula-ai\n` +
|
|
430
|
+
` • Open an issue: https://github.com/thirdweb-dev/nebula-cli/issues`,
|
|
269
431
|
);
|
|
270
432
|
process.exit(1);
|
|
271
433
|
}
|
|
272
434
|
}
|
|
273
435
|
|
|
436
|
+
// Fire-and-forget background version check (writes cache for next run).
|
|
437
|
+
if (!isUpdateCheckDisabled() && !isNpx()) {
|
|
438
|
+
maybeSpawnUpdateCheck(version);
|
|
439
|
+
}
|
|
440
|
+
|
|
274
441
|
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
275
442
|
stdio: "inherit",
|
|
276
443
|
});
|
|
444
|
+
|
|
445
|
+
// Show update notification (reads PREVIOUS check's cached result).
|
|
446
|
+
if (!isUpdateCheckDisabled() && !isNpx()) {
|
|
447
|
+
maybeShowUpdateNotification(version);
|
|
448
|
+
}
|
|
449
|
+
|
|
277
450
|
if (result.error) {
|
|
278
|
-
console.error(
|
|
451
|
+
console.error(
|
|
452
|
+
`nebula-ai: unable to start the application.\n` +
|
|
453
|
+
`\n` +
|
|
454
|
+
`This can happen if the downloaded runtime was corrupted.\n` +
|
|
455
|
+
`\n` +
|
|
456
|
+
`To fix this:\n` +
|
|
457
|
+
` 1. Clear the cache: rm -rf ${getCacheDir(version)}\n` +
|
|
458
|
+
` 2. Run nebula-ai again to re-download`,
|
|
459
|
+
);
|
|
279
460
|
process.exit(1);
|
|
280
461
|
}
|
|
281
462
|
process.exit(result.status ?? 0);
|