@stacksjs/actions 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 (261) hide show
  1. package/dist/action.d.ts +149 -0
  2. package/dist/action.js +78 -0
  3. package/dist/actions.d.ts +2 -0
  4. package/dist/actions.js +2 -0
  5. package/dist/add.d.ts +6 -0
  6. package/dist/add.js +20 -0
  7. package/dist/ai/request-model-access.d.ts +1 -0
  8. package/dist/ai/request-model-access.js +10 -0
  9. package/dist/auth/client.d.ts +1 -0
  10. package/dist/auth/client.js +44 -0
  11. package/dist/auth/prune.d.ts +1 -0
  12. package/dist/auth/prune.js +35 -0
  13. package/dist/auth/setup.d.ts +1 -0
  14. package/dist/auth/setup.js +322 -0
  15. package/dist/auth/token.d.ts +1 -0
  16. package/dist/auth/token.js +10 -0
  17. package/dist/blog-admin.d.ts +55 -0
  18. package/dist/blog-admin.js +156 -0
  19. package/dist/blog.d.ts +30 -0
  20. package/dist/blog.js +398 -0
  21. package/dist/build/cli.d.ts +1 -0
  22. package/dist/build/cli.js +6 -0
  23. package/dist/build/component-libs.d.ts +1 -0
  24. package/dist/build/component-libs.js +7 -0
  25. package/dist/build/core.d.ts +1 -0
  26. package/dist/build/core.js +55 -0
  27. package/dist/build/desktop.d.ts +1 -0
  28. package/dist/build/desktop.js +5 -0
  29. package/dist/build/docs.d.ts +1 -0
  30. package/dist/build/docs.js +5 -0
  31. package/dist/build/frontend-static.d.ts +16 -0
  32. package/dist/build/frontend-static.js +124 -0
  33. package/dist/build/server.d.ts +1 -0
  34. package/dist/build/server.js +6 -0
  35. package/dist/build/stacks.d.ts +1 -0
  36. package/dist/build/stacks.js +8 -0
  37. package/dist/build/views.d.ts +1 -0
  38. package/dist/build/views.js +11 -0
  39. package/dist/build.d.ts +9 -0
  40. package/dist/build.js +64 -0
  41. package/dist/bump.d.ts +1 -0
  42. package/dist/bump.js +163 -0
  43. package/dist/cdn/invalidate-cache.d.ts +1 -0
  44. package/dist/cdn/invalidate-cache.js +0 -0
  45. package/dist/changelog.d.ts +9 -0
  46. package/dist/changelog.js +27 -0
  47. package/dist/check/ports.d.ts +1 -0
  48. package/dist/check/ports.js +16 -0
  49. package/dist/clean.d.ts +1 -0
  50. package/dist/clean.js +4 -0
  51. package/dist/commit.d.ts +3 -0
  52. package/dist/commit.js +12 -0
  53. package/dist/copy-types.d.ts +1 -0
  54. package/dist/copy-types.js +10 -0
  55. package/dist/database/seed.d.ts +1 -0
  56. package/dist/database/seed.js +26 -0
  57. package/dist/deploy/create-receipt-rule.d.ts +1 -0
  58. package/dist/deploy/create-receipt-rule.js +36 -0
  59. package/dist/deploy/index.d.ts +1 -0
  60. package/dist/deploy/index.js +1574 -0
  61. package/dist/dev/api.d.ts +1 -0
  62. package/dist/dev/api.js +94 -0
  63. package/dist/dev/components.d.ts +1 -0
  64. package/dist/dev/components.js +7 -0
  65. package/dist/dev/dashboard-globals.d.ts +4 -0
  66. package/dist/dev/dashboard-globals.js +89 -0
  67. package/dist/dev/dashboard-utils.d.ts +150 -0
  68. package/dist/dev/dashboard-utils.js +380 -0
  69. package/dist/dev/dashboard.d.ts +22 -0
  70. package/dist/dev/dashboard.js +435 -0
  71. package/dist/dev/desktop.d.ts +1 -0
  72. package/dist/dev/desktop.js +5 -0
  73. package/dist/dev/docs.d.ts +1 -0
  74. package/dist/dev/docs.js +24 -0
  75. package/dist/dev/index.d.ts +10 -0
  76. package/dist/dev/index.js +29 -0
  77. package/dist/dev/migrate-watcher.d.ts +1 -0
  78. package/dist/dev/migrate-watcher.js +74 -0
  79. package/dist/dev/system-tray.d.ts +1 -0
  80. package/dist/dev/system-tray.js +5 -0
  81. package/dist/dev/views.d.ts +1 -0
  82. package/dist/dev/views.js +147 -0
  83. package/dist/discover-packages.d.ts +18 -0
  84. package/dist/discover-packages.js +32 -0
  85. package/dist/domains/add.d.ts +1 -0
  86. package/dist/domains/add.js +61 -0
  87. package/dist/domains/purchase.d.ts +1 -0
  88. package/dist/domains/purchase.js +66 -0
  89. package/dist/domains/remove.d.ts +1 -0
  90. package/dist/domains/remove.js +24 -0
  91. package/dist/examples.d.ts +5 -0
  92. package/dist/examples.js +34 -0
  93. package/dist/find-projects.d.ts +1 -0
  94. package/dist/find-projects.js +16 -0
  95. package/dist/fresh.d.ts +1 -0
  96. package/dist/fresh.js +17 -0
  97. package/dist/generate/action-types.d.ts +1 -0
  98. package/dist/generate/action-types.js +13 -0
  99. package/dist/generate/component-meta.d.ts +1 -0
  100. package/dist/generate/component-meta.js +9 -0
  101. package/dist/generate/custom-cli-file.d.ts +1 -0
  102. package/dist/generate/custom-cli-file.js +21 -0
  103. package/dist/generate/env-files.d.ts +1 -0
  104. package/dist/generate/env-files.js +66 -0
  105. package/dist/generate/ide-helpers.d.ts +1 -0
  106. package/dist/generate/ide-helpers.js +8 -0
  107. package/dist/generate/index.d.ts +36 -0
  108. package/dist/generate/index.js +158 -0
  109. package/dist/generate/lib-entries.d.ts +1 -0
  110. package/dist/generate/lib-entries.js +16 -0
  111. package/dist/generate/pantry-file.d.ts +1 -0
  112. package/dist/generate/pantry-file.js +46 -0
  113. package/dist/generate/symlink-core.d.ts +1 -0
  114. package/dist/generate/symlink-core.js +2 -0
  115. package/dist/generate/types.d.ts +1 -0
  116. package/dist/generate/types.js +3 -0
  117. package/dist/generate/vscode-custom-data.d.ts +1 -0
  118. package/dist/generate/vscode-custom-data.js +9 -0
  119. package/dist/generate/web-types.d.ts +1 -0
  120. package/dist/generate/web-types.js +0 -0
  121. package/dist/helpers/component-meta.d.ts +26 -0
  122. package/dist/helpers/component-meta.js +295 -0
  123. package/dist/helpers/index.d.ts +4 -0
  124. package/dist/helpers/index.js +1 -0
  125. package/dist/helpers/lib-entries.d.ts +12 -0
  126. package/dist/helpers/lib-entries.js +73 -0
  127. package/dist/helpers/package-json.d.ts +2 -0
  128. package/dist/helpers/package-json.js +74 -0
  129. package/dist/helpers/utils.d.ts +23 -0
  130. package/dist/helpers/utils.js +105 -0
  131. package/dist/helpers/vscode-custom-data.d.ts +3 -0
  132. package/dist/helpers/vscode-custom-data.js +30 -0
  133. package/dist/index.d.ts +62 -0
  134. package/dist/index.js +50 -0
  135. package/dist/key-generate.d.ts +1 -0
  136. package/dist/key-generate.js +24 -0
  137. package/dist/lint/fix.d.ts +1 -0
  138. package/dist/lint/fix.js +4 -0
  139. package/dist/lint/index.d.ts +1 -0
  140. package/dist/lint/index.js +4 -0
  141. package/dist/lint/lint.d.ts +14 -0
  142. package/dist/lint/lint.js +34 -0
  143. package/dist/make-command.d.ts +10 -0
  144. package/dist/make-command.js +95 -0
  145. package/dist/make-job.d.ts +5 -0
  146. package/dist/make-job.js +154 -0
  147. package/dist/make-policy.d.ts +10 -0
  148. package/dist/make-policy.js +142 -0
  149. package/dist/make-resource.d.ts +10 -0
  150. package/dist/make-resource.js +142 -0
  151. package/dist/make.d.ts +52 -0
  152. package/dist/make.js +328 -0
  153. package/dist/migrate/database.d.ts +1 -0
  154. package/dist/migrate/database.js +16 -0
  155. package/dist/migrate/fresh.d.ts +1 -0
  156. package/dist/migrate/fresh.js +31 -0
  157. package/dist/orm/base.d.ts +1 -0
  158. package/dist/orm/base.js +0 -0
  159. package/dist/orm/test.d.ts +1 -0
  160. package/dist/orm/test.js +0 -0
  161. package/dist/prepublish.d.ts +1 -0
  162. package/dist/prepublish.js +16 -0
  163. package/dist/queue/clear.d.ts +1 -0
  164. package/dist/queue/clear.js +42 -0
  165. package/dist/queue/dlq-purge.d.ts +1 -0
  166. package/dist/queue/dlq-purge.js +28 -0
  167. package/dist/queue/dlq-retry.d.ts +1 -0
  168. package/dist/queue/dlq-retry.js +35 -0
  169. package/dist/queue/dlq.d.ts +1 -0
  170. package/dist/queue/dlq.js +55 -0
  171. package/dist/queue/failed.d.ts +1 -0
  172. package/dist/queue/failed.js +69 -0
  173. package/dist/queue/flush.d.ts +1 -0
  174. package/dist/queue/flush.js +49 -0
  175. package/dist/queue/inspect.d.ts +1 -0
  176. package/dist/queue/inspect.js +154 -0
  177. package/dist/queue/list.d.ts +1 -0
  178. package/dist/queue/list.js +69 -0
  179. package/dist/queue/monitor.d.ts +1 -0
  180. package/dist/queue/monitor.js +95 -0
  181. package/dist/queue/pause.d.ts +1 -0
  182. package/dist/queue/pause.js +28 -0
  183. package/dist/queue/quarantine.d.ts +1 -0
  184. package/dist/queue/quarantine.js +42 -0
  185. package/dist/queue/resume.d.ts +1 -0
  186. package/dist/queue/resume.js +27 -0
  187. package/dist/queue/retry.d.ts +1 -0
  188. package/dist/queue/retry.js +41 -0
  189. package/dist/queue/schedule-list.d.ts +1 -0
  190. package/dist/queue/schedule-list.js +33 -0
  191. package/dist/queue/schedule.d.ts +1 -0
  192. package/dist/queue/schedule.js +12 -0
  193. package/dist/queue/status.d.ts +1 -0
  194. package/dist/queue/status.js +68 -0
  195. package/dist/queue/table.d.ts +1 -0
  196. package/dist/queue/table.js +10 -0
  197. package/dist/queue/unquarantine.d.ts +1 -0
  198. package/dist/queue/unquarantine.js +27 -0
  199. package/dist/queue/work.d.ts +1 -0
  200. package/dist/queue/work.js +57 -0
  201. package/dist/release.d.ts +1 -0
  202. package/dist/release.js +21 -0
  203. package/dist/route/list.d.ts +1 -0
  204. package/dist/route/list.js +78 -0
  205. package/dist/run-action.d.ts +2 -0
  206. package/dist/run-action.js +1 -0
  207. package/dist/saas/setup.d.ts +1 -0
  208. package/dist/saas/setup.js +9 -0
  209. package/dist/scaffold-crud.d.ts +25 -0
  210. package/dist/scaffold-crud.js +211 -0
  211. package/dist/schedule/run.d.ts +1 -0
  212. package/dist/schedule/run.js +7 -0
  213. package/dist/search/flush.d.ts +1 -0
  214. package/dist/search/flush.js +10 -0
  215. package/dist/search/import.d.ts +1 -0
  216. package/dist/search/import.js +10 -0
  217. package/dist/search/settings-list.d.ts +1 -0
  218. package/dist/search/settings-list.js +10 -0
  219. package/dist/search/settings.d.ts +1 -0
  220. package/dist/search/settings.js +9 -0
  221. package/dist/serve/api.d.ts +1 -0
  222. package/dist/serve/api.js +35 -0
  223. package/dist/setup/index.d.ts +1 -0
  224. package/dist/setup/index.js +1 -0
  225. package/dist/setup/ssl.d.ts +38 -0
  226. package/dist/setup/ssl.js +271 -0
  227. package/dist/stacks.d.ts +4 -0
  228. package/dist/stacks.js +265 -0
  229. package/dist/templates.d.ts +28 -0
  230. package/dist/templates.js +301 -0
  231. package/dist/test/feature.d.ts +1 -0
  232. package/dist/test/feature.js +11 -0
  233. package/dist/test/index.d.ts +1 -0
  234. package/dist/test/index.js +11 -0
  235. package/dist/test/ui.d.ts +1 -0
  236. package/dist/test/ui.js +4 -0
  237. package/dist/test/unit.d.ts +1 -0
  238. package/dist/test/unit.js +11 -0
  239. package/dist/typecheck.d.ts +1 -0
  240. package/dist/typecheck.js +4 -0
  241. package/dist/types.d.ts +3 -0
  242. package/dist/types.js +16 -0
  243. package/dist/upgrade/binary.d.ts +1 -0
  244. package/dist/upgrade/binary.js +29 -0
  245. package/dist/upgrade/bun.d.ts +1 -0
  246. package/dist/upgrade/bun.js +0 -0
  247. package/dist/upgrade/dependencies.d.ts +1 -0
  248. package/dist/upgrade/dependencies.js +0 -0
  249. package/dist/upgrade/framework-utils.d.ts +150 -0
  250. package/dist/upgrade/framework-utils.js +193 -0
  251. package/dist/upgrade/framework.d.ts +1 -0
  252. package/dist/upgrade/framework.js +310 -0
  253. package/dist/upgrade/index.d.ts +1 -0
  254. package/dist/upgrade/index.js +17 -0
  255. package/dist/upgrade/packages.d.ts +14 -0
  256. package/dist/upgrade/packages.js +99 -0
  257. package/dist/upgrade/shell.d.ts +1 -0
  258. package/dist/upgrade/shell.js +41 -0
  259. package/dist/upgrade.d.ts +7 -0
  260. package/dist/upgrade.js +38 -0
  261. package/package.json +16 -16
