nukebase 2.0.2 → 2.0.3

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/nukebase.js CHANGED
@@ -97,7 +97,9 @@ if (args.has('version') && !command) {
97
97
  } else if (ACCOUNT_COMMANDS.includes(command) || command === 'setup') {
98
98
  Promise.resolve()
99
99
  .then(() => require('../lib/account-commands')[command === 'setup' ? 'start' : command](args))
100
- .then(() => process.exit(0), err => { console.error('❌', err.message); process.exit(1); });
100
+ // No process.exit(): see fail() in lib/account-commands.js. Nothing these
101
+ // commands start outlives them, so Node exits as soon as they finish.
102
+ .catch(err => { console.error('❌', err.message); process.exitCode = 1; });
101
103
  } else if (PROJECT_COMMANDS.includes(command)) {
102
104
  require('../lib/project-commands');
103
105
  } else {
@@ -14,9 +14,12 @@ const PROJECT_NAME_RE = /^[a-z][a-z0-9_-]{2,30}$/;
14
14
 
15
15
  const cookieFor = (acct) => `uid=${acct.uid}; token=${acct.token}`;
16
16
 
17
+ // Stop the command with a message. Thrown rather than process.exit(): exiting
18
+ // while Node is still closing finished HTTP connections crashes it on Windows
19
+ // (libuv "UV_HANDLE_CLOSING" assertion). bin/nukebase.js prints it once and
20
+ // sets the exit code, and Node exits by itself when the work is done.
17
21
  function fail(message) {
18
- console.error(`❌ ${message}`);
19
- process.exit(1);
22
+ throw new Error(message);
20
23
  }
21
24
 
22
25
  function describe(acct) {
@@ -197,6 +200,37 @@ async function chooseAccount(args, hint, autoYes) {
197
200
  return picked;
198
201
  }
199
202
 
203
+ // ---- plan -----------------------------------------------------------------
204
+
205
+ const FREE_TIER = 'spark';
206
+
207
+ // The account's plan, starting the free plan first when it has none. An
208
+ // account whose paid plan was cancelled is left alone: that's a choice for
209
+ // the dashboard. Returns the plan name, or null if it couldn't be told.
210
+ async function ensurePlan(api, acct) {
211
+ const call = async (path) => {
212
+ const r = await fetch(`${api}${path}`, {
213
+ method: 'POST',
214
+ headers: { 'Content-Type': 'application/json', Cookie: cookieFor(acct) },
215
+ body: '{}'
216
+ });
217
+ let data = {};
218
+ try { data = await r.json(); } catch {}
219
+ return { r, data };
220
+ };
221
+ let who;
222
+ try { who = await call('/cli/whoami'); } catch { return null; }
223
+ if (who.r.status === 401) fail(`The sign-in for ${acct.label} is no longer valid. Run: nukebase login --email ${acct.label}`);
224
+ if (!who.r.ok) return null;
225
+ if (who.data.plan || who.data.canceled) return who.data.plan || null;
226
+
227
+ const started = await call('/start-free').catch(() => null);
228
+ if (!started || !started.r.ok) return null; // the create below says why
229
+ console.log(`\n✓ You're on the free plan: $${started.data.credit || 10} of credit each month, 1 app (128 MB), 1 GB storage.`);
230
+ console.log(` Upgrade any time at ${api}/`);
231
+ return 'free';
232
+ }
233
+
200
234
  // ---- create ---------------------------------------------------------------
201
235
 
202
236
  // "256mb", "256", "1gb", "1g", "512 MB" -> megabytes; null when not a size.
@@ -247,6 +281,11 @@ async function createCommand(args) {
247
281
  const api = apiFor(args, acct.api);
248
282
  const host = hostOf(api);
249
283
 
284
+ // Every account starts free: one with no plan is put on the free plan here,
285
+ // so a brand-new `npx nukebase start` goes straight to a running app.
286
+ const plan = await ensurePlan(api, acct);
287
+ const isFree = plan === 'free';
288
+
250
289
  let catalog;
251
290
  try {
252
291
  const r = await fetch(`${api}/tiers`);
@@ -262,6 +301,11 @@ async function createCommand(args) {
262
301
  if (sizeArg && !tier) {
263
302
  fail(`Unknown size "${sizeArg}". One of: ${order.map(k => `${catalog.tiers[k].memoryHigh.replace(/M$/, 'mb').replace(/G$/, 'gb').toLowerCase()} (${k})`).join(', ')}`);
264
303
  }
304
+ if (isFree) {
305
+ // The free plan has one size; no question to ask.
306
+ if (tier && tier !== FREE_TIER) fail(`The free plan runs 128 MB apps only. Upgrade for bigger sizes: ${api}/`);
307
+ tier = FREE_TIER;
308
+ }
265
309
  if (!tier) {
266
310
  if (autoYes || !process.stdin.isTTY) {
267
311
  tier = def;
@@ -275,10 +319,11 @@ async function createCommand(args) {
275
319
  }
276
320
 
277
321
  console.log(`\n Project ${name}`);
278
- console.log(` Size ${tierLabel(catalog, tier)}`);
322
+ console.log(` Size ${tierLabel(catalog, tier)}${isFree ? ' (free plan: paid from your $10 monthly credit)' : ''}`);
279
323
  console.log(` Account ${describe(acct)}`);
280
324
  console.log(` Region ${host}`);
281
- if (!(await confirm('\nCreate it? It is billed to this account. (y/n) '))) { console.log('Aborted. Nothing was created.'); return; }
325
+ const question = isFree ? '\nCreate it? (y/n) ' : '\nCreate it? It is billed to this account. (y/n) ';
326
+ if (!(await confirm(question))) { console.log('Aborted. Nothing was created.'); return; }
282
327
 
283
328
  console.log(`\n🚀 Creating ${name}...`);
284
329
  let res;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nukebase",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "NukeBase CLI: create, deploy and manage NukeBase projects",
5
5
  "bin": {
6
6
  "nukebase": "bin/nukebase.js"