nebula-ai 0.0.7 → 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 +343 -201
- package/package.json +1 -1
package/bin/nebula-ai.js
CHANGED
|
@@ -38,24 +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
|
-
import { spawnSync } from "node:child_process";
|
|
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
|
-
realpathSync,
|
|
50
|
-
renameSync,
|
|
51
|
-
rmSync,
|
|
52
|
-
} from "node:fs";
|
|
43
|
+
import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, } from "node:fs";
|
|
53
44
|
import { readFile } from "node:fs/promises";
|
|
54
45
|
import { homedir } from "node:os";
|
|
55
46
|
import { dirname, join } from "node:path";
|
|
56
47
|
import { pipeline } from "node:stream/promises";
|
|
57
48
|
import { fileURLToPath } from "node:url";
|
|
58
|
-
|
|
59
49
|
// The public half of the ed25519 key that signs `checksums.json`. The
|
|
60
50
|
// private half lives only in the `NEBULA_AI_SIGNING_PRIVATE_KEY` GitHub
|
|
61
51
|
// Actions secret.
|
|
@@ -63,224 +53,376 @@ const EMBEDDED_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
|
63
53
|
MCowBQYDK2VwAyEAd8QFvCTSMY0HmSsJgL6jtFQas00WedvSZhkc9bxdAl0=
|
|
64
54
|
-----END PUBLIC KEY-----
|
|
65
55
|
`;
|
|
66
|
-
|
|
67
56
|
// Stamped by `scripts/publish/prepare.mjs` at wrapper staging time with
|
|
68
57
|
// the channel-appropriate base URL (dev vs prod). The `__NEBULA_AI_…__`
|
|
69
58
|
// sentinel lets us keep the checked-in source runnable for local tests
|
|
70
59
|
// without pointing at any real host.
|
|
71
60
|
const STAMPED_DOWNLOAD_BASE_URL = "https://cli-assets.nebula-dev.ai/production";
|
|
72
|
-
const DOWNLOAD_BASE_URL =
|
|
73
|
-
process.env.NEBULA_AI_DOWNLOAD_BASE ?? STAMPED_DOWNLOAD_BASE_URL;
|
|
74
|
-
|
|
61
|
+
const DOWNLOAD_BASE_URL = process.env.NEBULA_AI_DOWNLOAD_BASE ?? STAMPED_DOWNLOAD_BASE_URL;
|
|
75
62
|
const SUPPORTED_PLATFORMS = new Set([
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
63
|
+
"darwin-arm64",
|
|
64
|
+
"darwin-x64",
|
|
65
|
+
"linux-x64",
|
|
66
|
+
"linux-arm64",
|
|
67
|
+
"win32-x64",
|
|
81
68
|
]);
|
|
82
|
-
|
|
69
|
+
// ─── update-check constants ────────────────────────────────────────────────
|
|
70
|
+
const UPDATE_CHECK_TTL = 4 * 60 * 60 * 1000; // 4 hours
|
|
83
71
|
// ─── pure helpers (exported for tests) ─────────────────────────────────────
|
|
84
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Return the path to the update-check cache file. Lives at the top of the
|
|
74
|
+
* cache directory (NOT under a version-specific subdirectory) so it persists
|
|
75
|
+
* across version upgrades.
|
|
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
|
+
}
|
|
85
131
|
export function sha256HexSync(buffer) {
|
|
86
|
-
|
|
132
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
87
133
|
}
|
|
88
|
-
|
|
89
134
|
/**
|
|
90
135
|
* Verify a signed checksums.json blob and return the parsed manifest.
|
|
91
136
|
* Throws if the signature is invalid or the manifest is malformed.
|
|
92
137
|
*/
|
|
93
138
|
export function verifyManifest(manifestBytes, signatureBase64, publicKeyPem) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
throw new Error("manifest is missing the `binaries` map");
|
|
116
|
-
}
|
|
117
|
-
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;
|
|
118
160
|
}
|
|
119
|
-
|
|
120
161
|
/**
|
|
121
162
|
* Given a verified manifest + a platform key + the bytes of a downloaded
|
|
122
163
|
* binary, confirm the binary's SHA-256 matches the manifest entry. Throws
|
|
123
164
|
* on mismatch.
|
|
124
165
|
*/
|
|
125
166
|
export function assertBinaryMatchesManifest(manifest, platformKey, binName, binaryBuffer) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
);
|
|
136
|
-
}
|
|
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
|
+
}
|
|
137
176
|
}
|
|
138
|
-
|
|
139
177
|
// ─── runtime (only runs when this file is the entrypoint) ─────────────────
|
|
140
|
-
|
|
141
178
|
const platformKey = `${process.platform}-${process.arch}`;
|
|
142
179
|
const binName = process.platform === "win32" ? "nebula-ai.exe" : "nebula-ai";
|
|
143
|
-
|
|
144
180
|
async function readVersion() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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");
|
|
153
198
|
}
|
|
154
|
-
|
|
155
199
|
function getCacheDir(version) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|
|
164
230
|
}
|
|
165
|
-
|
|
166
231
|
async function fetchManifest(version) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
const manifestBytes = Buffer.from(await manifestRes.arrayBuffer());
|
|
184
|
-
const signatureBase64 = await sigRes.text();
|
|
185
|
-
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);
|
|
186
247
|
}
|
|
187
|
-
|
|
188
248
|
async function downloadBinary(version, destPath) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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`);
|
|
262
|
+
try {
|
|
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;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ─── update-check runtime ───────────────────────────────────────────────────
|
|
281
|
+
/**
|
|
282
|
+
* If the cached check result is stale (or missing), spawn a detached
|
|
283
|
+
* background process to fetch the latest version from the npm registry and
|
|
284
|
+
* write it to the cache file. The process is unref'd so it does not keep the
|
|
285
|
+
* launcher alive.
|
|
286
|
+
*/
|
|
287
|
+
function maybeSpawnUpdateCheck(currentVersion) {
|
|
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 = `
|
|
300
|
+
const fs = require("node:fs");
|
|
301
|
+
const path = require("node:path");
|
|
302
|
+
const cachePath = ${JSON.stringify(cachePath)};
|
|
303
|
+
fetch("https://registry.npmjs.org/nebula-ai/${distTag}", {
|
|
304
|
+
headers: { "Accept": "application/vnd.npm.install-v1+json" },
|
|
305
|
+
signal: AbortSignal.timeout(10000),
|
|
306
|
+
})
|
|
307
|
+
.then(r => { if (!r.ok) throw new Error(r.status); return r.json(); })
|
|
308
|
+
.then(data => {
|
|
309
|
+
if (typeof data.version === "string") {
|
|
310
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
311
|
+
fs.writeFileSync(cachePath, JSON.stringify({
|
|
312
|
+
latest: data.version,
|
|
313
|
+
channel: "${distTag}",
|
|
314
|
+
checkedAt: Date.now(),
|
|
315
|
+
}));
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
.catch(() => {});
|
|
319
|
+
`;
|
|
221
320
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Read the cached check result (written by a *previous* invocation's
|
|
333
|
+
* background process) and print a one-line update notification to stderr
|
|
334
|
+
* if a newer version is available.
|
|
335
|
+
*/
|
|
336
|
+
function maybeShowUpdateNotification(currentVersion) {
|
|
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.
|
|
225
350
|
}
|
|
226
|
-
throw err;
|
|
227
|
-
}
|
|
228
351
|
}
|
|
229
|
-
|
|
230
352
|
async function main() {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
version = await readVersion();
|
|
252
|
-
} catch (err) {
|
|
253
|
-
console.error(`nebula-ai: ${err instanceof Error ? err.message : err}`);
|
|
254
|
-
process.exit(1);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const cacheDir = getCacheDir(version);
|
|
258
|
-
const binaryPath = join(cacheDir, binName);
|
|
259
|
-
|
|
260
|
-
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;
|
|
261
373
|
try {
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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);
|
|
282
425
|
}
|
|
283
|
-
|
|
284
426
|
// Only run main() when this file is the CLI entrypoint, so tests can
|
|
285
427
|
// import the pure helpers above without triggering a download.
|
|
286
428
|
//
|
|
@@ -290,16 +432,16 @@ async function main() {
|
|
|
290
432
|
// a naive string compare would always fail and main() would silently never
|
|
291
433
|
// run. realpathSync both sides so the comparison survives the bin shim.
|
|
292
434
|
function isCliEntrypoint() {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
+
}
|
|
302
444
|
}
|
|
303
445
|
if (isCliEntrypoint()) {
|
|
304
|
-
|
|
446
|
+
main();
|
|
305
447
|
}
|