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 +12 -3
- package/lib/account-commands.js +43 -0
- package/lib/login.js +60 -16
- package/package.json +1 -1
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
|
-
|
|
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');
|
package/lib/account-commands.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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) {
|