nukebase 2.0.0 → 2.0.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/bin/nukebase.js CHANGED
@@ -20,6 +20,9 @@ function usage() {
20
20
 
21
21
  Usage: nukebase <command> (or: npx nukebase <command>)
22
22
 
23
+ New here? Run: npx nukebase start
24
+ signs you in, creates a project and downloads it, ready to build
25
+
23
26
  Accounts
24
27
  login sign in by approving in your browser
25
28
  login --email you@x.com sign in as a specific account (adds it; others stay)
@@ -33,6 +36,8 @@ Accounts
33
36
  whoami who owns this project, and are you signed in
34
37
 
35
38
  New projects
39
+ start [name] [size] outside a project: sign in if needed, then create
40
+ (inside a project, start = restart)
36
41
  create <name> [size] [email]
37
42
  make a project and download it, e.g.
38
43
  nukebase create demoapp123 256mb you@example.com
@@ -79,15 +84,19 @@ dashboard under Signed-in devices).
79
84
  Sign-ins are kept in ~/.nukebase/credentials.json, never in a project.`);
80
85
  }
81
86
 
82
- const command = args.command;
87
+ let command = args.command;
88
+
89
+ // `start` means two things: inside a project it restarts the app (as it always
90
+ // has); anywhere else it is the new-user setup (sign in, create, download).
91
+ if (command === 'start' && !require('../lib/project').findProject(process.cwd())) command = 'setup';
83
92
 
84
93
  if (args.has('version') && !command) {
85
94
  console.log(version);
86
95
  } else if (!command || command === 'help' || (args.has('help') && !command)) {
87
96
  usage();
88
- } else if (ACCOUNT_COMMANDS.includes(command)) {
97
+ } else if (ACCOUNT_COMMANDS.includes(command) || command === 'setup') {
89
98
  Promise.resolve()
90
- .then(() => require('../lib/account-commands')[command](args))
99
+ .then(() => require('../lib/account-commands')[command === 'setup' ? 'start' : command](args))
91
100
  .then(() => process.exit(0), err => { console.error('❌', err.message); process.exit(1); });
92
101
  } else if (PROJECT_COMMANDS.includes(command)) {
93
102
  require('../lib/project-commands');
@@ -295,6 +295,10 @@ async function createCommand(args) {
295
295
  let msg = '';
296
296
  try { msg = (await res.json()).error; } catch {}
297
297
  if (res.status === 401) fail(`The sign-in for ${acct.label} is no longer valid. Run: nukebase login --email ${acct.label}`);
298
+ // No plan, or no credit: both are fixed on the dashboard.
299
+ if (res.status === 403 && /plan|funds|credit|wallet|balance/i.test(msg)) {
300
+ fail(`${msg}\n Choose a plan or add credit at ${api}/`);
301
+ }
298
302
  fail(`Create failed (${res.status}): ${msg || res.statusText}`);
299
303
  }
300
304
 
@@ -319,6 +323,44 @@ async function createCommand(args) {
319
323
  console.log(' nukebase push deploy your changes');
320
324
  }
321
325
 
326
+ // ---- start ----------------------------------------------------------------
327
+ // The one command a new user runs after signing up: sign in if needed, then
328
+ // create a project and download it. `nukebase start [name] [size] [email]`.
329
+ // (Inside a project folder `start` means restart; bin/nukebase.js routes it.)
330
+
331
+ async function startCommand(args) {
332
+ const autoYes = args.has('yes');
333
+ const { parse } = require('./args');
334
+ const passFlags = [];
335
+ for (const f of ['api', 'region']) if (args.get(f)) passFlags.push(`--${f}`, args.get(f));
336
+ if (autoYes) passFlags.push('--yes');
337
+ if (args.has('no-install')) passFlags.push('--no-install');
338
+
339
+ console.log('\n Welcome to NukeBase.');
340
+ const emailArg = args.positional.find(a => a.includes('@'));
341
+ if (!creds.list().length || (emailArg && !creds.find(emailArg))) {
342
+ console.log('\n Step 1 of 2: sign in. Click Allow in the browser window that opens.');
343
+ await loginCommand(parse(['login', ...(emailArg ? ['--email', emailArg] : []), ...passFlags]));
344
+ }
345
+
346
+ console.log(`\n ${creds.list().length ? 'Step 2 of 2: ' : ''}create your project`);
347
+ let name = args.positional.find(a => !a.includes('@') && PROJECT_NAME_RE.test(a));
348
+ if (!name) {
349
+ if (autoYes || !process.stdin.isTTY) fail('Give the project a name, e.g.: nukebase start myapp');
350
+ for (;;) {
351
+ name = (await makeAskLine(false)('\n Project name (lowercase, e.g. myapp): ')).trim().toLowerCase();
352
+ if (!name) fail('No name given.');
353
+ if (PROJECT_NAME_RE.test(name)) break;
354
+ console.log(' 3-31 characters: lowercase letters, numbers, - and _, starting with a letter.');
355
+ }
356
+ }
357
+ const rest = args.positional.filter(a => a !== name);
358
+ await createCommand(parse(['create', name, ...rest, ...passFlags]));
359
+
360
+ console.log('\n Building with Claude Code? Open the folder and describe your app.');
361
+ console.log(' Its CLAUDE.md explains NukeBase, and Claude can deploy with: npx nukebase push --yes');
362
+ }
363
+
322
364
  // ---- link -----------------------------------------------------------------
323
365
 
324
366
  async function linkCommand(args) {
@@ -364,6 +406,7 @@ module.exports = {
364
406
  use: useCommand,
365
407
  whoami: whoamiCommand,
366
408
  create: createCommand,
409
+ start: startCommand,
367
410
  link: linkCommand,
368
411
  resolveTier, sizeToMb
369
412
  };
package/lib/login.js CHANGED
@@ -31,7 +31,13 @@ function page(mark, title, message, accent) {
31
31
  }
32
32
 
33
33
  // A one-shot server on 127.0.0.1 for the browser to come back to. Resolves
34
- // with the callback's query once one arrives carrying the right state.
34
+ // with { query, reply } once a callback arrives carrying the right state.
35
+ // The browser's request is held open until the caller calls reply(), so the
36
+ // browser only lands on "signed in" once the CLI really has its token:
37
+ // reply({ redirect }) send the browser on (the dashboard)
38
+ // reply({ error }) show what went wrong
39
+ // reply() a plain "signed in, close this tab" page
40
+ // If reply() never comes, the page is sent after 30 seconds anyway.
35
41
  function startLoopback(state) {
36
42
  return new Promise((resolveStart, rejectStart) => {
37
43
  let finish;
@@ -44,14 +50,30 @@ function startLoopback(state) {
44
50
  res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
45
51
  return res.end(page('✕', 'Sign-in error', 'This sign-in could not be verified. Return to your terminal and try again.', '#dc2626'));
46
52
  }
47
- const denied = q.error === 'access_denied';
48
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
49
- res.end(denied
50
- ? page('✕', 'Cancelled', 'The CLI was not signed in. You can close this tab.', '#64748b')
51
- : page('✓', 'CLI signed in', 'You can close this tab and return to your terminal.', '#16a34a'));
52
53
  clearTimeout(timer);
53
54
  server.close();
54
- finish.res(q);
55
+ if (q.error === 'access_denied') {
56
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
57
+ res.end(page('✕', 'Cancelled', 'The CLI was not signed in. You can close this tab.', '#64748b'));
58
+ return finish.res({ query: q, reply: () => {} });
59
+ }
60
+ let sent = false;
61
+ const reply = ({ redirect, error } = {}) => {
62
+ if (sent) return;
63
+ sent = true;
64
+ clearTimeout(fallback);
65
+ if (redirect) {
66
+ res.writeHead(302, { Location: redirect });
67
+ return res.end();
68
+ }
69
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
70
+ res.end(error
71
+ ? page('✕', 'Sign-in failed', `${String(error).replace(/[<>&"]/g, '')} Return to your terminal.`, '#dc2626')
72
+ : page('✓', 'CLI signed in', 'You can close this tab and return to your terminal.', '#16a34a'));
73
+ };
74
+ const fallback = setTimeout(() => reply(), 30 * 1000);
75
+ fallback.unref();
76
+ finish.res({ query: q, reply });
55
77
  });
