nukebase 2.0.6 → 2.1.1
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 +4 -0
- package/bin/nukebase.js +20 -8
- package/lib/account-commands.js +79 -11
- package/lib/project-commands.js +37 -23
- package/lib/project.js +125 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,6 +32,10 @@ CLI sign-ins can deploy and manage projects but can't change billing, and stay
|
|
|
32
32
|
signed in until you sign them out here or under **Signed-in devices** on the
|
|
33
33
|
dashboard.
|
|
34
34
|
|
|
35
|
+
A project's settings — which account owns it, which region it lives in,
|
|
36
|
+
`exclude`, `backup` — are the `nukebase` section of its `package.json`.
|
|
37
|
+
Push and pull carry it, so a fresh clone is already linked.
|
|
38
|
+
|
|
35
39
|
## Projects
|
|
36
40
|
|
|
37
41
|
```
|
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
|
|
|
@@ -49,8 +49,7 @@ Accounts
|
|
|
49
49
|
whoami who owns this project, and are you signed in
|
|
50
50
|
|
|
51
51
|
New projects
|
|
52
|
-
start [name] [size]
|
|
53
|
-
(inside a project, start = restart)
|
|
52
|
+
start [name] [size] sign in if needed, then create and download
|
|
54
53
|
create <name> [size] [email]
|
|
55
54
|
make a project and download it, e.g.
|
|
56
55
|
nukebase create demoapp123 256mb you@example.com
|
|
@@ -73,7 +72,7 @@ In a project folder (runs as the account that owns the project)
|
|
|
73
72
|
then; discards writes since
|
|
74
73
|
rollback list show restore points (also: restorepoints)
|
|
75
74
|
pause stop the project (base fee + disk still accrue)
|
|
76
|
-
restart |
|
|
75
|
+
restart | resume restart it; also resumes a paused project
|
|
77
76
|
delete delete it on the server; asks for the name
|
|
78
77
|
domain connect a custom domain, step by step
|
|
79
78
|
domain <name> [--email e] connect in one go (DNS already set)
|
|
@@ -94,14 +93,27 @@ Options
|
|
|
94
93
|
CI: set NUKEBASE_TOKEN and NUKEBASE_UID (create a build token on the
|
|
95
94
|
dashboard under Signed-in devices).
|
|
96
95
|
|
|
97
|
-
Sign-ins are kept in ~/.nukebase/credentials.json, never in a project
|
|
96
|
+
Sign-ins are kept in ~/.nukebase/credentials.json, never in a project.
|
|
97
|
+
A project's settings (owner, region, exclude, backup) are the "nukebase"
|
|
98
|
+
section of its package.json.`);
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
let command = args.command;
|
|
101
102
|
|
|
102
|
-
// `start`
|
|
103
|
-
//
|
|
104
|
-
|
|
103
|
+
// `start` is the new-user setup (sign in, create, download). It used to double
|
|
104
|
+
// as a restart alias inside a project; now it stops there instead of guessing,
|
|
105
|
+
// so the same words do the same thing in every folder.
|
|
106
|
+
if (command === 'start') {
|
|
107
|
+
const here = require('../lib/project').findProject(process.cwd());
|
|
108
|
+
if (here) {
|
|
109
|
+
const { project } = require('../lib/project').readConfig(here);
|
|
110
|
+
console.error(`This folder is already the NukeBase project "${project}".`);
|
|
111
|
+
console.error(' nukebase restart restart it (resumes a paused project)');
|
|
112
|
+
console.error(' nukebase create <name> make a new project (run it elsewhere)');
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
command = 'setup';
|
|
116
|
+
}
|
|
105
117
|
|
|
106
118
|
if (args.has('version') && !command) {
|
|
107
119
|
console.log(version);
|
package/lib/account-commands.js
CHANGED
|
@@ -7,7 +7,7 @@ const { spawnSync } = require('child_process');
|
|
|
7
7
|
const creds = require('./credentials');
|
|
8
8
|
const login = require('./login');
|
|
9
9
|
const unzip = require('./unzip');
|
|
10
|
-
const { findProject, readConfig, writeConfig, newConfig } = require('./project');
|
|
10
|
+
const { findProject, hasConfig, readConfig, writeConfig, newConfig } = require('./project');
|
|
11
11
|
const { hostOf, apiFor, makeConfirm, makeAskLine, askHidden } = require('./ui');
|
|
12
12
|
|
|
13
13
|
const PROJECT_NAME_RE = /^[a-z][a-z0-9_-]{2,30}$/;
|
|
@@ -224,7 +224,7 @@ async function ensurePlan(api, acct) {
|
|
|
224
224
|
|
|
225
225
|
const started = await call('/start-free').catch(() => null);
|
|
226
226
|
if (!started || !started.r.ok) return null; // the create below says why
|
|
227
|
-
console.log(`\n✓ You're on the free plan: $${started.data.credit || 10} of credit each month,
|
|
227
|
+
console.log(`\n✓ You're on the free plan: $${started.data.credit || 10} of credit each month, any number of apps at any size, 1 GB storage.`);
|
|
228
228
|
console.log(` Upgrade any time at ${api}/`);
|
|
229
229
|
return 'free';
|
|
230
230
|
}
|
|
@@ -274,27 +274,78 @@ function sizeList(catalog) {
|
|
|
274
274
|
return catalog.order.map(k => `${catalog.tiers[k].memoryHigh.replace(/M$/, 'mb').replace(/G$/, 'gb').toLowerCase()} (${k})`).join(', ');
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
// A project's package.json must not be named "nukebase": npm resolves
|
|
278
|
+
// `npx nukebase` to the current project when the names match, and a project
|
|
279
|
+
// has no executable, so every command inside it fails with "could not
|
|
280
|
+
// determine executable to run". Older server templates shipped that name;
|
|
281
|
+
// the project's own name (a valid npm name) replaces it. Returns true when
|
|
282
|
+
// it changed something.
|
|
283
|
+
function namePackage(dir, name) {
|
|
284
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
285
|
+
try {
|
|
286
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
287
|
+
if (pkg.name !== 'nukebase' && pkg.name !== 'nukebase-app') return false;
|
|
288
|
+
pkg.name = name;
|
|
289
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
290
|
+
return true;
|
|
291
|
+
} catch { return false; }
|
|
292
|
+
}
|
|
293
|
+
|
|
277
294
|
// Create `name` on the server and unpack it into `dest`. Returns
|
|
278
295
|
// { exists: true } when the account already has a project by that name, so
|
|
279
296
|
// `start` can try the next one; any other failure stops the command.
|
|
280
297
|
async function createProject({ acct, api, name, tier, dest, install }) {
|
|
281
298
|
console.log(`\n🚀 Creating ${name}...`);
|
|
282
|
-
|
|
299
|
+
const headers = (extra = {}) => ({ ...extra, 'X-Project': name, Cookie: cookieFor(acct) });
|
|
300
|
+
|
|
301
|
+
// The connection dropped while the server was setting up. That is not the
|
|
302
|
+
// same as "not created": setup ends with an nginx reload, and a reload
|
|
303
|
+
// sends GOAWAY on an open HTTP/2 connection, which Node's fetch reports as
|
|
304
|
+
// a failed request after the project already exists. (Older servers reload
|
|
305
|
+
// before streaming the ZIP; current ones reload after.) So check, and if it
|
|
306
|
+
// is there, fetch its files the other way rather than leaving a project
|
|
307
|
+
// behind and trying the next name.
|
|
308
|
+
async function recover(err) {
|
|
309
|
+
const why = err.cause?.message || err.message;
|
|
310
|
+
let probe = null;
|
|
311
|
+
try {
|
|
312
|
+
probe = await fetch(`${api}/sync`, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
headers: headers({ 'Content-Type': 'application/json' }),
|
|
315
|
+
body: JSON.stringify({ exclude: [], files: {}, direction: 'pull', symlinks: false })
|
|
316
|
+
});
|
|
317
|
+
} catch {}
|
|
318
|
+
if (!probe || probe.status === 404) {
|
|
319
|
+
fail(`Create failed: ${why}. Check the dashboard before retrying; it may have been created.`);
|
|
320
|
+
}
|
|
321
|
+
if (!probe.ok) fail(`Create failed: ${why} (and the server answered ${probe.status} when asked whether ${name} exists).`);
|
|
322
|
+
console.log(` (the connection dropped while the server finished setting up ${name} — it exists, downloading it)`);
|
|
323
|
+
let dl;
|
|
324
|
+
try {
|
|
325
|
+
dl = await fetch(`${api}/download-project?project=${encodeURIComponent(name)}`, { headers: headers() });
|
|
326
|
+
} catch (e) {
|
|
327
|
+
fail(`${name} was created but could not be downloaded (${e.cause?.message || e.message}). Link to it: nukebase link ${name}`);
|
|
328
|
+
}
|
|
329
|
+
if (!dl.ok) fail(`${name} was created but could not be downloaded (${dl.status}). Link to it: nukebase link ${name}`);
|
|
330
|
+
return Buffer.from(await dl.arrayBuffer());
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let res = null, zip = null;
|
|
283
334
|
try {
|
|
284
335
|
res = await fetch(`${api}/project`, {
|
|
285
336
|
method: 'POST',
|
|
286
|
-
headers: { 'Content-Type': 'application/json'
|
|
337
|
+
headers: headers({ 'Content-Type': 'application/json' }),
|
|
287
338
|
body: JSON.stringify({ tier })
|
|
288
339
|
});
|
|
289
340
|
} catch (e) {
|
|
290
|
-
|
|
341
|
+
zip = await recover(e);
|
|
291
342
|
}
|
|
292
|
-
if (!res.ok) {
|
|
343
|
+
if (res && !res.ok) {
|
|
293
344
|
let msg = '';
|
|
294
345
|
try { msg = (await res.json()).error; } catch {}
|
|
295
346
|
if (res.status === 400 && /already exists/i.test(msg || '')) return { exists: true };
|
|
296
347
|
if (res.status === 401) fail(`The sign-in for ${acct.label} is no longer valid. Run: nukebase login --email ${acct.label}`);
|
|
297
|
-
// No plan
|
|
348
|
+
// No plan or no credit: fixed on the dashboard.
|
|
298
349
|
if (res.status === 403 && /plan|funds|credit|wallet|balance|upgrade/i.test(msg)) {
|
|
299
350
|
fail(`${msg}\n Manage your plan at ${api}/`);
|
|
300
351
|
}
|
|
@@ -302,18 +353,25 @@ async function createProject({ acct, api, name, tier, dest, install }) {
|
|
|
302
353
|
}
|
|
303
354
|
|
|
304
355
|
// The server finishes setup as the ZIP finishes streaming, so read it all.
|
|
305
|
-
|
|
356
|
+
// A drop here is the same case as above.
|
|
357
|
+
if (!zip) {
|
|
358
|
+
try { zip = Buffer.from(await res.arrayBuffer()); }
|
|
359
|
+
catch (e) { zip = await recover(e); }
|
|
360
|
+
}
|
|
306
361
|
fs.mkdirSync(dest, { recursive: true });
|
|
307
362
|
const count = unzip.extract(zip, dest);
|
|
308
363
|
|
|
309
364
|
let base = {};
|
|
310
365
|
try { base = readConfig(dest); } catch {}
|
|
311
366
|
writeConfig(dest, newConfig({ api, uid: acct.uid, project: name, account: acct.label }, base));
|
|
367
|
+
namePackage(dest, name);
|
|
312
368
|
console.log(`✓ Downloaded ${count} files into ${path.relative(process.cwd(), dest) || 'this folder'}`);
|
|
313
369
|
|
|
314
370
|
if (install && fs.existsSync(path.join(dest, 'package.json'))) {
|
|
315
371
|
console.log('\n📦 npm install\n');
|
|
316
|
-
|
|
372
|
+
// One command string under a shell: npm is npm.cmd on Windows, which
|
|
373
|
+
// needs a shell, and Node 26 deprecates passing separate args with one.
|
|
374
|
+
const r = spawnSync('npm install', [], { cwd: dest, stdio: 'inherit', shell: true });
|
|
317
375
|
if (r.status !== 0) console.log(`\n⚠️ npm install failed. Run it yourself in ${dest}.`);
|
|
318
376
|
}
|
|
319
377
|
return { ok: true, url: `https://${acct.uid}-${name}.${hostOf(api)}` };
|
|
@@ -402,7 +460,16 @@ async function startCommand(args) {
|
|
|
402
460
|
const emailArg = args.positional.find(a => a.includes('@')) || args.get('as');
|
|
403
461
|
console.log('\n Welcome to NukeBase.');
|
|
404
462
|
|
|
405
|
-
|
|
463
|
+
// With several accounts signed in, ask — creating a project bills the
|
|
464
|
+
// account, so never guess between them. Only interactively: an agent (no
|
|
465
|
+
// terminal) or --yes gets the default, as before.
|
|
466
|
+
let acct = emailArg ? creds.find(emailArg) : null;
|
|
467
|
+
if (!acct && !emailArg) {
|
|
468
|
+
const list = creds.list();
|
|
469
|
+
acct = (list.length > 1 && process.stdin.isTTY && !args.has('yes'))
|
|
470
|
+
? await chooseAccount(args, null, false)
|
|
471
|
+
: (creds.getDefault() || list[0]);
|
|
472
|
+
}
|
|
406
473
|
if (!acct) {
|
|
407
474
|
console.log('\n Confirm your Claude session: click Confirm in the browser window that opens.');
|
|
408
475
|
await loginCommand(parse(['login', ...(emailArg ? ['--email', emailArg] : []), ...passFlags]));
|
|
@@ -469,7 +536,7 @@ async function linkCommand(args) {
|
|
|
469
536
|
|
|
470
537
|
const root = process.cwd();
|
|
471
538
|
let base = {};
|
|
472
|
-
const existing =
|
|
539
|
+
const existing = hasConfig(root);
|
|
473
540
|
if (existing) {
|
|
474
541
|
try { base = readConfig(root); } catch {}
|
|
475
542
|
if (base.uid && base.uid !== acct.uid && !(await makeConfirm(autoYes)(`This folder belongs to ${base.account || base.uid}. Link it to ${acct.label} instead? (y/n) `))) {
|
|
@@ -483,6 +550,7 @@ async function linkCommand(args) {
|
|
|
483
550
|
}
|
|
484
551
|
|
|
485
552
|
module.exports = {
|
|
553
|
+
namePackage,
|
|
486
554
|
login: loginCommand,
|
|
487
555
|
logout: logoutCommand,
|
|
488
556
|
accounts: accountsCommand,
|
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');
|
|
@@ -15,7 +15,7 @@ const tar = require('tar');
|
|
|
15
15
|
const createClient = require('./sdk');
|
|
16
16
|
const creds = require('./credentials');
|
|
17
17
|
const loginFlows = require('./login');
|
|
18
|
-
const { findProject, readConfig, writeConfig, migrateLegacy } = require('./project');
|
|
18
|
+
const { findProject, readConfig, writeConfig, restoreSettings, migrateLegacy } = require('./project');
|
|
19
19
|
const { regionApi, hostOf } = require('./ui');
|
|
20
20
|
|
|
21
21
|
const args = require('./args').parse(process.argv.slice(2));
|
|
@@ -28,18 +28,17 @@ const BIN = require.resolve('../bin/nukebase.js');
|
|
|
28
28
|
|
|
29
29
|
const root = findProject(process.cwd());
|
|
30
30
|
if (!root) {
|
|
31
|
-
console.error('Not inside a NukeBase project (no
|
|
31
|
+
console.error('Not inside a NukeBase project (no package.json with a "nukebase" section here or above).');
|
|
32
32
|
console.error(' nukebase create <name> make a new one');
|
|
33
33
|
console.error(' nukebase link <name> connect this folder to an existing one');
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
36
|
-
const configPath = join(root, 'sys', 'config.json');
|
|
37
36
|
let config = readConfig(root);
|
|
38
37
|
let { project, exclude, API } = config;
|
|
39
38
|
let uid = null, token = null, accountLabel = null;
|
|
40
39
|
|
|
41
40
|
// `--region <name>` / `--api <url>` point THIS invocation at another region
|
|
42
|
-
// without touching
|
|
41
|
+
// without touching package.json. The account is the same everywhere (auth
|
|
43
42
|
// replicates), the project is not — so this is how a command reaches a copy
|
|
44
43
|
// of the project elsewhere: `nukebase delete --region usw3` after a transfer,
|
|
45
44
|
// or the transfer itself driving the target. A region's API is its name as
|
|
@@ -58,7 +57,7 @@ const AS = args.get('as');
|
|
|
58
57
|
// ---------------------------------------------------------------------------
|
|
59
58
|
// The backup set
|
|
60
59
|
// ---------------------------------------------------------------------------
|
|
61
|
-
//
|
|
60
|
+
// package.json carries two path lists. They are INDEPENDENT — each does
|
|
62
61
|
// exactly one job, and neither implies the other:
|
|
63
62
|
//
|
|
64
63
|
// exclude what push and pull skip. Nothing else reads it.
|
|
@@ -101,7 +100,7 @@ const backupPaths = Array.isArray(config.backup) && config.backup.length
|
|
|
101
100
|
// `nukebase rollback backup` — the word "backup" always names the same set.
|
|
102
101
|
const INCLUDE_BACKUP = args.get('include') === 'backup';
|
|
103
102
|
|
|
104
|
-
// `.env` is never part of a code push or pull, whatever
|
|
103
|
+
// `.env` is never part of a code push or pull, whatever package.json says.
|
|
105
104
|
// It is the input to `nukebase env push`, which carries it to a root-owned file
|
|
106
105
|
// OUTSIDE the project on the server. An ordinary push copying it into the
|
|
107
106
|
// project tree would put secrets where the ZIP download and every restore
|
|
@@ -132,7 +131,7 @@ const localExclude = INCLUDE_BACKUP
|
|
|
132
131
|
// idle, 520 Mbps with four at once. So a large transfer is split into STREAMS
|
|
133
132
|
// groups of roughly equal size that move concurrently.
|
|
134
133
|
//
|
|
135
|
-
//
|
|
134
|
+
// package.json "streams": 4 (default, 1-8)
|
|
136
135
|
// --streams <n> override for one command; --streams 1 is the old behaviour
|
|
137
136
|
//
|
|
138
137
|
// Small transfers (under PARALLEL_MIN_BYTES and PARALLEL_MIN_FILES) stay one
|
|
@@ -325,7 +324,7 @@ async function connectAndLogin() {
|
|
|
325
324
|
({ uid, token } = acct);
|
|
326
325
|
accountLabel = acct.label || acct.uid;
|
|
327
326
|
} else {
|
|
328
|
-
if (!config.uid) die('
|
|
327
|
+
if (!config.uid) die('The "nukebase" section of package.json has no "uid", so this folder has no owner.', 'Run: nukebase link <project>');
|
|
329
328
|
const acct = creds.get(config.uid);
|
|
330
329
|
if (!acct) {
|
|
331
330
|
die(`This project belongs to ${config.account || 'uid'} (${config.uid}), which isn't signed in on this machine.`,
|
|
@@ -659,9 +658,9 @@ function cleanHeaders(headers) {
|
|
|
659
658
|
//
|
|
660
659
|
// Creates it through the same /project the dashboard uses — template files,
|
|
661
660
|
// nginx block, unit started, project node with a tier — then returns so the
|
|
662
|
-
// push proceeds and replaces the template with the local files.
|
|
663
|
-
//
|
|
664
|
-
//
|
|
661
|
+
// push proceeds and replaces the template with the local files, package.json
|
|
662
|
+
// and its settings included. The mail key the new region's easy-login needs
|
|
663
|
+
// is in no file: the platform hands it to the process at every start.
|
|
665
664
|
//
|
|
666
665
|
// Size comes from --tier <name>, else a prompt, else the server's default.
|
|
667
666
|
// The server enforces plan and wallet for that region and its error is
|
|
@@ -778,7 +777,7 @@ function askLine(question) {
|
|
|
778
777
|
// ---------------------------------------------------------------------------
|
|
779
778
|
// Every one of these calls the same endpoint the dashboard does, with the same
|
|
780
779
|
// body, so behaviour cannot drift between the two. They all address the project
|
|
781
|
-
// in
|
|
780
|
+
// in package.json.
|
|
782
781
|
|
|
783
782
|
// POST to a project endpoint; resolves the parsed JSON, exits with the server's
|
|
784
783
|
// message on failure. A 401 says how to sign back in.
|
|
@@ -829,7 +828,7 @@ async function pauseProject() {
|
|
|
829
828
|
async function restartProject() {
|
|
830
829
|
await connectAndLogin();
|
|
831
830
|
await projectPost('/restart', {}, { label: command });
|
|
832
|
-
console.log(command === 'restart' ? `🔄 Restarting ${project}...` : `▶️
|
|
831
|
+
console.log(command === 'restart' ? `🔄 Restarting ${project}...` : `▶️ Resuming ${project}...`);
|
|
833
832
|
if (AUTO_YES) { await tailLogsUntilSettled(); process.exit(0); }
|
|
834
833
|
watchLogs();
|
|
835
834
|
}
|
|
@@ -863,7 +862,7 @@ async function deleteProject() {
|
|
|
863
862
|
// Ctrl+C to give up and come back later), then asks the server to verify and
|
|
864
863
|
// issue the certificate — the slow part, up to a minute at Let's Encrypt.
|
|
865
864
|
// --email is the cert contact; it defaults to the account email in
|
|
866
|
-
//
|
|
865
|
+
// package.json.
|
|
867
866
|
async function domainCommand() {
|
|
868
867
|
await connectAndLogin();
|
|
869
868
|
const sub = args.positional[0] || null;
|
|
@@ -1096,7 +1095,7 @@ async function downloadCommand() {
|
|
|
1096
1095
|
// 4 push the backup set up to the target (downtime ends)
|
|
1097
1096
|
// 5 copy the hosted environment variables
|
|
1098
1097
|
// 6 custom domain, if there is one — DNS has to move to the target's IP
|
|
1099
|
-
// 7 point
|
|
1098
|
+
// 7 point package.json at the target
|
|
1100
1099
|
//
|
|
1101
1100
|
// The source is left PAUSED, never deleted: verify the target, then
|
|
1102
1101
|
// `nukebase delete --region <source>` when satisfied. Base fee and disk keep
|
|
@@ -1155,7 +1154,7 @@ async function regionTransfer() {
|
|
|
1155
1154
|
// this uid and token; otherwise — or when `--to <email>` says so — another
|
|
1156
1155
|
// account, signed in on this machine like any other (~/.nukebase). Every
|
|
1157
1156
|
// target step then runs `--as <email>`, so the source steps keep this
|
|
1158
|
-
// identity and
|
|
1157
|
+
// identity and package.json is only rewritten at the very end, when the
|
|
1159
1158
|
// folder changes hands.
|
|
1160
1159
|
let toEmail = args.get('to') && args.get('to').includes('@') ? args.get('to').trim().toLowerCase() : null;
|
|
1161
1160
|
let targetUid = uid, targetToken = token;
|
|
@@ -1253,7 +1252,7 @@ async function regionTransfer() {
|
|
|
1253
1252
|
for (const rest of steps.slice(i)) if (rest.args) console.error(` nukebase ${rest.args.join(' ')}`);
|
|
1254
1253
|
console.error(` nukebase env push --region ${target}${asArgs.length ? ' ' + asArgs.join(' ') : ''} (after writing the variables to .env)`);
|
|
1255
1254
|
if (customDomain) console.error(` nukebase domain ${customDomain} --region ${target} --wait${asArgs.length ? ' ' + asArgs.join(' ') : ''}`);
|
|
1256
|
-
console.error(` then set "
|
|
1255
|
+
console.error(` then set "api" in the "nukebase" section of package.json to ${targetApi}`);
|
|
1257
1256
|
process.exit(1);
|
|
1258
1257
|
}
|
|
1259
1258
|
}
|
|
@@ -1308,7 +1307,7 @@ async function regionTransfer() {
|
|
|
1308
1307
|
fresh.account = creds.get(targetUid)?.label || toEmail;
|
|
1309
1308
|
}
|
|
1310
1309
|
writeConfig(root, fresh);
|
|
1311
|
-
console.log(` ✓
|
|
1310
|
+
console.log(` ✓ package.json "nukebase" api → ${targetApi}${toEmail ? `, account → ${fresh.account}` : ''}`);
|
|
1312
1311
|
|
|
1313
1312
|
const oldAs = toEmail ? ` --as ${accountLabel}` : '';
|
|
1314
1313
|
console.log(`\n✅ "${project}" is running in ${target}: https://${targetUid}-${project}.${targetHost}`);
|
|
@@ -1367,7 +1366,7 @@ async function push() {
|
|
|
1367
1366
|
if (res.status === 401) authExpired();
|
|
1368
1367
|
|
|
1369
1368
|
// The project is not on this server. That is the normal state when the API
|
|
1370
|
-
// in
|
|
1369
|
+
// in package.json was just pointed at a different region — the account
|
|
1371
1370
|
// is the same everywhere, the project is not — so offer to create it here
|
|
1372
1371
|
// and carry on, rather than failing with a bare 404.
|
|
1373
1372
|
if (res.status === 404) {
|
|
@@ -1618,6 +1617,12 @@ async function pull() {
|
|
|
1618
1617
|
const pullSizes = {};
|
|
1619
1618
|
const result = await downloadStreams(root, download, (f) => pullSizes[f] || 0, totalSize);
|
|
1620
1619
|
|
|
1620
|
+
// The server's package.json carries whichever settings were last pushed
|
|
1621
|
+
// to it — from another machine, or with --region another region's copy.
|
|
1622
|
+
// This folder's own settings are what the developer meant; put them back
|
|
1623
|
+
// (also after a stop, since the file may already have landed).
|
|
1624
|
+
if (download.includes('package.json')) restoreSettings(root, config);
|
|
1625
|
+
|
|
1621
1626
|
if (result.stopped) {
|
|
1622
1627
|
console.log('\n🛑 Pull stopped cleanly. Local file deletes were skipped. Re-run `nukebase pull` to resume.');
|
|
1623
1628
|
return;
|
|
@@ -1810,6 +1815,9 @@ async function update() {
|
|
|
1810
1815
|
}
|
|
1811
1816
|
|
|
1812
1817
|
if (!toWrite.length && !hasPackageWork(pkgPlan)) {
|
|
1818
|
+
if (!updateExclude.includes('package.json') && require('./account-commands').namePackage(root, project)) {
|
|
1819
|
+
console.log(`\n✓ package.json name -> "${project}" (was "nukebase"; that name broke npx nukebase in this folder)`);
|
|
1820
|
+
}
|
|
1813
1821
|
console.log('\n✓ Engine is already up to date');
|
|
1814
1822
|
process.exit(0);
|
|
1815
1823
|
}
|
|
@@ -1819,7 +1827,7 @@ async function update() {
|
|
|
1819
1827
|
const pkgCount = pkgPlan.changes.length + (pkgPlan.versionChanged ? 1 : 0);
|
|
1820
1828
|
console.log(`\n📊 Total: ${outdated} to update, ${added} new, ${pkgCount} package.json change(s)`);
|
|
1821
1829
|
console.log(' Local edits to these files will be overwritten — add a path to');
|
|
1822
|
-
console.log(' "updateExclude" in
|
|
1830
|
+
console.log(' "updateExclude" in the "nukebase" section of package.json to protect it permanently');
|
|
1823
1831
|
console.log(' ("package.json" there opts out of dependency pinning).');
|
|
1824
1832
|
|
|
1825
1833
|
const ok = await confirm(`Continue? (y/n) `);
|
|
@@ -1839,6 +1847,12 @@ async function update() {
|
|
|
1839
1847
|
applyPackageChanges(pkgPlan);
|
|
1840
1848
|
console.log(` ✓ package.json (${pkgCount} change(s))`);
|
|
1841
1849
|
}
|
|
1850
|
+
// Projects from older templates are named "nukebase", which breaks
|
|
1851
|
+
// `npx nukebase` inside them (see namePackage). Repaired here as well, so
|
|
1852
|
+
// one `nukebase update` fixes an existing project.
|
|
1853
|
+
if (!updateExclude.includes('package.json') && require('./account-commands').namePackage(root, project)) {
|
|
1854
|
+
console.log(` ✓ package.json name -> "${project}" (was "nukebase"; that name broke npx nukebase in this folder)`);
|
|
1855
|
+
}
|
|
1842
1856
|
|
|
1843
1857
|
console.log(`\n✅ Updated ${toWrite.length} file(s) to ${manifest.version}`);
|
|
1844
1858
|
if (pkgPlan.changes.length) console.log(' → run: npm install');
|
|
@@ -2329,9 +2343,9 @@ if (command === 'push' && args.positional[0] === 'env') {
|
|
|
2329
2343
|
run(backup);
|
|
2330
2344
|
} else if (command === 'pause') {
|
|
2331
2345
|
run(pauseProject);
|
|
2332
|
-
} else if (command === 'restart' || command === '
|
|
2346
|
+
} else if (command === 'restart' || command === 'resume') {
|
|
2333
2347
|
// One endpoint: /restart launches a stopped unit and bounces a running one,
|
|
2334
|
-
// so
|
|
2348
|
+
// so resume is the same call under the name the user reaches for.
|
|
2335
2349
|
run(restartProject);
|
|
2336
2350
|
} else if (command === 'delete') {
|
|
2337
2351
|
run(deleteProject);
|
package/lib/project.js
CHANGED
|
@@ -1,38 +1,133 @@
|
|
|
1
|
-
// A project folder: finding it
|
|
2
|
-
// there by the old per-project CLI into ~/.nukebase.
|
|
1
|
+
// A project folder: finding it and its settings.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Settings live under the "nukebase" key of package.json. Every project has
|
|
4
|
+
// one, `nukebase update` already rewrites it, and npm leaves keys it does not
|
|
5
|
+
// know alone. They hold no account sign-in — only api (the region), uid (which
|
|
6
|
+
// account owns the project), project, account (that account's email, for
|
|
7
|
+
// messages), mailKey (the project's key for hosted sign-in emails, read by the
|
|
8
|
+
// easy-login extension) and the exclude/backup/updateExclude/streams settings.
|
|
9
|
+
// Tokens live in
|
|
10
|
+
// ~/.nukebase/credentials.json (see credentials.js). Push and pull carry
|
|
11
|
+
// package.json, so a fresh clone of a project is already linked.
|
|
12
|
+
//
|
|
13
|
+
// Projects made before 2.1 keep the same settings in sys/config.json. That
|
|
14
|
+
// file is still read, and the first command moves it into package.json. The
|
|
15
|
+
// master project is the exception: its fallback CLI (sys/deploy.js) reads
|
|
16
|
+
// sys/config.json, so there the file is kept and merely stripped of sign-ins.
|
|
7
17
|
|
|
8
18
|
const fs = require('fs');
|
|
9
19
|
const path = require('path');
|
|
10
20
|
const creds = require('./credentials');
|
|
11
21
|
const login = require('./login');
|
|
12
22
|
|
|
13
|
-
const
|
|
23
|
+
const KEY = 'nukebase';
|
|
24
|
+
const DEFAULT_EXCLUDE = ['.env', 'server/data', 'server/backup', 'server/files'];
|
|
14
25
|
const DEFAULT_BACKUP = ['server/data', 'server/files'];
|
|
15
26
|
const SECRET_FIELDS = ['token', 'password', 'username', 'email', 'privateKey'];
|
|
27
|
+
// Written first, in this order, so the section reads the same in every project.
|
|
28
|
+
const FIELD_ORDER = ['api', 'uid', 'project', 'account', 'exclude', 'backup', 'updateExclude', 'streams'];
|
|
29
|
+
|
|
30
|
+
const pkgPathOf = (root) => path.join(root, 'package.json');
|
|
31
|
+
const legacyPathOf = (root) => path.join(root, 'sys', 'config.json');
|
|
32
|
+
const isMaster = (root) => fs.existsSync(path.join(root, 'server', 'region.config.json'));
|
|
33
|
+
|
|
34
|
+
function readPkg(root) {
|
|
35
|
+
try {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPathOf(root), 'utf8'));
|
|
37
|
+
return pkg && typeof pkg === 'object' && !Array.isArray(pkg) ? pkg : null;
|
|
38
|
+
} catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pkgSection(root) {
|
|
42
|
+
const pkg = readPkg(root);
|
|
43
|
+
const s = pkg && pkg[KEY];
|
|
44
|
+
return s && typeof s === 'object' && !Array.isArray(s) ? s : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Does this folder carry project settings, in either place?
|
|
48
|
+
function hasConfig(dir) {
|
|
49
|
+
return !!pkgSection(dir) || fs.existsSync(legacyPathOf(dir));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Settings only in sys/config.json: they have not moved into package.json yet.
|
|
53
|
+
function isLegacy(root) {
|
|
54
|
+
return !pkgSection(root) && fs.existsSync(legacyPathOf(root));
|
|
55
|
+
}
|
|
16
56
|
|
|
17
57
|
function findProject(dir) {
|
|
18
58
|
while (dir !== path.dirname(dir)) {
|
|
19
|
-
if (
|
|
59
|
+
if (hasConfig(dir)) return dir;
|
|
20
60
|
dir = path.dirname(dir);
|
|
21
61
|
}
|
|
22
62
|
return null;
|
|
23
63
|
}
|
|
24
64
|
|
|
25
|
-
|
|
26
|
-
|
|
65
|
+
// The settings as the commands use them: `API` is the one name every command
|
|
66
|
+
// reads (package.json stores it as `api`); everything else as stored.
|
|
67
|
+
function readConfig(root) {
|
|
68
|
+
const section = pkgSection(root);
|
|
69
|
+
const raw = section || JSON.parse(fs.readFileSync(legacyPathOf(root), 'utf8'));
|
|
70
|
+
const { api, ...rest } = raw;
|
|
71
|
+
return { API: rest.API || api, ...rest };
|
|
27
72
|
}
|
|
28
73
|
|
|
29
|
-
|
|
30
|
-
|
|
74
|
+
// The stored form: `api` first, then the known fields in a fixed order, then
|
|
75
|
+
// anything else the project added.
|
|
76
|
+
function toSection(config) {
|
|
77
|
+
const { API, api, ...rest } = config;
|
|
78
|
+
const all = { api: API || api, ...rest };
|
|
79
|
+
const section = {};
|
|
80
|
+
for (const k of FIELD_ORDER) if (all[k] !== undefined) section[k] = all[k];
|
|
81
|
+
for (const [k, v] of Object.entries(all)) if (!(k in section) && v !== undefined) section[k] = v;
|
|
82
|
+
return section;
|
|
31
83
|
}
|
|
32
84
|
|
|
85
|
+
// Write the settings into package.json. A folder without one (`nukebase link`
|
|
86
|
+
// into an empty directory) gets a minimal package.json; a package.json without
|
|
87
|
+
// the section gets it right after "version", where a reader looks first.
|
|
88
|
+
// sys/config.json, if present, goes — except in the master project, where it
|
|
89
|
+
// is rewritten without sign-ins for the fallback CLI that still reads it.
|
|
33
90
|
function writeConfig(root, config) {
|
|
34
|
-
|
|
35
|
-
|
|
91
|
+
const section = toSection(config);
|
|
92
|
+
const existing = readPkg(root);
|
|
93
|
+
let pkg;
|
|
94
|
+
if (!existing) {
|
|
95
|
+
pkg = { name: String(section.project || path.basename(root)).toLowerCase(), version: '0.0.0', private: true, [KEY]: section };
|
|
96
|
+
} else if (KEY in existing) {
|
|
97
|
+
pkg = { ...existing, [KEY]: section };
|
|
98
|
+
} else {
|
|
99
|
+
pkg = {};
|
|
100
|
+
const after = 'version' in existing ? 'version' : ('name' in existing ? 'name' : null);
|
|
101
|
+
for (const [k, v] of Object.entries(existing)) {
|
|
102
|
+
pkg[k] = v;
|
|
103
|
+
if (k === after) pkg[KEY] = section;
|
|
104
|
+
}
|
|
105
|
+
if (!after) pkg[KEY] = section;
|
|
106
|
+
}
|
|
107
|
+
fs.mkdirSync(root, { recursive: true });
|
|
108
|
+
fs.writeFileSync(pkgPathOf(root), JSON.stringify(pkg, null, 2) + '\n');
|
|
109
|
+
|
|
110
|
+
const legacy = legacyPathOf(root);
|
|
111
|
+
if (!fs.existsSync(legacy)) return;
|
|
112
|
+
if (isMaster(root)) {
|
|
113
|
+
const clean = { API: section.api, ...section };
|
|
114
|
+
delete clean.api;
|
|
115
|
+
for (const f of SECRET_FIELDS) delete clean[f];
|
|
116
|
+
fs.writeFileSync(legacy, JSON.stringify(clean, null, 2) + '\n');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
try { fs.unlinkSync(legacy); } catch {}
|
|
120
|
+
try { if (!fs.readdirSync(path.dirname(legacy)).length) fs.rmdirSync(path.dirname(legacy)); } catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// After a pull: the server's package.json carries whichever settings were
|
|
124
|
+
// last pushed to it — from another machine, or with --region another region's
|
|
125
|
+
// copy. The settings this folder had before the pull are what the developer
|
|
126
|
+
// meant, so they go back in.
|
|
127
|
+
function restoreSettings(root, config) {
|
|
128
|
+
const pulled = pkgSection(root);
|
|
129
|
+
if (pulled && pulled.mailKey && !config.mailKey) config = { ...config, mailKey: pulled.mailKey };
|
|
130
|
+
writeConfig(root, config);
|
|
36
131
|
}
|
|
37
132
|
|
|
38
133
|
// The settings a freshly linked or created project gets.
|
|
@@ -44,7 +139,7 @@ function newConfig({ api, uid, project, account }, base = {}) {
|
|
|
44
139
|
updateExclude: base.updateExclude || [],
|
|
45
140
|
streams: base.streams || 4
|
|
46
141
|
};
|
|
47
|
-
for (const [k, v] of Object.entries(base)) if (!(k in out) && !SECRET_FIELDS.includes(k)) out[k] = v;
|
|
142
|
+
for (const [k, v] of Object.entries(base)) if (!(k in out) && k !== 'api' && !SECRET_FIELDS.includes(k)) out[k] = v;
|
|
48
143
|
return out;
|
|
49
144
|
}
|
|
50
145
|
|
|
@@ -55,19 +150,29 @@ function sessionFiles(root) {
|
|
|
55
150
|
} catch { return []; }
|
|
56
151
|
}
|
|
57
152
|
|
|
153
|
+
// Settings still in sys/config.json move into package.json. Returns the
|
|
154
|
+
// config unchanged; the move is on disk.
|
|
155
|
+
function moveSettings(root, config) {
|
|
156
|
+
if (!isLegacy(root)) return config;
|
|
157
|
+
writeConfig(root, config);
|
|
158
|
+
console.log(`📦 Moved this project's settings from sys/config.json into package.json (its "${KEY}" section).`);
|
|
159
|
+
return config;
|
|
160
|
+
}
|
|
161
|
+
|
|
58
162
|
// Errors that mean "try again later" rather than "this sign-in is dead": a
|
|
59
163
|
// server without CLI sign-in yet, or no connection. The old sign-in is kept
|
|
60
164
|
// and used as before until the move can happen.
|
|
61
165
|
const isOldServer = (e) => /doesn't support CLI sign-in|Could not reach/.test(e.message);
|
|
62
166
|
|
|
63
|
-
// Move a project's old sign-in into ~/.nukebase, then strip it from
|
|
64
|
-
//
|
|
167
|
+
// Move a project's old sign-in into ~/.nukebase, then strip it from the
|
|
168
|
+
// settings; move the settings themselves into package.json. Returns the
|
|
169
|
+
// config to use.
|
|
65
170
|
// On a server without CLI sign-in yet, nothing changes and `legacy` is true:
|
|
66
171
|
// the caller then uses the token from sys/config.json as before.
|
|
67
172
|
async function migrateLegacy(root, config) {
|
|
68
173
|
const hasSecrets = SECRET_FIELDS.some(f => config[f]);
|
|
69
174
|
const sessions = sessionFiles(root);
|
|
70
|
-
if (!hasSecrets && !sessions.length) return { config, legacy: false };
|
|
175
|
+
if (!hasSecrets && !sessions.length) return { config: moveSettings(root, config), legacy: false };
|
|
71
176
|
|
|
72
177
|
const api = config.API;
|
|
73
178
|
const label = config.email || config.username || null;
|
|
@@ -113,13 +218,13 @@ async function migrateLegacy(root, config) {
|
|
|
113
218
|
const clean = { ...config, account: config.account || label || undefined };
|
|
114
219
|
for (const f of SECRET_FIELDS) delete clean[f];
|
|
115
220
|
writeConfig(root, clean);
|
|
116
|
-
console.log(
|
|
221
|
+
console.log(`🔒 Removed the sign-in (token, password) from this project's settings. Projects now hold only which account owns them, under "${KEY}" in package.json.`);
|
|
117
222
|
if (config.uid && !creds.get(config.uid)) {
|
|
118
223
|
console.log(` This project's account isn't signed in on this machine. Run: nukebase login${label && label.includes('@') ? ` --email ${label}` : ''}`);
|
|
119
224
|
}
|
|
120
225
|
return { config: clean, legacy: false };
|
|
121
226
|
}
|
|
122
|
-
return { config, legacy: false };
|
|
227
|
+
return { config: moveSettings(root, config), legacy: false };
|
|
123
228
|
}
|
|
124
229
|
|
|
125
|
-
module.exports = { findProject,
|
|
230
|
+
module.exports = { KEY, findProject, hasConfig, readConfig, writeConfig, restoreSettings, newConfig, migrateLegacy, DEFAULT_EXCLUDE, DEFAULT_BACKUP };
|