nukebase 2.1.2 → 2.2.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/bin/nukebase.js +8 -1
- package/lib/args.js +1 -1
- package/lib/guide.js +116 -0
- package/lib/project-commands.js +127 -0
- package/package.json +1 -1
package/bin/nukebase.js
CHANGED
|
@@ -33,7 +33,9 @@ function usage() {
|
|
|
33
33
|
|
|
34
34
|
Usage: nukebase <command> (or: npx nukebase <command>)
|
|
35
35
|
|
|
36
|
-
New here? Run: npx nukebase
|
|
36
|
+
New here? Run: npx nukebase guide
|
|
37
|
+
what NukeBase can do and how to plan your app with Claude; creates nothing
|
|
38
|
+
Ready to set up? Run: npx nukebase start
|
|
37
39
|
signs you in, creates a project and downloads it, ready to build
|
|
38
40
|
|
|
39
41
|
Accounts
|
|
@@ -79,6 +81,9 @@ In a project folder (runs as the account that owns the project)
|
|
|
79
81
|
domain connect a custom domain, step by step
|
|
80
82
|
domain <name> [--email e] connect in one go (DNS already set)
|
|
81
83
|
domain remove disconnect the custom domain
|
|
84
|
+
domain email send the app's emails from the custom domain
|
|
85
|
+
(--root | --sub, --wait)
|
|
86
|
+
domain email remove go back to sending from nukebase.com
|
|
82
87
|
logs [--follow] recent log output; --follow streams
|
|
83
88
|
download [--out f.zip] the project ZIP, exactly as the server has it
|
|
84
89
|
regiontransfer [region] move this project to another region, step by step
|
|
@@ -119,6 +124,8 @@ if (command === 'start') {
|
|
|
119
124
|
|
|
120
125
|
if (args.has('version') && !command) {
|
|
121
126
|
console.log(version);
|
|
127
|
+
} else if (command === 'guide') {
|
|
128
|
+
require('../lib/guide').guideCommand();
|
|
122
129
|
} else if (!command || command === 'help' || (args.has('help') && !command)) {
|
|
123
130
|
usage();
|
|
124
131
|
} else if (ACCOUNT_COMMANDS.includes(command) || command === 'setup') {
|
package/lib/args.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `--name=value` works too.
|
|
7
7
|
|
|
8
8
|
const VALUE_FLAGS = new Set([
|
|
9
|
-
'region', 'api', 'as', 'tier', 'streams', 'out', 'email', 'to', 'include', 'name'
|
|
9
|
+
'region', 'api', 'as', 'tier', 'streams', 'out', 'email', 'to', 'include', 'name', 'reply-to', 'from'
|
|
10
10
|
]);
|
|
11
11
|
|
|
12
12
|
const SHORT = { y: 'yes', f: 'follow', h: 'help', v: 'version' };
|
package/lib/guide.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// `nukebase guide`: the front door for someone who is not a coder.
|
|
2
|
+
//
|
|
3
|
+
// It creates nothing and signs nothing in. It prints a briefing written for
|
|
4
|
+
// the AI assistant the person is talking to (usually Claude Code), so the
|
|
5
|
+
// assistant knows what NukeBase can do and holds a conversation about the
|
|
6
|
+
// app before `nukebase start` creates anything. The person's own words on the
|
|
7
|
+
// landing page are just: "Run npx nukebase@latest guide, then help me build
|
|
8
|
+
// my app."
|
|
9
|
+
//
|
|
10
|
+
// No prices here on purpose: they change, and the dashboard shows them.
|
|
11
|
+
|
|
12
|
+
const GUIDE = `
|
|
13
|
+
NUKEBASE GUIDE
|
|
14
|
+
==============
|
|
15
|
+
|
|
16
|
+
For the AI assistant reading this
|
|
17
|
+
---------------------------------
|
|
18
|
+
The person you are helping may not be a coder. They want to build an app
|
|
19
|
+
with NukeBase. Nothing has been created yet, and nothing should be until they
|
|
20
|
+
agree to a plan.
|
|
21
|
+
|
|
22
|
+
1. Have a short, friendly conversation to understand what they want
|
|
23
|
+
(questions below). Ask one or two at a time, in plain words. No jargon:
|
|
24
|
+
never ask them about databases, frameworks or APIs.
|
|
25
|
+
2. Play the plan back in a few plain sentences, including the project name,
|
|
26
|
+
and ask: "Shall I set it up?"
|
|
27
|
+
3. Only after they say yes, run the setup command (Commands, below), then
|
|
28
|
+
read the CLAUDE.md file in the new project folder before writing any
|
|
29
|
+
code. It explains how NukeBase apps are built.
|
|
30
|
+
4. Save the agreed plan as PLAN.md in the project folder, so the next
|
|
31
|
+
session knows what was agreed.
|
|
32
|
+
5. Build it, put it live, and show them the link. Then offer the extras
|
|
33
|
+
they would want: their own domain, emails from that domain.
|
|
34
|
+
|
|
35
|
+
What NukeBase is
|
|
36
|
+
----------------
|
|
37
|
+
One place to build and run a web app: the website, its data, user sign-in,
|
|
38
|
+
email and hosting all come together, with nothing else to sign up for or
|
|
39
|
+
set up. Apps work on phones and computers and can be added to a phone's home
|
|
40
|
+
screen like an app from the store.
|
|
41
|
+
|
|
42
|
+
What it can do (features)
|
|
43
|
+
-------------------------
|
|
44
|
+
Website hosting Live at its own web address the moment it is set up.
|
|
45
|
+
Live data A built-in database. When one person changes
|
|
46
|
+
something, everyone else's screen updates instantly
|
|
47
|
+
(no refreshing): bookings, chats, sign-up lists.
|
|
48
|
+
User sign-in People sign in with a link sent to their email. No
|
|
49
|
+
passwords. One browser can stay signed in to several
|
|
50
|
+
accounts and switch between them.
|
|
51
|
+
Who can see what Rules for which users can read or change which data
|
|
52
|
+
(each user sees only their own orders, say).
|
|
53
|
+
File uploads Photos, documents and other files, stored with the app.
|
|
54
|
+
Emails Sign-in emails out of the box. With the person's own
|
|
55
|
+
domain: any email (reminders, receipts, notifications)
|
|
56
|
+
from addresses like support@theirdomain.com, and a
|
|
57
|
+
log of what was delivered or bounced.
|
|
58
|
+
Phone notifications Push notifications to phones and computers.
|
|
59
|
+
Works offline Pages open without signal; changes made offline are
|
|
60
|
+
sent when the connection comes back.
|
|
61
|
+
Own domain Their domain (theirbusiness.com) with SSL included.
|
|
62
|
+
Server code Logic that runs on the server: scheduled jobs,
|
|
63
|
+
actions triggered when data changes, calls to other
|
|
64
|
+
services (Stripe for payments, for example; payments
|
|
65
|
+
are not built in, but can be added).
|
|
66
|
+
Safety net Every deploy can be undone, and the data can be
|
|
67
|
+
backed up to their computer at any time.
|
|
68
|
+
Secrets API keys for other services, kept out of the code.
|
|
69
|
+
Sizes 128 MB, 256 MB, 512 MB or 1 GB. The default is fine to
|
|
70
|
+
start; it can be changed at any time from the
|
|
71
|
+
dashboard.
|
|
72
|
+
|
|
73
|
+
Good fits: booking and scheduling, member or client portals, sign-up lists
|
|
74
|
+
and waitlists, small shops and catalogs, dashboards and tools for a business,
|
|
75
|
+
chat and anything live, community and club sites, simple games.
|
|
76
|
+
Not the right tool: video processing, training AI models, or very large data
|
|
77
|
+
warehouses.
|
|
78
|
+
|
|
79
|
+
Questions to ask (adapt them; skip what is already clear)
|
|
80
|
+
----------------------------------------------------------
|
|
81
|
+
- What do you want to build, and who is it for?
|
|
82
|
+
- What should people be able to do in it? (book a time, post, order, ...)
|
|
83
|
+
- Do people need to sign in? Is there an admin (you) with extra powers?
|
|
84
|
+
- Should it send emails, like confirmations or reminders?
|
|
85
|
+
- Phone notifications? Uploading photos or files?
|
|
86
|
+
- Do you have a domain name you want to use, or one in mind?
|
|
87
|
+
- What do you want your project to be named?
|
|
88
|
+
(lowercase letters, numbers and dashes, 3 to 31 characters, starting
|
|
89
|
+
with a letter, e.g. "bloom-bookings". It becomes part of the web address.)
|
|
90
|
+
|
|
91
|
+
Commands
|
|
92
|
+
--------
|
|
93
|
+
Setup (run in an empty folder; it signs them in first if needed: a browser
|
|
94
|
+
window opens to confirm, or a short code to type on any device):
|
|
95
|
+
npx nukebase@latest start <project-name> --yes
|
|
96
|
+
|
|
97
|
+
Then, inside the project folder:
|
|
98
|
+
npx nukebase push --yes put the latest version live
|
|
99
|
+
npx nukebase logs see what the app printed (errors too)
|
|
100
|
+
npx nukebase domain connect their own domain, step by step
|
|
101
|
+
npx nukebase domain email send the app's emails from that domain
|
|
102
|
+
npx nukebase rollback undo the last push (their data is kept)
|
|
103
|
+
npx nukebase backup copy the app's data to this computer
|
|
104
|
+
npx nukebase env push send secrets from .env to the server
|
|
105
|
+
npx nukebase pause / restart stop the app, or start it again
|
|
106
|
+
npx nukebase help every command
|
|
107
|
+
|
|
108
|
+
The dashboard at https://nukebase.com shows all their apps, usage and
|
|
109
|
+
billing, and does most of the above with buttons too.
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
function guideCommand() {
|
|
113
|
+
console.log(GUIDE.trimEnd() + '\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = { guideCommand, GUIDE };
|
package/lib/project-commands.js
CHANGED
|
@@ -854,6 +854,7 @@ async function deleteProject() {
|
|
|
854
854
|
// nukebase domain guided: DNS records, wait, connect
|
|
855
855
|
// nukebase domain <name> [--email you@x] one shot, for scripts (no waiting)
|
|
856
856
|
// nukebase domain remove disconnect it
|
|
857
|
+
// nukebase domain email [remove] send the app's emails from it
|
|
857
858
|
//
|
|
858
859
|
// The guided flow is the dashboard's modal in the terminal: it prints the two
|
|
859
860
|
// A records and the ownership TXT (nukebase.<domain> = nukebase-uid=<uid>,
|
|
@@ -878,6 +879,11 @@ async function domainCommand() {
|
|
|
878
879
|
process.exit(0);
|
|
879
880
|
}
|
|
880
881
|
|
|
882
|
+
if (sub === 'email') {
|
|
883
|
+
await emailDomainCommand(args.positional[1] || null);
|
|
884
|
+
process.exit(0);
|
|
885
|
+
}
|
|
886
|
+
|
|
881
887
|
const email = args.get('email') || (String(accountLabel).includes('@') ? accountLabel : null);
|
|
882
888
|
|
|
883
889
|
// One shot: name given, no hand-holding. Scripts and people who already
|
|
@@ -892,6 +898,7 @@ async function domainCommand() {
|
|
|
892
898
|
await waitForDns(sub, ip, needTxt);
|
|
893
899
|
}
|
|
894
900
|
await connectDomain(sub, email);
|
|
901
|
+
if (args.has('email-domain')) await emailDomainCommand(null);
|
|
895
902
|
process.exit(0);
|
|
896
903
|
}
|
|
897
904
|
|
|
@@ -923,6 +930,13 @@ async function domainCommand() {
|
|
|
923
930
|
|
|
924
931
|
// Step 3 — the server does the real verification and issues the cert.
|
|
925
932
|
await connectDomain(domain, email);
|
|
933
|
+
|
|
934
|
+
// Optional: the app's emails from this domain too.
|
|
935
|
+
if (args.has('email-domain') || await confirm(`\n✉️ Send this app's emails from ${domain} too (instead of nukebase.com)? (y/n) `)) {
|
|
936
|
+
await emailDomainCommand(null);
|
|
937
|
+
} else {
|
|
938
|
+
console.log(' Later: nukebase domain email');
|
|
939
|
+
}
|
|
926
940
|
process.exit(0);
|
|
927
941
|
|
|
928
942
|
async function currentDomain() {
|
|
@@ -931,6 +945,110 @@ async function domainCommand() {
|
|
|
931
945
|
}
|
|
932
946
|
}
|
|
933
947
|
|
|
948
|
+
// nukebase domain email [--root | --sub] [--wait]
|
|
949
|
+
// nukebase domain email remove
|
|
950
|
+
//
|
|
951
|
+
// Send the app's emails from its custom domain. The setup is only the
|
|
952
|
+
// domain and its DNS: the address, sender name and Reply-To are chosen per
|
|
953
|
+
// email in code (sendEmail's `from` / `replyTo`). The sending domain is either the
|
|
954
|
+
// custom domain itself (--root) or mail.<domain> (--sub; safest when the
|
|
955
|
+
// domain already has email). Prints the two DNS records to add, then waits
|
|
956
|
+
// for the mail provider to verify them (Enter to check now, Ctrl+C to leave
|
|
957
|
+
// it: the server keeps checking every 10 minutes for a week). Run again to
|
|
958
|
+
// see the status, change the Reply-To, or switch between --root and --sub.
|
|
959
|
+
async function emailDomainCommand(action) {
|
|
960
|
+
if (action === 'remove') {
|
|
961
|
+
if (!(await confirm(`Stop sending ${project}'s emails from its domain? They go back to nukebase.com. (y/n) `))) {
|
|
962
|
+
console.log('Aborted.'); process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
await projectPost('/remove-email-domain', {}, { label: 'email remove' });
|
|
965
|
+
console.log('✓ Email sending domain removed. Emails are sent from nukebase.com again.');
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const check = await projectPost('/email-domain-check', {}, { label: 'email' });
|
|
970
|
+
const domain = check.domain;
|
|
971
|
+
const current = check.current;
|
|
972
|
+
const wantRoot = args.has('root') ? true : args.has('sub') ? false : null;
|
|
973
|
+
const replyTo = args.get('reply-to');
|
|
974
|
+
const fromArg = args.get('from');
|
|
975
|
+
const changing = wantRoot !== null && (!current || current.root !== wantRoot);
|
|
976
|
+
|
|
977
|
+
let rec = current;
|
|
978
|
+
if (!current || changing || replyTo !== undefined || fromArg !== undefined) {
|
|
979
|
+
// --from / --reply-to are only carried by a region transfer, for setups
|
|
980
|
+
// made when those were part of it.
|
|
981
|
+
const from = fromArg;
|
|
982
|
+
let root = wantRoot;
|
|
983
|
+
if (root === null) root = current ? current.root : null;
|
|
984
|
+
if (root === null) {
|
|
985
|
+
const suggestRoot = check.suggested === 'root';
|
|
986
|
+
console.log(`\n✉️ Send ${project}'s emails from ${domain}\n`);
|
|
987
|
+
console.log(` 1) anything@${domain}`);
|
|
988
|
+
console.log(` ${check.hasEmail ? 'This domain already has email set up' + (check.spf ? ' (you will update its SPF record)' : '') + '.' : 'Recommended: this domain has no email set up yet.'}`);
|
|
989
|
+
console.log(` 2) anything@${domain}, sent via mail.${domain}`);
|
|
990
|
+
console.log(' Safest if you already use email on this domain: its records are not touched.\n');
|
|
991
|
+
if (process.stdin.isTTY && !AUTO_YES) {
|
|
992
|
+
const a = (await askLine(` Choose 1 or 2 [${suggestRoot ? 1 : 2}]: `)).trim();
|
|
993
|
+
root = a === '1' ? true : a === '2' ? false : suggestRoot;
|
|
994
|
+
} else {
|
|
995
|
+
root = suggestRoot;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const reply = replyTo;
|
|
999
|
+
const body = { root };
|
|
1000
|
+
if (typeof from === 'string' && from) body.from = from;
|
|
1001
|
+
if (typeof reply === 'string') body.replyTo = reply;
|
|
1002
|
+
rec = (await projectPost('/email-domain', body, { label: 'email' })).emailDomain;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
printEmailRecords(rec);
|
|
1006
|
+
if (rec.status === 'verified') return;
|
|
1007
|
+
if (!args.has('wait') && (!process.stdin.isTTY || AUTO_YES)) {
|
|
1008
|
+
console.log(' The server checks every 10 minutes. See the status with: nukebase domain email');
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
await waitForEmailDomain();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function printEmailRecords(rec) {
|
|
1015
|
+
const ok = rec.status === 'verified';
|
|
1016
|
+
const fromDomain = rec.fromDomain || (rec.root ? rec.name : rec.name.replace(/^mail\./, ''));
|
|
1017
|
+
console.log(`\n Sending domain: ${rec.name} ${ok ? '✓ verified' : '… waiting for DNS'}`);
|
|
1018
|
+
console.log(` Sends from: any address @${fromDomain} — your code picks it: sendEmail({ from: "Your Name <support>" })`);
|
|
1019
|
+
if (ok) { console.log(''); return; }
|
|
1020
|
+
console.log('\n At your registrar, add these DNS records:\n');
|
|
1021
|
+
for (const r of rec.records || []) {
|
|
1022
|
+
console.log(` ${r.valid ? '✓' : '…'} TXT ${r.host}`);
|
|
1023
|
+
if (r.replace) console.log(' REPLACE your existing SPF record (only one is allowed) with:');
|
|
1024
|
+
console.log(` ${r.value}\n`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
async function waitForEmailDomain() {
|
|
1029
|
+
console.log(' Waiting for verification. Enter to check now, Ctrl+C to stop (the server keeps checking).\n');
|
|
1030
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1031
|
+
let poke = null;
|
|
1032
|
+
rl.on('line', () => { if (poke) poke(); });
|
|
1033
|
+
const wait = (ms) => new Promise(res => { poke = res; setTimeout(res, ms); });
|
|
1034
|
+
try {
|
|
1035
|
+
for (;;) {
|
|
1036
|
+
const r = await projectPost('/email-domain-status', {}, { label: 'email status' });
|
|
1037
|
+
const rec = r.emailDomain;
|
|
1038
|
+
if (!rec) { console.log('\n The sending domain is gone. Run: nukebase domain email'); return; }
|
|
1039
|
+
const marks = (rec.records || []).map(x => `${x.valid ? '✓' : '…'} ${x.kind.toUpperCase()}`).join(' ');
|
|
1040
|
+
process.stdout.write(`\r ${marks} `);
|
|
1041
|
+
if (rec.status === 'verified') {
|
|
1042
|
+
console.log(`\n\n✓ Verified. ${project}'s emails now go out from @${rec.fromDomain || rec.name}.`);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
await wait(20000);
|
|
1046
|
+
}
|
|
1047
|
+
} finally {
|
|
1048
|
+
rl.close();
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
934
1052
|
async function connectDomain(domain, email) {
|
|
935
1053
|
console.log(`🔐 Step 3 — verifying DNS from the server and issuing SSL for ${domain} (up to a minute)...`);
|
|
936
1054
|
const r = await projectPost('/custom-domain', email ? { domain, email } : { domain }, {
|
|
@@ -1289,6 +1407,15 @@ async function regionTransfer() {
|
|
|
1289
1407
|
if (r.status !== 0) {
|
|
1290
1408
|
console.error(`\n⚠️ The domain was not connected on ${target}. It still works on ${source}.`);
|
|
1291
1409
|
console.error(` Finish later with: nukebase domain ${customDomain} --region ${target} --wait${asArgs.length ? ' ' + asArgs.join(' ') : ''}`);
|
|
1410
|
+
} else if (node.emailDomain && node.emailDomain.name) {
|
|
1411
|
+
// Same sending domain on the target: the mail provider already has it
|
|
1412
|
+
// verified (its DNS records do not change), so the target adopts it.
|
|
1413
|
+
const em = node.emailDomain;
|
|
1414
|
+
const emArgs = ['domain', 'email', em.root ? '--root' : '--sub', '--region', target, '--yes', ...asArgs];
|
|
1415
|
+
if (em.replyTo) emArgs.push('--reply-to', em.replyTo);
|
|
1416
|
+
if (em.fromLocal) emArgs.push('--from', em.fromLocal);
|
|
1417
|
+
const e = spawnSync(process.execPath, [BIN, ...emArgs], { stdio: 'inherit', cwd: root });
|
|
1418
|
+
if (e.status !== 0) console.error(` Email sending: finish later with: nukebase domain email --region ${target}${asArgs.length ? ' ' + asArgs.join(' ') : ''}`);
|
|
1292
1419
|
}
|
|
1293
1420
|
} else {
|
|
1294
1421
|
console.log(` Skipped. ${customDomain} stays on ${source} for now. When ready:`);
|