nebula-ai 0.0.8 → 0.0.9
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 +274 -313
- package/package.json +1 -1
package/bin/nebula-ai.js
CHANGED
|
@@ -38,26 +38,14 @@
|
|
|
38
38
|
// so keep the old key valid (publish one more release with it)
|
|
39
39
|
// long enough for the dist-tag to catch up, then delete the old
|
|
40
40
|
// key material.
|
|
41
|
-
|
|
42
41
|
import { spawn, spawnSync } from "node:child_process";
|
|
43
42
|
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
44
|
-
import {
|
|
45
|
-
chmodSync,
|
|
46
|
-
createWriteStream,
|
|
47
|
-
existsSync,
|
|
48
|
-
mkdirSync,
|
|
49
|
-
readFileSync,
|
|
50
|
-
realpathSync,
|
|
51
|
-
renameSync,
|
|
52
|
-
rmSync,
|
|
53
|
-
statSync,
|
|
54
|
-
} from "node:fs";
|
|
43
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, } from "node:fs";
|
|
55
44
|
import { readFile } from "node:fs/promises";
|
|
56
45
|
import { homedir } from "node:os";
|
|
57
46
|
import { dirname, join } from "node:path";
|
|
58
47
|
import { pipeline } from "node:stream/promises";
|
|
59
48
|
import { fileURLToPath } from "node:url";
|
|
60
|
-
|
|
61
49
|
// The public half of the ed25519 key that signs `checksums.json`. The
|
|
62
50
|
// private half lives only in the `NEBULA_AI_SIGNING_PRIVATE_KEY` GitHub
|
|
63
51
|
// Actions secret.
|
|
@@ -65,239 +53,231 @@ const EMBEDDED_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
|
65
53
|
MCowBQYDK2VwAyEAd8QFvCTSMY0HmSsJgL6jtFQas00WedvSZhkc9bxdAl0=
|
|
66
54
|
-----END PUBLIC KEY-----
|
|
67
55
|
`;
|
|
68
|
-
|
|
69
56
|
// Stamped by `scripts/publish/prepare.mjs` at wrapper staging time with
|
|
70
57
|
// the channel-appropriate base URL (dev vs prod). The `__NEBULA_AI_…__`
|
|
71
58
|
// sentinel lets us keep the checked-in source runnable for local tests
|
|
72
59
|
// without pointing at any real host.
|
|
73
60
|
const STAMPED_DOWNLOAD_BASE_URL = "https://cli-assets.nebula-dev.ai/production";
|
|
74
|
-
const DOWNLOAD_BASE_URL =
|
|
75
|
-
process.env.NEBULA_AI_DOWNLOAD_BASE ?? STAMPED_DOWNLOAD_BASE_URL;
|
|
76
|
-
|
|
61
|
+
const DOWNLOAD_BASE_URL = process.env.NEBULA_AI_DOWNLOAD_BASE ?? STAMPED_DOWNLOAD_BASE_URL;
|
|
77
62
|
const SUPPORTED_PLATFORMS = new Set([
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
63
|
+
"darwin-arm64",
|
|
64
|
+
"darwin-x64",
|
|
65
|
+
"linux-x64",
|
|
66
|
+
"linux-arm64",
|
|
67
|
+
"win32-x64",
|
|
83
68
|
]);
|
|
84
|
-
|
|
85
69
|
// ─── update-check constants ────────────────────────────────────────────────
|
|
86
|
-
|
|
87
70
|
const UPDATE_CHECK_TTL = 4 * 60 * 60 * 1000; // 4 hours
|
|
88
|
-
|
|
89
71
|
// ─── pure helpers (exported for tests) ─────────────────────────────────────
|
|
90
|
-
|
|
91
72
|
/**
|
|
92
73
|
* Return the path to the update-check cache file. Lives at the top of the
|
|
93
74
|
* cache directory (NOT under a version-specific subdirectory) so it persists
|
|
94
75
|
* across version upgrades.
|
|
95
76
|
*/
|
|
96
77
|
export function getUpdateCheckCachePath() {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return join(base, "nebula-ai", "update-check.json");
|
|
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");
|
|
103
83
|
}
|
|
104
|
-
|
|
105
84
|
/**
|
|
106
85
|
* Determine the release channel from a version string.
|
|
107
86
|
* Dev versions look like `x.y.z-dev.YYYYMMDDHHMMSS.sha`.
|
|
108
87
|
*/
|
|
109
88
|
export function getChannel(version) {
|
|
110
|
-
|
|
89
|
+
return version.includes("-dev.") ? "dev" : "prod";
|
|
111
90
|
}
|
|
112
|
-
|
|
113
91
|
/**
|
|
114
92
|
* Return true if `latest` is a newer semver than `current` (prod), or simply
|
|
115
93
|
* different (dev — timestamps are monotonically increasing).
|
|
116
94
|
*/
|
|
117
95
|
export function isNewerVersion(latest, current) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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;
|
|
132
114
|
}
|
|
133
|
-
|
|
134
115
|
/**
|
|
135
116
|
* Return true when the update check should be suppressed entirely.
|
|
136
117
|
*/
|
|
137
118
|
export function isUpdateCheckDisabled() {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
!process.stderr.isTTY
|
|
143
|
-
);
|
|
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);
|
|
144
123
|
}
|
|
145
|
-
|
|
146
124
|
/**
|
|
147
125
|
* Return true when running under `npx` (which always fetches the latest
|
|
148
126
|
* wrapper, making the check redundant).
|
|
149
127
|
*/
|
|
150
128
|
export function isNpx() {
|
|
151
|
-
|
|
129
|
+
return process.env.npm_command === "exec";
|
|
152
130
|
}
|
|
153
|
-
|
|
154
131
|
export function sha256HexSync(buffer) {
|
|
155
|
-
|
|
132
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
156
133
|
}
|
|
157
|
-
|
|
158
134
|
/**
|
|
159
135
|
* Verify a signed checksums.json blob and return the parsed manifest.
|
|
160
136
|
* Throws if the signature is invalid or the manifest is malformed.
|
|
161
137
|
*/
|
|
162
138
|
export function verifyManifest(manifestBytes, signatureBase64, publicKeyPem) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
throw new Error("manifest is missing the `binaries` map");
|
|
185
|
-
}
|
|
186
|
-
return manifest;
|
|
139
|
+
const pubKey = createPublicKey(publicKeyPem);
|
|
140
|
+
if (pubKey.asymmetricKeyType !== "ed25519") {
|
|
141
|
+
throw new Error(`embedded public key is not ed25519 (got ${pubKey.asymmetricKeyType})`);
|
|
142
|
+
}
|
|
143
|
+
const sig = Buffer.from(signatureBase64.trim(), "base64");
|
|
144
|
+
const ok = cryptoVerify(null, manifestBytes, pubKey, sig);
|
|
145
|
+
if (!ok) {
|
|
146
|
+
throw new Error("manifest signature verification failed");
|
|
147
|
+
}
|
|
148
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
149
|
+
if (!manifest || typeof manifest !== "object") {
|
|
150
|
+
throw new Error("manifest is not a JSON object");
|
|
151
|
+
}
|
|
152
|
+
const m = manifest;
|
|
153
|
+
if (m.algorithm !== "sha256") {
|
|
154
|
+
throw new Error(`unsupported manifest algorithm: ${m.algorithm ?? "(missing)"}`);
|
|
155
|
+
}
|
|
156
|
+
if (!m.binaries || typeof m.binaries !== "object") {
|
|
157
|
+
throw new Error("manifest is missing the `binaries` map");
|
|
158
|
+
}
|
|
159
|
+
return manifest;
|
|
187
160
|
}
|
|
188
|
-
|
|
189
161
|
/**
|
|
190
162
|
* Given a verified manifest + a platform key + the bytes of a downloaded
|
|
191
163
|
* binary, confirm the binary's SHA-256 matches the manifest entry. Throws
|
|
192
164
|
* on mismatch.
|
|
193
165
|
*/
|
|
194
166
|
export function assertBinaryMatchesManifest(manifest, platformKey, binName, binaryBuffer) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
);
|
|
205
|
-
}
|
|
167
|
+
const entryKey = `${platformKey}/${binName}`;
|
|
168
|
+
const expected = manifest.binaries[entryKey];
|
|
169
|
+
if (typeof expected !== "string") {
|
|
170
|
+
throw new Error(`manifest has no entry for ${entryKey}`);
|
|
171
|
+
}
|
|
172
|
+
const actual = sha256HexSync(binaryBuffer);
|
|
173
|
+
if (actual !== expected) {
|
|
174
|
+
throw new Error(`binary digest mismatch for ${entryKey}\n expected: ${expected}\n actual: ${actual}`);
|
|
175
|
+
}
|
|
206
176
|
}
|
|
207
|
-
|
|
208
177
|
// ─── runtime (only runs when this file is the entrypoint) ─────────────────
|
|
209
|
-
|
|
210
178
|
const platformKey = `${process.platform}-${process.arch}`;
|
|
211
179
|
const binName = process.platform === "win32" ? "nebula-ai.exe" : "nebula-ai";
|
|
212
|
-
|
|
213
180
|
async function readVersion() {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
181
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
182
|
+
const pkgPath = join(here, "..", "package.json");
|
|
183
|
+
const raw = await readFile(pkgPath, "utf8");
|
|
184
|
+
const pkg = JSON.parse(raw);
|
|
185
|
+
if (typeof pkg.version !== "string") {
|
|
186
|
+
throw new Error("nebula-ai: package.json is missing a version field");
|
|
187
|
+
}
|
|
188
|
+
return pkg.version;
|
|
189
|
+
}
|
|
190
|
+
function getCacheBaseDir() {
|
|
191
|
+
// Respect XDG_CACHE_HOME on linux, honor LOCALAPPDATA on windows, default
|
|
192
|
+
// to ~/.cache elsewhere. Keeps behavior predictable across platforms.
|
|
193
|
+
const base = process.env.NEBULA_AI_CACHE_DIR ||
|
|
194
|
+
(process.platform === "win32"
|
|
195
|
+
? process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local")
|
|
196
|
+
: process.env.XDG_CACHE_HOME || join(homedir(), ".cache"));
|
|
197
|
+
return join(base, "nebula-ai");
|
|
222
198
|
}
|
|
223
|
-
|
|
224
199
|
function getCacheDir(version) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
200
|
+
return join(getCacheBaseDir(), `v${version}`, platformKey);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Remove cached binaries for versions other than the current one.
|
|
204
|
+
* Only touches directories whose names are `v<semver>` — leaves
|
|
205
|
+
* non-version entries (e.g. `boxlite/`, `update-check.json`) untouched.
|
|
206
|
+
* Best-effort: individual removal failures are silently ignored so one
|
|
207
|
+
* stuck directory doesn't prevent the rest from being cleaned up.
|
|
208
|
+
*/
|
|
209
|
+
const VERSION_CACHE_DIR_RE = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
210
|
+
function cleanupOldVersions(currentVersion) {
|
|
211
|
+
try {
|
|
212
|
+
const base = getCacheBaseDir();
|
|
213
|
+
const currentDir = `v${currentVersion}`;
|
|
214
|
+
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
215
|
+
if (entry.isDirectory() &&
|
|
216
|
+
VERSION_CACHE_DIR_RE.test(entry.name) &&
|
|
217
|
+
entry.name !== currentDir) {
|
|
218
|
+
try {
|
|
219
|
+
rmSync(join(base, entry.name), { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// best-effort — skip this entry and continue
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Parent directory may not exist yet, or permissions issue — ignore.
|
|
229
|
+
}
|
|
233
230
|
}
|
|
234
|
-
|
|
235
231
|
async function fetchManifest(version) {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
const manifestBytes = Buffer.from(await manifestRes.arrayBuffer());
|
|
253
|
-
const signatureBase64 = await sigRes.text();
|
|
254
|
-
return verifyManifest(manifestBytes, signatureBase64, EMBEDDED_PUBLIC_KEY_PEM);
|
|
232
|
+
const manifestUrl = `${DOWNLOAD_BASE_URL}/v${version}/checksums.json`;
|
|
233
|
+
const sigUrl = `${DOWNLOAD_BASE_URL}/v${version}/checksums.json.sig`;
|
|
234
|
+
const [manifestRes, sigRes] = await Promise.all([
|
|
235
|
+
fetch(manifestUrl, { redirect: "follow" }),
|
|
236
|
+
fetch(sigUrl, { redirect: "follow" }),
|
|
237
|
+
]);
|
|
238
|
+
if (!manifestRes.ok) {
|
|
239
|
+
throw new Error(`manifest download failed: ${manifestRes.status} ${manifestRes.statusText}\n url: ${manifestUrl}`);
|
|
240
|
+
}
|
|
241
|
+
if (!sigRes.ok) {
|
|
242
|
+
throw new Error(`signature download failed: ${sigRes.status} ${sigRes.statusText}\n url: ${sigUrl}`);
|
|
243
|
+
}
|
|
244
|
+
const manifestBytes = Buffer.from(await manifestRes.arrayBuffer());
|
|
245
|
+
const signatureBase64 = await sigRes.text();
|
|
246
|
+
return verifyManifest(manifestBytes, signatureBase64, EMBEDDED_PUBLIC_KEY_PEM);
|
|
255
247
|
}
|
|
256
|
-
|
|
257
248
|
async function downloadBinary(version, destPath) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
mkdirSync(dirname(destPath), { recursive: true });
|
|
274
|
-
|
|
275
|
-
// Stage under a unique temp name in the SAME directory as destPath so the
|
|
276
|
-
// final renameSync is always an intra-filesystem move. tmpdir() can live on
|
|
277
|
-
// a different mount (tmpfs on linux, user-local tmp on some sandboxes) and
|
|
278
|
-
// rename across mounts fails with EXDEV.
|
|
279
|
-
const tmpPath = join(
|
|
280
|
-
dirname(destPath),
|
|
281
|
-
`.nebula-ai-${version}-${process.pid}-${Date.now()}.partial`,
|
|
282
|
-
);
|
|
283
|
-
try {
|
|
284
|
-
await pipeline(res.body, createWriteStream(tmpPath));
|
|
285
|
-
const tmpBytes = await readFile(tmpPath);
|
|
286
|
-
assertBinaryMatchesManifest(manifest, platformKey, binName, tmpBytes);
|
|
287
|
-
if (process.platform !== "win32") chmodSync(tmpPath, 0o755);
|
|
288
|
-
renameSync(tmpPath, destPath);
|
|
289
|
-
} catch (err) {
|
|
249
|
+
const url = `${DOWNLOAD_BASE_URL}/v${version}/${platformKey}/${binName}`;
|
|
250
|
+
process.stderr.write(`nebula-ai: downloading runtime for your platform (one-time setup)…\n`);
|
|
251
|
+
const manifest = await fetchManifest(version);
|
|
252
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
253
|
+
if (!res.ok || !res.body) {
|
|
254
|
+
throw new Error(`download failed: ${res.status} ${res.statusText}\n url: ${url}`);
|
|
255
|
+
}
|
|
256
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
257
|
+
// Stage under a unique temp name in the SAME directory as destPath so the
|
|
258
|
+
// final renameSync is always an intra-filesystem move. tmpdir() can live on
|
|
259
|
+
// a different mount (tmpfs on linux, user-local tmp on some sandboxes) and
|
|
260
|
+
// rename across mounts fails with EXDEV.
|
|
261
|
+
const tmpPath = join(dirname(destPath), `.nebula-ai-${version}-${process.pid}-${Date.now()}.partial`);
|
|
290
262
|
try {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
263
|
+
await pipeline(res.body, createWriteStream(tmpPath));
|
|
264
|
+
const tmpBytes = await readFile(tmpPath);
|
|
265
|
+
assertBinaryMatchesManifest(manifest, platformKey, binName, tmpBytes);
|
|
266
|
+
if (process.platform !== "win32")
|
|
267
|
+
chmodSync(tmpPath, 0o755);
|
|
268
|
+
renameSync(tmpPath, destPath);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
try {
|
|
272
|
+
rmSync(tmpPath, { force: true });
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// best-effort cleanup
|
|
276
|
+
}
|
|
277
|
+
throw err;
|
|
294
278
|
}
|
|
295
|
-
throw err;
|
|
296
|
-
}
|
|
297
279
|
}
|
|
298
|
-
|
|
299
280
|
// ─── update-check runtime ───────────────────────────────────────────────────
|
|
300
|
-
|
|
301
281
|
/**
|
|
302
282
|
* If the cached check result is stale (or missing), spawn a detached
|
|
303
283
|
* background process to fetch the latest version from the npm registry and
|
|
@@ -305,19 +285,18 @@ async function downloadBinary(version, destPath) {
|
|
|
305
285
|
* launcher alive.
|
|
306
286
|
*/
|
|
307
287
|
function maybeSpawnUpdateCheck(currentVersion) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const script = `
|
|
288
|
+
const cachePath = getUpdateCheckCachePath();
|
|
289
|
+
// Skip if the cache is still fresh (stat is cheaper than read + parse).
|
|
290
|
+
try {
|
|
291
|
+
if (Date.now() - statSync(cachePath).mtimeMs < UPDATE_CHECK_TTL)
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// File missing — fall through to spawn.
|
|
296
|
+
}
|
|
297
|
+
const channel = getChannel(currentVersion);
|
|
298
|
+
const distTag = channel === "dev" ? "dev" : "latest";
|
|
299
|
+
const script = `
|
|
321
300
|
const fs = require("node:fs");
|
|
322
301
|
const path = require("node:path");
|
|
323
302
|
const cachePath = ${JSON.stringify(cachePath)};
|
|
@@ -338,130 +317,112 @@ function maybeSpawnUpdateCheck(currentVersion) {
|
|
|
338
317
|
})
|
|
339
318
|
.catch(() => {});
|
|
340
319
|
`;
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
320
|
+
try {
|
|
321
|
+
const child = spawn(process.execPath, ["-e", script], {
|
|
322
|
+
detached: true,
|
|
323
|
+
stdio: "ignore",
|
|
324
|
+
});
|
|
325
|
+
child.unref();
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// If we cannot spawn, silently skip.
|
|
329
|
+
}
|
|
351
330
|
}
|
|
352
|
-
|
|
353
331
|
/**
|
|
354
332
|
* Read the cached check result (written by a *previous* invocation's
|
|
355
333
|
* background process) and print a one-line update notification to stderr
|
|
356
334
|
* if a newer version is available.
|
|
357
335
|
*/
|
|
358
336
|
function maybeShowUpdateNotification(currentVersion) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
337
|
+
try {
|
|
338
|
+
const raw = readFileSync(getUpdateCheckCachePath(), "utf8");
|
|
339
|
+
const cached = JSON.parse(raw);
|
|
340
|
+
if (cached.latest && isNewerVersion(cached.latest, currentVersion)) {
|
|
341
|
+
const cmd = currentVersion.includes("-dev.")
|
|
342
|
+
? "npm install -g nebula-ai@dev"
|
|
343
|
+
: "npm install -g nebula-ai";
|
|
344
|
+
process.stderr.write(`\nnebula-ai: update available ${currentVersion} \u2192 ${cached.latest}\n` +
|
|
345
|
+
` Run: ${cmd}\n`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// Cache missing or corrupt — do nothing.
|
|
350
|
+
}
|
|
374
351
|
}
|
|
375
|
-
|
|
376
352
|
async function main() {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
` https://github.com/thirdweb-dev/nebula-cli/issues`,
|
|
398
|
-
);
|
|
399
|
-
process.exit(1);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
let version;
|
|
403
|
-
try {
|
|
404
|
-
version = await readVersion();
|
|
405
|
-
} catch (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
|
-
);
|
|
412
|
-
process.exit(1);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const cacheDir = getCacheDir(version);
|
|
416
|
-
const binaryPath = join(cacheDir, binName);
|
|
417
|
-
|
|
418
|
-
if (!existsSync(binaryPath)) {
|
|
353
|
+
if (DOWNLOAD_BASE_URL.includes("__NEBULA_AI_DOWNLOAD_BASE__")) {
|
|
354
|
+
console.error("nebula-ai: unable to download — this installation appears to be incomplete.\n" +
|
|
355
|
+
"\n" +
|
|
356
|
+
"To fix this, reinstall nebula-ai:\n" +
|
|
357
|
+
" npm install -g nebula-ai\n" +
|
|
358
|
+
"\n" +
|
|
359
|
+
"If the problem persists, please open an issue:\n" +
|
|
360
|
+
" https://github.com/thirdweb-dev/nebula-cli/issues");
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
if (!SUPPORTED_PLATFORMS.has(platformKey)) {
|
|
364
|
+
console.error(`nebula-ai: your platform (${platformKey}) is not yet supported.\n` +
|
|
365
|
+
`\n` +
|
|
366
|
+
`Supported platforms: ${[...SUPPORTED_PLATFORMS].join(", ")}.\n` +
|
|
367
|
+
`\n` +
|
|
368
|
+
`Please open an issue to request support:\n` +
|
|
369
|
+
` https://github.com/thirdweb-dev/nebula-cli/issues`);
|
|
370
|
+
process.exit(1);
|
|
371
|
+
}
|
|
372
|
+
let version;
|
|
419
373
|
try {
|
|
420
|
-
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
`nebula-ai:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
374
|
+
version = await readVersion();
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
console.error(`nebula-ai: unable to determine the installed version.\n` +
|
|
378
|
+
`\n` +
|
|
379
|
+
`This usually means the installation is corrupted. To fix this, reinstall:\n` +
|
|
380
|
+
` npm install -g nebula-ai`);
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
const cacheDir = getCacheDir(version);
|
|
384
|
+
const binaryPath = join(cacheDir, binName);
|
|
385
|
+
if (!existsSync(binaryPath)) {
|
|
386
|
+
try {
|
|
387
|
+
await downloadBinary(version, binaryPath);
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
console.error(`nebula-ai: could not download the runtime.\n` +
|
|
391
|
+
`\n` +
|
|
392
|
+
`This is usually a network issue. Please check your internet connection and try again.\n` +
|
|
393
|
+
`\n` +
|
|
394
|
+
`If the problem persists:\n` +
|
|
395
|
+
` • Clear the download cache: rm -rf ${getCacheDir(version)}\n` +
|
|
396
|
+
` • Reinstall: npm install -g nebula-ai\n` +
|
|
397
|
+
` • Open an issue: https://github.com/thirdweb-dev/nebula-cli/issues`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
// New version downloaded — purge stale version directories.
|
|
401
|
+
cleanupOldVersions(version);
|
|
402
|
+
}
|
|
403
|
+
// Fire-and-forget background version check (writes cache for next run).
|
|
404
|
+
if (!isUpdateCheckDisabled() && !isNpx()) {
|
|
405
|
+
maybeSpawnUpdateCheck(version);
|
|
406
|
+
}
|
|
407
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
408
|
+
stdio: "inherit",
|
|
409
|
+
});
|
|
410
|
+
// Show update notification (reads PREVIOUS check's cached result).
|
|
411
|
+
if (!isUpdateCheckDisabled() && !isNpx()) {
|
|
412
|
+
maybeShowUpdateNotification(version);
|
|
413
|
+
}
|
|
414
|
+
if (result.error) {
|
|
415
|
+
console.error(`nebula-ai: unable to start the application.\n` +
|
|
416
|
+
`\n` +
|
|
417
|
+
`This can happen if the downloaded runtime was corrupted.\n` +
|
|
418
|
+
`\n` +
|
|
419
|
+
`To fix this:\n` +
|
|
420
|
+
` 1. Clear the cache: rm -rf ${getCacheDir(version)}\n` +
|
|
421
|
+
` 2. Run nebula-ai again to re-download`);
|
|
422
|
+
process.exit(1);
|
|
423
|
+
}
|
|
424
|
+
process.exit(result.status ?? 0);
|
|
463
425
|
}
|
|
464
|
-
|
|
465
426
|
// Only run main() when this file is the CLI entrypoint, so tests can
|
|
466
427
|
// import the pure helpers above without triggering a download.
|
|
467
428
|
//
|
|
@@ -471,16 +432,16 @@ async function main() {
|
|
|
471
432
|
// a naive string compare would always fail and main() would silently never
|
|
472
433
|
// run. realpathSync both sides so the comparison survives the bin shim.
|
|
473
434
|
function isCliEntrypoint() {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
435
|
+
if (!process.argv[1])
|
|
436
|
+
return false;
|
|
437
|
+
try {
|
|
438
|
+
return (realpathSync(fileURLToPath(import.meta.url)) ===
|
|
439
|
+
realpathSync(process.argv[1]));
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
483
444
|
}
|
|
484
445
|
if (isCliEntrypoint()) {
|
|
485
|
-
|
|
446
|
+
main();
|
|
486
447
|
}
|