@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.
Files changed (143) hide show
  1. package/dist/cli.d.ts +1 -0
  2. package/dist/cli.js +165 -0
  3. package/dist/commands/about.d.ts +2 -0
  4. package/dist/commands/about.js +35 -0
  5. package/dist/commands/add.d.ts +2 -0
  6. package/dist/commands/add.js +33 -0
  7. package/dist/commands/auth.d.ts +2 -0
  8. package/dist/commands/auth.js +68 -0
  9. package/dist/commands/build.d.ts +2 -0
  10. package/dist/commands/build.js +153 -0
  11. package/dist/commands/cd.d.ts +20 -0
  12. package/dist/commands/cd.js +39 -0
  13. package/dist/commands/changelog.d.ts +2 -0
  14. package/dist/commands/changelog.js +32 -0
  15. package/dist/commands/clean.d.ts +2 -0
  16. package/dist/commands/clean.js +37 -0
  17. package/dist/commands/cloud.d.ts +2 -0
  18. package/dist/commands/cloud.js +503 -0
  19. package/dist/commands/commit.d.ts +2 -0
  20. package/dist/commands/commit.js +16 -0
  21. package/dist/commands/completion.d.ts +2 -0
  22. package/dist/commands/completion.js +143 -0
  23. package/dist/commands/config-migrate.d.ts +26 -0
  24. package/dist/commands/config-migrate.js +94 -0
  25. package/dist/commands/configure.d.ts +2 -0
  26. package/dist/commands/configure.js +48 -0
  27. package/dist/commands/create.d.ts +2 -0
  28. package/dist/commands/create.js +141 -0
  29. package/dist/commands/deploy.d.ts +107 -0
  30. package/dist/commands/deploy.js +1439 -0
  31. package/dist/commands/dev.d.ts +10 -0
  32. package/dist/commands/dev.js +907 -0
  33. package/dist/commands/dns.d.ts +2 -0
  34. package/dist/commands/dns.js +102 -0
  35. package/dist/commands/doctor.d.ts +2 -0
  36. package/dist/commands/doctor.js +285 -0
  37. package/dist/commands/domains.d.ts +2 -0
  38. package/dist/commands/domains.js +159 -0
  39. package/dist/commands/email.d.ts +2 -0
  40. package/dist/commands/email.js +524 -0
  41. package/dist/commands/env.d.ts +2 -0
  42. package/dist/commands/env.js +246 -0
  43. package/dist/commands/extension.d.ts +8 -0
  44. package/dist/commands/extension.js +78 -0
  45. package/dist/commands/features.d.ts +180 -0
  46. package/dist/commands/features.js +422 -0
  47. package/dist/commands/fresh.d.ts +2 -0
  48. package/dist/commands/fresh.js +39 -0
  49. package/dist/commands/generate.d.ts +2 -0
  50. package/dist/commands/generate.js +104 -0
  51. package/dist/commands/http.d.ts +2 -0
  52. package/dist/commands/http.js +33 -0
  53. package/dist/commands/index.d.ts +53 -0
  54. package/dist/commands/index.js +53 -0
  55. package/dist/commands/install.d.ts +2 -0
  56. package/dist/commands/install.js +22 -0
  57. package/dist/commands/key.d.ts +2 -0
  58. package/dist/commands/key.js +23 -0
  59. package/dist/commands/lint.d.ts +2 -0
  60. package/dist/commands/lint.js +46 -0
  61. package/dist/commands/list.d.ts +2 -0
  62. package/dist/commands/list.js +108 -0
  63. package/dist/commands/mail.d.ts +2 -0
  64. package/dist/commands/mail.js +693 -0
  65. package/dist/commands/maintenance.d.ts +2 -0
  66. package/dist/commands/maintenance.js +141 -0
  67. package/dist/commands/make.d.ts +2 -0
  68. package/dist/commands/make.js +375 -0
  69. package/dist/commands/migrate-project.d.ts +2 -0
  70. package/dist/commands/migrate-project.js +49 -0
  71. package/dist/commands/migrate.d.ts +4 -0
  72. package/dist/commands/migrate.js +455 -0
  73. package/dist/commands/outdated.d.ts +2 -0
  74. package/dist/commands/outdated.js +19 -0
  75. package/dist/commands/package.d.ts +2 -0
  76. package/dist/commands/package.js +17 -0
  77. package/dist/commands/phone.d.ts +2 -0
  78. package/dist/commands/phone.js +188 -0
  79. package/dist/commands/ports.d.ts +2 -0
  80. package/dist/commands/ports.js +118 -0
  81. package/dist/commands/prepublish.d.ts +2 -0
  82. package/dist/commands/prepublish.js +17 -0
  83. package/dist/commands/projects.d.ts +2 -0
  84. package/dist/commands/projects.js +29 -0
  85. package/dist/commands/publish.d.ts +2 -0
  86. package/dist/commands/publish.js +172 -0
  87. package/dist/commands/queue.d.ts +2 -0
  88. package/dist/commands/queue.js +248 -0
  89. package/dist/commands/release.d.ts +2 -0
  90. package/dist/commands/release.js +30 -0
  91. package/dist/commands/route.d.ts +2 -0
  92. package/dist/commands/route.js +21 -0
  93. package/dist/commands/saas.d.ts +2 -0
  94. package/dist/commands/saas.js +25 -0
  95. package/dist/commands/schedule.d.ts +2 -0
  96. package/dist/commands/schedule.js +61 -0
  97. package/dist/commands/search.d.ts +2 -0
  98. package/dist/commands/search.js +84 -0
  99. package/dist/commands/seed.d.ts +2 -0
  100. package/dist/commands/seed.js +71 -0
  101. package/dist/commands/serve.d.ts +25 -0
  102. package/dist/commands/serve.js +182 -0
  103. package/dist/commands/setup.d.ts +12 -0
  104. package/dist/commands/setup.js +215 -0
  105. package/dist/commands/share.d.ts +2 -0
  106. package/dist/commands/share.js +214 -0
  107. package/dist/commands/sms.d.ts +2 -0
  108. package/dist/commands/sms.js +328 -0
  109. package/dist/commands/stacks.d.ts +2 -0
  110. package/dist/commands/stacks.js +69 -0
  111. package/dist/commands/telemetry.d.ts +2 -0
  112. package/dist/commands/telemetry.js +74 -0
  113. package/dist/commands/test.d.ts +2 -0
  114. package/dist/commands/test.js +130 -0
  115. package/dist/commands/tinker.d.ts +2 -0
  116. package/dist/commands/tinker.js +37 -0
  117. package/dist/commands/types.d.ts +2 -0
  118. package/dist/commands/types.js +18 -0
  119. package/dist/commands/upgrade.d.ts +2 -0
  120. package/dist/commands/upgrade.js +98 -0
  121. package/dist/commands/version.d.ts +2 -0
  122. package/dist/commands/version.js +16 -0
  123. package/dist/config.d.ts +43 -0
  124. package/dist/config.js +223 -0
  125. package/dist/custom-cli.d.ts +1 -0
  126. package/dist/custom-cli.js +23 -0
  127. package/dist/index.d.ts +1 -0
  128. package/dist/index.js +1 -0
  129. package/dist/lazy-commands.d.ts +61 -0
  130. package/dist/lazy-commands.js +188 -0
  131. package/dist/migrators/index.d.ts +9 -0
  132. package/dist/migrators/index.js +62 -0
  133. package/dist/migrators/laravel/index.d.ts +2 -0
  134. package/dist/migrators/laravel/index.js +148 -0
  135. package/dist/migrators/laravel/migrations.d.ts +52 -0
  136. package/dist/migrators/laravel/migrations.js +232 -0
  137. package/dist/migrators/laravel/models.d.ts +34 -0
  138. package/dist/migrators/laravel/models.js +134 -0
  139. package/dist/migrators/rails/index.d.ts +2 -0
  140. package/dist/migrators/rails/index.js +11 -0
  141. package/dist/migrators/types.d.ts +44 -0
  142. package/dist/migrators/types.js +0 -0
  143. package/package.json +2 -2
@@ -0,0 +1,2 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ export declare function dns(buddy: CLI): void;
@@ -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,2 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ export declare function doctor(buddy: CLI): void;
@@ -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,2 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ export declare function domains(buddy: CLI): void;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import type { CLI } from '@stacksjs/types';
2
+ export declare function email(buddy: CLI): void;