nukebase 2.1.0 → 2.1.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/README.md +2 -2
- package/bin/nukebase.js +21 -9
- package/lib/account-commands.js +10 -2
- package/lib/login.js +100 -38
- package/lib/project-commands.js +5 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ signed in at once; each project folder knows which account owns it, so you can
|
|
|
18
18
|
deploy to different accounts from different terminals at the same time.
|
|
19
19
|
|
|
20
20
|
```
|
|
21
|
-
nukebase login approve in your browser
|
|
21
|
+
nukebase login approve in your browser, or enter the code it shows on any device
|
|
22
22
|
nukebase login --email you@x.com add another account
|
|
23
|
-
nukebase login --device
|
|
23
|
+
nukebase login --device only the code (SSH, a VM, no browser)
|
|
24
24
|
nukebase accounts list signed-in accounts
|
|
25
25
|
nukebase use you@x.com default for commands outside a project
|
|
26
26
|
nukebase logout you@x.com sign out one account
|
package/bin/nukebase.js
CHANGED
|
@@ -24,7 +24,7 @@ if (!(args.has('version') && !args.command)) {
|
|
|
24
24
|
const ACCOUNT_COMMANDS = ['login', 'logout', 'accounts', 'use', 'whoami', 'create', 'link'];
|
|
25
25
|
const PROJECT_COMMANDS = [
|
|
26
26
|
'push', 'pull', 'update', 'rollback', 'restorepoints', 'restore-points', 'backup', 'env',
|
|
27
|
-
'pause', 'restart', '
|
|
27
|
+
'pause', 'restart', 'resume', 'delete', 'domain', 'logs', 'download',
|
|
28
28
|
'regiontransfer', 'region-transfer'
|
|
29
29
|
];
|
|
30
30
|
|
|
@@ -37,9 +37,11 @@ New here? Run: npx nukebase start
|
|
|
37
37
|
signs you in, creates a project and downloads it, ready to build
|
|
38
38
|
|
|
39
39
|
Accounts
|
|
40
|
-
login sign in
|
|
40
|
+
login sign in: approve in your browser, or enter the
|
|
41
|
+
code it shows on any device (VM / SSH)
|
|
41
42
|
login --email you@x.com sign in as a specific account (adds it; others stay)
|
|
42
|
-
login --device
|
|
43
|
+
login --device only the code (no browser is opened)
|
|
44
|
+
login --browser only the browser (no code)
|
|
43
45
|
login --email-link sign in with an emailed link instead
|
|
44
46
|
login --password username + password (the master account)
|
|
45
47
|
logout <email> sign out one account
|
|
@@ -49,8 +51,7 @@ Accounts
|
|
|
49
51
|
whoami who owns this project, and are you signed in
|
|
50
52
|
|
|
51
53
|
New projects
|
|
52
|
-
start [name] [size]
|
|
53
|
-
(inside a project, start = restart)
|
|
54
|
+
start [name] [size] sign in if needed, then create and download
|
|
54
55
|
create <name> [size] [email]
|
|
55
56
|
make a project and download it, e.g.
|
|
56
57
|
nukebase create demoapp123 256mb you@example.com
|
|
@@ -73,7 +74,7 @@ In a project folder (runs as the account that owns the project)
|
|
|
73
74
|
then; discards writes since
|
|
74
75
|
rollback list show restore points (also: restorepoints)
|
|
75
76
|
pause stop the project (base fee + disk still accrue)
|
|
76
|
-
restart |
|
|
77
|
+
restart | resume restart it; also resumes a paused project
|
|
77
78
|
delete delete it on the server; asks for the name
|
|
78
79
|
domain connect a custom domain, step by step
|
|
79
80
|
domain <name> [--email e] connect in one go (DNS already set)
|
|
@@ -101,9 +102,20 @@ section of its package.json.`);
|
|
|
101
102
|
|
|
102
103
|
let command = args.command;
|
|
103
104
|
|
|
104
|
-
// `start`
|
|
105
|
-
//
|
|
106
|
-
|
|
105
|
+
// `start` is the new-user setup (sign in, create, download). It used to double
|
|
106
|
+
// as a restart alias inside a project; now it stops there instead of guessing,
|
|
107
|
+
// so the same words do the same thing in every folder.
|
|
108
|
+
if (command === 'start') {
|
|
109
|
+
const here = require('../lib/project').findProject(process.cwd());
|
|
110
|
+
if (here) {
|
|
111
|
+
const { project } = require('../lib/project').readConfig(here);
|
|
112
|
+
console.error(`This folder is already the NukeBase project "${project}".`);
|
|
113
|
+
console.error(' nukebase restart restart it (resumes a paused project)');
|
|
114
|
+
console.error(' nukebase create <name> make a new project (run it elsewhere)');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
command = 'setup';
|
|
118
|
+
}
|
|
107
119
|
|
|
108
120
|
if (args.has('version') && !command) {
|
|
109
121
|
console.log(version);
|
package/lib/account-commands.js
CHANGED
|
@@ -62,9 +62,14 @@ async function loginCommand(args) {
|
|
|
62
62
|
if (!email) email = (await askLine('Email: ')).trim().toLowerCase();
|
|
63
63
|
if (!email.includes('@')) fail('An email address is required.');
|
|
64
64
|
out = await login.emailLink(api, email);
|
|
65
|
-
} else {
|
|
65
|
+
} else if (args.has('browser')) {
|
|
66
66
|
method = 'browser';
|
|
67
67
|
out = await login.browser(api, { email });
|
|
68
|
+
} else {
|
|
69
|
+
// Browser and device code together, whichever is approved first, so a
|
|
70
|
+
// VM or SSH session can sign in too.
|
|
71
|
+
method = 'browser';
|
|
72
|
+
out = await login.browserOrDevice(api, { email });
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
// Signing in again replaces this machine's token for that account; sign the
|
|
@@ -456,6 +461,7 @@ async function startCommand(args) {
|
|
|
456
461
|
const { parse } = require('./args');
|
|
457
462
|
const passFlags = [];
|
|
458
463
|
for (const f of ['api', 'region']) if (args.get(f)) passFlags.push(`--${f}`, args.get(f));
|
|
464
|
+
for (const f of ['device', 'browser']) if (args.has(f)) passFlags.push(`--${f}`);
|
|
459
465
|
|
|
460
466
|
const emailArg = args.positional.find(a => a.includes('@')) || args.get('as');
|
|
461
467
|
console.log('\n Welcome to NukeBase.');
|
|
@@ -471,7 +477,9 @@ async function startCommand(args) {
|
|
|
471
477
|
: (creds.getDefault() || list[0]);
|
|
472
478
|
}
|
|
473
479
|
if (!acct) {
|
|
474
|
-
console.log(
|
|
480
|
+
console.log(args.has('device')
|
|
481
|
+
? '\n Sign in: open the link below on any device and enter the code.'
|
|
482
|
+
: '\n Confirm your Claude session: click Confirm in the browser window that opens,\n or, with no browser on this machine, enter the code shown below on any device.');
|
|
475
483
|
await loginCommand(parse(['login', ...(emailArg ? ['--email', emailArg] : []), ...passFlags]));
|
|
476
484
|
acct = emailArg ? creds.find(emailArg) : (creds.getDefault() || creds.list()[0]);
|
|
477
485
|
if (!acct) fail('Sign-in did not finish. Run: npx nukebase start');
|
package/lib/login.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
// browser open /cli-auth.html, approve, a one-time code comes back to a
|
|
5
5
|
// local server, swapped (with a PKCE secret) for the token
|
|
6
6
|
// device a short code to type on the website, for SSH / no browser
|
|
7
|
+
// browserOrDevice both at once, whichever is approved first: the default,
|
|
8
|
+
// so a VM or SSH session (where a browser elsewhere cannot reach
|
|
9
|
+
// this machine's 127.0.0.1) can still sign in
|
|
7
10
|
// emailLink the email sign-in link, delivered back to a local server
|
|
8
11
|
// password username + password (the master account)
|
|
9
12
|
//
|
|
@@ -76,8 +79,11 @@ function startLoopback(state) {
|
|
|
76
79
|
finish.res({ query: q, reply });
|
|
77
80
|
});
|
|
78
81
|
const timer = setTimeout(() => { server.close(); finish.rej(new Error('No response within 5 minutes. Run nukebase login again.')); }, LOGIN_TIMEOUT_MS);
|
|
82
|
+
// Stop waiting (the device code won instead). Settling an already
|
|
83
|
+
// settled promise is a no-op, so this is safe after a callback too.
|
|
84
|
+
const close = () => { clearTimeout(timer); server.close(); finish.rej(new Error('Cancelled')); };
|
|
79
85
|
server.on('error', rejectStart);
|
|
80
|
-
server.listen(0, '127.0.0.1', () => resolveStart({ port: server.address().port, result }));
|
|
86
|
+
server.listen(0, '127.0.0.1', () => resolveStart({ port: server.address().port, result, close }));
|
|
81
87
|
});
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -102,11 +108,13 @@ function needsNewerServer(r, api) {
|
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
|
|
111
|
+
// Start a browser sign-in: the loopback server and the approval URL. finish()
|
|
112
|
+
// waits for the approval and swaps its code for the token; close() gives up.
|
|
113
|
+
async function browserStart(api, { email } = {}) {
|
|
106
114
|
const state = b64url(crypto.randomBytes(24));
|
|
107
115
|
const verifier = b64url(crypto.randomBytes(32));
|
|
108
116
|
const challenge = b64url(crypto.createHash('sha256').update(verifier).digest());
|
|
109
|
-
const { port, result } = await startLoopback(state);
|
|
117
|
+
const { port, result, close } = await startLoopback(state);
|
|
110
118
|
|
|
111
119
|
const url = new URL(`${api}/cli-auth.html`);
|
|
112
120
|
url.searchParams.set('port', String(port));
|
|
@@ -115,56 +123,110 @@ async function browser(api, { email } = {}) {
|
|
|
115
123
|
url.searchParams.set('device', deviceName());
|
|
116
124
|
if (email) url.searchParams.set('email', email);
|
|
117
125
|
|
|
126
|
+
async function finish() {
|
|
127
|
+
// The usual reason for a long wait: no account yet, so no sign-in email
|
|
128
|
+
// ever arrives (the server doesn't say which addresses have accounts).
|
|
129
|
+
const hint = setTimeout(() => {
|
|
130
|
+
console.log(`\n Still waiting. No account yet? Sign up at ${api}/start.html first, then run this again.`);
|
|
131
|
+
}, 90 * 1000);
|
|
132
|
+
hint.unref();
|
|
133
|
+
const { query: q, reply } = await result.finally(() => clearTimeout(hint));
|
|
134
|
+
if (q.error) throw new Error('Sign-in was cancelled in the browser.');
|
|
135
|
+
try {
|
|
136
|
+
if (!q.code) throw new Error('The browser did not return a sign-in code.');
|
|
137
|
+
const r = await postJson(`${api}/cli/token`, { code: q.code, verifier });
|
|
138
|
+
needsNewerServer(r, api);
|
|
139
|
+
if (!r.ok) throw new Error(r.data.error || `Sign-in failed (${r.status})`);
|
|
140
|
+
// Signed in: finish the browser on the dashboard, which says so.
|
|
141
|
+
reply({ redirect: `${api}/?cli=signed-in` });
|
|
142
|
+
return r.data;
|
|
143
|
+
} catch (e) {
|
|
144
|
+
reply({ error: e.message });
|
|
145
|
+
throw e;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { url: url.toString(), finish, close };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function browser(api, { email } = {}) {
|
|
152
|
+
const b = await browserStart(api, { email });
|
|
118
153
|
console.log('\n Opening your browser to approve this CLI. If it doesn\'t open, visit:');
|
|
119
|
-
console.log(` ${url}\n`);
|
|
154
|
+
console.log(` ${b.url}\n`);
|
|
120
155
|
console.log(' Waiting for approval… (Ctrl+C to cancel)');
|
|
121
|
-
openBrowser(url
|
|
122
|
-
|
|
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));
|
|
130
|
-
if (q.error) throw new Error('Sign-in was cancelled in the browser.');
|
|
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
|
-
}
|
|
156
|
+
openBrowser(b.url);
|
|
157
|
+
return b.finish();
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
async function
|
|
160
|
+
async function deviceStart(api) {
|
|
146
161
|
const start = await postJson(`${api}/cli/device-start`, { device: deviceName() });
|
|
147
162
|
needsNewerServer(start, api);
|
|
148
163
|
if (!start.ok) throw new Error(start.data.error || `Could not start sign-in (${start.status})`);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log('\n On any device, open:');
|
|
152
|
-
console.log(` ${verifyUrl}`);
|
|
153
|
-
console.log(' and enter this code:\n');
|
|
154
|
-
console.log(` ${userCode}\n`);
|
|
155
|
-
console.log(` Waiting for approval… (expires in ${Math.round(expiresIn / 60)} minutes)`);
|
|
164
|
+
return start.data;
|
|
165
|
+
}
|
|
156
166
|
|
|
167
|
+
// Poll until the code is approved. `stop.cancelled` ends it quietly (the
|
|
168
|
+
// browser won instead), resolving null.
|
|
169
|
+
async function devicePoll(api, { deviceCode, interval = 5, expiresIn = 600 }, stop = {}) {
|
|
157
170
|
let wait = Math.max(interval, 2) * 1000;
|
|
158
171
|
const deadline = Date.now() + expiresIn * 1000;
|
|
159
172
|
while (Date.now() < deadline) {
|
|
160
|
-
await new Promise(r => setTimeout(r, wait));
|
|
173
|
+
await new Promise(r => setTimeout(r, wait).unref());
|
|
174
|
+
if (stop.cancelled) return null;
|
|
161
175
|
const r = await postJson(`${api}/cli/device-poll`, { deviceCode });
|
|
162
|
-
if (
|
|
176
|
+
if (stop.cancelled) return null;
|
|
177
|
+
if (r.status === 410) throw new Error('The code expired. Run nukebase login again.');
|
|
163
178
|
if (!r.ok) throw new Error(r.data.error || `Sign-in failed (${r.status})`);
|
|
164
179
|
if (r.data.status === 'slow_down') { wait += 1000; continue; }
|
|
165
180
|
if (r.data.status === 'approved') return { uid: r.data.uid, token: r.data.token, label: r.data.label };
|
|
166
181
|
}
|
|
167
|
-
throw new Error('The code expired. Run nukebase login
|
|
182
|
+
throw new Error('The code expired. Run nukebase login again.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function device(api) {
|
|
186
|
+
const d = await deviceStart(api);
|
|
187
|
+
console.log('\n On any device, open:');
|
|
188
|
+
console.log(` ${d.verifyUrl}`);
|
|
189
|
+
console.log(' and enter this code:\n');
|
|
190
|
+
console.log(` ${d.userCode}\n`);
|
|
191
|
+
console.log(` Waiting for approval… (expires in ${Math.round((d.expiresIn || 600) / 60)} minutes)`);
|
|
192
|
+
return devicePoll(api, d);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The default sign-in: open the browser AND show a device code, and take
|
|
196
|
+
// whichever is approved first. On a laptop the browser wins as before; on a
|
|
197
|
+
// VM or over SSH there is no browser here, and a browser elsewhere cannot
|
|
198
|
+
// reach this machine's 127.0.0.1, so the code is the way in. A server too old
|
|
199
|
+
// for device codes gets the browser alone, exactly as before.
|
|
200
|
+
async function browserOrDevice(api, { email } = {}) {
|
|
201
|
+
let d = null;
|
|
202
|
+
try { d = await deviceStart(api); } catch {}
|
|
203
|
+
if (!d || !d.deviceCode) return browser(api, { email });
|
|
204
|
+
|
|
205
|
+
const b = await browserStart(api, { email });
|
|
206
|
+
console.log('\n Opening your browser to approve this CLI. If it doesn\'t open, visit:');
|
|
207
|
+
console.log(` ${b.url}\n`);
|
|
208
|
+
console.log(' No browser on this machine (a VM, SSH, a container)? On any phone or computer, open');
|
|
209
|
+
console.log(` ${d.verifyUrl}`);
|
|
210
|
+
console.log(` and enter the code ${d.userCode}\n`);
|
|
211
|
+
console.log(' Waiting for approval… (Ctrl+C to cancel)');
|
|
212
|
+
openBrowser(b.url);
|
|
213
|
+
|
|
214
|
+
const stop = { cancelled: false };
|
|
215
|
+
const viaBrowser = b.finish();
|
|
216
|
+
const viaDevice = devicePoll(api, d, stop).then((out) => out || new Promise(() => {}));
|
|
217
|
+
// A failed leg (the browser's 5-minute wait running out, say) must not end
|
|
218
|
+
// the sign-in while the other can still be approved: Promise.any settles on
|
|
219
|
+
// the first success and fails only when both have failed.
|
|
220
|
+
try {
|
|
221
|
+
return await Promise.any([viaBrowser, viaDevice]);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
const first = (e && e.errors && e.errors.find(x => x && x.message !== 'Cancelled')) || e;
|
|
224
|
+
throw first;
|
|
225
|
+
} finally {
|
|
226
|
+
stop.cancelled = true;
|
|
227
|
+
viaBrowser.catch(() => {});
|
|
228
|
+
b.close();
|
|
229
|
+
}
|
|
168
230
|
}
|
|
169
231
|
|
|
170
232
|
// Swap an ordinary session for a CLI token, then sign the session out.
|
|
@@ -208,4 +270,4 @@ async function password(api, username, pass) {
|
|
|
208
270
|
return { ...out, label: out.label || username };
|
|
209
271
|
}
|
|
210
272
|
|
|
211
|
-
module.exports = { browser, device, emailLink, password, exchange, deviceName };
|
|
273
|
+
module.exports = { browser, browserOrDevice, device, emailLink, password, exchange, deviceName };
|
package/lib/project-commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Commands that act on the project in the current folder: push, pull, update,
|
|
2
|
-
// rollback, backup, env, pause/restart/delete, domain, logs, download and
|
|
2
|
+
// rollback, backup, env, pause/restart/resume/delete, domain, logs, download and
|
|
3
3
|
// regiontransfer. Loaded by bin/nukebase.js, which runs the command named in
|
|
4
4
|
// process.argv when this module is required.
|
|
5
5
|
const { existsSync, readdirSync, statSync, lstatSync, readlinkSync, symlinkSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmdirSync } = require('fs');
|
|
@@ -828,7 +828,7 @@ async function pauseProject() {
|
|
|
828
828
|
async function restartProject() {
|
|
829
829
|
await connectAndLogin();
|
|
830
830
|
await projectPost('/restart', {}, { label: command });
|
|
831
|
-
console.log(command === 'restart' ? `🔄 Restarting ${project}...` : `▶️
|
|
831
|
+
console.log(command === 'restart' ? `🔄 Restarting ${project}...` : `▶️ Resuming ${project}...`);
|
|
832
832
|
if (AUTO_YES) { await tailLogsUntilSettled(); process.exit(0); }
|
|
833
833
|
watchLogs();
|
|
834
834
|
}
|
|
@@ -1190,7 +1190,7 @@ async function regionTransfer() {
|
|
|
1190
1190
|
if (!acct) {
|
|
1191
1191
|
console.log(`\n🔑 Signing in to ${targetHost} as ${toEmail}...`);
|
|
1192
1192
|
try {
|
|
1193
|
-
const out = await loginFlows.
|
|
1193
|
+
const out = await loginFlows.browserOrDevice(targetApi, { email: toEmail });
|
|
1194
1194
|
creds.put(out.uid, { label: out.label, api: targetApi, token: out.token, method: 'browser', created: Date.now(), lastUsed: Date.now() });
|
|
1195
1195
|
acct = creds.get(out.uid);
|
|
1196
1196
|
} catch (e) {
|
|
@@ -2343,9 +2343,9 @@ if (command === 'push' && args.positional[0] === 'env') {
|
|
|
2343
2343
|
run(backup);
|
|
2344
2344
|
} else if (command === 'pause') {
|
|
2345
2345
|
run(pauseProject);
|
|
2346
|
-
} else if (command === 'restart' || command === '
|
|
2346
|
+
} else if (command === 'restart' || command === 'resume') {
|
|
2347
2347
|
// One endpoint: /restart launches a stopped unit and bounces a running one,
|
|
2348
|
-
// so
|
|
2348
|
+
// so resume is the same call under the name the user reaches for.
|
|
2349
2349
|
run(restartProject);
|
|
2350
2350
|
} else if (command === 'delete') {
|
|
2351
2351
|
run(deleteProject);
|