@@ -0,0 +1,1574 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import process from "node:process";
3
+ import { runCommand, spinner } from "@stacksjs/cli";
4
+ import { config } from "@stacksjs/config";
5
+ import { path as p } from "@stacksjs/path";
6
+ import { storage } from "@stacksjs/storage";
7
+ import { ExitCode } from "@stacksjs/types";
8
+ const log = {
9
+ debug: (...args) => {
10
+ if (process.argv.includes("--verbose") || process.argv.includes("-v"))
11
+ console.log("\uD83D\uDD0D", ...args);
12
+ },
13
+ success: (...args) => console.log("\u2713", ...args),
14
+ error: (...args) => console.error("\u2717", ...args)
15
+ }, MAIL_PACKAGE_DOMAIN = "github.com/mail-os/mail", MAIL_PACKAGE_SPEC = `${MAIL_PACKAGE_DOMAIN}@0.1.0`, MAIL_TARGET_PLATFORM = "linux-x86_64", MAIL_BINARY_NAMES = ["mail", "mail-x86_64-linux", "mail-x86_64-linux-gnu"], isVerbose = process.argv.includes("--verbose") || process.argv.includes("-v");
16
+ async function collectMatchingFiles(root, names, maxDepth = 8) {
17
+ const { existsSync, readdirSync, statSync } = await import("node:fs"), { join, basename } = await import("node:path"), nameSet = new Set(names), matches = [];
18
+ if (!existsSync(root))
19
+ return matches;
20
+ function walk(dir, depth) {
21
+ if (depth > maxDepth)
22
+ return;
23
+ for (const entry of readdirSync(dir)) {
24
+ if (entry === ".git" || entry === "node_modules")
25
+ continue;
26
+ const fullPath = join(dir, entry), stat = statSync(fullPath);
27
+ if (stat.isDirectory())
28
+ walk(fullPath, depth + 1);
29
+ else if (stat.isFile() && nameSet.has(basename(fullPath)))
30
+ matches.push(fullPath);
31
+ }
32
+ }
33
+ walk(root, 0);
34
+ return matches;
35
+ }
36
+ async function isElfBinary(filePath) {
37
+ const { readFileSync } = await import("node:fs"), header = readFileSync(filePath).slice(0, 4);
38
+ return header[0] === 127 && header[1] === 69 && header[2] === 76 && header[3] === 70;
39
+ }
40
+ async function resolvePantryInstallCommand() {
41
+ const { existsSync } = await import("node:fs"), localPantryCli = `${process.env.HOME}/Code/Tools/pantry/packages/ts-pantry/bin/cli.ts`;
42
+ if (existsSync(localPantryCli))
43
+ return { command: "bun", args: [localPantryCli] };
44
+ const projectPantry = p.projectPath("pantry/.bin/pantry");
45
+ if (existsSync(projectPantry))
46
+ return { command: projectPantry, args: [] };
47
+ const globalPantry = `${process.env.HOME}/.local/share/pantry/global/bin/pantry`;
48
+ if (existsSync(globalPantry))
49
+ return { command: globalPantry, args: [] };
50
+ return { command: "pantry", args: [] };
51
+ }
52
+ async function installMailBinaryWithPantry() {
53
+ const { execFileSync } = await import("node:child_process"), installDir = p.projectPath("pantry"), pantry = await resolvePantryInstallCommand();
54
+ execFileSync(pantry.command, [
55
+ ...pantry.args,
56
+ "install",
57
+ MAIL_PACKAGE_SPEC,
58
+ "--install-dir",
59
+ installDir,
60
+ "--platform",
61
+ MAIL_TARGET_PLATFORM,
62
+ "--quiet"
63
+ ], {
64
+ cwd: p.projectPath(),
65
+ stdio: isVerbose ? "inherit" : "pipe",
66
+ env: process.env
67
+ });
68
+ }
69
+ async function findPantryMailBinary() {
70
+ const { existsSync } = await import("node:fs"), { join } = await import("node:path"), pantryRoots = [
71
+ p.projectPath("pantry"),
72
+ `${process.env.HOME}/.local/share/pantry`
73
+ ], directCandidates = [
74
+ ...MAIL_BINARY_NAMES.map((name) => p.projectPath(`pantry/.bin/${name}`)),
75
+ ...MAIL_BINARY_NAMES.map((name) => join(`${process.env.HOME}/.local/share/pantry/global/bin`, name))
76
+ ];
77
+ for (const candidate of directCandidates)
78
+ if (existsSync(candidate) && await isElfBinary(candidate))
79
+ return candidate;
80
+ for (const root of pantryRoots) {
81
+ const candidates = await collectMatchingFiles(root, MAIL_BINARY_NAMES);
82
+ for (const candidate of candidates)
83
+ if (await isElfBinary(candidate))
84
+ return candidate;
85
+ }
86
+ return null;
87
+ }
88
+ const MIME_TYPES = {
89
+ ".html": "text/html",
90
+ ".css": "text/css",
91
+ ".js": "application/javascript",
92
+ ".json": "application/json",
93
+ ".png": "image/png",
94
+ ".jpg": "image/jpeg",
95
+ ".jpeg": "image/jpeg",
96
+ ".gif": "image/gif",
97
+ ".svg": "image/svg+xml",
98
+ ".ico": "image/x-icon",
99
+ ".woff": "font/woff",
100
+ ".woff2": "font/woff2",
101
+ ".ttf": "font/ttf",
102
+ ".eot": "application/vnd.ms-fontobject",
103
+ ".otf": "font/otf",
104
+ ".webp": "image/webp",
105
+ ".mp4": "video/mp4",
106
+ ".webm": "video/webm",
107
+ ".mp3": "audio/mpeg",
108
+ ".wav": "audio/wav",
109
+ ".pdf": "application/pdf",
110
+ ".xml": "application/xml",
111
+ ".txt": "text/plain"
112
+ };
113
+ function getMimeType(filePath) {
114
+ const dotIndex = filePath.lastIndexOf("."), ext = dotIndex >= 0 ? filePath.slice(dotIndex).toLowerCase() : "";
115
+ return MIME_TYPES[ext] || "application/octet-stream";
116
+ }
117
+ async function traverseDirectory(dir, callback, prefix = "", skipPatterns = [".DS_Store"]) {
118
+ const { readdirSync, statSync } = await import("node:fs"), { join } = await import("node:path"), items = readdirSync(dir);
119
+ for (const item of items) {
120
+ if (skipPatterns.includes(item))
121
+ continue;
122
+ const filePath = join(dir, item), key = prefix ? `${prefix}/${item}` : item;
123
+ if (statSync(filePath).isDirectory())
124
+ await traverseDirectory(filePath, callback, key, skipPatterns);
125
+ else
126
+ await callback(filePath, key);
127
+ }
128
+ }
129
+ async function withS3Retry(fn, label = "s3 operation") {
130
+ const maxRetries = 3;
131
+ for (let attempt = 0;attempt <= maxRetries; attempt++)
132
+ try {
133
+ return await fn();
134
+ } catch (error) {
135
+ if (attempt >= maxRetries)
136
+ throw error;
137
+ const delay = Math.min(500 * 2 ** attempt + Math.random() * 200, 5000);
138
+ if (isVerbose)
139
+ log.debug(` Retrying ${label} (attempt ${attempt + 2}/${maxRetries + 1}) in ${Math.round(delay)}ms...`);
140
+ await new Promise((resolve) => setTimeout(resolve, delay));
141
+ }
142
+ throw Error(`${label} failed after ${maxRetries + 1} attempts`);
143
+ }
144
+ function normalizeArray(value) {
145
+ if (!value)
146
+ return [];
147
+ return Array.isArray(value) ? value : [value];
148
+ }
149
+ function buildCloudFrontXmlElement(name, value, indent = "") {
150
+ if (value === null || value === void 0)
151
+ return "";
152
+ if (typeof value === "boolean")
153
+ return `${indent}<${name}>${value}</${name}>
154
+ `;
155
+ if (typeof value === "number" || typeof value === "string")
156
+ return `${indent}<${name}>${value}</${name}>
157
+ `;
158
+ if (Array.isArray(value))
159
+ return value.map((item) => buildCloudFrontXmlElement(name, item, indent)).join("");
160
+ if (typeof value === "object") {
161
+ if (name.startsWith("@_") || name === "?xml")
162
+ return "";
163
+ let children = "";
164
+ for (const [key, val] of Object.entries(value))
165
+ if (!key.startsWith("@_") && key !== "#text")
166
+ children += buildCloudFrontXmlElement(key, val, `${indent} `);
167
+ if (value["#text"] !== void 0)
168
+ return `${indent}<${name}>${value["#text"]}</${name}>
169
+ `;
170
+ return `${indent}<${name}>
171
+ ${children}${indent}</${name}>
172
+ `;
173
+ }
174
+ return "";
175
+ }
176
+ function buildCloudFrontDistributionConfigXml(config) {
177
+ return `<?xml version="1.0" encoding="UTF-8"?>
178
+ <DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
179
+ ${Object.entries(config).filter(([key]) => !key.startsWith("@_")).map(([key, val]) => buildCloudFrontXmlElement(key, val, " ")).join("")}</DistributionConfig>
180
+ `;
181
+ }
182
+ function firewallOpenCommand(ports) {
183
+ const uniquePorts = [...new Set(ports)].filter((port) => Number.isFinite(port) && port > 0).sort((a, b) => a - b);
184
+ if (uniquePorts.length === 0)
185
+ return "true";
186
+ return `if command -v firewall-cmd >/dev/null 2>&1; then firewall-cmd --permanent ${uniquePorts.map((port) => `--add-port=${port}/tcp`).join(" ")} && firewall-cmd --reload; fi`;
187
+ }
188
+ async function ensureSecurityGroupPort(ec2, securityGroupId, port) {
189
+ if (!securityGroupId)
190
+ return;
191
+ if (((await ec2.describeSecurityGroups({ GroupIds: [securityGroupId] }))?.SecurityGroups?.[0]?.IpPermissions || []).some((permission) => {
192
+ return permission.IpProtocol === "tcp" && Number(permission.FromPort) <= port && Number(permission.ToPort) >= port && normalizeArray(permission.IpRanges).some((range) => range.CidrIp === "0.0.0.0/0");
193
+ }))
194
+ return;
195
+ try {
196
+ await ec2.authorizeSecurityGroupIngress({
197
+ GroupId: securityGroupId,
198
+ IpPermissions: [{
199
+ IpProtocol: "tcp",
200
+ FromPort: port,
201
+ ToPort: port,
202
+ IpRanges: [{ CidrIp: "0.0.0.0/0", Description: `Stacks API port ${port}` }]
203
+ }]
204
+ });
205
+ } catch (error) {
206
+ if (!String(error?.message || "").includes("InvalidPermission.Duplicate"))
207
+ throw error;
208
+ }
209
+ }
210
+ async function ensureCloudFrontApiOriginPort(awsClient, distributionId, port) {
211
+ if (!distributionId)
212
+ return;
213
+ const getResult = await awsClient.request({
214
+ service: "cloudfront",
215
+ region: "us-east-1",
216
+ method: "GET",
217
+ path: `/2020-05-31/distribution/${distributionId}/config`,
218
+ returnHeaders: !0
219
+ }), etag = getResult.headers?.etag || getResult.headers?.ETag || "", currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig || getResult.body;
220
+ if (!etag || !currentConfig?.Origins?.Items)
221
+ return;
222
+ const apiOrigin = normalizeArray(currentConfig.Origins.Items.Origin).find((origin) => String(origin.Id || "").includes("-api"));
223
+ if (!apiOrigin?.CustomOriginConfig)
224
+ return;
225
+ if (Number(apiOrigin.CustomOriginConfig.HTTPPort) === port)
226
+ return;
227
+ apiOrigin.CustomOriginConfig.HTTPPort = port;
228
+ await awsClient.request({
229
+ service: "cloudfront",
230
+ region: "us-east-1",
231
+ method: "PUT",
232
+ path: `/2020-05-31/distribution/${distributionId}/config`,
233
+ body: buildCloudFrontDistributionConfigXml(currentConfig),
234
+ headers: {
235
+ "Content-Type": "application/xml",
236
+ "If-Match": etag
237
+ }
238
+ });
239
+ }
240
+ const frameworkBuildSpinner = spinner("Building framework...");
241
+ frameworkBuildSpinner.start();
242
+ await runCommand("bun run build", {
243
+ cwd: p.frameworkPath("core"),
244
+ quiet: !1
245
+ });
246
+ frameworkBuildSpinner.succeed("Framework built");
247
+ const docsDir = p.projectPath("docs"), docsBuildDir = p.projectPath("dist/docs/.bunpress");
248
+ if (storage.hasFiles(docsDir)) {
249
+ const docsSpinner = spinner("Building documentation with BunPress...");
250
+ docsSpinner.start();
251
+ try {
252
+ const { rm } = await import("node:fs/promises"), localBunpressCli = `${process.env.HOME}/Code/Tools/bunpress/packages/bunpress/bin/cli.ts`, installedBunpressCli = p.projectPath("node_modules/@stacksjs/bunpress/dist/bin/cli.js"), hasLocalBunpress = await Bun.file(localBunpressCli).exists(), hasInstalledBunpress = await Bun.file(installedBunpressCli).exists(), command = hasLocalBunpress ? `bun ${localBunpressCli} build --dir ${docsDir} --outdir ${p.projectPath("dist/docs")}` : hasInstalledBunpress ? `bun ${installedBunpressCli} build --dir ${docsDir} --outdir ${p.projectPath("dist/docs")}` : "bunx @stacksjs/bunpress@0.1.5 build --dir ./docs --outdir ./dist/docs";
253
+ await rm(docsBuildDir, { recursive: !0, force: !0 });
254
+ await runCommand(command, {
255
+ cwd: p.projectPath(),
256
+ quiet: !isVerbose
257
+ });
258
+ docsSpinner.succeed(`Documentation built with BunPress${hasLocalBunpress ? " (local checkout)" : ""}`);
259
+ } catch (docsError) {
260
+ docsSpinner.stop();
261
+ console.log(`\u26A0 Documentation build skipped: ${docsError.message}`);
262
+ if (isVerbose)
263
+ log.debug("To build docs manually: cd ~/Code/Tools/bunpress && bun packages/bunpress/bin/cli.ts build --dir /path/to/docs --outdir /path/to/dist/docs");
264
+ }
265
+ } else if (isVerbose)
266
+ log.debug("No docs directory found, skipping documentation build");
267
+ const blogDistExists = storage.hasFiles(p.projectPath("dist/blog")), blogFontAssetsExist = [
268
+ "CampmateScript-Regular.woff2",
269
+ "SequoiaSans-Regular.woff2",
270
+ "Switchback-Regular.woff2"
271
+ ].every((file) => existsSync(p.projectPath(`dist/blog/assets/fonts/nps/${file}`))), blogImageAssetsExist = [
272
+ "topography.svg",
273
+ "park-ridge.svg"
274
+ ].every((file) => existsSync(p.projectPath(`dist/blog/assets/images/${file}`))), blogIndexPath = p.projectPath("dist/blog/index.html"), blogIndexHtml = existsSync(blogIndexPath) ? readFileSync(blogIndexPath, "utf8") : "", blogHtmlUsesCurrentFontFormat = blogIndexHtml.length > 0 && !blogIndexHtml.includes("woff2-variations"), blogHtmlUsesCurrentParkTheme = blogIndexHtml.includes("--on-primary") && blogIndexHtml.includes("--newsletter-bg"), blogHtmlUsesCurrentParkImages = blogIndexHtml.includes("topography.svg") && blogIndexHtml.includes("park-ridge.svg"), blogHtmlOmitsRetiredDecorations = !blogIndexHtml.includes("stamp-trail") && !blogIndexHtml.includes("blog-sign-tree.svg"), blogBuilderPath = p.frameworkPath("core/cms/src/build.ts"), blogConfigPath = p.projectPath("config/blog.ts"), blogHtmlIsFresh = existsSync(blogIndexPath) && [blogBuilderPath, blogConfigPath].filter((path) => existsSync(path)).every((path) => statSync(blogIndexPath).mtimeMs >= statSync(path).mtimeMs);
275
+ if (!blogDistExists || !blogFontAssetsExist || !blogImageAssetsExist || !blogHtmlUsesCurrentFontFormat || !blogHtmlUsesCurrentParkTheme || !blogHtmlUsesCurrentParkImages || !blogHtmlOmitsRetiredDecorations || !blogHtmlIsFresh) {
276
+ const blogBuildSpinner = spinner("Building blog...");
277
+ blogBuildSpinner.start();
278
+ try {
279
+ const blogConfig = (await import(p.projectPath("config/blog"))).default, { buildBlogSite } = await import(p.frameworkPath("core/cms/src/build"));
280
+ await buildBlogSite({ config: blogConfig, outDir: p.projectPath("dist/blog") });
281
+ blogBuildSpinner.succeed("Blog built successfully");
282
+ } catch (blogBuildError) {
283
+ blogBuildSpinner.stop();
284
+ console.log(`\u26A0 Blog build skipped: ${blogBuildError.message}`);
285
+ if (isVerbose)
286
+ log.debug(`Blog build error: ${blogBuildError.stack}`);
287
+ }
288
+ } else if (isVerbose)
289
+ log.debug("Pre-built blog found at dist/blog, skipping build");
290
+ if (isVerbose)
291
+ log.debug("Skipping views build (vite-config not configured)");
292
+ const earlyCloudConfig = await import(p.projectPath("config/cloud")), earlyDeploymentMode = earlyCloudConfig?.tsCloud?.mode || "server";
293
+ if (earlyDeploymentMode === "serverless") {
294
+ const serverSpinner = spinner("Building server...");
295
+ serverSpinner.start();
296
+ await runCommand("bun build.ts", {
297
+ cwd: p.frameworkPath("server"),
298
+ quiet: !isVerbose
299
+ });
300
+ serverSpinner.succeed("Server built");
301
+ const packageSpinner = spinner("Packaging for deployment...");
302
+ packageSpinner.start();
303
+ await runCommand("bun zip.ts", {
304
+ cwd: p.corePath("cloud"),
305
+ quiet: !isVerbose
306
+ });
307
+ packageSpinner.succeed("Package ready");
308
+ } else if (isVerbose)
309
+ log.debug("Skipping server build and packaging (server mode)");
310
+ if (!process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY) {
311
+ const { existsSync, readFileSync } = await import("node:fs"), environment = process.env.APP_ENV || process.env.NODE_ENV || "production", envFiles = [
312
+ p.projectPath(`.env.${environment}`),
313
+ p.projectPath(".env")
314
+ ];
315
+ for (const envPath of envFiles)
316
+ if (existsSync(envPath)) {
317
+ const content = readFileSync(envPath, "utf-8");
318
+ for (const line of content.split(`
319
+ `)) {
320
+ const trimmed = line.trim();
321
+ if (trimmed.startsWith("#") || !trimmed.includes("="))
322
+ continue;
323
+ const eqIndex = trimmed.indexOf("="), key = trimmed.slice(0, eqIndex).trim();
324
+ let value = trimmed.slice(eqIndex + 1).trim();
325
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))
326
+ value = value.slice(1, -1);
327
+ if (key === "AWS_ACCESS_KEY_ID" && value)
328
+ process.env.AWS_ACCESS_KEY_ID = value;
329
+ else if (key === "AWS_SECRET_ACCESS_KEY" && value)
330
+ process.env.AWS_SECRET_ACCESS_KEY = value;
331
+ else if (key === "AWS_REGION" && value && !process.env.AWS_REGION)
332
+ process.env.AWS_REGION = value;
333
+ else if (key === "AWS_ACCOUNT_ID" && value && !process.env.AWS_ACCOUNT_ID)
334
+ process.env.AWS_ACCOUNT_ID = value;
335
+ else if (key.startsWith("MAIL_PASSWORD_") && value && !process.env[key])
336
+ process.env[key] = value;
337
+ }
338
+ if (isVerbose)
339
+ log.debug(`Loaded AWS credentials from ${envPath}`);
340
+ break;
341
+ }
342
+ }
343
+ try {
344
+ const { deployStack, deployFrontend: _deployFrontend } = await import("../../deploy"), environment = process.env.APP_ENV || process.env.NODE_ENV || "production", region = process.env.AWS_REGION || "us-east-1", cloudConfigModule = await import(p.projectPath("config/cloud")), deploymentMode = cloudConfigModule?.tsCloud?.mode || "server", projectName = cloudConfigModule?.tsCloud?.project?.name || cloudConfigModule?.tsCloud?.project?.slug || "stacks";
345
+ if (deploymentMode === "serverless") {
346
+ console.log("");
347
+ console.log("Serverless deployment mode detected");
348
+ console.log("Building and pushing container image...");
349
+ console.log("");
350
+ const accountId = process.env.AWS_ACCOUNT_ID || await getAwsAccountId(region);
351
+ if (!accountId || accountId.length < 10)
352
+ throw Error(`Invalid AWS Account ID: "${accountId || "(empty)"}"
353
+ Set AWS_ACCOUNT_ID in your .env file or ensure valid AWS credentials are configured.`);
354
+ const ecrRepository = `${accountId}.dkr.ecr.${region}.amazonaws.com/${projectName}-${environment}-api`, imageTag = `${ecrRepository}:latest`;
355
+ if (isVerbose)
356
+ log.debug(`ECR repository: ${ecrRepository}`);
357
+ if (isVerbose)
358
+ log.debug("ECR credentials will be validated before push...");
359
+ const buildSpinner = spinner("Building Docker image...");
360
+ buildSpinner.start();
361
+ try {
362
+ const { exec } = await import("node:child_process"), { promisify } = await import("node:util"), execAsync = promisify(exec), buildCmd = `docker build --platform linux/amd64 -t ${imageTag} -f Dockerfile .`, { stdout, stderr } = await execAsync(buildCmd, {
363
+ cwd: p.projectPath(),
364
+ env: process.env,
365
+ maxBuffer: 10485760,
366
+ timeout: 900000
367
+ });
368
+ if (isVerbose && stdout)
369
+ log.debug(stdout);
370
+ if (isVerbose && stderr)
371
+ log.debug(stderr);
372
+ buildSpinner.succeed(`Docker image built: ${imageTag}`);
373
+ } catch (error) {
374
+ buildSpinner.fail("Docker build failed");
375
+ if (isVerbose)
376
+ log.debug(`Docker build error: ${error.message}`);
377
+ throw error;
378
+ }
379
+ const repoSpinner = spinner("Checking ECR repository...");
380
+ repoSpinner.start();
381
+ try {
382
+ const { ECRClient } = await import("@stacksjs/ts-cloud"), ecr = new ECRClient(region), repoName = `${projectName}-${environment}-api`;
383
+ try {
384
+ await ecr.describeRepositories({ repositoryNames: [repoName] });
385
+ repoSpinner.succeed(`ECR repository exists: ${repoName}`);
386
+ } catch (err) {
387
+ if (err.code === "RepositoryNotFoundException" || err.message?.includes("RepositoryNotFoundException")) {
388
+ repoSpinner.text = `Creating ECR repository: ${repoName}...`;
389
+ await ecr.createRepository({
390
+ repositoryName: repoName,
391
+ imageScanningConfiguration: { scanOnPush: !0 },
392
+ imageTagMutability: "MUTABLE"
393
+ });
394
+ repoSpinner.succeed(`ECR repository created: ${repoName}`);
395
+ } else
396
+ throw err;
397
+ }
398
+ } catch (error) {
399
+ repoSpinner.fail("ECR repository check failed");
400
+ if (isVerbose)
401
+ log.debug(`ECR repository error: ${error.message}`);
402
+ throw error;
403
+ }
404
+ const authSpinner = spinner("Authenticating to ECR...");
405
+ authSpinner.start();
406
+ try {
407
+ const { ECRClient } = await import("@stacksjs/ts-cloud"), token = (await new ECRClient(region).getAuthorizationToken()).authorizationData?.[0];
408
+ if (!token?.authorizationToken)
409
+ throw Error("No authorization token received from ECR");
410
+ const decoded = Buffer.from(token.authorizationToken, "base64").toString("utf-8"), colonIndex = decoded.indexOf(":");
411
+ if (colonIndex === -1)
412
+ throw Error("ECR authorization token has unexpected format (missing colon separator)");
413
+ const password = decoded.slice(colonIndex + 1), loginResult = Bun.spawnSync([
414
+ "docker",
415
+ "login",
416
+ "--username",
417
+ "AWS",
418
+ "--password-stdin",
419
+ `${accountId}.dkr.ecr.${region}.amazonaws.com`
420
+ ], {
421
+ stdin: new TextEncoder().encode(password),
422
+ stdout: "pipe",
423
+ stderr: "pipe"
424
+ });
425
+ if (loginResult.exitCode !== 0) {
426
+ const loginError = new TextDecoder().decode(loginResult.stderr);
427
+ throw Error(`Docker login failed: ${loginError}`);
428
+ }
429
+ authSpinner.succeed("Authenticated to ECR");
430
+ } catch (error) {
431
+ authSpinner.fail("ECR authentication failed");
432
+ if (isVerbose)
433
+ log.debug(`ECR auth error: ${error.message}`);
434
+ throw error;
435
+ }
436
+ console.log("");
437
+ const pushSpinner = spinner("Pushing image to ECR...");
438
+ pushSpinner.start();
439
+ try {
440
+ const { exec } = await import("node:child_process"), { promisify } = await import("node:util"), execAsync = promisify(exec), pushCmd = `docker push ${imageTag}`;
441
+ await execAsync(pushCmd, { env: process.env });
442
+ pushSpinner.succeed(`Image pushed: ${imageTag}`);
443
+ } catch (error) {
444
+ pushSpinner.fail("Docker push failed");
445
+ if (isVerbose)
446
+ log.debug(`Push error: ${error.message}`);
447
+ throw error;
448
+ }
449
+ console.log("");
450
+ console.log("Container image ready in ECR. CloudFormation will use this image.");
451
+ console.log("");
452
+ } else {
453
+ console.log("");
454
+ console.log("Server deployment mode detected");
455
+ console.log("Deploying EC2 infrastructure via CloudFormation...");
456
+ console.log("");
457
+ }
458
+ async function getAwsAccountId(region) {
459
+ try {
460
+ const { STSClient } = await import("@stacksjs/ts-cloud"), accountId = (await new STSClient(region).getCallerIdentity()).Account || process.env.AWS_ACCOUNT_ID || "";
461
+ if (isVerbose)
462
+ log.debug(`Retrieved AWS Account ID: ${accountId || "(empty)"}`);
463
+ return accountId;
464
+ } catch (error) {
465
+ if (isVerbose)
466
+ log.debug(`Failed to get AWS Account ID: ${error.message}`);
467
+ return process.env.AWS_ACCOUNT_ID || "";
468
+ }
469
+ }
470
+ try {
471
+ const deployScript = await import(p.projectPath("cloud/deploy-script"));
472
+ if (typeof deployScript.beforeDeploy === "function")
473
+ await deployScript.beforeDeploy({ environment, region });
474
+ } catch (hookError) {
475
+ if (hookError.code === "ERR_MODULE_NOT_FOUND" || hookError.code === "MODULE_NOT_FOUND") {
476
+ if (isVerbose)
477
+ log.debug("No beforeDeploy hook found (cloud/deploy-script not found)");
478
+ } else {
479
+ console.warn(`\u26A0 beforeDeploy hook failed: ${hookError.message}`);
480
+ if (isVerbose)
481
+ log.debug(`beforeDeploy error stack: ${hookError.stack}`);
482
+ }
483
+ }
484
+ await deployStack({
485
+ environment,
486
+ region,
487
+ waitForCompletion: !0,
488
+ verbose: isVerbose
489
+ });
490
+ try {
491
+ const deployScript = await import(p.projectPath("cloud/deploy-script"));
492
+ if (typeof deployScript.afterDeploy === "function") {
493
+ const { CloudFormationClient } = await import("@stacksjs/ts-cloud"), cf = new CloudFormationClient(region), stackName = `${projectName}-cloud`;
494
+ let stackOutputs = {};
495
+ try {
496
+ stackOutputs = await cf.getStackOutputs(stackName);
497
+ } catch (e) {
498
+ if (isVerbose)
499
+ log.debug(`Failed to get stack outputs: ${e instanceof Error ? e.message : String(e)}`);
500
+ }
501
+ await deployScript.afterDeploy({ environment, region, outputs: stackOutputs });
502
+ }
503
+ } catch (hookError) {
504
+ if (hookError.code === "ERR_MODULE_NOT_FOUND" || hookError.code === "MODULE_NOT_FOUND") {
505
+ if (isVerbose)
506
+ log.debug("No afterDeploy hook found (cloud/deploy-script not found)");
507
+ } else {
508
+ console.warn(`\u26A0 afterDeploy hook failed: ${hookError.message}`);
509
+ if (isVerbose)
510
+ log.debug(`afterDeploy error stack: ${hookError.stack}`);
511
+ }
512
+ }
513
+ const migrateSpinner = spinner("Running database migrations...");
514
+ migrateSpinner.start();
515
+ try {
516
+ const { generateMigrations, runDatabaseMigration } = await import("@stacksjs/database");
517
+ await generateMigrations();
518
+ await runDatabaseMigration();
519
+ migrateSpinner.succeed("Database migrations completed");
520
+ } catch (migrationError) {
521
+ migrateSpinner.stop();
522
+ console.log(`\u26A0 Database migrations skipped: ${migrationError.message}`);
523
+ if (isVerbose)
524
+ log.debug(`Migration error: ${migrationError.stack}`);
525
+ }
526
+ const frameworkTablesSpinner = spinner("Ensuring framework tables (auth, notifications, RBAC)...");
527
+ frameworkTablesSpinner.start();
528
+ try {
529
+ const { migrateAuthTables, migrateNotificationTables, migrateRbacTables } = await import("@stacksjs/database"), failures = [], steps = [
530
+ ["auth", migrateAuthTables],
531
+ ["notification", migrateNotificationTables],
532
+ ["RBAC", migrateRbacTables]
533
+ ];
534
+ for (const [name, migrateTables] of steps) {
535
+ const result = await migrateTables({ verbose: isVerbose });
536
+ if (!result.success)
537
+ failures.push(`${name}: ${result.error}`);
538
+ }
539
+ if (failures.length > 0) {
540
+ frameworkTablesSpinner.stop();
541
+ console.log(`\u26A0 Some framework tables could not be migrated: ${failures.join("; ")}`);
542
+ } else
543
+ frameworkTablesSpinner.succeed("Framework tables ensured (auth, notifications, RBAC)");
544
+ } catch (frameworkTablesError) {
545
+ frameworkTablesSpinner.stop();
546
+ console.log(`\u26A0 Framework table migrations skipped: ${frameworkTablesError.message}`);
547
+ if (isVerbose)
548
+ log.debug(`Framework table migration error: ${frameworkTablesError.stack}`);
549
+ }
550
+ if (deploymentMode === "server") {
551
+ const serverOutputSpinner = spinner("Retrieving server instance details...");
552
+ serverOutputSpinner.start();
553
+ try {
554
+ const { CloudFormationClient } = await import("@stacksjs/ts-cloud"), cf = new CloudFormationClient(region), stackName = `${projectName}-cloud`, outputs = await cf.getStackOutputs(stackName);
555
+ serverOutputSpinner.succeed("Server instances deployed");
556
+ console.log("");
557
+ console.log("Server instances:");
558
+ for (const [key, value] of Object.entries(outputs)) {
559
+ if (key.endsWith("PublicIp"))
560
+ console.log(` ${key}: ${value}`);
561
+ if (key.endsWith("InstanceId"))
562
+ console.log(` ${key}: ${value}`);
563
+ }
564
+ console.log("");
565
+ } catch (outputError) {
566
+ serverOutputSpinner.stop();
567
+ console.log(`\u26A0 Could not retrieve server details: ${outputError.message}`);
568
+ if (isVerbose)
569
+ log.debug(`Output retrieval error: ${outputError.stack}`);
570
+ }
571
+ const serverDeploySpinner = spinner("Deploying API server to EC2...");
572
+ serverDeploySpinner.start();
573
+ try {
574
+ const { S3Client, CloudFormationClient, AWSClient } = await import("@stacksjs/ts-cloud"), { readFileSync: readServerFile, existsSync: serverFileExists } = await import("node:fs"), { resolve } = await import("node:path"), s3 = new S3Client(region), cf = new CloudFormationClient(region), awsClient = new AWSClient, stackName = `${projectName}-cloud`, outputs = await cf.getStackOutputs(stackName), bucketName = outputs.FrontendBucketName, apiServerPort = Number(config.ports?.api || 3008);
575
+ let instanceId;
576
+ for (const [key, value] of Object.entries(outputs))
577
+ if (key.endsWith("InstanceId") && typeof value === "string") {
578
+ instanceId = value;
579
+ break;
580
+ }
581
+ let securityGroupId;
582
+ if (instanceId) {
583
+ const { EC2Client } = await import("@stacksjs/ts-cloud"), ec2 = new EC2Client(region);
584
+ securityGroupId = (await ec2.describeInstances({
585
+ Filters: [{ Name: "instance-id", Values: [instanceId] }]
586
+ }))?.Reservations?.[0]?.Instances?.[0]?.SecurityGroups?.[0]?.GroupId;
587
+ await ensureSecurityGroupPort(ec2, securityGroupId, apiServerPort);
588
+ }
589
+ if (!instanceId || !bucketName) {
590
+ serverDeploySpinner.stop();
591
+ if (!instanceId)
592
+ console.log("\u26A0 No EC2 instance found in stack outputs, skipping server deploy");
593
+ if (!bucketName)
594
+ console.log("\u26A0 No S3 bucket found for staging server artifacts");
595
+ } else {
596
+ const publicDistributionId = outputs.publicCloudFrontDistributionId || outputs.CloudFrontDistributionId;
597
+ await ensureCloudFrontApiOriginPort(awsClient, publicDistributionId, apiServerPort);
598
+ const tsCloudDistPath = resolve(p.projectPath("node_modules/@stacksjs/ts-cloud/dist/index.js"));
599
+ if (serverFileExists(tsCloudDistPath)) {
600
+ const tsCloudDist = readServerFile(tsCloudDistPath);
601
+ await withS3Retry(() => s3.putObject({
602
+ bucket: bucketName,
603
+ key: "_deploy/ts-cloud-dist.js",
604
+ body: tsCloudDist,
605
+ contentType: "application/javascript"
606
+ }), "upload ts-cloud dist");
607
+ if (isVerbose)
608
+ log.debug(" Uploaded ts-cloud dist to S3");
609
+ }
610
+ const serverCode = `import { Database } from "bun:sqlite";
611
+ import { SESClient } from "./ts-cloud-dist.js";
612
+
613
+ const db = new Database("/var/www/api/stacks.db");
614
+ db.run(\`CREATE TABLE IF NOT EXISTS subscribers (
615
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
616
+ email TEXT NOT NULL UNIQUE,
617
+ status TEXT DEFAULT 'subscribed',
618
+ source TEXT DEFAULT 'homepage',
619
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
620
+ )\`);
621
+
622
+ const CORS_HEADERS = {
623
+ "Access-Control-Allow-Origin": "*",
624
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
625
+ "Access-Control-Allow-Headers": "Content-Type",
626
+ };
627
+
628
+ const ses = new SESClient("${region}");
629
+
630
+ async function readBody(req) {
631
+ const contentType = req.headers.get("content-type") || "";
632
+
633
+ if (contentType.includes("application/json")) {
634
+ return await req.json();
635
+ }
636
+
637
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
638
+ const form = await req.formData();
639
+ return Object.fromEntries(form.entries());
640
+ }
641
+
642
+ const text = await req.text();
643
+ return Object.fromEntries(new URLSearchParams(text));
644
+ }
645
+
646
+ async function sendWelcomeEmail(email) {
647
+ try {
648
+ await ses.sendSimpleEmail({
649
+ from: "hello@stacksjs.com",
650
+ to: email,
651
+ subject: "Welcome to Stacks!",
652
+ html: \`<!DOCTYPE html>
653
+ <html>
654
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
655
+ <body style="margin:0;padding:0;background-color:#0e0e0e;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;">
656
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:600px;margin:0 auto;padding:40px 20px;">
657
+ <tr><td style="text-align:center;padding-bottom:32px;"><h1 style="color:#ececec;font-size:28px;font-weight:600;margin:0;">Stacks</h1></td></tr>
658
+ <tr><td style="background-color:#1a1a1a;border-radius:12px;padding:32px;">
659
+ <h2 style="color:#ececec;font-size:22px;font-weight:600;margin:0 0 16px;">Welcome aboard!</h2>
660
+ <p style="color:#a0a0a0;font-size:16px;line-height:1.6;margin:0 0 16px;">Thanks for subscribing to Stacks. You will be the first to know about new releases, features, and updates.</p>
661
+ <p style="color:#a0a0a0;font-size:16px;line-height:1.6;margin:0 0 24px;">Stacks is a rapid application development framework that helps you build type-safe web apps, APIs, and cloud infrastructure with ease.</p>
662
+ <a href="https://stacksjs.com" style="display:inline-block;background-color:#6366f1;color:#ffffff;text-decoration:none;padding:12px 24px;border-radius:8px;font-size:14px;font-weight:500;">Visit Stacks</a>
663
+ </td></tr>
664
+ <tr><td style="text-align:center;padding-top:32px;"><p style="color:#666;font-size:13px;margin:0;">You received this email because you subscribed at stacksjs.com</p></td></tr>
665
+ </table>
666
+ </body>
667
+ </html>\`,
668
+ text: "Welcome to Stacks! Thanks for subscribing. You will be the first to know about new releases, features, and updates. Visit us at https://stacksjs.com",
669
+ });
670
+ console.log(\`Welcome email sent to \${email}\`);
671
+ } catch (err) {
672
+ console.error(\`Failed to send welcome email to \${email}:\`, err.message);
673
+ }
674
+ }
675
+
676
+ const server = Bun.serve({
677
+ port: ${apiServerPort},
678
+ hostname: "0.0.0.0",
679
+ async fetch(req) {
680
+ const url = new URL(req.url);
681
+
682
+ if (req.method === "OPTIONS") {
683
+ return new Response(null, { status: 204, headers: CORS_HEADERS });
684
+ }
685
+
686
+ if (url.pathname === "/health" || url.pathname === "/api/health") {
687
+ return Response.json({ status: "ok" }, { headers: CORS_HEADERS });
688
+ }
689
+
690
+ if (url.pathname === "/api/email/subscribe" && req.method === "POST") {
691
+ try {
692
+ const body = await readBody(req);
693
+ const email = (body.email || "").trim().toLowerCase();
694
+ if (!email || !email.includes("@")) {
695
+ return Response.json(
696
+ { success: false, message: "A valid email is required" },
697
+ { status: 400, headers: CORS_HEADERS },
698
+ );
699
+ }
700
+
701
+ const existing = db.query("SELECT id FROM subscribers WHERE email = ?").get(email);
702
+ if (existing) {
703
+ return Response.json(
704
+ { success: true, message: "Already subscribed" },
705
+ { headers: CORS_HEADERS },
706
+ );
707
+ }
708
+
709
+ db.run("INSERT INTO subscribers (email, source) VALUES (?, ?)", [email, body.source || "homepage"]);
710
+
711
+ // Send welcome email asynchronously (do not block the response)
712
+ sendWelcomeEmail(email);
713
+
714
+ return Response.json(
715
+ { success: true, message: "Thanks for subscribing!" },
716
+ { headers: CORS_HEADERS },
717
+ );
718
+ } catch (e) {
719
+ return Response.json(
720
+ { success: false, message: "Server error" },
721
+ { status: 500, headers: CORS_HEADERS },
722
+ );
723
+ }
724
+ }
725
+
726
+ return Response.json({ error: "Not found" }, { status: 404, headers: CORS_HEADERS });
727
+ },
728
+ });
729
+
730
+ console.log(\`Stacks API server running on port \${server.port}\`);
731
+ `;
732
+ await withS3Retry(() => s3.putObject({
733
+ bucket: bucketName,
734
+ key: "_deploy/server.ts",
735
+ body: Buffer.from(serverCode),
736
+ contentType: "application/typescript"
737
+ }), "upload server.ts");
738
+ if (isVerbose)
739
+ log.debug(" Uploaded server.ts to S3");
740
+ const ssmCommand = [
741
+ "mkdir -p /var/www/api",
742
+ `aws s3 cp s3://${bucketName}/_deploy/ts-cloud-dist.js /var/www/api/ts-cloud-dist.js`,
743
+ `aws s3 cp s3://${bucketName}/_deploy/server.ts /var/www/api/server.ts`,
744
+ firewallOpenCommand([apiServerPort]),
745
+ `cat > /etc/systemd/system/stacks-api.service << 'SERVICEFILE'
746
+ [Unit]
747
+ Description=Stacks Bun API Server
748
+ After=network.target
749
+
750
+ [Service]
751
+ Type=simple
752
+ WorkingDirectory=/var/www/api
753
+ ExecStart=/root/.bun/bin/bun server.ts
754
+ Restart=on-failure
755
+ RestartSec=5
756
+ Environment=NODE_ENV=production
757
+
758
+ [Install]
759
+ WantedBy=multi-user.target
760
+ SERVICEFILE
761
+ systemctl daemon-reload
762
+ systemctl enable stacks-api`,
763
+ "systemctl reset-failed stacks-api || true",
764
+ "systemctl restart stacks-api",
765
+ `for i in $(seq 1 20); do curl -fsS http://127.0.0.1:${apiServerPort}/api/health && break; sleep 1; done`,
766
+ `curl -fsS http://127.0.0.1:${apiServerPort}/api/health`,
767
+ `echo "API server deployed and restarted on port ${apiServerPort}"`
768
+ ].join(" && "), commandId = (await awsClient.request({
769
+ service: "ssm",
770
+ region,
771
+ method: "POST",
772
+ path: "/",
773
+ headers: {
774
+ "Content-Type": "application/x-amz-json-1.1",
775
+ "X-Amz-Target": "AmazonSSM.SendCommand"
776
+ },
777
+ body: JSON.stringify({
778
+ InstanceIds: [instanceId],
779
+ DocumentName: "AWS-RunShellScript",
780
+ Parameters: {
781
+ commands: [ssmCommand]
782
+ },
783
+ TimeoutSeconds: 120
784
+ })
785
+ }))?.Command?.CommandId;
786
+ if (commandId) {
787
+ let attempts = 0;
788
+ while (attempts < 30) {
789
+ await new Promise((resolve) => setTimeout(resolve, 3000));
790
+ try {
791
+ const invocation = await awsClient.request({
792
+ service: "ssm",
793
+ region,
794
+ method: "POST",
795
+ path: "/",
796
+ headers: {
797
+ "Content-Type": "application/x-amz-json-1.1",
798
+ "X-Amz-Target": "AmazonSSM.GetCommandInvocation"
799
+ },
800
+ body: JSON.stringify({
801
+ CommandId: commandId,
802
+ InstanceId: instanceId
803
+ })
804
+ }), status = invocation?.Status;
805
+ if (status === "Success") {
806
+ serverDeploySpinner.succeed("API server deployed to EC2");
807
+ break;
808
+ } else if (status === "Failed" || status === "Cancelled" || status === "TimedOut") {
809
+ const errOutput = invocation?.StandardErrorContent || "Unknown error";
810
+ serverDeploySpinner.fail(`API server deploy failed: ${errOutput}`);
811
+ break;
812
+ }
813
+ } catch {}
814
+ attempts++;
815
+ }
816
+ if (attempts >= 30)
817
+ serverDeploySpinner.fail("API server deploy timed out waiting for SSM command");
818
+ } else
819
+ serverDeploySpinner.fail("Failed to send SSM command");
820
+ try {
821
+ await s3.deleteObject({ bucket: bucketName, key: "_deploy/ts-cloud-dist.js" });
822
+ await s3.deleteObject({ bucket: bucketName, key: "_deploy/server.ts" });
823
+ } catch {}
824
+ }
825
+ } catch (serverDeployError) {
826
+ serverDeploySpinner.fail(`API server deployment failed: ${serverDeployError.message}`);
827
+ if (isVerbose)
828
+ log.debug(`Server deploy error: ${serverDeployError.stack}`);
829
+ }
830
+ const emailConfig = config.email;
831
+ if (emailConfig?.server?.enabled) {
832
+ const mailServerMode = emailConfig?.server?.mode || "server", smtpSpinner = spinner(`Deploying mail server (${mailServerMode} mode) to EC2...`);
833
+ smtpSpinner.start();
834
+ try {
835
+ const { S3Client, AWSClient, EC2Client } = await import("@stacksjs/ts-cloud"), { readFileSync: readSmtpFile, existsSync: smtpFileExists } = await import("node:fs"), { resolve: resolvePath, join: joinPath } = await import("node:path"), { execSync } = await import("node:child_process"), s3 = new S3Client(region), awsClient = new AWSClient, ec2 = new EC2Client(region), stackName = `${projectName}-cloud`;
836
+ let smtpInstanceId, smtpBucketName, emailBucketName, securityGroupId;
837
+ try {
838
+ const { AWSCloudFormationClient: CfnClient } = await import("@stacksjs/ts-cloud"), cfnOutputs = await new CfnClient(region).getStackOutputs(stackName);
839
+ for (const [key, value] of Object.entries(cfnOutputs)) {
840
+ if (key.endsWith("InstanceId") && typeof value === "string")
841
+ smtpInstanceId = value;
842
+ if (key === "FrontendBucketName" && typeof value === "string")
843
+ smtpBucketName = value;
844
+ if (key === "emailBucketName" && typeof value === "string")
845
+ emailBucketName = value;
846
+ }
847
+ } catch {}
848
+ if (!emailBucketName)
849
+ emailBucketName = `${projectName}-production-s3-email`;
850
+ if (smtpInstanceId && smtpBucketName) {
851
+ const mailPorts = [25, 80, 110, 143, 465, 587, 993, 995];
852
+ try {
853
+ const sgs = (await ec2.describeInstances({
854
+ Filters: [{ Name: "instance-id", Values: [smtpInstanceId] }]
855
+ }))?.Reservations?.[0]?.Instances?.[0]?.SecurityGroups;
856
+ if (sgs?.[0]?.GroupId)
857
+ securityGroupId = sgs[0].GroupId;
858
+ } catch {
859
+ if (isVerbose)
860
+ log.debug("Could not get security group for instance");
861
+ }
862
+ if (securityGroupId)
863
+ for (const port of mailPorts)
864
+ try {
865
+ await ec2.authorizeSecurityGroupIngress({
866
+ GroupId: securityGroupId,
867
+ IpPermissions: [{
868
+ IpProtocol: "tcp",
869
+ FromPort: port,
870
+ ToPort: port,
871
+ IpRanges: [{ CidrIp: "0.0.0.0/0", Description: `Mail port ${port}` }]
872
+ }]
873
+ });
874
+ if (isVerbose)
875
+ log.debug(` Opened port ${port} on security group ${securityGroupId}`);
876
+ } catch (e) {
877
+ if (!e.message?.includes("already exists")) {
878
+ if (isVerbose)
879
+ log.debug(` Port ${port}: ${e.message}`);
880
+ }
881
+ }
882
+ const domain = emailConfig.domain || "stacksjs.com", mailSubdomain = emailConfig?.server?.subdomain || "mail";
883
+ if (mailServerMode === "server") {
884
+ if (isVerbose)
885
+ log.debug("Deploying Zig mail server binary...");
886
+ try {
887
+ await installMailBinaryWithPantry();
888
+ } catch (error) {
889
+ if (isVerbose)
890
+ log.debug(` Pantry mail install failed: ${error.message}`);
891
+ }
892
+ const linuxBinaryPath = await findPantryMailBinary();
893
+ if (linuxBinaryPath && smtpFileExists(linuxBinaryPath)) {
894
+ if (!await isElfBinary(linuxBinaryPath)) {
895
+ smtpSpinner.fail(`Pantry installed ${MAIL_PACKAGE_DOMAIN}, but the ${MAIL_TARGET_PLATFORM} binary is not an ELF file.`);
896
+ throw Error("Linux binary is not a valid ELF file");
897
+ }
898
+ const binaryContent = readSmtpFile(linuxBinaryPath);
899
+ await s3.putObject({
900
+ bucket: emailBucketName,
901
+ key: "mail-server/smtp-server",
902
+ body: binaryContent,
903
+ contentType: "application/octet-stream"
904
+ });
905
+ if (isVerbose)
906
+ log.debug(` Uploaded Zig binary (${(binaryContent.length / 1024 / 1024).toFixed(1)}MB) to S3`);
907
+ const zigSsmCommand = [
908
+ "systemctl stop stacks-mail 2>/dev/null || true",
909
+ "systemctl disable stacks-mail 2>/dev/null || true",
910
+ "systemctl stop stacks-smtp 2>/dev/null || true",
911
+ "systemctl disable stacks-smtp 2>/dev/null || true",
912
+ "systemctl stop smtp-server 2>/dev/null || true",
913
+ "mkdir -p /opt/smtp-server /var/lib/smtp-server /var/log/smtp-server /var/spool/mail /etc/smtp-server /var/lib/smtp-server/backups",
914
+ `aws s3 cp s3://${emailBucketName}/mail-server/smtp-server /opt/smtp-server/smtp-server --region ${region}`,
915
+ "chmod +x /opt/smtp-server/smtp-server",
916
+ 'file /opt/smtp-server/smtp-server | grep -q ELF || { echo "Binary is not ELF"; exit 1; }',
917
+ firewallOpenCommand(mailPorts),
918
+ `cat > /etc/smtp-server/smtp-server.env << 'ENVEOF'
919
+ SMTP_PROFILE=production
920
+ SMTP_HOST=0.0.0.0
921
+ SMTP_PORT=25
922
+ SMTP_HOSTNAME=${mailSubdomain}.${domain}
923
+ SMTP_ENABLE_TLS=false
924
+ SMTP_ENABLE_AUTH=true
925
+ SMTP_DB_PATH=/var/lib/smtp-server/smtp.db
926
+ AWS_S3_BUCKET=${emailBucketName}
927
+ AWS_REGION=${region}
928
+ SMTP_ENABLE_JSON_LOGGING=true
929
+ SMTP_LOG_LEVEL=info
930
+ SMTP_MAILBOX_PATH=/var/spool/mail
931
+ SMTP_BACKUP_PATH=/var/lib/smtp-server/backups
932
+ SMTP_MAX_CONNECTIONS=1000
933
+ SMTP_MAX_MESSAGE_SIZE=52428800
934
+ SMTP_MAX_RECIPIENTS=100
935
+ SMTP_RATE_LIMIT_PER_IP=100
936
+ SMTP_RATE_LIMIT_PER_USER=200
937
+ SMTP_IO_MODE=epoll
938
+ ENVEOF`,
939
+ "chmod 600 /etc/smtp-server/smtp-server.env",
940
+ `cat > /etc/systemd/system/smtp-server.service << 'SVCEOF'
941
+ [Unit]
942
+ Description=Stacks Mail Server (Zig)
943
+ After=network.target
944
+
945
+ [Service]
946
+ Type=simple
947
+ WorkingDirectory=/opt/smtp-server
948
+ EnvironmentFile=/etc/smtp-server/smtp-server.env
949
+ ExecStart=/opt/smtp-server/smtp-server
950
+ Restart=always
951
+ RestartSec=10
952
+ StandardOutput=journal
953
+ StandardError=journal
954
+ SyslogIdentifier=smtp-server
955
+
956
+ [Install]
957
+ WantedBy=multi-user.target
958
+ SVCEOF`,
959
+ "systemctl daemon-reload",
960
+ "systemctl enable smtp-server",
961
+ "systemctl restart smtp-server",
962
+ "sleep 3",
963
+ 'systemctl is-active smtp-server && echo "Mail server is running" || echo "Mail server failed to start"',
964
+ 'ss -tlnp | grep -E "(25|143)" || echo "Warning: mail ports not listening yet"'
965
+ ].join(" && "), cmdId = (await awsClient.request({
966
+ service: "ssm",
967
+ region,
968
+ method: "POST",
969
+ path: "/",
970
+ headers: {
971
+ "Content-Type": "application/x-amz-json-1.1",
972
+ "X-Amz-Target": "AmazonSSM.SendCommand"
973
+ },
974
+ body: JSON.stringify({
975
+ InstanceIds: [smtpInstanceId],
976
+ DocumentName: "AWS-RunShellScript",
977
+ Parameters: { commands: [zigSsmCommand] },
978
+ TimeoutSeconds: 300
979
+ })
980
+ }))?.Command?.CommandId;
981
+ if (cmdId) {
982
+ let ssmAttempts = 0;
983
+ while (ssmAttempts < 100) {
984
+ await new Promise((resolve) => setTimeout(resolve, 3000));
985
+ try {
986
+ const invocation = await awsClient.request({
987
+ service: "ssm",
988
+ region,
989
+ method: "POST",
990
+ path: "/",
991
+ headers: {
992
+ "Content-Type": "application/x-amz-json-1.1",
993
+ "X-Amz-Target": "AmazonSSM.GetCommandInvocation"
994
+ },
995
+ body: JSON.stringify({ CommandId: cmdId, InstanceId: smtpInstanceId })
996
+ }), status = invocation?.Status;
997
+ if (status === "Success") {
998
+ smtpSpinner.succeed("Zig mail server deployed to EC2");
999
+ if (isVerbose && invocation?.StandardOutputContent)
1000
+ console.log(invocation.StandardOutputContent);
1001
+ break;
1002
+ } else if (status === "Failed" || status === "Cancelled" || status === "TimedOut") {
1003
+ const errOutput = invocation?.StandardErrorContent || invocation?.StandardOutputContent || "Unknown error";
1004
+ smtpSpinner.fail(`Zig mail server deploy failed: ${errOutput.slice(0, 500)}`);
1005
+ break;
1006
+ }
1007
+ } catch {}
1008
+ ssmAttempts++;
1009
+ }
1010
+ if (ssmAttempts >= 100)
1011
+ smtpSpinner.fail("Zig mail server deploy timed out");
1012
+ } else
1013
+ smtpSpinner.fail("Failed to send SSM command for mail server");
1014
+ } else
1015
+ smtpSpinner.fail(`No Pantry-provided ${MAIL_TARGET_PLATFORM} mail binary found. Release ${MAIL_PACKAGE_DOMAIN}, then run the Pantry binary sync for that package.`);
1016
+ } else {
1017
+ const smtpServerSrc = resolvePath(p.projectPath("storage/framework/core/cloud/src/imap/smtp-server.ts")), bundleOutDir = joinPath(p.projectPath(), ".stacks/tmp/smtp-bundle");
1018
+ execSync(`bun build ${smtpServerSrc} --target=bun --outdir=${bundleOutDir}`, { stdio: "pipe" });
1019
+ const bundlePath = joinPath(bundleOutDir, "smtp-server.js");
1020
+ if (smtpFileExists(bundlePath)) {
1021
+ const smtpBundle = readSmtpFile(bundlePath), mailboxes = emailConfig.mailboxes || ["chris", "blake", "glenn"], usersEntries = mailboxes.map((name) => {
1022
+ const envKey = `MAIL_PASSWORD_${name.toUpperCase()}`;
1023
+ return ` "${name}": { password: process.env["${envKey}"] || "", email: "${name}@${domain}" }`;
1024
+ }).join(`,
1025
+ `), entryPoint = `import { SmtpServer } from "./smtp-server.js";
1026
+ import * as fs from "fs";
1027
+ const domain = "${domain}";
1028
+ const users = { ${usersEntries} };
1029
+ const certDir = \`/etc/letsencrypt/live/mail.\${domain}\`;
1030
+ let tlsConfig = undefined;
1031
+ if (fs.existsSync(\`\${certDir}/privkey.pem\`)) {
1032
+ tlsConfig = { key: \`\${certDir}/privkey.pem\`, cert: \`\${certDir}/fullchain.pem\` };
1033
+ }
1034
+ const server = new SmtpServer({ port: 587, tlsPort: 465, host: "0.0.0.0", domain, users, tls: tlsConfig, sentBucket: "${projectName}-production-email", sentPrefix: "sent/" });
1035
+ await server.start();
1036
+ `;
1037
+ await s3.putObject({ bucket: smtpBucketName, key: "_deploy/smtp-server.js", body: smtpBundle, contentType: "application/javascript" });
1038
+ await s3.putObject({ bucket: smtpBucketName, key: "_deploy/smtp-entry.ts", body: Buffer.from(entryPoint), contentType: "application/typescript" });
1039
+ const envLines = mailboxes.map((name) => {
1040
+ const envKey = `MAIL_PASSWORD_${name.toUpperCase()}`;
1041
+ return `Environment=${envKey}=${process.env[envKey] || ""}`;
1042
+ }).join(`
1043
+ `), smtpSsmCommand = [
1044
+ "mkdir -p /var/www/smtp",
1045
+ `aws s3 cp s3://${smtpBucketName}/_deploy/smtp-server.js /var/www/smtp/smtp-server.js`,
1046
+ `aws s3 cp s3://${smtpBucketName}/_deploy/smtp-entry.ts /var/www/smtp/smtp-entry.ts`,
1047
+ firewallOpenCommand(mailPorts),
1048
+ "if ! command -v certbot &> /dev/null; then dnf install -y certbot || yum install -y certbot || apt-get install -y certbot || true; fi",
1049
+ `if [ ! -d /etc/letsencrypt/live/mail.${domain} ]; then certbot certonly --standalone --non-interactive --agree-tos --email admin@${domain} -d mail.${domain} --http-01-port 80 || echo "Certbot failed"; fi`,
1050
+ `cat > /etc/systemd/system/stacks-smtp.service << 'SERVICEFILE'
1051
+ [Unit]
1052
+ Description=Stacks SMTP Proxy Server
1053
+ After=network.target
1054
+ [Service]
1055
+ Type=simple
1056
+ WorkingDirectory=/var/www/smtp
1057
+ ExecStart=/root/.bun/bin/bun smtp-entry.ts
1058
+ Restart=on-failure
1059
+ RestartSec=5
1060
+ Environment=NODE_ENV=production
1061
+ Environment=AWS_REGION=${region}
1062
+ ${envLines}
1063
+ [Install]
1064
+ WantedBy=multi-user.target
1065
+ SERVICEFILE`,
1066
+ "systemctl daemon-reload",
1067
+ "systemctl enable stacks-smtp",
1068
+ "systemctl restart stacks-smtp"
1069
+ ].join(" && "), cmdId = (await awsClient.request({
1070
+ service: "ssm",
1071
+ region,
1072
+ method: "POST",
1073
+ path: "/",
1074
+ headers: { "Content-Type": "application/x-amz-json-1.1", "X-Amz-Target": "AmazonSSM.SendCommand" },
1075
+ body: JSON.stringify({ InstanceIds: [smtpInstanceId], DocumentName: "AWS-RunShellScript", Parameters: { commands: [smtpSsmCommand] }, TimeoutSeconds: 180 })
1076
+ }))?.Command?.CommandId;
1077
+ if (cmdId) {
1078
+ let ssmAttempts = 0;
1079
+ while (ssmAttempts < 60) {
1080
+ await new Promise((resolve) => setTimeout(resolve, 3000));
1081
+ try {
1082
+ const invocation = await awsClient.request({
1083
+ service: "ssm",
1084
+ region,
1085
+ method: "POST",
1086
+ path: "/",
1087
+ headers: { "Content-Type": "application/x-amz-json-1.1", "X-Amz-Target": "AmazonSSM.GetCommandInvocation" },
1088
+ body: JSON.stringify({ CommandId: cmdId, InstanceId: smtpInstanceId })
1089
+ }), status = invocation?.Status;
1090
+ if (status === "Success") {
1091
+ smtpSpinner.succeed("SMTP proxy deployed to EC2");
1092
+ break;
1093
+ } else if (status === "Failed" || status === "Cancelled" || status === "TimedOut") {
1094
+ smtpSpinner.fail(`SMTP proxy deploy failed: ${invocation?.StandardErrorContent || "Unknown"}`);
1095
+ break;
1096
+ }
1097
+ } catch {}
1098
+ ssmAttempts++;
1099
+ }
1100
+ if (ssmAttempts >= 60)
1101
+ smtpSpinner.fail("SMTP proxy deploy timed out");
1102
+ } else
1103
+ smtpSpinner.fail("Failed to send SSM command for SMTP proxy");
1104
+ try {
1105
+ await s3.deleteObject({ bucket: smtpBucketName, key: "_deploy/smtp-server.js" });
1106
+ await s3.deleteObject({ bucket: smtpBucketName, key: "_deploy/smtp-entry.ts" });
1107
+ } catch {}
1108
+ } else {
1109
+ smtpSpinner.stop();
1110
+ if (isVerbose)
1111
+ log.debug("SMTP server bundle not found, skipping");
1112
+ }
1113
+ }
1114
+ } else {
1115
+ smtpSpinner.stop();
1116
+ if (isVerbose)
1117
+ log.debug("No EC2 instance or bucket found, skipping mail server deploy");
1118
+ }
1119
+ } catch (smtpDeployError) {
1120
+ smtpSpinner.fail(`Mail server deployment failed: ${smtpDeployError.message}`);
1121
+ if (isVerbose)
1122
+ log.debug(`Mail server deploy error: ${smtpDeployError.stack}`);
1123
+ }
1124
+ }
1125
+ }
1126
+ const frontendSpinner = spinner("Deploying frontend to S3...");
1127
+ frontendSpinner.start();
1128
+ try {
1129
+ const { S3Client, CloudFormationClient } = await import("@stacksjs/ts-cloud"), { readFileSync } = await import("node:fs"), { buildFrontendStatic } = await import("../build/frontend-static"), s3 = new S3Client(region), cf = new CloudFormationClient(region), stackName = `${projectName}-cloud`, stackOutputs = await cf.getStackOutputs(stackName), bucketName = stackOutputs.FrontendBucketName;
1130
+ if (!bucketName) {
1131
+ frontendSpinner.stop();
1132
+ console.log("\u26A0 Frontend bucket not found in stack outputs (add storage.public to infrastructure config)");
1133
+ } else {
1134
+ const buildDir = await buildFrontendStatic();
1135
+ await (async (dir, _prefix = "") => {
1136
+ await traverseDirectory(dir, async (filePath, key) => {
1137
+ const content = readFileSync(filePath), contentType = getMimeType(filePath), cacheControl = key === "index.html" || key.endsWith("/index.html") ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000, immutable";
1138
+ await withS3Retry(() => s3.putObject({ bucket: bucketName, key, body: content, contentType, cacheControl }), `upload ${key}`);
1139
+ if (isVerbose)
1140
+ log.debug(` Uploaded: ${key}`);
1141
+ });
1142
+ })(buildDir);
1143
+ const bootstrapPath = p.projectPath("bootstrap");
1144
+ if (existsSync(bootstrapPath)) {
1145
+ const bootstrapContent = readFileSync(bootstrapPath);
1146
+ await withS3Retry(() => s3.putObject({
1147
+ bucket: bucketName,
1148
+ key: "install",
1149
+ body: bootstrapContent,
1150
+ contentType: "text/plain",
1151
+ cacheControl: "no-cache, no-store, must-revalidate"
1152
+ }), "upload bootstrap install script");
1153
+ if (isVerbose)
1154
+ log.debug(" Uploaded bootstrap install script");
1155
+ }
1156
+ const distributionId = stackOutputs.publicCloudFrontDistributionId || stackOutputs.CloudFrontDistributionId;
1157
+ if (distributionId) {
1158
+ if (isVerbose)
1159
+ log.debug(" Invalidating CloudFront cache...");
1160
+ const { AWSClient } = await import("@stacksjs/ts-cloud");
1161
+ await new AWSClient().request({
1162
+ service: "cloudfront",
1163
+ region: "us-east-1",
1164
+ method: "POST",
1165
+ path: `/2020-05-31/distribution/${distributionId}/invalidation`,
1166
+ headers: {
1167
+ "Content-Type": "application/xml"
1168
+ },
1169
+ body: `<?xml version="1.0" encoding="UTF-8"?>
1170
+ <InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
1171
+ <CallerReference>${Date.now()}</CallerReference>
1172
+ <Paths>
1173
+ <Quantity>1</Quantity>
1174
+ <Items>
1175
+ <Path>/*</Path>
1176
+ </Items>
1177
+ </Paths>
1178
+ </InvalidationBatch>`
1179
+ });
1180
+ }
1181
+ frontendSpinner.succeed(`Frontend deployed to S3 (${bucketName})`);
1182
+ }
1183
+ } catch (frontendError) {
1184
+ frontendSpinner.fail(`Frontend deployment failed: ${frontendError.message}`);
1185
+ if (isVerbose)
1186
+ log.debug(`Frontend error: ${frontendError.stack}`);
1187
+ }
1188
+ const docsDistPath = p.projectPath("dist/docs/.bunpress"), { existsSync: docsExists } = await import("node:fs");
1189
+ if (docsExists(docsDistPath)) {
1190
+ const docsDeploySpinner = spinner("Deploying documentation to S3...");
1191
+ docsDeploySpinner.start();
1192
+ try {
1193
+ const { S3Client, CloudFormationClient } = await import("@stacksjs/ts-cloud"), { readFileSync } = await import("node:fs"), s3 = new S3Client(region), cf = new CloudFormationClient(region), stackName = `${projectName}-cloud`, docsOutputs = await cf.getStackOutputs(stackName), docsBucketName = docsOutputs.DocsBucketName;
1194
+ if (!docsBucketName) {
1195
+ docsDeploySpinner.stop();
1196
+ console.log("\u26A0 Docs bucket not found in stack outputs (infrastructure may not be updated yet)");
1197
+ } else {
1198
+ await (async (dir, _prefix = "") => {
1199
+ await traverseDirectory(dir, async (filePath, key) => {
1200
+ const content = readFileSync(filePath), contentType = getMimeType(filePath), cacheControl = key.endsWith(".html") ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000, immutable";
1201
+ await withS3Retry(() => s3.putObject({ bucket: docsBucketName, key, body: content, contentType, cacheControl }), `upload docs ${key}`);
1202
+ if (isVerbose)
1203
+ log.debug(` Uploaded docs: ${key}`);
1204
+ }, "", [".DS_Store", ".bunpress"]);
1205
+ })(docsDistPath);
1206
+ const docsDistributionId = docsOutputs.docsCloudFrontDistributionId || docsOutputs.publicCloudFrontDistributionId || docsOutputs.CloudFrontDistributionId;
1207
+ if (docsDistributionId) {
1208
+ if (isVerbose)
1209
+ log.debug(" Invalidating CloudFront cache for docs...");
1210
+ const { AWSClient } = await import("@stacksjs/ts-cloud"), client = new AWSClient, docsInvalidationPaths = docsOutputs.docsCloudFrontDistributionId ? ["/*"] : ["/docs", "/docs/*"];
1211
+ await client.request({
1212
+ service: "cloudfront",
1213
+ region: "us-east-1",
1214
+ method: "POST",
1215
+ path: `/2020-05-31/distribution/${docsDistributionId}/invalidation`,
1216
+ headers: {
1217
+ "Content-Type": "application/xml"
1218
+ },
1219
+ body: `<?xml version="1.0" encoding="UTF-8"?>
1220
+ <InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
1221
+ <CallerReference>docs-${Date.now()}</CallerReference>
1222
+ <Paths>
1223
+ <Quantity>${docsInvalidationPaths.length}</Quantity>
1224
+ <Items>
1225
+ ${docsInvalidationPaths.map((path) => `<Path>${path}</Path>`).join(`
1226
+ `)}
1227
+ </Items>
1228
+ </Paths>
1229
+ </InvalidationBatch>`
1230
+ });
1231
+ }
1232
+ docsDeploySpinner.succeed(`Documentation deployed to S3 (${docsBucketName})`);
1233
+ }
1234
+ } catch (docsDeployError) {
1235
+ docsDeploySpinner.fail(`Documentation deployment failed: ${docsDeployError.message}`);
1236
+ if (isVerbose)
1237
+ log.debug(`Docs deploy error: ${docsDeployError.stack}`);
1238
+ }
1239
+ } else if (isVerbose)
1240
+ log.debug("No docs build found at dist/docs, skipping docs deployment");
1241
+ const blogDistPath = p.projectPath("dist/blog"), { existsSync: blogExists } = await import("node:fs");
1242
+ if (blogExists(blogDistPath)) {
1243
+ const blogDeploySpinner = spinner("Deploying blog to S3...");
1244
+ blogDeploySpinner.start();
1245
+ try {
1246
+ const { S3Client: BlogS3Client, CloudFormationClient: BlogCFClient } = await import("@stacksjs/ts-cloud"), { readFileSync: readBlogFile } = await import("node:fs"), blogS3 = new BlogS3Client(region), blogCf = new BlogCFClient(region), stackName = `${projectName}-cloud`, outputs = await blogCf.getStackOutputs(stackName), blogBucketName = outputs.BlogBucketName;
1247
+ if (!blogBucketName) {
1248
+ blogDeploySpinner.stop();
1249
+ console.log("\u26A0 Blog bucket not found in stack outputs");
1250
+ } else {
1251
+ await traverseDirectory(blogDistPath, async (filePath, key) => {
1252
+ const content = readBlogFile(filePath), contentType = getMimeType(filePath), cacheControl = key.endsWith(".html") ? "no-cache, no-store, must-revalidate" : "public, max-age=31536000, immutable";
1253
+ await withS3Retry(() => blogS3.putObject({ bucket: blogBucketName, key, body: content, contentType, cacheControl }), `upload blog ${key}`);
1254
+ if (isVerbose)
1255
+ log.debug(` Uploaded blog: ${key}`);
1256
+ }, "", [".DS_Store"]);
1257
+ const blogDistributionId = outputs.blogCloudFrontDistributionId || outputs.publicCloudFrontDistributionId || outputs.CloudFrontDistributionId;
1258
+ if (blogDistributionId) {
1259
+ if (isVerbose)
1260
+ log.debug(" Invalidating CloudFront cache for blog...");
1261
+ const { AWSClient: BlogAWSClient } = await import("@stacksjs/ts-cloud"), blogClient = new BlogAWSClient, blogInvalidationPaths = outputs.blogCloudFrontDistributionId ? ["/*"] : ["/blog", "/blog/*"];
1262
+ await blogClient.request({
1263
+ service: "cloudfront",
1264
+ region: "us-east-1",
1265
+ method: "POST",
1266
+ path: `/2020-05-31/distribution/${blogDistributionId}/invalidation`,
1267
+ headers: {
1268
+ "Content-Type": "application/xml"
1269
+ },
1270
+ body: `<?xml version="1.0" encoding="UTF-8"?>
1271
+ <InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
1272
+ <CallerReference>blog-${Date.now()}</CallerReference>
1273
+ <Paths>
1274
+ <Quantity>${blogInvalidationPaths.length}</Quantity>
1275
+ <Items>
1276
+ ${blogInvalidationPaths.map((path) => `<Path>${path}</Path>`).join(`
1277
+ `)}
1278
+ </Items>
1279
+ </Paths>
1280
+ </InvalidationBatch>`
1281
+ });
1282
+ }
1283
+ blogDeploySpinner.succeed(`Blog deployed to S3 (${blogBucketName})`);
1284
+ }
1285
+ } catch (blogDeployError) {
1286
+ blogDeploySpinner.fail(`Blog deployment failed: ${blogDeployError.message}`);
1287
+ if (isVerbose)
1288
+ log.debug(`Blog deploy error: ${blogDeployError.stack}`);
1289
+ }
1290
+ } else if (isVerbose)
1291
+ log.debug("No blog build found at dist/blog, skipping blog deployment");
1292
+ const errorPageSpinner = spinner("Configuring 404 error page...");
1293
+ errorPageSpinner.start();
1294
+ try {
1295
+ const { S3Client, CloudFormationClient, AWSClient } = await import("@stacksjs/ts-cloud"), { existsSync: exists404, readFileSync: read404 } = await import("node:fs"), s3 = new S3Client(region), cf = new CloudFormationClient(region), awsClient = new AWSClient, stackName = `${projectName}-cloud`, outputs = await cf.getStackOutputs(stackName), frontendBucket = outputs.FrontendBucketName, docsBucket = outputs.DocsBucketName, distributionId = outputs.publicCloudFrontDistributionId || outputs.CloudFrontDistributionId, docs404Path = p.projectPath("dist/docs/.bunpress/404.html");
1296
+ let html404Content;
1297
+ if (exists404(docs404Path)) {
1298
+ html404Content = read404(docs404Path, "utf-8");
1299
+ if (isVerbose)
1300
+ log.debug(" Using BunPress-generated 404.html");
1301
+ } else {
1302
+ html404Content = `<!DOCTYPE html>
1303
+ <html lang="en">
1304
+ <head>
1305
+ <meta charset="UTF-8">
1306
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1307
+ <title>404 - Page Not Found</title>
1308
+ <meta name="robots" content="noindex">
1309
+ <style>
1310
+ :root {
1311
+ --vp-c-bg: #ffffff;
1312
+ --vp-c-divider: #e2e2e3;
1313
+ --vp-c-text-1: rgba(60, 60, 67);
1314
+ --vp-c-text-2: rgba(60, 60, 67, 0.78);
1315
+ --vp-c-brand-1: #3451b2;
1316
+ --vp-c-brand-2: #3a5ccc;
1317
+ }
1318
+ @media (prefers-color-scheme: dark) {
1319
+ :root {
1320
+ --vp-c-bg: #1b1b1f;
1321
+ --vp-c-divider: #2e2e32;
1322
+ --vp-c-text-1: rgba(255, 255, 245, 0.86);
1323
+ --vp-c-text-2: rgba(235, 235, 245, 0.6);
1324
+ --vp-c-brand-1: #a8b1ff;
1325
+ --vp-c-brand-2: #5c73e7;
1326
+ }
1327
+ }
1328
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1329
+ body { min-height: 100vh; background: var(--vp-c-bg); color: var(--vp-c-text-1); font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
1330
+ .NotFound { padding: 96px 32px 168px; text-align: center; min-height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; }
1331
+ .code { font-size: 64px; font-weight: 600; line-height: 64px; }
1332
+ .title { padding-top: 12px; letter-spacing: 2px; font-size: 20px; font-weight: 700; }
1333
+ .divider { margin: 24px auto 18px; width: 64px; height: 1px; background: var(--vp-c-divider); }
1334
+ .quote { max-width: 256px; font-size: 14px; font-weight: 500; color: var(--vp-c-text-2); }
1335
+ .action { padding-top: 20px; }
1336
+ .link { display: inline-block; border: 1px solid var(--vp-c-brand-1); border-radius: 16px; padding: 3px 16px; font-size: 14px; font-weight: 500; color: var(--vp-c-brand-1); text-decoration: none; transition: border-color 0.25s, color 0.25s; }
1337
+ .link:hover { border-color: var(--vp-c-brand-2); color: var(--vp-c-brand-2); }
1338
+ </style>
1339
+ </head>
1340
+ <body>
1341
+ <div class="NotFound">
1342
+ <p class="code">404</p>
1343
+ <h1 class="title">PAGE NOT FOUND</h1>
1344
+ <div class="divider"></div>
1345
+ <blockquote class="quote">But if you don't change your direction, and if you keep looking, you may end up where you are heading.</blockquote>
1346
+ <div class="action"><a class="link" href="/docs/">Take me home</a></div>
1347
+ </div>
1348
+ </body>
1349
+ </html>`;
1350
+ if (isVerbose)
1351
+ log.debug(" Using default 404.html template");
1352
+ }
1353
+ if (frontendBucket) {
1354
+ await withS3Retry(() => s3.putObject({
1355
+ bucket: frontendBucket,
1356
+ key: "404.html",
1357
+ body: html404Content,
1358
+ contentType: "text/html",
1359
+ cacheControl: "no-cache, no-store, must-revalidate"
1360
+ }), "upload 404.html to frontend bucket");
1361
+ if (isVerbose)
1362
+ log.debug(` Uploaded 404.html to frontend bucket: ${frontendBucket}`);
1363
+ }
1364
+ if (docsBucket) {
1365
+ await withS3Retry(() => s3.putObject({
1366
+ bucket: docsBucket,
1367
+ key: "404.html",
1368
+ body: html404Content,
1369
+ contentType: "text/html",
1370
+ cacheControl: "no-cache, no-store, must-revalidate"
1371
+ }), "upload 404.html to docs bucket");
1372
+ if (isVerbose)
1373
+ log.debug(` Uploaded 404.html to docs bucket: ${docsBucket}`);
1374
+ }
1375
+ if (distributionId) {
1376
+ if (isVerbose)
1377
+ log.debug(" Configuring CloudFront custom error responses...");
1378
+ const getResult = await awsClient.request({
1379
+ service: "cloudfront",
1380
+ region: "us-east-1",
1381
+ method: "GET",
1382
+ path: `/2020-05-31/distribution/${distributionId}/config`,
1383
+ returnHeaders: !0
1384
+ }), etag = getResult.headers?.etag || getResult.headers?.ETag || "", currentConfig = getResult.body?.DistributionConfig || getResult.DistributionConfig;
1385
+ if (currentConfig && etag) {
1386
+ if (!((currentConfig.CustomErrorResponses?.Quantity || 0) > 0 && currentConfig.CustomErrorResponses?.Items?.CustomErrorResponse?.some?.((err) => err.ErrorCode === 404 && err.ResponsePagePath === "/404.html"))) {
1387
+ currentConfig.CustomErrorResponses = {
1388
+ Quantity: 2,
1389
+ Items: {
1390
+ CustomErrorResponse: [
1391
+ {
1392
+ ErrorCode: 403,
1393
+ ResponsePagePath: "/404.html",
1394
+ ResponseCode: 404,
1395
+ ErrorCachingMinTTL: 10
1396
+ },
1397
+ {
1398
+ ErrorCode: 404,
1399
+ ResponsePagePath: "/404.html",
1400
+ ResponseCode: 404,
1401
+ ErrorCachingMinTTL: 10
1402
+ }
1403
+ ]
1404
+ }
1405
+ };
1406
+ const buildXmlElement = (name, value, indent = "") => {
1407
+ if (value === null || value === void 0)
1408
+ return "";
1409
+ if (typeof value === "boolean")
1410
+ return `${indent}<${name}>${value}</${name}>
1411
+ `;
1412
+ if (typeof value === "number" || typeof value === "string")
1413
+ return `${indent}<${name}>${value}</${name}>
1414
+ `;
1415
+ if (Array.isArray(value))
1416
+ return value.map((item) => buildXmlElement(name, item, indent)).join("");
1417
+ if (typeof value === "object") {
1418
+ if (name.startsWith("@_") || name === "?xml")
1419
+ return "";
1420
+ let children = "";
1421
+ for (const [key, val] of Object.entries(value))
1422
+ if (!key.startsWith("@_") && key !== "#text")
1423
+ children += buildXmlElement(key, val, `${indent} `);
1424
+ if (value["#text"] !== void 0)
1425
+ return `${indent}<${name}>${value["#text"]}</${name}>
1426
+ `;
1427
+ return `${indent}<${name}>
1428
+ ${children}${indent}</${name}>
1429
+ `;
1430
+ }
1431
+ return "";
1432
+ }, xml = `<?xml version="1.0" encoding="UTF-8"?>
1433
+ <DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
1434
+ ${Object.entries(currentConfig).filter(([key]) => !key.startsWith("@_")).map(([key, val]) => buildXmlElement(key, val, " ")).join("")}</DistributionConfig>
1435
+ `;
1436
+ await awsClient.request({
1437
+ service: "cloudfront",
1438
+ region: "us-east-1",
1439
+ method: "PUT",
1440
+ path: `/2020-05-31/distribution/${distributionId}/config`,
1441
+ body: xml,
1442
+ headers: {
1443
+ "Content-Type": "application/xml",
1444
+ "If-Match": etag
1445
+ }
1446
+ });
1447
+ if (isVerbose)
1448
+ log.debug(" CloudFront custom error responses configured");
1449
+ } else if (isVerbose)
1450
+ log.debug(" CloudFront custom error responses already configured");
1451
+ }
1452
+ }
1453
+ errorPageSpinner.succeed("404 error page configured");
1454
+ } catch (errorPageError) {
1455
+ errorPageSpinner.stop();
1456
+ console.log(`\u26A0 404 page configuration skipped: ${errorPageError.message}`);
1457
+ if (isVerbose)
1458
+ log.debug(`Error page config error: ${errorPageError.stack}`);
1459
+ }
1460
+ try {
1461
+ const tunnelCloudConfig = await import(p.projectPath("config/cloud")), tunnelConfig = tunnelCloudConfig?.tsCloud?.infrastructure?.tunnel;
1462
+ if (tunnelConfig?.enabled) {
1463
+ const tunnelDomain = tunnelConfig.domain || "";
1464
+ if (!tunnelDomain || tunnelDomain === "localtunnel.dev" || tunnelDomain === "api.localtunnel.dev")
1465
+ console.log("\u2139 Tunnel: Using shared localtunnel.dev (no custom tunnel deployment needed)");
1466
+ else {
1467
+ const tunnelSpinner = spinner(`Deploying tunnel server to ${tunnelDomain}...`);
1468
+ tunnelSpinner.start();
1469
+ try {
1470
+ const { deployTunnelServer } = await import("@stacksjs/tunnel");
1471
+ await deployTunnelServer({
1472
+ domain: tunnelDomain,
1473
+ region: tunnelConfig.region || tunnelCloudConfig?.tsCloud?.project?.region || "us-east-1",
1474
+ instanceType: tunnelConfig.instanceType,
1475
+ prefix: tunnelConfig.prefix,
1476
+ enableSsl: tunnelConfig.ssl?.enabled,
1477
+ porkbunApiKey: tunnelConfig.ssl?.porkbunApiKey,
1478
+ porkbunSecretKey: tunnelConfig.ssl?.porkbunSecretKey,
1479
+ verbose: isVerbose
1480
+ });
1481
+ tunnelSpinner.succeed(`Tunnel server deployed to ${tunnelDomain}`);
1482
+ } catch (tunnelError) {
1483
+ tunnelSpinner.stop();
1484
+ console.log(`\u26A0 Tunnel deployment skipped: ${tunnelError.message}`);
1485
+ if (isVerbose)
1486
+ log.debug(`Tunnel deploy error: ${tunnelError.stack}`);
1487
+ }
1488
+ }
1489
+ }
1490
+ } catch {}
1491
+ const dnsSpinner = spinner("Reconciling DNS records...");
1492
+ dnsSpinner.start();
1493
+ try {
1494
+ const { Route53Client, CloudFormationClient: DnsCfClient } = await import("@stacksjs/ts-cloud"), dnsCf = new DnsCfClient(region), dnsStackName = `${projectName}-cloud`, dnsOutputs = await dnsCf.getStackOutputs(dnsStackName), dnsCloudConfig = await import(p.projectPath("config/cloud")), hostedZoneId = dnsCloudConfig?.tsCloud?.infrastructure?.dns?.hostedZoneId, siteDomain = dnsCloudConfig?.tsCloud?.infrastructure?.dns?.domain || "stacksjs.com";
1495
+ if (hostedZoneId) {
1496
+ const route53 = new Route53Client(region), cfHostedZoneId = "Z2FDTNDATAQYW2", domainMappings = [
1497
+ { domain: siteDomain, cfDomain: dnsOutputs.publicCloudFrontDomain },
1498
+ { domain: `www.${siteDomain}`, cfDomain: dnsOutputs.publicCloudFrontDomain },
1499
+ { domain: `docs.${siteDomain}`, cfDomain: dnsOutputs.docsCloudFrontDomain },
1500
+ { domain: `blog.${siteDomain}`, cfDomain: dnsOutputs.blogCloudFrontDomain }
1501
+ ];
1502
+ let recordsCreated = 0;
1503
+ for (const { domain, cfDomain } of domainMappings) {
1504
+ if (!cfDomain)
1505
+ continue;
1506
+ try {
1507
+ if (!((await route53.listResourceRecordSets({
1508
+ HostedZoneId: hostedZoneId,
1509
+ StartRecordName: domain,
1510
+ StartRecordType: "A",
1511
+ MaxItems: "1"
1512
+ }))?.ResourceRecordSets || []).some((r) => r.Name === `${domain}.` && r.Type === "A")) {
1513
+ await route53.changeResourceRecordSets({
1514
+ HostedZoneId: hostedZoneId,
1515
+ ChangeBatch: {
1516
+ Changes: [
1517
+ {
1518
+ Action: "UPSERT",
1519
+ ResourceRecordSet: {
1520
+ Name: domain,
1521
+ Type: "A",
1522
+ AliasTarget: {
1523
+ HostedZoneId: cfHostedZoneId,
1524
+ DNSName: cfDomain,
1525
+ EvaluateTargetHealth: !1
1526
+ }
1527
+ }
1528
+ },
1529
+ {
1530
+ Action: "UPSERT",
1531
+ ResourceRecordSet: {
1532
+ Name: domain,
1533
+ Type: "AAAA",
1534
+ AliasTarget: {
1535
+ HostedZoneId: cfHostedZoneId,
1536
+ DNSName: cfDomain,
1537
+ EvaluateTargetHealth: !1
1538
+ }
1539
+ }
1540
+ }
1541
+ ]
1542
+ }
1543
+ });
1544
+ recordsCreated++;
1545
+ if (isVerbose)
1546
+ log.debug(` Created DNS records: ${domain} -> ${cfDomain}`);
1547
+ } else if (isVerbose)
1548
+ log.debug(` DNS record exists: ${domain}`);
1549
+ } catch (dnsErr) {
1550
+ if (isVerbose)
1551
+ log.debug(` DNS check/create failed for ${domain}: ${dnsErr.message}`);
1552
+ }
1553
+ }
1554
+ if (recordsCreated > 0)
1555
+ dnsSpinner.succeed(`DNS records reconciled (${recordsCreated} created)`);
1556
+ else
1557
+ dnsSpinner.succeed("DNS records verified");
1558
+ } else {
1559
+ dnsSpinner.stop();
1560
+ if (isVerbose)
1561
+ log.debug("No hosted zone ID configured, skipping DNS reconciliation");
1562
+ }
1563
+ } catch (dnsError) {
1564
+ dnsSpinner.stop();
1565
+ console.log(`\u26A0 DNS reconciliation skipped: ${dnsError.message}`);
1566
+ if (isVerbose)
1567
+ log.debug(`DNS error: ${dnsError.stack}`);
1568
+ }
1569
+ console.log("");
1570
+ log.success("Deployment completed successfully!");
1571
+ } catch (error) {
1572
+ log.error("Deployment failed:", error);
1573
+ process.exit(ExitCode.FatalError);
1574
+ }