@stacksjs/buddy 0.70.88 → 0.70.90
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/cli.d.ts +1 -0
- package/dist/cli.js +165 -0
- package/dist/commands/about.d.ts +2 -0
- package/dist/commands/about.js +35 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +33 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +68 -0
- package/dist/commands/build.d.ts +2 -0
- package/dist/commands/build.js +153 -0
- package/dist/commands/cd.d.ts +20 -0
- package/dist/commands/cd.js +39 -0
- package/dist/commands/changelog.d.ts +2 -0
- package/dist/commands/changelog.js +32 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.js +37 -0
- package/dist/commands/cloud.d.ts +2 -0
- package/dist/commands/cloud.js +503 -0
- package/dist/commands/commit.d.ts +2 -0
- package/dist/commands/commit.js +16 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +143 -0
- package/dist/commands/config-migrate.d.ts +26 -0
- package/dist/commands/config-migrate.js +94 -0
- package/dist/commands/configure.d.ts +2 -0
- package/dist/commands/configure.js +48 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +141 -0
- package/dist/commands/deploy.d.ts +107 -0
- package/dist/commands/deploy.js +1439 -0
- package/dist/commands/dev.d.ts +10 -0
- package/dist/commands/dev.js +907 -0
- package/dist/commands/dns.d.ts +2 -0
- package/dist/commands/dns.js +102 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +285 -0
- package/dist/commands/domains.d.ts +2 -0
- package/dist/commands/domains.js +159 -0
- package/dist/commands/email.d.ts +2 -0
- package/dist/commands/email.js +524 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +246 -0
- package/dist/commands/extension.d.ts +8 -0
- package/dist/commands/extension.js +78 -0
- package/dist/commands/features.d.ts +180 -0
- package/dist/commands/features.js +422 -0
- package/dist/commands/fresh.d.ts +2 -0
- package/dist/commands/fresh.js +39 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +104 -0
- package/dist/commands/http.d.ts +2 -0
- package/dist/commands/http.js +33 -0
- package/dist/commands/index.d.ts +53 -0
- package/dist/commands/index.js +53 -0
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +22 -0
- package/dist/commands/key.d.ts +2 -0
- package/dist/commands/key.js +23 -0
- package/dist/commands/lint.d.ts +2 -0
- package/dist/commands/lint.js +46 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +108 -0
- package/dist/commands/mail.d.ts +2 -0
- package/dist/commands/mail.js +693 -0
- package/dist/commands/maintenance.d.ts +2 -0
- package/dist/commands/maintenance.js +141 -0
- package/dist/commands/make.d.ts +2 -0
- package/dist/commands/make.js +375 -0
- package/dist/commands/migrate-project.d.ts +2 -0
- package/dist/commands/migrate-project.js +49 -0
- package/dist/commands/migrate.d.ts +4 -0
- package/dist/commands/migrate.js +455 -0
- package/dist/commands/outdated.d.ts +2 -0
- package/dist/commands/outdated.js +19 -0
- package/dist/commands/package.d.ts +2 -0
- package/dist/commands/package.js +17 -0
- package/dist/commands/phone.d.ts +2 -0
- package/dist/commands/phone.js +188 -0
- package/dist/commands/ports.d.ts +2 -0
- package/dist/commands/ports.js +118 -0
- package/dist/commands/prepublish.d.ts +2 -0
- package/dist/commands/prepublish.js +17 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +29 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +172 -0
- package/dist/commands/queue.d.ts +2 -0
- package/dist/commands/queue.js +248 -0
- package/dist/commands/release.d.ts +2 -0
- package/dist/commands/release.js +30 -0
- package/dist/commands/route.d.ts +2 -0
- package/dist/commands/route.js +21 -0
- package/dist/commands/saas.d.ts +2 -0
- package/dist/commands/saas.js +25 -0
- package/dist/commands/schedule.d.ts +2 -0
- package/dist/commands/schedule.js +61 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +84 -0
- package/dist/commands/seed.d.ts +2 -0
- package/dist/commands/seed.js +71 -0
- package/dist/commands/serve.d.ts +25 -0
- package/dist/commands/serve.js +182 -0
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.js +215 -0
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +214 -0
- package/dist/commands/sms.d.ts +2 -0
- package/dist/commands/sms.js +328 -0
- package/dist/commands/stacks.d.ts +2 -0
- package/dist/commands/stacks.js +69 -0
- package/dist/commands/telemetry.d.ts +2 -0
- package/dist/commands/telemetry.js +74 -0
- package/dist/commands/test.d.ts +2 -0
- package/dist/commands/test.js +130 -0
- package/dist/commands/tinker.d.ts +2 -0
- package/dist/commands/tinker.js +37 -0
- package/dist/commands/types.d.ts +2 -0
- package/dist/commands/types.js +18 -0
- package/dist/commands/upgrade.d.ts +2 -0
- package/dist/commands/upgrade.js +98 -0
- package/dist/commands/version.d.ts +2 -0
- package/dist/commands/version.js +16 -0
- package/dist/config.d.ts +43 -0
- package/dist/config.js +223 -0
- package/dist/custom-cli.d.ts +1 -0
- package/dist/custom-cli.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lazy-commands.d.ts +61 -0
- package/dist/lazy-commands.js +188 -0
- package/dist/migrators/index.d.ts +9 -0
- package/dist/migrators/index.js +62 -0
- package/dist/migrators/laravel/index.d.ts +2 -0
- package/dist/migrators/laravel/index.js +148 -0
- package/dist/migrators/laravel/migrations.d.ts +52 -0
- package/dist/migrators/laravel/migrations.js +232 -0
- package/dist/migrators/laravel/models.d.ts +34 -0
- package/dist/migrators/laravel/models.js +134 -0
- package/dist/migrators/rails/index.d.ts +2 -0
- package/dist/migrators/rails/index.js +11 -0
- package/dist/migrators/types.d.ts +44 -0
- package/dist/migrators/types.js +0 -0
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { log, onUnknownSubcommand } from "@stacksjs/cli";
|
|
3
|
+
import { config } from "@stacksjs/config";
|
|
4
|
+
import { renderDnsConfig, resolveLiveRecords, syncDnsConfig } from "@stacksjs/dns";
|
|
5
|
+
import { ExitCode } from "@stacksjs/types";
|
|
6
|
+
export function dns(buddy) {
|
|
7
|
+
const descriptions = {
|
|
8
|
+
dns: "Lists the DNS records for a domain",
|
|
9
|
+
query: "Host name or IP address to query",
|
|
10
|
+
type: "Type of the DNS record being queried (A, MX, NS\u2026)",
|
|
11
|
+
nameserver: "Address of the nameserver to send packets to",
|
|
12
|
+
class: "Network class of the DNS record being queried (IN, CH, HS)",
|
|
13
|
+
udp: "Use the DNS protocol over UDP",
|
|
14
|
+
tcp: "Use the DNS protocol over TCP",
|
|
15
|
+
tls: "Use the DNS-over-TLS protocol",
|
|
16
|
+
https: "Use the DNS-over-HTTPS protocol",
|
|
17
|
+
short: "Short mode: display nothing but the first result",
|
|
18
|
+
json: "Display the output as JSON",
|
|
19
|
+
project: "Target a specific project",
|
|
20
|
+
verbose: "Enable verbose output"
|
|
21
|
+
};
|
|
22
|
+
buddy.command("dns [domain]", descriptions.dns).option("-q, --query <query>", descriptions.query).option("-t, --type <type>", descriptions.type, { default: "A" }).option("-n, --nameserver <nameserver>", descriptions.nameserver).option("--class <class>", descriptions.class).option("-U, --udp", descriptions.udp).option("-T, --tcp", descriptions.tcp).option("-S, --tls", descriptions.tls).option("-H, --https", descriptions.https).option("-1, --short", descriptions.short, { default: !1 }).option("-J, --json", descriptions.json, { default: !1 }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
23
|
+
log.debug("Running `buddy dns [domain]` ...", options);
|
|
24
|
+
const targetDomain = domain || config.app.url;
|
|
25
|
+
let DnsClient, formatOutput;
|
|
26
|
+
try {
|
|
27
|
+
const dnsx = await import("@stacksjs/dnsx");
|
|
28
|
+
DnsClient = dnsx.DnsClient;
|
|
29
|
+
formatOutput = dnsx.formatOutput;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
log.error("`buddy dns` needs the @stacksjs/dnsx runtime, but only the type declarations are currently published. Install a build with the JS runtime (or wait for the next dnsx release) and re-run.");
|
|
32
|
+
log.debug(`[dns] import failure: ${err instanceof Error ? err.message : String(err)}`);
|
|
33
|
+
process.exit(ExitCode.FatalError);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const client = new DnsClient({
|
|
37
|
+
domains: [targetDomain],
|
|
38
|
+
type: options.type,
|
|
39
|
+
nameserver: options.nameserver,
|
|
40
|
+
class: options.class,
|
|
41
|
+
udp: options.udp,
|
|
42
|
+
tcp: options.tcp,
|
|
43
|
+
tls: options.tls,
|
|
44
|
+
https: options.https,
|
|
45
|
+
short: options.short,
|
|
46
|
+
json: options.json,
|
|
47
|
+
verbose: options.verbose
|
|
48
|
+
}), startTime = performance.now(), responses = await client.query(), duration = performance.now() - startTime, output = formatOutput(responses, {
|
|
49
|
+
json: options.json ?? !1,
|
|
50
|
+
short: options.short ?? !1,
|
|
51
|
+
showDuration: duration,
|
|
52
|
+
colors: { enabled: !0 },
|
|
53
|
+
rawSeconds: !1
|
|
54
|
+
});
|
|
55
|
+
console.log(output);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
log.error(`DNS query failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
process.exit(ExitCode.FatalError);
|
|
59
|
+
}
|
|
60
|
+
process.exit(ExitCode.Success);
|
|
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
|
+
});
|
|
101
|
+
onUnknownSubcommand(buddy, "dns");
|
|
102
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { bold, dim, green, intro, log, onUnknownSubcommand, red, yellow } from "@stacksjs/cli";
|
|
3
|
+
import { feature } from "@stacksjs/config";
|
|
4
|
+
import { storage } from "@stacksjs/storage";
|
|
5
|
+
import { FEATURE_NAMES, featurePathsPresent } from "./features";
|
|
6
|
+
async function probe(checks, name, fn, timeoutMs = 2000) {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
try {
|
|
9
|
+
const ac = new AbortController, timer = setTimeout(() => ac.abort(), timeoutMs), message = await Promise.race([
|
|
10
|
+
fn(),
|
|
11
|
+
new Promise((_, rej) => ac.signal.addEventListener("abort", () => rej(Error(`timed out (>${Math.round(timeoutMs / 1000)}s)`))))
|
|
12
|
+
]);
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
checks.push({ name, status: "pass", message: `${message} (${Date.now() - start}ms)` });
|
|
15
|
+
} catch (err) {
|
|
16
|
+
checks.push({
|
|
17
|
+
name,
|
|
18
|
+
status: "fail",
|
|
19
|
+
message: err instanceof Error ? err.message : String(err)
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function doctor(buddy) {
|
|
24
|
+
buddy.command("doctor", "Run health checks on your Stacks installation").option("--no-fail", "Print results but always exit 0 (CI ramp-up)").action(async (options) => {
|
|
25
|
+
log.debug("Running `buddy doctor` ...");
|
|
26
|
+
await intro("buddy doctor");
|
|
27
|
+
const checks = [], bunVersion = process.versions.bun;
|
|
28
|
+
if (bunVersion)
|
|
29
|
+
if (Number.parseInt(bunVersion.split(".")[0] || "0", 10) >= 1)
|
|
30
|
+
checks.push({
|
|
31
|
+
name: "Bun Runtime",
|
|
32
|
+
status: "pass",
|
|
33
|
+
message: `v${bunVersion}`
|
|
34
|
+
});
|
|
35
|
+
else
|
|
36
|
+
checks.push({
|
|
37
|
+
name: "Bun Runtime",
|
|
38
|
+
status: "warn",
|
|
39
|
+
message: `v${bunVersion} (v1.0+ recommended)`
|
|
40
|
+
});
|
|
41
|
+
else
|
|
42
|
+
checks.push({
|
|
43
|
+
name: "Bun Runtime",
|
|
44
|
+
status: "fail",
|
|
45
|
+
message: "Not found"
|
|
46
|
+
});
|
|
47
|
+
const nodeVersion = process.versions.node;
|
|
48
|
+
if (nodeVersion)
|
|
49
|
+
if (Number.parseInt(nodeVersion.split(".")[0] || "0", 10) >= 18)
|
|
50
|
+
checks.push({
|
|
51
|
+
name: "Node.js",
|
|
52
|
+
status: "pass",
|
|
53
|
+
message: `v${nodeVersion}`
|
|
54
|
+
});
|
|
55
|
+
else
|
|
56
|
+
checks.push({
|
|
57
|
+
name: "Node.js",
|
|
58
|
+
status: "warn",
|
|
59
|
+
message: `v${nodeVersion} (v18+ recommended)`
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
const pkg = await storage.readPackageJson("./package.json");
|
|
63
|
+
if (pkg.name)
|
|
64
|
+
checks.push({
|
|
65
|
+
name: "package.json",
|
|
66
|
+
status: "pass",
|
|
67
|
+
message: `Found: ${pkg.name}`
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
checks.push({
|
|
71
|
+
name: "package.json",
|
|
72
|
+
status: "fail",
|
|
73
|
+
message: "Not found in current directory"
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await storage.readTextFile(".env");
|
|
78
|
+
checks.push({
|
|
79
|
+
name: ".env file",
|
|
80
|
+
status: "pass",
|
|
81
|
+
message: "Found"
|
|
82
|
+
});
|
|
83
|
+
} catch {
|
|
84
|
+
checks.push({
|
|
85
|
+
name: ".env file",
|
|
86
|
+
status: "warn",
|
|
87
|
+
message: "Not found (optional)"
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const appKey = process.env.APP_KEY;
|
|
91
|
+
if (appKey && appKey.length > 0)
|
|
92
|
+
checks.push({
|
|
93
|
+
name: "APP_KEY",
|
|
94
|
+
status: appKey.length >= 32 ? "pass" : "warn",
|
|
95
|
+
message: appKey.length >= 32 ? "Set (\u226532 chars)" : `Set but short (${appKey.length} chars; \u226532 recommended)`
|
|
96
|
+
});
|
|
97
|
+
else
|
|
98
|
+
checks.push({
|
|
99
|
+
name: "APP_KEY",
|
|
100
|
+
status: "fail",
|
|
101
|
+
message: "Not set \u2014 features that depend on it (encrypted columns, signed URLs, env decryption) will refuse to run. Run `buddy key:generate`."
|
|
102
|
+
});
|
|
103
|
+
await probe(checks, "Database", async () => {
|
|
104
|
+
const { db } = await import("@stacksjs/database");
|
|
105
|
+
await db.unsafe?.("SELECT 1");
|
|
106
|
+
return "Reachable";
|
|
107
|
+
});
|
|
108
|
+
await probe(checks, "Database FKs", async () => {
|
|
109
|
+
const { auditForeignKeys } = await import("@stacksjs/database"), result = await auditForeignKeys();
|
|
110
|
+
if (result.declared.length === 0)
|
|
111
|
+
return "No belongsTo declarations";
|
|
112
|
+
if (result.missing.length === 0)
|
|
113
|
+
return `${result.declared.length} declared FKs all present`;
|
|
114
|
+
const sample = result.missing.slice(0, 5).map((fk) => `${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn}`).join(", "), more = result.missing.length > 5 ? ` (+${result.missing.length - 5} more)` : "";
|
|
115
|
+
throw Error(`${result.missing.length}/${result.declared.length} declared FKs missing from live schema: ${sample}${more}. Run \`buddy migrate:fresh\` (will reset data) or \`buddy migrate\` against a clean DB.`);
|
|
116
|
+
});
|
|
117
|
+
await probe(checks, "Unique indexes", async () => {
|
|
118
|
+
const { auditUniqueIndexes } = await import("@stacksjs/database"), result = await auditUniqueIndexes();
|
|
119
|
+
if (!result.supported)
|
|
120
|
+
return "Dialect not audited (skipped)";
|
|
121
|
+
const skipped = result.skippedTables.length > 0 ? `, ${result.skippedTables.length} tables skipped (not migrated)` : "";
|
|
122
|
+
if (result.missing.length === 0)
|
|
123
|
+
return `${result.declared.length} declared unique constraints all indexed${skipped}`;
|
|
124
|
+
const sample = result.missing.slice(0, 5).map((u) => `${u.table}.${u.columns.join("+")}`).join(", "), more = result.missing.length > 5 ? ` (+${result.missing.length - 5} more)` : "", first = result.missing[0];
|
|
125
|
+
if (!first)
|
|
126
|
+
throw Error("Unique-index audit reported missing entries without details");
|
|
127
|
+
const example = `CREATE UNIQUE INDEX IF NOT EXISTS "${first.table}_${first.columns.join("_")}_unique" ON "${first.table}" ("${first.columns.join('", "')}")`;
|
|
128
|
+
throw Error(`${result.missing.length}/${result.declared.length} declared unique constraints have no UNIQUE index: ${sample}${more}. Run \`buddy migrate\` (re-queues missing unique-index migrations, #1952) \u2014 dedupe duplicate rows first or migrate hard-fails; if no migration file exists run \`buddy generate:migrations\`, or create manually: ${example}`);
|
|
129
|
+
}, 1e4);
|
|
130
|
+
await probe(checks, "FK orphans", async () => {
|
|
131
|
+
const { findFkOrphans } = await import("@stacksjs/database"), result = await findFkOrphans();
|
|
132
|
+
if (!result.supported)
|
|
133
|
+
return "Dialect not audited (skipped)";
|
|
134
|
+
if (result.total === 0)
|
|
135
|
+
return "No orphan rows (PRAGMA foreign_key_check clean)";
|
|
136
|
+
const sample = result.orphans.slice(0, 5).map((o) => `${o.table}.${o.column} \u2192 ${o.parent} (${o.count})`).join(", "), more = result.orphans.length > 5 ? ` (+${result.orphans.length - 5} more)` : "", first = result.orphans[0];
|
|
137
|
+
if (!first)
|
|
138
|
+
throw Error("Foreign-key audit reported orphan rows without details");
|
|
139
|
+
throw Error(`${result.total} orphan rows violate FKs: ${sample}${more}. Legacy rows written under foreign_keys=OFF (#1951). Review and clean manually, e.g. DELETE FROM ${first.table} WHERE ${first.column} IS NOT NULL AND ${first.column} NOT IN (SELECT id FROM ${first.parent}) \u2014 doctor never deletes data.`);
|
|
140
|
+
}, 1e4);
|
|
141
|
+
await probe(checks, "Cache", async () => {
|
|
142
|
+
const { cache } = await import("@stacksjs/cache"), k = `__doctor_${Date.now()}`;
|
|
143
|
+
await cache.set(k, 1, 5);
|
|
144
|
+
const v = await cache.get(k);
|
|
145
|
+
await cache.del(k);
|
|
146
|
+
if (v !== 1)
|
|
147
|
+
throw Error("round-trip failed");
|
|
148
|
+
return "Round-trip ok";
|
|
149
|
+
});
|
|
150
|
+
await probe(checks, "Queue driver", async () => {
|
|
151
|
+
const { config } = await import("@stacksjs/config");
|
|
152
|
+
return `Driver: ${config.queue?.default ?? "sync"}`;
|
|
153
|
+
});
|
|
154
|
+
await probe(checks, "Mail driver", async () => {
|
|
155
|
+
const { config } = await import("@stacksjs/config");
|
|
156
|
+
return `Driver: ${config.email?.default ?? "log"}`;
|
|
157
|
+
});
|
|
158
|
+
const hasAwsCreds = Boolean(process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY), hasAwsRole = Boolean(process.env.AWS_PROFILE) || Boolean(process.env.AWS_ROLE_ARN);
|
|
159
|
+
if (hasAwsCreds || hasAwsRole)
|
|
160
|
+
checks.push({
|
|
161
|
+
name: "AWS credentials",
|
|
162
|
+
status: "pass",
|
|
163
|
+
message: hasAwsCreds ? "Static keys in env" : "IAM role configured"
|
|
164
|
+
});
|
|
165
|
+
else
|
|
166
|
+
checks.push({
|
|
167
|
+
name: "AWS credentials",
|
|
168
|
+
status: "warn",
|
|
169
|
+
message: "Not configured (cloud / SES / S3 commands will fail)"
|
|
170
|
+
});
|
|
171
|
+
await probe(checks, ".env decryption", async () => {
|
|
172
|
+
const fs = await import("node:fs");
|
|
173
|
+
if (!fs.existsSync(".env"))
|
|
174
|
+
return "No .env (skipped)";
|
|
175
|
+
const content = fs.readFileSync(".env", "utf8");
|
|
176
|
+
if (!/(?:^|=)(?:enc|encrypted):/m.test(content))
|
|
177
|
+
return "No encrypted values";
|
|
178
|
+
const privateKey = process.env.DOTENV_PRIVATE_KEY;
|
|
179
|
+
if (!privateKey)
|
|
180
|
+
throw Error("Encrypted values present but DOTENV_PRIVATE_KEY is unset");
|
|
181
|
+
const { parse } = await import("@stacksjs/env"), { errors } = parse(content, { privateKey });
|
|
182
|
+
if (errors.length > 0)
|
|
183
|
+
throw Error(errors.join("; "));
|
|
184
|
+
return "All encrypted values decrypt cleanly";
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
const { filesystems } = await import("@stacksjs/config"), driver = filesystems.driver ?? "local";
|
|
188
|
+
if (driver === "s3") {
|
|
189
|
+
const bucket = process.env.S3_BUCKET ?? filesystems.s3?.bucket, accessKeyId = process.env.AWS_ACCESS_KEY_ID, secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY, missing = [];
|
|
190
|
+
if (!bucket)
|
|
191
|
+
missing.push("S3_BUCKET");
|
|
192
|
+
if (!accessKeyId)
|
|
193
|
+
missing.push("AWS_ACCESS_KEY_ID");
|
|
194
|
+
if (!secretAccessKey)
|
|
195
|
+
missing.push("AWS_SECRET_ACCESS_KEY");
|
|
196
|
+
if (missing.length > 0)
|
|
197
|
+
checks.push({
|
|
198
|
+
name: "Storage credentials",
|
|
199
|
+
status: "warn",
|
|
200
|
+
message: `Default disk is 's3' but missing: ${missing.join(", ")}. Uploads will throw on first use.`
|
|
201
|
+
});
|
|
202
|
+
else
|
|
203
|
+
checks.push({
|
|
204
|
+
name: "Storage credentials",
|
|
205
|
+
status: "pass",
|
|
206
|
+
message: "s3 default disk has bucket + AWS credentials"
|
|
207
|
+
});
|
|
208
|
+
} else
|
|
209
|
+
checks.push({
|
|
210
|
+
name: "Storage credentials",
|
|
211
|
+
status: "pass",
|
|
212
|
+
message: `Default disk is '${driver}' (no remote credentials required)`
|
|
213
|
+
});
|
|
214
|
+
} catch (err) {
|
|
215
|
+
checks.push({
|
|
216
|
+
name: "Storage credentials",
|
|
217
|
+
status: "warn",
|
|
218
|
+
message: `Could not audit storage config: ${err instanceof Error ? err.message : String(err)}`
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const orphans = [];
|
|
223
|
+
for (const name of FEATURE_NAMES) {
|
|
224
|
+
if (feature(name))
|
|
225
|
+
continue;
|
|
226
|
+
const present = featurePathsPresent(name);
|
|
227
|
+
if (present.length > 0)
|
|
228
|
+
orphans.push({ feature: name, count: present.length });
|
|
229
|
+
}
|
|
230
|
+
if (orphans.length > 0) {
|
|
231
|
+
const summary = orphans.map((o) => `${o.feature} (${o.count} path${o.count === 1 ? "" : "s"})`).join(", ");
|
|
232
|
+
checks.push({
|
|
233
|
+
name: "Feature scaffolding",
|
|
234
|
+
status: "warn",
|
|
235
|
+
message: `Stamped files remain for disabled features: ${summary}. Run \`./buddy <feature>:uninstall\` to remove or \`<feature>:install\` to re-enable.`
|
|
236
|
+
});
|
|
237
|
+
} else
|
|
238
|
+
checks.push({
|
|
239
|
+
name: "Feature scaffolding",
|
|
240
|
+
status: "pass",
|
|
241
|
+
message: "No orphan files for disabled features"
|
|
242
|
+
});
|
|
243
|
+
} catch (err) {
|
|
244
|
+
checks.push({
|
|
245
|
+
name: "Feature scaffolding",
|
|
246
|
+
status: "warn",
|
|
247
|
+
message: `Could not audit feature scaffolding: ${err instanceof Error ? err.message : String(err)}`
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
log.info("");
|
|
251
|
+
log.info(bold("Health Check Results:"));
|
|
252
|
+
log.info(dim("\u2500".repeat(60)));
|
|
253
|
+
log.info("");
|
|
254
|
+
let hasFailures = !1, hasWarnings = !1;
|
|
255
|
+
for (const check of checks) {
|
|
256
|
+
let statusIcon = "", statusColor = (text) => text;
|
|
257
|
+
if (check.status === "pass") {
|
|
258
|
+
statusIcon = "\u2713";
|
|
259
|
+
statusColor = green;
|
|
260
|
+
} else if (check.status === "warn") {
|
|
261
|
+
statusIcon = "\u26A0";
|
|
262
|
+
statusColor = yellow;
|
|
263
|
+
hasWarnings = !0;
|
|
264
|
+
} else {
|
|
265
|
+
statusIcon = "\u2717";
|
|
266
|
+
statusColor = red;
|
|
267
|
+
hasFailures = !0;
|
|
268
|
+
}
|
|
269
|
+
log.info(`${statusColor(statusIcon)} ${bold(check.name.padEnd(20))} ${dim(check.message)}`);
|
|
270
|
+
}
|
|
271
|
+
log.info("");
|
|
272
|
+
log.info(dim("\u2500".repeat(60)));
|
|
273
|
+
log.info("");
|
|
274
|
+
if (hasFailures) {
|
|
275
|
+
log.error("Some critical checks failed. Please address the issues above.");
|
|
276
|
+
if (options?.fail !== !1)
|
|
277
|
+
process.exit(1);
|
|
278
|
+
} else if (hasWarnings)
|
|
279
|
+
log.info(yellow("Some checks have warnings. Your system should work but may have issues."));
|
|
280
|
+
else
|
|
281
|
+
log.success(green("All checks passed! Your Stacks installation looks healthy."));
|
|
282
|
+
log.info("");
|
|
283
|
+
});
|
|
284
|
+
onUnknownSubcommand(buddy, "doctor");
|
|
285
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { runAction } from "@stacksjs/actions";
|
|
3
|
+
import { bgCyan, bold, intro, italic, log, onUnknownSubcommand, outro, prompts } from "@stacksjs/cli";
|
|
4
|
+
import { config } from "@stacksjs/config";
|
|
5
|
+
import { addDomain } from "@stacksjs/dns";
|
|
6
|
+
import { Action } from "@stacksjs/enums";
|
|
7
|
+
import { ExitCode } from "@stacksjs/types";
|
|
8
|
+
export function domains(buddy) {
|
|
9
|
+
const descriptions = {
|
|
10
|
+
purchase: "Purchase a domain",
|
|
11
|
+
add: "Add a domain to your cloud",
|
|
12
|
+
remove: "Remove a domain from your cloud",
|
|
13
|
+
skip: "Skip the confirmation prompt",
|
|
14
|
+
project: "Target a specific project",
|
|
15
|
+
verbose: "Enable verbose output"
|
|
16
|
+
}, c = config.dns.contactInfo;
|
|
17
|
+
buddy.command("domains:purchase <domain>", descriptions.purchase).option("--years <years>", "Number of years to purchase the domain for", {
|
|
18
|
+
default: 1
|
|
19
|
+
}).option("--privacy", "Enable privacy protection", { default: !0 }).option("--auto-renew", "Enable auto-renew", { default: !0 }).option("--first-name <firstName>", "Registrant first name", {
|
|
20
|
+
default: c?.firstName
|
|
21
|
+
}).option("--last-name <lastName>", "Registrant last name", {
|
|
22
|
+
default: c?.lastName
|
|
23
|
+
}).option("--organization <organization>", "Registrant organization name", {
|
|
24
|
+
default: c?.organizationName
|
|
25
|
+
}).option("--address-line1 <address>", "Registrant address line 1", {
|
|
26
|
+
default: c?.addressLine1
|
|
27
|
+
}).option("--address-line2 <address>", "Registrant address line 2", {
|
|
28
|
+
default: c?.addressLine2
|
|
29
|
+
}).option("--city <city>", "Registrant city", { default: c?.city }).option("--state <state>", "Registrant state", { default: c?.state }).option("--country <country>", "Registrant country code", {
|
|
30
|
+
default: c?.countryCode
|
|
31
|
+
}).option("--zip <zip>", "Registrant zip", { default: c?.zip }).option("--phone <phone>", "Registrant phone", { default: c?.phoneNumber }).option("--email <email>", "Registrant email", { default: c?.email }).option("--admin-first-name <firstName>", "Admin first name", {
|
|
32
|
+
default: c?.admin?.firstName || c?.firstName
|
|
33
|
+
}).option("--admin-last-name <lastName>", "Admin last name", {
|
|
34
|
+
default: c?.admin?.lastName || c?.lastName
|
|
35
|
+
}).option("--admin-organization <organization>", "Admin organization", {
|
|
36
|
+
default: c?.admin?.organizationName || c?.organizationName
|
|
37
|
+
}).option("--admin-address-line1 <address>", "Admin address line 1", {
|
|
38
|
+
default: c?.admin?.addressLine1 || c?.addressLine1
|
|
39
|
+
}).option("--admin-address-line2 <address>", "Admin address line 2", {
|
|
40
|
+
default: c?.admin?.addressLine2 || c?.addressLine2
|
|
41
|
+
}).option("--admin-city <city>", "Admin city", {
|
|
42
|
+
default: c?.admin?.city || c?.city
|
|
43
|
+
}).option("--admin-state <state>", "Admin state", {
|
|
44
|
+
default: c?.admin?.state || c?.state
|
|
45
|
+
}).option("--admin-country <country>", "Admin country code", {
|
|
46
|
+
default: c?.admin?.countryCode || c?.countryCode
|
|
47
|
+
}).option("--admin-zip <zip>", "Admin zip", {
|
|
48
|
+
default: c?.admin?.zip || c?.zip
|
|
49
|
+
}).option("--admin-phone <phone>", "Admin phone number", {
|
|
50
|
+
default: c?.admin?.phoneNumber || c?.phoneNumber
|
|
51
|
+
}).option("--admin-email <email>", "Admin email", {
|
|
52
|
+
default: c?.admin?.email || c?.email
|
|
53
|
+
}).option("--tech-first-name <firstName>", "Tech first name", {
|
|
54
|
+
default: c?.tech?.firstName || c?.firstName
|
|
55
|
+
}).option("--tech-last-name <lastName>", "Tech last name", {
|
|
56
|
+
default: c?.tech?.lastName || c?.lastName
|
|
57
|
+
}).option("--tech-organization <organization>", "Tech organization name", {
|
|
58
|
+
default: c?.tech?.organizationName || c?.organizationName
|
|
59
|
+
}).option("--tech-address-line1 <address>", "Tech address line 1", {
|
|
60
|
+
default: c?.tech?.addressLine1 || c?.addressLine1
|
|
61
|
+
}).option("--tech-address-line2 <address>", "Tech address line 2", {
|
|
62
|
+
default: c?.tech?.addressLine2 || c?.addressLine2
|
|
63
|
+
}).option("--tech-city <city>", "Tech city", {
|
|
64
|
+
default: c?.tech?.city || c?.city
|
|
65
|
+
}).option("--tech-state <state>", "Tech state", {
|
|
66
|
+
default: c?.tech?.state || c?.state
|
|
67
|
+
}).option("--tech-country <country>", "Tech country", {
|
|
68
|
+
default: c?.tech?.countryCode || c?.countryCode
|
|
69
|
+
}).option("--tech-zip <zip>", "Tech zip", { default: c?.tech?.zip || c?.zip }).option("--tech-phone <phone>", "Tech phone", {
|
|
70
|
+
default: c?.tech?.phoneNumber || c?.phoneNumber
|
|
71
|
+
}).option("--tech-email <email>", "Tech email", {
|
|
72
|
+
default: c?.tech?.email || c?.email
|
|
73
|
+
}).option("--privacy-admin", "Enable privacy protection for admin", {
|
|
74
|
+
default: c?.privacyAdmin || c?.privacy || !0
|
|
75
|
+
}).option("--privacy-tech", "Enable privacy protection for tech", {
|
|
76
|
+
default: c?.privacyTech || c?.privacy || !0
|
|
77
|
+
}).option("--privacy-registrant", "Enable privacy protection for registrant", {
|
|
78
|
+
default: c?.privacyRegistrant || c?.privacy || !0
|
|
79
|
+
}).option("--contact-type <type>", "Contact type", { default: "person" }).option("-p, --project [project]", descriptions.project, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (domain, options) => {
|
|
80
|
+
log.debug("Running `buddy domains:purchase <domain>` ...", options);
|
|
81
|
+
options.domain = domain;
|
|
82
|
+
const startTime = await intro("buddy domains:purchase"), result = await runAction(Action.DomainsPurchase, options);
|
|
83
|
+
if (result.isErr) {
|
|
84
|
+
await outro("While running the domains:purchase command, there was an issue", { startTime, useSeconds: !0 }, result.error);
|
|
85
|
+
process.exit(ExitCode.FatalError);
|
|
86
|
+
}
|
|
87
|
+
const { confirm } = await prompts({
|
|
88
|
+
name: "confirm",
|
|
89
|
+
type: "confirm",
|
|
90
|
+
message: `Would you like to set ${domain} as your APP_URL?`
|
|
91
|
+
});
|
|
92
|
+
if (!confirm) {
|
|
93
|
+
await outro(`Alrighty! ${italic(domain)} was added to your account.`, {
|
|
94
|
+
startTime,
|
|
95
|
+
useSeconds: !0,
|
|
96
|
+
type: "success"
|
|
97
|
+
});
|
|
98
|
+
log.info(`Please note, you may need to validate your email address. Check your ${italic(options.registrantEmail)} inbox.`);
|
|
99
|
+
process.exit(ExitCode.Success);
|
|
100
|
+
}
|
|
101
|
+
const { writeEnv } = await import("@stacksjs/env");
|
|
102
|
+
writeEnv("APP_URL", domain);
|
|
103
|
+
let message = `Great! ${italic(domain)} was added to your account.`;
|
|
104
|
+
message += `
|
|
105
|
+
|
|
106
|
+
And your APP_URL has been set to ${italic(domain)}.
|
|
107
|
+
|
|
108
|
+
Please note, this change has not been deployed yet.
|
|
109
|
+
|
|
110
|
+
The next time you run ${bgCyan(italic(bold(" buddy deploy ")))}, your app will deploy to ${italic(domain)}.
|
|
111
|
+
|
|
112
|
+
${italic("You may need to deploy 2-3 times for the changes to take effect. Issue tracked here: https://github.com/stacksjs/stacks/issues/685")}`;
|
|
113
|
+
await outro(message, { startTime, useSeconds: !0, type: "info" });
|
|
114
|
+
process.exit(ExitCode.Success);
|
|
115
|
+
});
|
|
116
|
+
buddy.command("domains:add <domain>", descriptions.add).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
117
|
+
log.debug("Running `buddy domains:add <domain>` ...", options);
|
|
118
|
+
const startTime = await intro("buddy domains:add"), result = await addDomain({
|
|
119
|
+
...options,
|
|
120
|
+
startTime
|
|
121
|
+
});
|
|
122
|
+
if (result.isErr) {
|
|
123
|
+
await outro("While running the `buddy deploy`, there was an issue", { startTime, useSeconds: !0 }, result.error);
|
|
124
|
+
process.exit(ExitCode.FatalError);
|
|
125
|
+
}
|
|
126
|
+
await outro("Added your domain.", { startTime, useSeconds: !0 });
|
|
127
|
+
process.exit(ExitCode.Success);
|
|
128
|
+
});
|
|
129
|
+
buddy.command("domains:remove <domain>", descriptions.remove).option("--yes", descriptions.skip, { default: !1 }).option("--verbose", descriptions.verbose, { default: !1 }).action(async (options) => {
|
|
130
|
+
log.debug("Running `buddy domains:remove <domain>` ...", options);
|
|
131
|
+
const domain = options.domain || config.app.url, opts = { ...options, domain }, startTime = await intro("buddy domains:remove");
|
|
132
|
+
if (!opts.yes) {
|
|
133
|
+
const { confirm } = await prompts({
|
|
134
|
+
name: "confirm",
|
|
135
|
+
type: "confirm",
|
|
136
|
+
message: `Are you sure you want to remove ${domain}?`
|
|
137
|
+
});
|
|
138
|
+
if (!confirm) {
|
|
139
|
+
await outro("Cancelled the domains:remove command", {
|
|
140
|
+
startTime,
|
|
141
|
+
useSeconds: !0,
|
|
142
|
+
type: "info"
|
|
143
|
+
});
|
|
144
|
+
process.exit(ExitCode.Success);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const result = await runAction(Action.DomainsRemove, opts);
|
|
148
|
+
if (result.isErr) {
|
|
149
|
+
await outro("While running the domains:remove command, there was an issue", { startTime, useSeconds: !0 }, result.error);
|
|
150
|
+
process.exit(ExitCode.FatalError);
|
|
151
|
+
}
|
|
152
|
+
await outro("Removed your domain DNS records.", {
|
|
153
|
+
startTime,
|
|
154
|
+
useSeconds: !0
|
|
155
|
+
});
|
|
156
|
+
process.exit(ExitCode.Success);
|
|
157
|
+
});
|
|
158
|
+
onUnknownSubcommand(buddy, "domains");
|
|
159
|
+
}
|