nukebase 2.2.1 → 2.2.2
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/lib/account-commands.js +87 -31
- package/package.json +1 -1
package/lib/account-commands.js
CHANGED
|
@@ -296,10 +296,69 @@ function namePackage(dir, name) {
|
|
|
296
296
|
} catch { return false; }
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
299
|
+
// Read a download to the end, showing how much has arrived: one line that
|
|
300
|
+
// updates in a terminal, or a line every 10 MB when the output goes to a file
|
|
301
|
+
// or an AI agent (where \r updates would pile up). The archives are streamed
|
|
302
|
+
// without a length, so it shows MB so far, not a percentage.
|
|
303
|
+
async function readWithProgress(res, label) {
|
|
304
|
+
const tty = process.stdout.isTTY;
|
|
305
|
+
const mb = (n) => (n / 1048576).toFixed(1);
|
|
306
|
+
const chunks = [];
|
|
307
|
+
let got = 0, lastShown = 0, lastLine = 0;
|
|
308
|
+
const show = (final) => {
|
|
309
|
+
if (tty) process.stdout.write(`\r ${label}… ${mb(got)} MB${final ? '\n' : ''}`);
|
|
310
|
+
else if (final || got - lastLine >= 10 * 1048576) { console.log(` ${label}… ${mb(got)} MB`); lastLine = got; }
|
|
311
|
+
};
|
|
312
|
+
if (!res.body || typeof res.body.getReader !== 'function') {
|
|
313
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
314
|
+
got = buf.length; show(true);
|
|
315
|
+
return buf;
|
|
316
|
+
}
|
|
317
|
+
const reader = res.body.getReader();
|
|
318
|
+
try {
|
|
319
|
+
for (;;) {
|
|
320
|
+
const { done, value } = await reader.read();
|
|
321
|
+
if (done) break;
|
|
322
|
+
chunks.push(Buffer.from(value));
|
|
323
|
+
got += value.length;
|
|
324
|
+
if (Date.now() - lastShown > 250) { lastShown = Date.now(); show(false); }
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
// End the half-drawn progress line, so whatever is printed next starts clean.
|
|
328
|
+
if (tty && got) process.stdout.write('\n');
|
|
329
|
+
throw e;
|
|
330
|
+
}
|
|
331
|
+
show(true);
|
|
332
|
+
return Buffer.concat(chunks);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The project's files as the server has them, without node_modules (an
|
|
336
|
+
// `npm install` follows; servers too old for ?skip send them anyway). One
|
|
337
|
+
// retry, since this runs right after a dropped connection.
|
|
338
|
+
async function downloadExisting({ acct, api, name }) {
|
|
339
|
+
const headers = { 'X-Project': name, Cookie: cookieFor(acct) };
|
|
340
|
+
const url = `${api}/download-project?project=${encodeURIComponent(name)}&skip=node_modules`;
|
|
341
|
+
let lastErr = null;
|
|
342
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
343
|
+
try {
|
|
344
|
+
const dl = await fetch(url, { headers });
|
|
345
|
+
if (!dl.ok) { lastErr = new Error(`the server answered ${dl.status}`); continue; }
|
|
346
|
+
return await readWithProgress(dl, `Downloading ${name}`);
|
|
347
|
+
} catch (e) {
|
|
348
|
+
lastErr = e;
|
|
349
|
+
if (attempt === 1) console.log(' (download interrupted, trying again)');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
fail(`${name} exists but could not be downloaded (${lastErr.cause?.message || lastErr.message}).\n` +
|
|
353
|
+
` In an empty folder, run: nukebase link ${name} then: nukebase pull --yes then: npm install`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Create `name` on the server and unpack it into `dest`. When the account
|
|
357
|
+
// already has a project by that name: with `reuse`, that project is
|
|
358
|
+
// downloaded into `dest` instead (`start`: running it again finishes the
|
|
359
|
+
// job rather than making name-2); without, returns { exists: true }. Any
|
|
360
|
+
// other failure stops the command.
|
|
361
|
+
async function createProject({ acct, api, name, tier, dest, install, reuse = false }) {
|
|
303
362
|
console.log(`\n🚀 Creating ${name}...`);
|
|
304
363
|
const headers = (extra = {}) => ({ ...extra, 'X-Project': name, Cookie: cookieFor(acct) });
|
|
305
364
|
|
|
@@ -325,14 +384,7 @@ async function createProject({ acct, api, name, tier, dest, install }) {
|
|
|
325
384
|
}
|
|
326
385
|
if (!probe.ok) fail(`Create failed: ${why} (and the server answered ${probe.status} when asked whether ${name} exists).`);
|
|
327
386
|
console.log(` (the connection dropped while the server finished setting up ${name} — it exists, downloading it)`);
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
dl = await fetch(`${api}/download-project?project=${encodeURIComponent(name)}`, { headers: headers() });
|
|
331
|
-
} catch (e) {
|
|
332
|
-
fail(`${name} was created but could not be downloaded (${e.cause?.message || e.message}). Link to it: nukebase link ${name}`);
|
|
333
|
-
}
|
|
334
|
-
if (!dl.ok) fail(`${name} was created but could not be downloaded (${dl.status}). Link to it: nukebase link ${name}`);
|
|
335
|
-
return Buffer.from(await dl.arrayBuffer());
|
|
387
|
+
return downloadExisting({ acct, api, name });
|
|
336
388
|
}
|
|
337
389
|
|
|
338
390
|
let res = null, zip = null;
|
|
@@ -348,19 +400,25 @@ async function createProject({ acct, api, name, tier, dest, install }) {
|
|
|
348
400
|
if (res && !res.ok) {
|
|
349
401
|
let msg = '';
|
|
350
402
|
try { msg = (await res.json()).error; } catch {}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
403
|
+
const taken = res.status === 400 && /already exists/i.test(msg || '');
|
|
404
|
+
if (taken && !reuse) return { exists: true };
|
|
405
|
+
if (taken) {
|
|
406
|
+
console.log(` ${name} already exists in your account, so downloading it here instead of making a new one.`);
|
|
407
|
+
zip = await downloadExisting({ acct, api, name });
|
|
408
|
+
} else {
|
|
409
|
+
if (res.status === 401) fail(`The sign-in for ${acct.label} is no longer valid. Run: nukebase login --email ${acct.label}`);
|
|
410
|
+
// No plan or no credit: fixed on the dashboard.
|
|
411
|
+
if (res.status === 403 && /plan|funds|credit|wallet|balance|upgrade/i.test(msg)) {
|
|
412
|
+
fail(`${msg}\n Manage your plan at ${api}/`);
|
|
413
|
+
}
|
|
414
|
+
fail(`Create failed (${res.status}): ${msg || res.statusText}`);
|
|
356
415
|
}
|
|
357
|
-
fail(`Create failed (${res.status}): ${msg || res.statusText}`);
|
|
358
416
|
}
|
|
359
417
|
|
|
360
418
|
// The server finishes setup as the ZIP finishes streaming, so read it all.
|
|
361
419
|
// A drop here is the same case as above.
|
|
362
420
|
if (!zip) {
|
|
363
|
-
try { zip =
|
|
421
|
+
try { zip = await readWithProgress(res, `Downloading ${name}`); }
|
|
364
422
|
catch (e) { zip = await recover(e); }
|
|
365
423
|
}
|
|
366
424
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -373,7 +431,7 @@ async function createProject({ acct, api, name, tier, dest, install }) {
|
|
|
373
431
|
console.log(`✓ Downloaded ${count} files into ${path.relative(process.cwd(), dest) || 'this folder'}`);
|
|
374
432
|
|
|
375
433
|
if (install && fs.existsSync(path.join(dest, 'package.json'))) {
|
|
376
|
-
console.log('\n📦 npm install
|
|
434
|
+
console.log('\n📦 Installing packages (npm install)…\n');
|
|
377
435
|
// One command string under a shell: npm is npm.cmd on Windows, which
|
|
378
436
|
// needs a shell, and Node 26 deprecates passing separate args with one.
|
|
379
437
|
const r = spawnSync('npm install', [], { cwd: dest, stdio: 'inherit', shell: true });
|
|
@@ -496,19 +554,17 @@ async function startCommand(args) {
|
|
|
496
554
|
const intoHere = isEmptyFolder(here);
|
|
497
555
|
const given = args.positional.find(a => !a.includes('@') && PROJECT_NAME_RE.test(a) && !resolveTier(a, catalog));
|
|
498
556
|
const base = given || (intoHere && nameFromFolder(here)) || 'myapp';
|
|
499
|
-
|
|
557
|
+
// One name. If the account already has a project by it, that project is
|
|
558
|
+
// downloaded here rather than a name-2 being created: running start again
|
|
559
|
+
// after a hiccup finishes the job.
|
|
560
|
+
const name = base;
|
|
561
|
+
const dest = intoHere ? here : path.resolve(here, name);
|
|
562
|
+
if (!intoHere && fs.existsSync(dest) && fs.readdirSync(dest).length) {
|
|
563
|
+
fail(`There is already a folder named ${name} here with files in it. Run this in an empty folder, or choose another name: npx nukebase start <name>`);
|
|
564
|
+
}
|
|
500
565
|
|
|
501
566
|
console.log(` Size: ${tierLabel(catalog, tier)}${isFree ? ' (paid from your $10 monthly credit)' : ''}`);
|
|
502
|
-
|
|
503
|
-
for (const candidate of candidates) {
|
|
504
|
-
dest = intoHere ? here : path.resolve(here, candidate);
|
|
505
|
-
if (!intoHere && fs.existsSync(dest) && fs.readdirSync(dest).length) continue;
|
|
506
|
-
const out = await createProject({ acct, api, name: candidate, tier, dest, install: !args.has('no-install') });
|
|
507
|
-
if (out.exists) { console.log(` (${candidate} is taken, trying another name)`); continue; }
|
|
508
|
-
created = out; name = candidate;
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
511
|
-
if (!created) fail(given ? `You already have a project named ${given}. Pick another name: npx nukebase start <name>` : 'Could not find a free project name. Run: npx nukebase start <name>');
|
|
567
|
+
const created = await createProject({ acct, api, name, tier, dest, install: !args.has('no-install'), reuse: true });
|
|
512
568
|
|
|
513
569
|
// Written for whoever runs this next, person or AI agent.
|
|
514
570
|
console.log(`\n✅ Your app "${name}" is live at ${created.url}`);
|