@stacksjs/buddy 0.70.74 → 0.70.76
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/dist/commands/create.js +4 -0
- package/dist/commands/deploy.d.ts +4 -2
- package/dist/commands/deploy.js +32 -5
- package/dist/commands/dns.js +40 -0
- package/dist/lazy-commands.js +3 -0
- package/package.json +2 -2
package/dist/commands/create.js
CHANGED
|
@@ -106,6 +106,10 @@ async function install(path, options) {
|
|
|
106
106
|
log.error(result.error);
|
|
107
107
|
process.exit(ExitCode.FatalError);
|
|
108
108
|
}
|
|
109
|
+
log.info("Removing template-encrypted env files...");
|
|
110
|
+
const { rm } = await import("node:fs/promises");
|
|
111
|
+
for (const stale of [".env.development", ".env.staging", ".env.production", ".env.keys"])
|
|
112
|
+
await rm(`${path}/${stale}`, { force: !0 });
|
|
109
113
|
log.info("Generating application key...");
|
|
110
114
|
const keyResult = await runAction(Action.KeyGenerate, { ...options, cwd: path });
|
|
111
115
|
if (keyResult.isErr) {
|
|
@@ -71,8 +71,10 @@ export declare function provisionMailTenant(ip: string, logger: typeof log): Pro
|
|
|
71
71
|
* per record): MX → the mail host, SPF authorizing the box IP, the domain's DKIM
|
|
72
72
|
* public key at `mail._domainkey`, and a DMARC policy. Best-effort — logged, not
|
|
73
73
|
* thrown. No-op without Porkbun credentials (the records are printed to add by
|
|
74
|
-
* hand). MX targets the
|
|
75
|
-
* mail
|
|
74
|
+
* hand). MX targets the tenant's own `mail.<domain>` when that name already
|
|
75
|
+
* resolves to the box (own-brand mail host; requires the mail cert to cover it
|
|
76
|
+
* as a SAN), and falls back to the shared mail host (`mail.stacksjs.com`)
|
|
77
|
+
* otherwise, where no per-domain mail A record or extra TLS SAN is needed.
|
|
76
78
|
*/
|
|
77
79
|
export declare function reconcileMailDns(res: MailTenantResult, ip: string, logger: typeof log): Promise<void>;
|
|
78
80
|
export declare function deploy(buddy: CLI): void;
|
package/dist/commands/deploy.js
CHANGED
|
@@ -4,8 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { runAction } from "@stacksjs/actions";
|
|
6
6
|
import { italic, onUnknownSubcommand, outro, prompts, runCommand } from "@stacksjs/cli";
|
|
7
|
-
import { app, email as emailConfig, cloud as cloudConfig } from "@stacksjs/config";
|
|
8
|
-
import { addDomain, hasUserDomainBeenAddedToCloud } from "@stacksjs/dns";
|
|
7
|
+
import { app, dns as dnsConfig, email as emailConfig, cloud as cloudConfig } from "@stacksjs/config";
|
|
8
|
+
import { addDomain, hasUserDomainBeenAddedToCloud, syncDnsConfig } from "@stacksjs/dns";
|
|
9
9
|
import { encryptEnv, env } from "@stacksjs/env";
|
|
10
10
|
import { Action } from "@stacksjs/enums";
|
|
11
11
|
import { path as p } from "@stacksjs/path";
|
|
@@ -691,7 +691,7 @@ async function runHetznerDeploy(args) {
|
|
|
691
691
|
log.info(`Attaching to '${attachTo}' box '${box.serverName}' (${ip}) \u2014 skipping provisioning`);
|
|
692
692
|
const compute = (tsCloudConfig.infrastructure ??= {}).compute ??= {};
|
|
693
693
|
compute.webServer = "rpx";
|
|
694
|
-
compute.proxy = { ...compute.proxy ?? {}, engine: "rpx" };
|
|
694
|
+
compute.proxy = { onDemandTls: !0, ...compute.proxy ?? {}, engine: "rpx" };
|
|
695
695
|
const stackName = tsCloudConfig.project?.stackName || `${tsCloudConfig.project?.slug || "app"}-${environment}`, stateDir = join(process.cwd(), ".ts-cloud", "state");
|
|
696
696
|
mkdirSync(stateDir, { recursive: !0 });
|
|
697
697
|
writeFileSync(join(stateDir, `${stackName}.json`), `${JSON.stringify({
|
|
@@ -760,7 +760,7 @@ async function runHetznerDeploy(args) {
|
|
|
760
760
|
}
|
|
761
761
|
const root = site.root || ".", tarballPath = join(tmpdir(), `${slug}-${siteName}-${sha}.tar.gz`);
|
|
762
762
|
log.info(`Packaging ${root} \u2192 ${tarballPath}...`);
|
|
763
|
-
execSync(`tar czf "${tarballPath}" ${tarExcludes.join(" ")} -C "${root}" .`, { stdio: verbose ? "inherit" : "pipe" });
|
|
763
|
+
execSync(`tar czf "${tarballPath}" ${tarExcludes.join(" ")} -C "${root}" .`, { stdio: verbose ? "inherit" : "pipe", env: { ...process.env, COPYFILE_DISABLE: "1" } });
|
|
764
764
|
const sizeMb = Math.max(1, Math.round(statSync(tarballPath).size / 1048576));
|
|
765
765
|
log.info(`Release tarball: ~${sizeMb} MB`);
|
|
766
766
|
tarballs.set(siteName, tarballPath);
|
|
@@ -796,6 +796,8 @@ async function runHetznerDeploy(args) {
|
|
|
796
796
|
});
|
|
797
797
|
if (ok)
|
|
798
798
|
await reconcileHetznerDns(onlySite ? { [onlySite]: sites[onlySite] } : sites, ip, log);
|
|
799
|
+
if (ok)
|
|
800
|
+
await reconcileConfigDns(onlySite ? { [onlySite]: sites[onlySite] } : sites, log);
|
|
799
801
|
if (ok) {
|
|
800
802
|
const mailRes = await provisionMailTenant(ip, log);
|
|
801
803
|
if (mailRes)
|
|
@@ -967,7 +969,14 @@ if [ "$ENV_CHANGED" = 1 ]; then systemctl restart mail 2>/dev/null || true; echo
|
|
|
967
969
|
}
|
|
968
970
|
}
|
|
969
971
|
export async function reconcileMailDns(res, ip, logger) {
|
|
970
|
-
const { domain,
|
|
972
|
+
const { domain, dkimPubB64 } = res;
|
|
973
|
+
let { mailHost } = res;
|
|
974
|
+
try {
|
|
975
|
+
const dns = await import("node:dns"), own = `mail.${domain}`;
|
|
976
|
+
if (own !== mailHost && (await dns.promises.resolve4(own)).includes(ip))
|
|
977
|
+
mailHost = own;
|
|
978
|
+
} catch {}
|
|
979
|
+
const spf = `v=spf1 ip4:${ip} ~all`, cfg = emailConfig || {}, firstBox = resolveMailboxes(cfg.mailboxes, domain)[0]?.address, dmarc = `v=DMARC1; p=quarantine; rua=mailto:${(typeof cfg.from?.address === "string" && cfg.from.address.includes("@") ? cfg.from.address : void 0) || firstBox || `chris@${domain}`}`, dkim = dkimPubB64 ? `v=DKIM1; k=rsa; p=${dkimPubB64}` : void 0, apiKey = process.env.PORKBUN_API_KEY, secretKey = process.env.PORKBUN_SECRET_KEY;
|
|
971
980
|
if (!apiKey || !secretKey) {
|
|
972
981
|
logger.warn(`Mail DNS: no Porkbun credentials \u2014 add these records for ${domain} by hand:`);
|
|
973
982
|
logger.info(` MX @ 10 ${mailHost}`);
|
|
@@ -1001,6 +1010,24 @@ export async function reconcileMailDns(res, ip, logger) {
|
|
|
1001
1010
|
logger.warn(`Mail DNS reconcile skipped for ${domain}: ${getErrorMessage(err)}`);
|
|
1002
1011
|
}
|
|
1003
1012
|
}
|
|
1013
|
+
async function reconcileConfigDns(sites, logger) {
|
|
1014
|
+
if (["a", "aaaa", "cname", "mx", "txt"].reduce((total, key) => total + (Array.isArray(dnsConfig?.[key]) ? dnsConfig[key].length : 0), 0) === 0)
|
|
1015
|
+
return;
|
|
1016
|
+
const domains = new Set;
|
|
1017
|
+
for (const site of Object.values(sites))
|
|
1018
|
+
if (site?.domain && typeof site.domain === "string")
|
|
1019
|
+
domains.add(site.domain.replace(/^www\./, ""));
|
|
1020
|
+
for (const domain of domains)
|
|
1021
|
+
try {
|
|
1022
|
+
const result = await syncDnsConfig(domain, dnsConfig);
|
|
1023
|
+
if (!result.provider)
|
|
1024
|
+
continue;
|
|
1025
|
+
if (result.created || result.failed)
|
|
1026
|
+
logger.info(`DNS (config/dns.ts) ${domain}: ${result.created} created, ${result.kept} kept${result.failed ? `, ${result.failed} failed` : ""}`);
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
logger.warn(`DNS (config/dns.ts) reconcile for ${domain} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1004
1031
|
async function reconcileHetznerDns(sites, ip, logger) {
|
|
1005
1032
|
const domains = new Set;
|
|
1006
1033
|
for (const site of Object.values(sites))
|
package/dist/commands/dns.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { log, onUnknownSubcommand } from "@stacksjs/cli";
|
|
3
3
|
import { config } from "@stacksjs/config";
|
|
4
|
+
import { renderDnsConfig, resolveLiveRecords, syncDnsConfig } from "@stacksjs/dns";
|
|
4
5
|
import { ExitCode } from "@stacksjs/types";
|
|
5
6
|
export function dns(buddy) {
|
|
6
7
|
const descriptions = {
|
|
@@ -58,5 +59,44 @@ export function dns(buddy) {
|
|
|
58
59
|
}
|
|
59
60
|
process.exit(ExitCode.Success);
|
|
60
61
|
});
|
|
62
|
+
const bareDomain = (input) => (input || config.app.url || "").replace(/^[a-z]+:\/\//i, "").replace(/[/:].*$/, "");
|
|
63
|
+
buddy.command("dns:pull [domain]", "Print a domain's live DNS records as a config/dns.ts block").option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
64
|
+
const target = bareDomain(domain);
|
|
65
|
+
log.debug(`Running \`buddy dns:pull ${target}\` ...`, options);
|
|
66
|
+
const records = await resolveLiveRecords(target);
|
|
67
|
+
if (!records.length) {
|
|
68
|
+
log.error(`No DNS records resolved for ${target}.`);
|
|
69
|
+
process.exit(ExitCode.FatalError);
|
|
70
|
+
}
|
|
71
|
+
console.log(renderDnsConfig(target, records));
|
|
72
|
+
process.exit(ExitCode.Success);
|
|
73
|
+
});
|
|
74
|
+
buddy.command("dns:diff [domain]", "Show which config/dns.ts records are missing from the live zone").option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
75
|
+
const target = bareDomain(domain);
|
|
76
|
+
log.debug(`Running \`buddy dns:diff ${target}\` ...`, options);
|
|
77
|
+
const { plan, provider } = await syncDnsConfig(target, config.dns, { dryRun: !0 });
|
|
78
|
+
for (const item of plan.items) {
|
|
79
|
+
const detail = item.record.type === "TXT" || item.record.type === "MX" ? ` ${item.record.content}` : ` \u2192 ${item.record.content}`;
|
|
80
|
+
console.log(` ${item.action === "create" ? "+ create" : " keep "} ${item.record.type.padEnd(5)} ${item.record.name}${detail}`);
|
|
81
|
+
}
|
|
82
|
+
console.log(`
|
|
83
|
+
${plan.create.length} to create, ${plan.keep.length} already present (${provider ? `registrar: ${provider}` : "public DNS"})`);
|
|
84
|
+
process.exit(ExitCode.Success);
|
|
85
|
+
});
|
|
86
|
+
buddy.command("dns:sync [domain]", "Additively sync config/dns.ts to the registrar (creates missing records; never deletes or overwrites)").option("--dry-run", "Show the plan without writing any records", { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
87
|
+
const target = bareDomain(domain);
|
|
88
|
+
log.debug(`Running \`buddy dns:sync ${target}\` ...`, options);
|
|
89
|
+
const result = await syncDnsConfig(target, config.dns, { dryRun: options.dryRun });
|
|
90
|
+
if (!result.provider && !options.dryRun) {
|
|
91
|
+
log.warn(`No DNS provider credentials found (e.g. PORKBUN_API_KEY / PORKBUN_SECRET_KEY) \u2014 nothing was synced. ${result.plan.create.length} record(s) would be created.`);
|
|
92
|
+
process.exit(ExitCode.Success);
|
|
93
|
+
}
|
|
94
|
+
for (const record of result.plan.create)
|
|
95
|
+
console.log(` ${result.applied ? "created" : "would create"} ${record.type.padEnd(5)} ${record.name} \u2192 ${record.content}`);
|
|
96
|
+
const verb = result.applied ? "created" : "to create", count = result.applied ? result.created : result.plan.create.length;
|
|
97
|
+
console.log(`
|
|
98
|
+
dns:sync ${target}: ${count} ${verb}, ${result.kept} kept${result.failed ? `, ${result.failed} failed` : ""}${result.provider ? ` (${result.provider})` : ""}`);
|
|
99
|
+
process.exit(result.failed > 0 ? ExitCode.FatalError : ExitCode.Success);
|
|
100
|
+
});
|
|
61
101
|
onUnknownSubcommand(buddy, "dns");
|
|
62
102
|
}
|
package/dist/lazy-commands.js
CHANGED
|
@@ -15,6 +15,9 @@ const commandRegistry = {
|
|
|
15
15
|
deploy: { path: "./commands/deploy.js", exportName: "deploy" },
|
|
16
16
|
dev: { path: "./commands/dev.js", exportName: "dev" },
|
|
17
17
|
dns: { path: "./commands/dns.js", exportName: "dns" },
|
|
18
|
+
"dns:pull": { path: "./commands/dns.js", exportName: "dns" },
|
|
19
|
+
"dns:diff": { path: "./commands/dns.js", exportName: "dns" },
|
|
20
|
+
"dns:sync": { path: "./commands/dns.js", exportName: "dns" },
|
|
18
21
|
doctor: { path: "./commands/doctor.js", exportName: "doctor" },
|
|
19
22
|
domains: { path: "./commands/domains.js", exportName: "domains" },
|
|
20
23
|
email: { path: "./commands/email.js", exportName: "email" },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/buddy",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.76",
|
|
5
5
|
"description": "Meet Buddy. The Stacks runtime.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"contributors": [
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"@stacksjs/ui": "^0.70.23",
|
|
136
136
|
"@stacksjs/utils": "^0.70.23",
|
|
137
137
|
"@stacksjs/validation": "^0.70.23",
|
|
138
|
-
"@stacksjs/ts-cloud": "^0.7.
|
|
138
|
+
"@stacksjs/ts-cloud": "^0.7.15"
|
|
139
139
|
},
|
|
140
140
|
"devDependencies": {
|
|
141
141
|
"better-dx": "^0.2.16"
|