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.
Files changed (2) hide show
  1. package/bin/nebula-ai.js +194 -13
  2. 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: fetching ${platformKey} v${version} prebuilt binary…\n`,
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: this build of the wrapper was not stamped with a download base URL.\n" +
234
- " Set NEBULA_AI_DOWNLOAD_BASE to a reachable mirror, or install\n" +
235
- " nebula-ai from a dev checkout of https://github.com/thirdweb-dev/nebula-cli.",
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: no prebuilt binary for ${platformKey}.\n` +
392
+ `nebula-ai: your platform (${platformKey}) is not yet supported.\n` +
393
+ `\n` +
243
394
  `Supported platforms: ${[...SUPPORTED_PLATFORMS].join(", ")}.\n` +
244
- `File an issue at https://github.com/thirdweb-dev/nebula-cli/issues`,
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(`nebula-ai: ${err instanceof Error ? err.message : err}`);
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: failed to install prebuilt binary.\n ${msg}\n` +
267
- `\nIf your environment blocks outbound HTTPS, set NEBULA_AI_DOWNLOAD_BASE\n` +
268
- `to a reachable mirror, or install from source.`,
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(`nebula-ai: failed to exec binary: ${result.error.message}`);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nebula-ai",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "CLI/TUI for the nebula.gg AI task orchestration platform",
5
5
  "type": "module",
6
6
  "bin": {