56
78
  const timer = setTimeout(() => { server.close(); finish.rej(new Error('No response within 5 minutes. Run nukebase login again.')); }, LOGIN_TIMEOUT_MS);
57
79
  server.on('error', rejectStart);
@@ -98,13 +120,26 @@ async function browser(api, { email } = {}) {
98
120
  console.log(' Waiting for approval… (Ctrl+C to cancel)');
99
121
  openBrowser(url.toString());
100
122
 
101
- const q = await result;
123
+ // The usual reason for a long wait: no account yet, so no sign-in email
124
+ // ever arrives (the server doesn't say which addresses have accounts).
125
+ const hint = setTimeout(() => {
126
+ console.log(`\n Still waiting. No account yet? Sign up at ${api}/start.html first, then run this again.`);
127
+ }, 90 * 1000);
128
+ hint.unref();
129
+ const { query: q, reply } = await result.finally(() => clearTimeout(hint));
102
130
  if (q.error) throw new Error('Sign-in was cancelled in the browser.');
103
- if (!q.code) throw new Error('The browser did not return a sign-in code.');
104
- const r = await postJson(`${api}/cli/token`, { code: q.code, verifier });
105
- needsNewerServer(r, api);
106
- if (!r.ok) throw new Error(r.data.error || `Sign-in failed (${r.status})`);
107
- return r.data;
131
+ try {
132
+ if (!q.code) throw new Error('The browser did not return a sign-in code.');
133
+ const r = await postJson(`${api}/cli/token`, { code: q.code, verifier });
134
+ needsNewerServer(r, api);
135
+ if (!r.ok) throw new Error(r.data.error || `Sign-in failed (${r.status})`);
136
+ // Signed in: finish the browser on the dashboard, which says so.
137
+ reply({ redirect: `${api}/?cli=signed-in` });
138
+ return r.data;
139
+ } catch (e) {
140
+ reply({ error: e.message });
141
+ throw e;
142
+ }
108
143
  }
