nukebase 2.2.0 → 2.2.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 +6 -2
- package/lib/account-commands.js +87 -31
- package/lib/args.js +1 -1
- package/lib/guide.js +116 -0
- package/lib/project-commands.js +18 -13
- 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
|
|
@@ -80,7 +82,7 @@ In a project folder (runs as the account that owns the project)
|
|
|
80
82
|
domain <name> [--email e] connect in one go (DNS already set)
|
|
81
83
|
domain remove disconnect the custom domain
|
|
82
84
|
domain email send the app's emails from the custom domain
|
|
83
|
-
(--root | --sub, --
|
|
85
|
+
(--root | --sub, --wait)
|
|
84
86
|
domain email remove go back to sending from nukebase.com
|
|
85
87
|
logs [--follow] recent log output; --follow streams
|
|
86
88
|
download [--out f.zip] the project ZIP, exactly as the server has it
|
|
@@ -122,6 +124,8 @@ if (command === 'start') {
|
|
|
122
124
|
|
|
123
125
|
if (args.has('version') && !command) {
|
|
124
126
|
console.log(version);
|
|
127
|
+
} else if (command === 'guide') {
|
|
128
|
+
require('../lib/guide').guideCommand();
|
|
125
129
|
} else if (!command || command === 'help' || (args.has('help') && !command)) {
|
|
126
130
|
usage();
|
|
127
131
|
} else if (ACCOUNT_COMMANDS.includes(command) || command === 'setup') {
|
package/lib/account-commands.js
CHANGED
|
@@ -296,10 +296,69 @@ function namePackage(dir, name) {
|
|
|
296
296
|
} catch { return false; }
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
|
|
299
|
+
// Read a download to the end, showing how much has arrived: one line that
|
|
300
|
+
// updates in a terminal, or a line every 10 MB when the output goes to a file
|
|
301
|
+
// or an AI agent (where \r updates would pile up). The archives are streamed
|
|
302
|
+
// without a length, so it shows MB so far, not a percentage.
|
|
303
|
+
async function readWithProgress(res, label) {
|
|
304
|
+
const tty = process.stdout.isTTY;
|
|
305
|
+
const mb = (n) => (n / 1048576).toFixed(1);
|
|
306
|
+
const chunks = [];
|
|
307
|
+
let got = 0, lastShown = 0, lastLine = 0;
|
|
308
|
+
const show = (final) => {
|
|
309
|
+
if (tty) process.stdout.write(`\r ${label}… ${mb(got)} MB${final ? '\n' : ''}`);
|
|
310
|
+
else if (final || got - lastLine >= 10 * 1048576) { console.log(` ${label}… ${mb(got)} MB`); lastLine = got; }
|
|
311
|
+
};
|
|
312
|
+
if (!res.body || typeof res.body.getReader !== 'function') {
|
|
313
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
314
|
+
got = buf.length; show(true);
|
|
315
|
+
return buf;
|
|
316
|
+
}
|
|
317
|
+
const reader = res.body.getReader();
|
|
318
|
+
try {
|
|
319
|
+
for (;;) {
|
|
320
|
+
const { done, value } = await reader.read();
|
|
321
|
+
if (done) break;
|
|
322
|
+
chunks.push(Buffer.from(value));
|
|
323
|
+
got += value.length;
|
|
324
|
+
if (Date.now() - lastShown > 250) { lastShown = Date.now(); show(false); }
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
// End the half-drawn progress line, so whatever is printed next starts clean.
|
|
328
|
+
if (tty && got) process.stdout.write('\n');
|
|
329
|
+
throw e;
|
|
330
|
+
}
|
|
331
|
+
show(true);
|
|
332
|
+
return Buffer.concat(chunks);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The project's files as the server has them, without node_modules (an
|
|
336
|
+
// `npm install` follows; servers too old for ?skip send them anyway). One
|
|
337
|
+
// retry, since this runs right after a dropped connection.
|
|
338
|
+
async function downloadExisting({ acct, api, name }) {
|
|
339
|
+
const headers = { 'X-Project': name, Cookie: cookieFor(acct) };
|
|
340
|
+
const url = `${api}/download-project?project=${encodeURIComponent(name)}&skip=node_modules`;
|
|
341
|
+
let lastErr = null;
|
|
342
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
343
|
+
try {
|
|
344
|
+
const dl = await fetch(url, { headers });
|
|
345
|
+
if (!dl.ok) { lastErr = new Error(`the server answered ${dl.status}`); continue; }
|
|
346
|
+
return await readWithProgress(dl, `Downloading ${name}`);
|
|
347
|
+
} catch (e) {
|
|
348
|
+
lastErr = e;
|
|
349
|
+
if (attempt === 1) console.log(' (download interrupted, trying again)');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
fail(`${name} exists but could not be downloaded (${lastErr.cause?.message || lastErr.message}).\n` +
|
|
353
|
+
` In an empty folder, run: nukebase link ${name} then: nukebase pull --yes then: npm install`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Create `name` on the server and unpack it into `dest`. When the account
|
|
357
|
+
// already has a project by that name: with `reuse`, that project is
|
|
358
|
+
// downloaded into `dest` instead (`start`: running it again finishes the
|
|
359
|
+
// job rather than making name-2); without, returns { exists: true }. Any
|
|
360
|
+
// other failure stops the command.
|
|
361
|
+
async function createProject({ acct, api, name, tier, dest, install, reuse = false }) {
|
|
303
362
|
console.log(`\n🚀 Creating ${name}...`);
|
|
304
363
|
const headers = (extra = {}) => ({ ...extra, 'X-Project': name, Cookie: cookieFor(acct) });
|
|
305
364
|
|
|
@@ -325,14 +384,7 @@ async function createProject({ acct, api, name, tier, dest, install }) {
|
|
|
325
384
|
}
|
|
326
385
|
if (!probe.ok) fail(`Create failed: ${why} (and the server answered ${probe.status} when asked whether ${name} exists).`);
|
|
327
386
|
console.log(` (the connection dropped while the server finished setting up ${name} — it exists, downloading it)`);
|
|
328
|
-
|
|
329
|
-
try {
|
|
330
|
-
dl = await fetch(`${api}/download-project?project=${encodeURIComponent(name)}`, { headers: headers() });
|
|
331
|
-
} catch (e) {
|
|
332
|
-
fail(`${name} was created but could not be downloaded (${e.cause?.message || e.message}). Link to it: nukebase link ${name}`);
|
|
333
|
-
}
|
|
334
|
-
if (!dl.ok) fail(`${name} was created but could not be downloaded (${dl.status}). Link to it: nukebase link ${name}`);
|
|
335
|
-
return Buffer.from(await dl.arrayBuffer());
|
|
387
|
+
return downloadExisting({ acct, api, name });
|
|
336
388
|
}
|
|
337
389
|
|
|
338
390
|
let res = null, zip = null;
|
|
@@ -348,19 +400,25 @@ async function createProject({ acct, api, name, tier, dest, install }) {
|
|
|
348
400
|
if (res && !res.ok) {
|
|
349
401
|
let msg = '';
|
|
350
402
|
try { msg = (await res.json()).error; } catch {}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
403
|
+
const taken = res.status === 400 && /already exists/i.test(msg || '');
|
|
404
|
+
if (taken && !reuse) return { exists: true };
|
|
405
|
+
if (taken) {
|
|
406
|
+
console.log(` ${name} already exists in your account, so downloading it here instead of making a new one.`);
|
|
407
|
+
zip = await downloadExisting({ acct, api, name });
|
|
408
|
+
} else {
|
|
409
|
+
if (res.status === 401) fail(`The sign-in for ${acct.label} is no longer valid. Run: nukebase login --email ${acct.label}`);
|
|
410
|
+
// No plan or no credit: fixed on the dashboard.
|
|
411
|
+
if (res.status === 403 && /plan|funds|credit|wallet|balance|upgrade/i.test(msg)) {
|
|
412
|
+
fail(`${msg}\n Manage your plan at ${api}/`);
|
|
413
|
+
}
|
|
414
|
+
fail(`Create failed (${res.status}): ${msg || res.statusText}`);
|
|
356
415
|
}
|
|
357
|
-
fail(`Create failed (${res.status}): ${msg || res.statusText}`);
|
|
358
416
|
}
|
|
359
417
|
|
|
360
418
|
// The server finishes setup as the ZIP finishes streaming, so read it all.
|
|
361
419
|
// A drop here is the same case as above.
|
|
362
420
|
if (!zip) {
|
|
363
|
-
try { zip =
|
|
421
|
+
try { zip = await readWithProgress(res, `Downloading ${name}`); }
|
|
364
422
|
catch (e) { zip = await recover(e); }
|
|
365
423
|
}
|
|
366
424
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -373,7 +431,7 @@ async function createProject({ acct, api, name, tier, dest, install }) {
|
|
|
373
431
|
console.log(`✓ Downloaded ${count} files into ${path.relative(process.cwd(), dest) || 'this folder'}`);
|
|
374
432
|
|
|
375
433
|
if (install && fs.existsSync(path.join(dest, 'package.json'))) {
|
|
376
|
-
console.log('\n📦 npm install
|
|
434
|
+
console.log('\n📦 Installing packages (npm install)…\n');
|
|
377
435
|
// One command string under a shell: npm is npm.cmd on Windows, which
|
|
378
436
|
// needs a shell, and Node 26 deprecates passing separate args with one.
|
|
379
437
|
const r = spawnSync('npm install', [], { cwd: dest, stdio: 'inherit', shell: true });
|
|
@@ -496,19 +554,17 @@ async function startCommand(args) {
|
|
|
496
554
|
const intoHere = isEmptyFolder(here);
|
|
497
555
|
const given = args.positional.find(a => !a.includes('@') && PROJECT_NAME_RE.test(a) && !resolveTier(a, catalog));
|
|
498
556
|
const base = given || (intoHere && nameFromFolder(here)) || 'myapp';
|
|
499
|
-
|
|
557
|
+
// One name. If the account already has a project by it, that project is
|
|
558
|
+
// downloaded here rather than a name-2 being created: running start again
|
|
559
|
+
// after a hiccup finishes the job.
|
|
560
|
+
const name = base;
|
|
561
|
+
const dest = intoHere ? here : path.resolve(here, name);
|
|
562
|
+
if (!intoHere && fs.existsSync(dest) && fs.readdirSync(dest).length) {
|
|
563
|
+
fail(`There is already a folder named ${name} here with files in it. Run this in an empty folder, or choose another name: npx nukebase start <name>`);
|
|
564
|
+
}
|
|
500
565
|
|
|
501
566
|
console.log(` Size: ${tierLabel(catalog, tier)}${isFree ? ' (paid from your $10 monthly credit)' : ''}`);
|
|
502
|
-
|
|
503
|
-
for (const candidate of candidates) {
|
|
504
|
-
dest = intoHere ? here : path.resolve(here, candidate);
|
|
505
|
-
if (!intoHere && fs.existsSync(dest) && fs.readdirSync(dest).length) continue;
|
|
506
|
-
const out = await createProject({ acct, api, name: candidate, tier, dest, install: !args.has('no-install') });
|
|
507
|
-
if (out.exists) { console.log(` (${candidate} is taken, trying another name)`); continue; }
|
|
508
|
-
created = out; name = candidate;
|
|
509
|
-
break;
|
|
510
|
-
}
|
|
511
|
-
if (!created) fail(given ? `You already have a project named ${given}. Pick another name: npx nukebase start <name>` : 'Could not find a free project name. Run: npx nukebase start <name>');
|
|
567
|
+
const created = await createProject({ acct, api, name, tier, dest, install: !args.has('no-install'), reuse: true });
|
|
512
568
|
|
|
513
569
|
// Written for whoever runs this next, person or AI agent.
|
|
514
570
|
console.log(`\n✅ Your app "${name}" is live at ${created.url}`);
|
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', 'reply-to'
|
|
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
|
@@ -945,11 +945,12 @@ async function domainCommand() {
|
|
|
945
945
|
}
|
|
946
946
|
}
|
|
947
947
|
|
|
948
|
-
// nukebase domain email [--root | --sub] [--
|
|
948
|
+
// nukebase domain email [--root | --sub] [--wait]
|
|
949
949
|
// nukebase domain email remove
|
|
950
950
|
//
|
|
951
|
-
// Send the app's emails
|
|
952
|
-
//
|
|
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
|
|
953
954
|
// custom domain itself (--root) or mail.<domain> (--sub; safest when the
|
|
954
955
|
// domain already has email). Prints the two DNS records to add, then waits
|
|
955
956
|
// for the mail provider to verify them (Enter to check now, Ctrl+C to leave
|
|
@@ -970,18 +971,22 @@ async function emailDomainCommand(action) {
|
|
|
970
971
|
const current = check.current;
|
|
971
972
|
const wantRoot = args.has('root') ? true : args.has('sub') ? false : null;
|
|
972
973
|
const replyTo = args.get('reply-to');
|
|
974
|
+
const fromArg = args.get('from');
|
|
973
975
|
const changing = wantRoot !== null && (!current || current.root !== wantRoot);
|
|
974
976
|
|
|
975
977
|
let rec = current;
|
|
976
|
-
if (!current || changing || replyTo !== undefined) {
|
|
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;
|
|
977
982
|
let root = wantRoot;
|
|
978
983
|
if (root === null) root = current ? current.root : null;
|
|
979
984
|
if (root === null) {
|
|
980
985
|
const suggestRoot = check.suggested === 'root';
|
|
981
986
|
console.log(`\n✉️ Send ${project}'s emails from ${domain}\n`);
|
|
982
|
-
console.log(` 1)
|
|
987
|
+
console.log(` 1) anything@${domain}`);
|
|
983
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.'}`);
|
|
984
|
-
console.log(` 2)
|
|
989
|
+
console.log(` 2) anything@${domain}, sent via mail.${domain}`);
|
|
985
990
|
console.log(' Safest if you already use email on this domain: its records are not touched.\n');
|
|
986
991
|
if (process.stdin.isTTY && !AUTO_YES) {
|
|
987
992
|
const a = (await askLine(` Choose 1 or 2 [${suggestRoot ? 1 : 2}]: `)).trim();
|
|
@@ -990,11 +995,9 @@ async function emailDomainCommand(action) {
|
|
|
990
995
|
root = suggestRoot;
|
|
991
996
|
}
|
|
992
997
|
}
|
|
993
|
-
|
|
994
|
-
if (reply === undefined && !current && process.stdin.isTTY && !AUTO_YES) {
|
|
995
|
-
reply = (await askLine(' Where should replies go? (e.g. you@gmail.com, Enter to skip): ')).trim();
|
|
996
|
-
}
|
|
998
|
+
const reply = replyTo;
|
|
997
999
|
const body = { root };
|
|
1000
|
+
if (typeof from === 'string' && from) body.from = from;
|
|
998
1001
|
if (typeof reply === 'string') body.replyTo = reply;
|
|
999
1002
|
rec = (await projectPost('/email-domain', body, { label: 'email' })).emailDomain;
|
|
1000
1003
|
}
|
|
@@ -1010,8 +1013,9 @@ async function emailDomainCommand(action) {
|
|
|
1010
1013
|
|
|
1011
1014
|
function printEmailRecords(rec) {
|
|
1012
1015
|
const ok = rec.status === 'verified';
|
|
1013
|
-
|
|
1014
|
-
|
|
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>" })`);
|
|
1015
1019
|
if (ok) { console.log(''); return; }
|
|
1016
1020
|
console.log('\n At your registrar, add these DNS records:\n');
|
|
1017
1021
|
for (const r of rec.records || []) {
|
|
@@ -1035,7 +1039,7 @@ async function waitForEmailDomain() {
|
|
|
1035
1039
|
const marks = (rec.records || []).map(x => `${x.valid ? '✓' : '…'} ${x.kind.toUpperCase()}`).join(' ');
|
|
1036
1040
|
process.stdout.write(`\r ${marks} `);
|
|
1037
1041
|
if (rec.status === 'verified') {
|
|
1038
|
-
console.log(`\n\n✓ Verified. ${project}'s emails now go out
|
|
1042
|
+
console.log(`\n\n✓ Verified. ${project}'s emails now go out from @${rec.fromDomain || rec.name}.`);
|
|
1039
1043
|
return;
|
|
1040
1044
|
}
|
|
1041
1045
|
await wait(20000);
|
|
@@ -1409,6 +1413,7 @@ async function regionTransfer() {
|
|
|
1409
1413
|
const em = node.emailDomain;
|
|
1410
1414
|
const emArgs = ['domain', 'email', em.root ? '--root' : '--sub', '--region', target, '--yes', ...asArgs];
|
|
1411
1415
|
if (em.replyTo) emArgs.push('--reply-to', em.replyTo);
|
|
1416
|
+
if (em.fromLocal) emArgs.push('--from', em.fromLocal);
|
|
1412
1417
|
const e = spawnSync(process.execPath, [BIN, ...emArgs], { stdio: 'inherit', cwd: root });
|
|
1413
1418
|
if (e.status !== 0) console.error(` Email sending: finish later with: nukebase domain email --region ${target}${asArgs.length ? ' ' + asArgs.join(' ') : ''}`);
|
|
1414
1419
|
}
|