109
144
 
110
145
  async function device(api) {
@@ -150,9 +185,18 @@ async function emailLink(api, email) {
150
185
  console.log(`\n ✉ Sign-in link sent to ${email} (if it has a NukeBase account).`);
151
186
  console.log(' Open it in a browser on this computer.');
152
187
  console.log('\n Waiting for the link to be clicked… (expires in 5 minutes)');
153
- const q = await result;
154
- if (!q.uid || !q.token) throw new Error('The sign-in link did not return a session.');
155
- return exchange(api, q.uid, q.token);
188
+ // This browser has no dashboard session (the link signed in the CLI, not
189
+ // the browser), so it gets the plain "signed in" page rather than a redirect.
190
+ const { query: q, reply } = await result;
191
+ try {
192
+ if (!q.uid || !q.token) throw new Error('The sign-in link did not return a session.');
193
+ const out = await exchange(api, q.uid, q.token);
194
+ reply();
195
+ return out;
196
+ } catch (e) {
197
+ reply({ error: e.message });
198
+ throw e;
199
+ }
156
200
  }
157
201
 
158
202
  async function password(api, username, pass) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nukebase",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "NukeBase CLI: create, deploy and manage NukeBase projects",
5
5
  "bin": {
6
6
  "nukebase": "bin/nukebase.js"