@stacksjs/actions 0.70.87 → 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 (159) hide show
  1. package/dist/action.js +78 -2
  2. package/dist/actions.js +2 -2
  3. package/dist/add.js +20 -2
  4. package/dist/ai/request-model-access.js +10 -2
  5. package/dist/auth/client.js +44 -3
  6. package/dist/auth/prune.js +35 -2
  7. package/dist/auth/setup.js +152 -36
  8. package/dist/auth/token.js +10 -2
  9. package/dist/blog-admin.d.ts +55 -0
  10. package/dist/blog-admin.js +156 -0
  11. package/dist/blog.d.ts +11 -0
  12. package/dist/blog.js +306 -56
  13. package/dist/build/cli.js +6 -2
  14. package/dist/build/component-libs.js +7 -2
  15. package/dist/build/core.js +55 -2
  16. package/dist/build/desktop.js +5 -2
  17. package/dist/build/docs.js +5 -2
  18. package/dist/build/frontend-static.js +123 -8
  19. package/dist/build/server.js +6 -2
  20. package/dist/build/stacks.js +8 -2
  21. package/dist/build/views.js +11 -2
  22. package/dist/build.js +64 -2
  23. package/dist/bump.js +162 -7
  24. package/dist/cdn/invalidate-cache.js +0 -1
  25. package/dist/changelog.js +27 -3
  26. package/dist/check/ports.js +16 -2
  27. package/dist/clean.js +4 -2
  28. package/dist/commit.js +12 -2
  29. package/dist/copy-types.js +10 -3
  30. package/dist/database/seed.js +26 -2
  31. package/dist/deploy/create-receipt-rule.js +36 -2
  32. package/dist/deploy/index.js +1318 -44
  33. package/dist/dev/api.js +94 -6
  34. package/dist/dev/components.js +7 -2
  35. package/dist/dev/dashboard-globals.js +89 -3
  36. package/dist/dev/dashboard-utils.js +380 -2
  37. package/dist/dev/dashboard.js +435 -8
  38. package/dist/dev/desktop.js +5 -2
  39. package/dist/dev/docs.js +24 -2
  40. package/dist/dev/index.js +29 -2
  41. package/dist/dev/migrate-watcher.js +74 -2
  42. package/dist/dev/system-tray.js +5 -2
  43. package/dist/dev/views.js +147 -2
  44. package/dist/discover-packages.js +32 -2
  45. package/dist/domains/add.js +61 -2
  46. package/dist/domains/purchase.js +66 -2
  47. package/dist/domains/remove.js +24 -2
  48. package/dist/examples.js +34 -2
  49. package/dist/find-projects.js +16 -2
  50. package/dist/fresh.js +17 -2
  51. package/dist/generate/action-types.js +9 -4
  52. package/dist/generate/component-meta.js +9 -3
  53. package/dist/generate/custom-cli-file.js +20 -3
  54. package/dist/generate/env-files.js +46 -6
  55. package/dist/generate/ide-helpers.js +8 -3
  56. package/dist/generate/index.js +158 -2
  57. package/dist/generate/lib-entries.js +16 -3
  58. package/dist/generate/pantry-file.js +46 -2
  59. package/dist/generate/symlink-core.js +2 -2
  60. package/dist/generate/types.js +3 -2
  61. package/dist/generate/vscode-custom-data.js +9 -3
  62. package/dist/generate/web-types.js +0 -1
  63. package/dist/helpers/component-meta.js +295 -2
  64. package/dist/helpers/index.js +1 -2
  65. package/dist/helpers/lib-entries.js +73 -2
  66. package/dist/helpers/package-json.js +45 -14
  67. package/dist/helpers/utils.js +105 -2
  68. package/dist/helpers/vscode-custom-data.js +30 -2
  69. package/dist/index.d.ts +13 -0
  70. package/dist/index.js +50 -2
  71. package/dist/key-generate.js +24 -2
  72. package/dist/lint/fix.js +4 -3
  73. package/dist/lint/index.js +4 -3
  74. package/dist/lint/lint.js +34 -2
  75. package/dist/make-command.js +64 -14
  76. package/dist/make-job.js +41 -16
  77. package/dist/make-policy.js +74 -26
  78. package/dist/make-resource.js +50 -21
  79. package/dist/make.js +328 -2
  80. package/dist/migrate/database.js +16 -2
  81. package/dist/migrate/fresh.js +31 -2
  82. package/dist/orm/base.js +0 -2
  83. package/dist/orm/test.js +0 -1
  84. package/dist/prepublish.js +16 -2
  85. package/dist/queue/clear.js +42 -2
  86. package/dist/queue/dlq-purge.js +28 -2
  87. package/dist/queue/dlq-retry.js +35 -2
  88. package/dist/queue/dlq.js +55 -5
  89. package/dist/queue/failed.js +69 -5
  90. package/dist/queue/flush.js +49 -3
  91. package/dist/queue/inspect.js +154 -8
  92. package/dist/queue/list.js +69 -5
  93. package/dist/queue/monitor.js +94 -6
  94. package/dist/queue/pause.js +28 -2
  95. package/dist/queue/quarantine.js +42 -3
  96. package/dist/queue/resume.js +27 -2
  97. package/dist/queue/retry.js +41 -2
  98. package/dist/queue/schedule-list.js +33 -5
  99. package/dist/queue/schedule.js +12 -2
  100. package/dist/queue/status.js +68 -5
  101. package/dist/queue/table.js +10 -2
  102. package/dist/queue/unquarantine.js +27 -2
  103. package/dist/queue/work.js +57 -2
  104. package/dist/release.js +21 -3
  105. package/dist/route/list.js +78 -3
  106. package/dist/run-action.js +1 -2
  107. package/dist/saas/setup.js +9 -2
  108. package/dist/scaffold-crud.js +164 -40
  109. package/dist/schedule/run.js +7 -2
  110. package/dist/search/flush.js +10 -2
  111. package/dist/search/import.js +10 -2
  112. package/dist/search/settings-list.js +10 -2
  113. package/dist/search/settings.js +9 -2
  114. package/dist/serve/api.js +35 -2
  115. package/dist/setup/index.js +1 -2
  116. package/dist/setup/ssl.js +271 -2
  117. package/dist/stacks.js +265 -2
  118. package/dist/templates.js +301 -2
  119. package/dist/test/feature.js +11 -2
  120. package/dist/test/index.js +11 -2
  121. package/dist/test/ui.js +4 -2
  122. package/dist/test/unit.js +11 -2
  123. package/dist/typecheck.js +4 -2
  124. package/dist/types.js +16 -2
  125. package/dist/upgrade/binary.js +29 -2
  126. package/dist/upgrade/bun.js +0 -1
  127. package/dist/upgrade/dependencies.js +0 -2
  128. package/dist/upgrade/framework-utils.js +193 -2
  129. package/dist/upgrade/framework.js +310 -7
  130. package/dist/upgrade/index.js +17 -2
  131. package/dist/upgrade/packages.js +99 -12
  132. package/dist/upgrade/shell.js +40 -4
  133. package/dist/upgrade.js +38 -2
  134. package/package.json +16 -16
  135. package/dist/chunk-0c8sd1t6.js +0 -2
  136. package/dist/chunk-0g2nzhx9.js +0 -240
  137. package/dist/chunk-0gadhvn7.js +0 -3
  138. package/dist/chunk-0h27aa4r.js +0 -93
  139. package/dist/chunk-3peh8kqs.js +0 -851
  140. package/dist/chunk-62gdfav8.js +0 -3
  141. package/dist/chunk-6bk53y02.js +0 -3
  142. package/dist/chunk-78c5n3s2.js +0 -3
  143. package/dist/chunk-c74qv38e.js +0 -17
  144. package/dist/chunk-cdk2r2jd.js +0 -1
  145. package/dist/chunk-dbz9dp2h.js +0 -3
  146. package/dist/chunk-e142dheq.js +0 -3
  147. package/dist/chunk-ec4gky40.js +0 -3
  148. package/dist/chunk-h3d81wrg.js +0 -3
  149. package/dist/chunk-m3q0x4h0.js +0 -688
  150. package/dist/chunk-mqgbyhph.js +0 -5
  151. package/dist/chunk-n24m8prb.js +0 -3
  152. package/dist/chunk-n5eqwhck.js +0 -7
  153. package/dist/chunk-nsvnqv8y.js +0 -5
  154. package/dist/chunk-nty6mmx4.js +0 -283
  155. package/dist/chunk-s735m9mg.js +0 -1
  156. package/dist/chunk-tpb8jgsk.js +0 -7
  157. package/dist/chunk-tsbk5vne.js +0 -3
  158. package/dist/chunk-xg9xscqz.js +0 -1
  159. package/dist/chunk-y6e448he.js +0 -7
@@ -1,15 +1,613 @@
1
- // @bun
2
- import{Lb as Z}from"../chunk-h3d81wrg.js";import{existsSync as JJ,readFileSync as BJ,statSync as TJ}from"fs";import T from"process";import{runCommand as KJ,spinner as S}from"@stacksjs/cli";import{config as OJ}from"@stacksjs/config";import{path as X}from"@stacksjs/path";import{storage as RJ}from"@stacksjs/storage";import{ExitCode as EJ}from"@stacksjs/types";var $={debug:(...J)=>{if(T.argv.includes("--verbose")||T.argv.includes("-v"))console.log("\uD83D\uDD0D",...J)},success:(...J)=>console.log("\u2713",...J),error:(...J)=>console.error("\u2717",...J)},GJ="github.com/mail-os/mail",NJ=`${GJ}@0.1.0`,jJ="linux-x86_64",XJ=["mail","mail-x86_64-linux","mail-x86_64-linux-gnu"],V=T.argv.includes("--verbose")||T.argv.includes("-v");async function DJ(J,U,H=8){let{existsSync:Q,readdirSync:_,statSync:b}=await import("fs"),{join:G,basename:y}=await import("path"),c=new Set(U),k=[];if(!Q(J))return k;function N(QJ,UJ){if(UJ>H)return;for(let o of _(QJ)){if(o===".git"||o==="node_modules")continue;let r=G(QJ,o),n=b(r);if(n.isDirectory())N(r,UJ+1);else if(n.isFile()&&c.has(y(r)))k.push(r)}}return N(J,0),k}async function MJ(J){let{readFileSync:U}=await import("fs"),H=U(J).slice(0,4);return H[0]===127&&H[1]===69&&H[2]===76&&H[3]===70}async function IJ(){let{existsSync:J}=await import("fs"),U=`${T.env.HOME}/Code/Tools/pantry/packages/ts-pantry/bin/cli.ts`;if(J(U))return{command:"bun",args:[U]};let H=X.projectPath("pantry/.bin/pantry");if(J(H))return{command:H,args:[]};let Q=`${T.env.HOME}/.local/share/pantry/global/bin/pantry`;if(J(Q))return{command:Q,args:[]};return{command:"pantry",args:[]}}async function PJ(){let{execFileSync:J}=await import("child_process"),U=X.projectPath("pantry"),H=await IJ();J(H.command,[...H.args,"install",NJ,"--install-dir",U,"--platform",jJ,"--quiet"],{cwd:X.projectPath(),stdio:V?"inherit":"pipe",env:T.env})}async function fJ(){let{existsSync:J}=await import("fs"),{join:U}=await import("path"),H=[X.projectPath("pantry"),`${T.env.HOME}/.local/share/pantry`],Q=[...XJ.map((_)=>X.projectPath(`pantry/.bin/${_}`)),...XJ.map((_)=>U(`${T.env.HOME}/.local/share/pantry/global/bin`,_))];for(let _ of Q)if(J(_)&&await MJ(_))return _;for(let _ of H){let b=await DJ(_,XJ);for(let G of b)if(await MJ(G))return G}return null}var bJ={".html":"text/html",".css":"text/css",".js":"application/javascript",".json":"application/json",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".ico":"image/x-icon",".woff":"font/woff",".woff2":"font/woff2",".ttf":"font/ttf",".eot":"application/vnd.ms-fontobject",".otf":"font/otf",".webp":"image/webp",".mp4":"video/mp4",".webm":"video/webm",".mp3":"audio/mpeg",".wav":"audio/wav",".pdf":"application/pdf",".xml":"application/xml",".txt":"text/plain"};function WJ(J){let U=J.lastIndexOf("."),H=U>=0?J.slice(U).toLowerCase():"";return bJ[H]||"application/octet-stream"}async function HJ(J,U,H="",Q=[".DS_Store"]){let{readdirSync:_,statSync:b}=await import("fs"),{join:G}=await import("path"),y=_(J);for(let c of y){if(Q.includes(c))continue;let k=G(J,c),N=H?`${H}/${c}`:c;if(b(k).isDirectory())await HJ(k,U,N,Q);else await U(k,N)}}async function a(J,U="s3 operation"){for(let Q=0;Q<=3;Q++)try{return await J()}catch(_){if(Q>=3)throw _;let b=Math.min(500*2**Q+Math.random()*200,5000);if(V)$.debug(` Retrying ${U} (attempt ${Q+2}/4) in ${Math.round(b)}ms...`);await new Promise((G)=>setTimeout(G,b))}throw Error(`${U} failed after 4 attempts`)}function wJ(J){if(!J)return[];return Array.isArray(J)?J:[J]}function AJ(J,U,H=""){if(U===null||U===void 0)return"";if(typeof U==="boolean")return`${H}<${J}>${U}</${J}>
3
- `;if(typeof U==="number"||typeof U==="string")return`${H}<${J}>${U}</${J}>
4
- `;if(Array.isArray(U))return U.map((Q)=>AJ(J,Q,H)).join("");if(typeof U==="object"){if(J.startsWith("@_")||J==="?xml")return"";let Q="";for(let[_,b]of Object.entries(U))if(!_.startsWith("@_")&&_!=="#text")Q+=AJ(_,b,`${H} `);if(U["#text"]!==void 0)return`${H}<${J}>${U["#text"]}</${J}>
5
- `;return`${H}<${J}>
6
- ${Q}${H}</${J}>
7
- `}return""}function hJ(J){return`<?xml version="1.0" encoding="UTF-8"?>
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"?>
8
178
  <DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
9
- ${Object.entries(J).filter(([U])=>!U.startsWith("@_")).map(([U,H])=>AJ(U,H," ")).join("")}</DistributionConfig>
10
- `}function _J(J){let U=[...new Set(J)].filter((Q)=>Number.isFinite(Q)&&Q>0).sort((Q,_)=>Q-_);if(U.length===0)return"true";return`if command -v firewall-cmd >/dev/null 2>&1; then firewall-cmd --permanent ${U.map((Q)=>`--add-port=${Q}/tcp`).join(" ")} && firewall-cmd --reload; fi`}async function CJ(J,U,H){if(!U)return;if(((await J.describeSecurityGroups({GroupIds:[U]}))?.SecurityGroups?.[0]?.IpPermissions||[]).some((G)=>{return G.IpProtocol==="tcp"&&Number(G.FromPort)<=H&&Number(G.ToPort)>=H&&wJ(G.IpRanges).some((y)=>y.CidrIp==="0.0.0.0/0")}))return;try{await J.authorizeSecurityGroupIngress({GroupId:U,IpPermissions:[{IpProtocol:"tcp",FromPort:H,ToPort:H,IpRanges:[{CidrIp:"0.0.0.0/0",Description:`Stacks API port ${H}`}]}]})}catch(G){if(!String(G?.message||"").includes("InvalidPermission.Duplicate"))throw G}}async function kJ(J,U,H){if(!U)return;let Q=await J.request({service:"cloudfront",region:"us-east-1",method:"GET",path:`/2020-05-31/distribution/${U}/config`,returnHeaders:!0}),_=Q.headers?.etag||Q.headers?.ETag||"",b=Q.body?.DistributionConfig||Q.DistributionConfig||Q.body;if(!_||!b?.Origins?.Items)return;let y=wJ(b.Origins.Items.Origin).find((c)=>String(c.Id||"").includes("-api"));if(!y?.CustomOriginConfig)return;if(Number(y.CustomOriginConfig.HTTPPort)===H)return;y.CustomOriginConfig.HTTPPort=H,await J.request({service:"cloudfront",region:"us-east-1",method:"PUT",path:`/2020-05-31/distribution/${U}/config`,body:hJ(b),headers:{"Content-Type":"application/xml","If-Match":_}})}var xJ=S("Building framework...");xJ.start();await KJ("bun run build",{cwd:X.frameworkPath("core"),quiet:!1});xJ.succeed("Framework built");var LJ=X.projectPath("docs"),SJ=X.projectPath("dist/docs/.bunpress");if(RJ.hasFiles(LJ)){let J=S("Building documentation with BunPress...");J.start();try{let{rm:U}=await import("fs/promises"),H=`${T.env.HOME}/Code/Tools/bunpress/packages/bunpress/bin/cli.ts`,Q=X.projectPath("node_modules/@stacksjs/bunpress/dist/bin/cli.js"),_=await Bun.file(H).exists(),b=await Bun.file(Q).exists(),G=_?`bun ${H} build --dir ${LJ} --outdir ${X.projectPath("dist/docs")}`:b?`bun ${Q} build --dir ${LJ} --outdir ${X.projectPath("dist/docs")}`:"bunx @stacksjs/bunpress@0.1.5 build --dir ./docs --outdir ./dist/docs";await U(SJ,{recursive:!0,force:!0}),await KJ(G,{cwd:X.projectPath(),quiet:!V}),J.succeed(`Documentation built with BunPress${_?" (local checkout)":""}`)}catch(U){if(J.stop(),console.log(`\u26A0 Documentation build skipped: ${U.message}`),V)$.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")}}else if(V)$.debug("No docs directory found, skipping documentation build");var vJ=RJ.hasFiles(X.projectPath("dist/blog")),yJ=["CampmateScript-Regular.woff2","SequoiaSans-Regular.woff2","Switchback-Regular.woff2"].every((J)=>JJ(X.projectPath(`dist/blog/assets/fonts/nps/${J}`))),gJ=["topography.svg","park-ridge.svg"].every((J)=>JJ(X.projectPath(`dist/blog/assets/images/${J}`))),ZJ=X.projectPath("dist/blog/index.html"),i=JJ(ZJ)?BJ(ZJ,"utf8"):"",cJ=i.length>0&&!i.includes("woff2-variations"),mJ=i.includes("--on-primary")&&i.includes("--newsletter-bg"),dJ=i.includes("topography.svg")&&i.includes("park-ridge.svg"),pJ=!i.includes("stamp-trail")&&!i.includes("blog-sign-tree.svg"),uJ=X.frameworkPath("core/cms/src/build.ts"),sJ=X.projectPath("config/blog.ts"),lJ=JJ(ZJ)&&[uJ,sJ].filter((J)=>JJ(J)).every((J)=>TJ(ZJ).mtimeMs>=TJ(J).mtimeMs);if(!vJ||!yJ||!gJ||!cJ||!mJ||!dJ||!pJ||!lJ){let J=S("Building blog...");J.start();try{let U=(await import(X.projectPath("config/blog"))).default,{buildBlogSite:H}=await import(X.frameworkPath("core/cms/src/build"));await H({config:U,outDir:X.projectPath("dist/blog")}),J.succeed("Blog built successfully")}catch(U){if(J.stop(),console.log(`\u26A0 Blog build skipped: ${U.message}`),V)$.debug(`Blog build error: ${U.stack}`)}}else if(V)$.debug("Pre-built blog found at dist/blog, skipping build");if(V)$.debug("Skipping views build (vite-config not configured)");var aJ=await import(X.projectPath("config/cloud")),iJ=aJ?.tsCloud?.mode||"server";if(iJ==="serverless"){let J=S("Building server...");J.start(),await KJ("bun build.ts",{cwd:X.frameworkPath("server"),quiet:!V}),J.succeed("Server built");let U=S("Packaging for deployment...");U.start(),await KJ("bun zip.ts",{cwd:X.corePath("cloud"),quiet:!V}),U.succeed("Package ready")}else if(V)$.debug("Skipping server build and packaging (server mode)");if(!T.env.AWS_ACCESS_KEY_ID||!T.env.AWS_SECRET_ACCESS_KEY){let{existsSync:J,readFileSync:U}=await import("fs"),H=T.env.APP_ENV||T.env.NODE_ENV||"production",Q=[X.projectPath(`.env.${H}`),X.projectPath(".env")];for(let _ of Q)if(J(_)){let b=U(_,"utf-8");for(let G of b.split(`
11
- `)){let y=G.trim();if(y.startsWith("#")||!y.includes("="))continue;let c=y.indexOf("="),k=y.slice(0,c).trim(),N=y.slice(c+1).trim();if(N.startsWith('"')&&N.endsWith('"')||N.startsWith("'")&&N.endsWith("'"))N=N.slice(1,-1);if(k==="AWS_ACCESS_KEY_ID"&&N)T.env.AWS_ACCESS_KEY_ID=N;else if(k==="AWS_SECRET_ACCESS_KEY"&&N)T.env.AWS_SECRET_ACCESS_KEY=N;else if(k==="AWS_REGION"&&N&&!T.env.AWS_REGION)T.env.AWS_REGION=N;else if(k==="AWS_ACCOUNT_ID"&&N&&!T.env.AWS_ACCOUNT_ID)T.env.AWS_ACCOUNT_ID=N;else if(k.startsWith("MAIL_PASSWORD_")&&N&&!T.env[k])T.env[k]=N}if(V)$.debug(`Loaded AWS credentials from ${_}`);break}}try{let{deployStack:J,deployFrontend:U}=await import("../chunk-0h27aa4r.js"),H=T.env.APP_ENV||T.env.NODE_ENV||"production",Q=T.env.AWS_REGION||"us-east-1",_=await import(X.projectPath("config/cloud")),b=_?.tsCloud?.mode||"server",G=_?.tsCloud?.project?.name||_?.tsCloud?.project?.slug||"stacks";if(b==="serverless"){console.log(""),console.log("Serverless deployment mode detected"),console.log("Building and pushing container image..."),console.log("");let z=T.env.AWS_ACCOUNT_ID||await y(Q);if(!z||z.length<10)throw Error(`Invalid AWS Account ID: "${z||"(empty)"}"
12
- Set AWS_ACCOUNT_ID in your .env file or ensure valid AWS credentials are configured.`);let q=`${z}.dkr.ecr.${Q}.amazonaws.com/${G}-${H}-api`,A=`${q}:latest`;if(V)$.debug(`ECR repository: ${q}`);if(V)$.debug("ECR credentials will be validated before push...");let W=S("Building Docker image...");W.start();try{let{exec:Y}=await import("child_process"),{promisify:F}=await import("util"),x=F(Y),E=`docker build --platform linux/amd64 -t ${A} -f Dockerfile .`,{stdout:M,stderr:O}=await x(E,{cwd:X.projectPath(),env:T.env,maxBuffer:10485760,timeout:900000});if(V&&M)$.debug(M);if(V&&O)$.debug(O);W.succeed(`Docker image built: ${A}`)}catch(Y){if(W.fail("Docker build failed"),V)$.debug(`Docker build error: ${Y.message}`);throw Y}let K=S("Checking ECR repository...");K.start();try{let{ECRClient:Y}=await import("@stacksjs/ts-cloud"),F=new Y(Q),x=`${G}-${H}-api`;try{await F.describeRepositories({repositoryNames:[x]}),K.succeed(`ECR repository exists: ${x}`)}catch(E){if(E.code==="RepositoryNotFoundException"||E.message?.includes("RepositoryNotFoundException"))K.text=`Creating ECR repository: ${x}...`,await F.createRepository({repositoryName:x,imageScanningConfiguration:{scanOnPush:!0},imageTagMutability:"MUTABLE"}),K.succeed(`ECR repository created: ${x}`);else throw E}}catch(Y){if(K.fail("ECR repository check failed"),V)$.debug(`ECR repository error: ${Y.message}`);throw Y}let w=S("Authenticating to ECR...");w.start();try{let{ECRClient:Y}=await import("@stacksjs/ts-cloud"),E=(await new Y(Q).getAuthorizationToken()).authorizationData?.[0];if(!E?.authorizationToken)throw Error("No authorization token received from ECR");let M=Buffer.from(E.authorizationToken,"base64").toString("utf-8"),O=M.indexOf(":");if(O===-1)throw Error("ECR authorization token has unexpected format (missing colon separator)");let L=M.slice(O+1),j=Bun.spawnSync(["docker","login","--username","AWS","--password-stdin",`${z}.dkr.ecr.${Q}.amazonaws.com`],{stdin:new TextEncoder().encode(L),stdout:"pipe",stderr:"pipe"});if(j.exitCode!==0){let P=new TextDecoder().decode(j.stderr);throw Error(`Docker login failed: ${P}`)}w.succeed("Authenticated to ECR")}catch(Y){if(w.fail("ECR authentication failed"),V)$.debug(`ECR auth error: ${Y.message}`);throw Y}console.log("");let I=S("Pushing image to ECR...");I.start();try{let{exec:Y}=await import("child_process"),{promisify:F}=await import("util"),x=F(Y),E=`docker push ${A}`;await x(E,{env:T.env}),I.succeed(`Image pushed: ${A}`)}catch(Y){if(I.fail("Docker push failed"),V)$.debug(`Push error: ${Y.message}`);throw Y}console.log(""),console.log("Container image ready in ECR. CloudFormation will use this image."),console.log("")}else console.log(""),console.log("Server deployment mode detected"),console.log("Deploying EC2 infrastructure via CloudFormation..."),console.log("");async function y(z){try{let{STSClient:q}=await import("@stacksjs/ts-cloud"),K=(await new q(z).getCallerIdentity()).Account||T.env.AWS_ACCOUNT_ID||"";if(V)$.debug(`Retrieved AWS Account ID: ${K||"(empty)"}`);return K}catch(q){if(V)$.debug(`Failed to get AWS Account ID: ${q.message}`);return T.env.AWS_ACCOUNT_ID||""}}try{let z=await import(X.projectPath("cloud/deploy-script"));if(typeof z.beforeDeploy==="function")await z.beforeDeploy({environment:H,region:Q})}catch(z){if(z.code==="ERR_MODULE_NOT_FOUND"||z.code==="MODULE_NOT_FOUND"){if(V)$.debug("No beforeDeploy hook found (cloud/deploy-script not found)")}else if(console.warn(`\u26A0 beforeDeploy hook failed: ${z.message}`),V)$.debug(`beforeDeploy error stack: ${z.stack}`)}await J({environment:H,region:Q,waitForCompletion:!0,verbose:V});try{let z=await import(X.projectPath("cloud/deploy-script"));if(typeof z.afterDeploy==="function"){let{CloudFormationClient:q}=await import("@stacksjs/ts-cloud"),A=new q(Q),W=`${G}-cloud`,K={};try{K=await A.getStackOutputs(W)}catch(w){if(V)$.debug(`Failed to get stack outputs: ${w instanceof Error?w.message:String(w)}`)}await z.afterDeploy({environment:H,region:Q,outputs:K})}}catch(z){if(z.code==="ERR_MODULE_NOT_FOUND"||z.code==="MODULE_NOT_FOUND"){if(V)$.debug("No afterDeploy hook found (cloud/deploy-script not found)")}else if(console.warn(`\u26A0 afterDeploy hook failed: ${z.message}`),V)$.debug(`afterDeploy error stack: ${z.stack}`)}let c=S("Running database migrations...");c.start();try{let{generateMigrations:z,runDatabaseMigration:q}=await import("@stacksjs/database");await z(),await q(),c.succeed("Database migrations completed")}catch(z){if(c.stop(),console.log(`\u26A0 Database migrations skipped: ${z.message}`),V)$.debug(`Migration error: ${z.stack}`)}let k=S("Ensuring framework tables (auth, notifications, RBAC)...");k.start();try{let{migrateAuthTables:z,migrateNotificationTables:q,migrateRbacTables:A}=await import("@stacksjs/database"),W=[],K=[["auth",z],["notification",q],["RBAC",A]];for(let[w,I]of K){let Y=await I({verbose:V});if(!Y.success)W.push(`${w}: ${Y.error}`)}if(W.length>0)k.stop(),console.log(`\u26A0 Some framework tables could not be migrated: ${W.join("; ")}`);else k.succeed("Framework tables ensured (auth, notifications, RBAC)")}catch(z){if(k.stop(),console.log(`\u26A0 Framework table migrations skipped: ${z.message}`),V)$.debug(`Framework table migration error: ${z.stack}`)}if(b==="server"){let z=S("Retrieving server instance details...");z.start();try{let{CloudFormationClient:W}=await import("@stacksjs/ts-cloud"),K=new W(Q),w=`${G}-cloud`,I=await K.getStackOutputs(w);z.succeed("Server instances deployed"),console.log(""),console.log("Server instances:");for(let[Y,F]of Object.entries(I)){if(Y.endsWith("PublicIp"))console.log(` ${Y}: ${F}`);if(Y.endsWith("InstanceId"))console.log(` ${Y}: ${F}`)}console.log("")}catch(W){if(z.stop(),console.log(`\u26A0 Could not retrieve server details: ${W.message}`),V)$.debug(`Output retrieval error: ${W.stack}`)}let q=S("Deploying API server to EC2...");q.start();try{let{S3Client:W,CloudFormationClient:K,AWSClient:w}=await import("@stacksjs/ts-cloud"),{readFileSync:I,existsSync:Y}=await import("fs"),{resolve:F}=await import("path"),x=new W(Q),E=new K(Q),M=new w,O=`${G}-cloud`,L=await E.getStackOutputs(O),j=L.FrontendBucketName,P=Number(OJ.ports?.api||3008),C;for(let[g,v]of Object.entries(L))if(g.endsWith("InstanceId")&&typeof v==="string"){C=v;break}let f;if(C){let{EC2Client:g}=await import("@stacksjs/ts-cloud"),v=new g(Q);f=(await v.describeInstances({Filters:[{Name:"instance-id",Values:[C]}]}))?.Reservations?.[0]?.Instances?.[0]?.SecurityGroups?.[0]?.GroupId,await CJ(v,f,P)}if(!C||!j){if(q.stop(),!C)console.log("\u26A0 No EC2 instance found in stack outputs, skipping server deploy");if(!j)console.log("\u26A0 No S3 bucket found for staging server artifacts")}else{let g=L.publicCloudFrontDistributionId||L.CloudFrontDistributionId;await kJ(M,g,P);let v=F(X.projectPath("node_modules/@stacksjs/ts-cloud/dist/index.js"));if(Y(v)){let R=I(v);if(await a(()=>x.putObject({bucket:j,key:"_deploy/ts-cloud-dist.js",body:R,contentType:"application/javascript"}),"upload ts-cloud dist"),V)$.debug(" Uploaded ts-cloud dist to S3")}let m=`import { Database } from "bun:sqlite";
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";
13
611
  import { SESClient } from "./ts-cloud-dist.js";
14
612
 
15
613
  const db = new Database("/var/www/api/stacks.db");
@@ -27,7 +625,7 @@ const CORS_HEADERS = {
27
625
  "Access-Control-Allow-Headers": "Content-Type",
28
626
  };
29
627
 
30
- const ses = new SESClient("${Q}");
628
+ const ses = new SESClient("${region}");
31
629
 
32
630
  async function readBody(req) {
33
631
  const contentType = req.headers.get("content-type") || "";
@@ -76,7 +674,7 @@ async function sendWelcomeEmail(email) {
76
674
  }
77
675
 
78
676
  const server = Bun.serve({
79
- port: ${P},
677
+ port: ${apiServerPort},
80
678
  hostname: "0.0.0.0",
81
679
  async fetch(req) {
82
680
  const url = new URL(req.url);
@@ -130,7 +728,21 @@ const server = Bun.serve({
130
728
  });
131
729
 
132
730
  console.log(\`Stacks API server running on port \${server.port}\`);
133
- `;if(await a(()=>x.putObject({bucket:j,key:"_deploy/server.ts",body:Buffer.from(m),contentType:"application/typescript"}),"upload server.ts"),V)$.debug(" Uploaded server.ts to S3");let p=["mkdir -p /var/www/api",`aws s3 cp s3://${j}/_deploy/ts-cloud-dist.js /var/www/api/ts-cloud-dist.js`,`aws s3 cp s3://${j}/_deploy/server.ts /var/www/api/server.ts`,_J([P]),`cat > /etc/systemd/system/stacks-api.service << 'SERVICEFILE'
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'
134
746
  [Unit]
135
747
  Description=Stacks Bun API Server
136
748
  After=network.target
@@ -147,16 +759,172 @@ Environment=NODE_ENV=production
147
759
  WantedBy=multi-user.target
148
760
  SERVICEFILE
149
761
  systemctl daemon-reload
150
- systemctl enable stacks-api`,"systemctl reset-failed stacks-api || true","systemctl restart stacks-api",`for i in $(seq 1 20); do curl -fsS http://127.0.0.1:${P}/api/health && break; sleep 1; done`,`curl -fsS http://127.0.0.1:${P}/api/health`,`echo "API server deployed and restarted on port ${P}"`].join(" && "),h=(await M.request({service:"ssm",region:Q,method:"POST",path:"/",headers:{"Content-Type":"application/x-amz-json-1.1","X-Amz-Target":"AmazonSSM.SendCommand"},body:JSON.stringify({InstanceIds:[C],DocumentName:"AWS-RunShellScript",Parameters:{commands:[p]},TimeoutSeconds:120})}))?.Command?.CommandId;if(h){let R=0;while(R<30){await new Promise((B)=>setTimeout(B,3000));try{let B=await M.request({service:"ssm",region:Q,method:"POST",path:"/",headers:{"Content-Type":"application/x-amz-json-1.1","X-Amz-Target":"AmazonSSM.GetCommandInvocation"},body:JSON.stringify({CommandId:h,InstanceId:C})}),d=B?.Status;if(d==="Success"){q.succeed("API server deployed to EC2");break}else if(d==="Failed"||d==="Cancelled"||d==="TimedOut"){let t=B?.StandardErrorContent||"Unknown error";q.fail(`API server deploy failed: ${t}`);break}}catch{}R++}if(R>=30)q.fail("API server deploy timed out waiting for SSM command")}else q.fail("Failed to send SSM command");try{await x.deleteObject({bucket:j,key:"_deploy/ts-cloud-dist.js"}),await x.deleteObject({bucket:j,key:"_deploy/server.ts"})}catch{}}}catch(W){if(q.fail(`API server deployment failed: ${W.message}`),V)$.debug(`Server deploy error: ${W.stack}`)}let A=OJ.email;if(A?.server?.enabled){let W=A?.server?.mode||"server",K=S(`Deploying mail server (${W} mode) to EC2...`);K.start();try{let{S3Client:w,AWSClient:I,EC2Client:Y}=await import("@stacksjs/ts-cloud"),{readFileSync:F,existsSync:x}=await import("fs"),{resolve:E,join:M}=await import("path"),{execSync:O}=await import("child_process"),L=new w(Q),j=new I,P=new Y(Q),C=`${G}-cloud`,f,g,v,m;try{let{AWSCloudFormationClient:p}=await import("@stacksjs/ts-cloud"),h=await new p(Q).getStackOutputs(C);for(let[R,B]of Object.entries(h)){if(R.endsWith("InstanceId")&&typeof B==="string")f=B;if(R==="FrontendBucketName"&&typeof B==="string")g=B;if(R==="emailBucketName"&&typeof B==="string")v=B}}catch{}if(!v)v=`${G}-production-s3-email`;if(f&&g){let p=[25,80,110,143,465,587,993,995];try{let B=(await P.describeInstances({Filters:[{Name:"instance-id",Values:[f]}]}))?.Reservations?.[0]?.Instances?.[0]?.SecurityGroups;if(B?.[0]?.GroupId)m=B[0].GroupId}catch{if(V)$.debug("Could not get security group for instance")}if(m)for(let R of p)try{if(await P.authorizeSecurityGroupIngress({GroupId:m,IpPermissions:[{IpProtocol:"tcp",FromPort:R,ToPort:R,IpRanges:[{CidrIp:"0.0.0.0/0",Description:`Mail port ${R}`}]}]}),V)$.debug(` Opened port ${R} on security group ${m}`)}catch(B){if(!B.message?.includes("already exists")){if(V)$.debug(` Port ${R}: ${B.message}`)}}let D=A.domain||"stacksjs.com",h=A?.server?.subdomain||"mail";if(W==="server"){if(V)$.debug("Deploying Zig mail server binary...");try{await PJ()}catch(B){if(V)$.debug(` Pantry mail install failed: ${B.message}`)}let R=await fJ();if(R&&x(R)){if(!await MJ(R))throw K.fail(`Pantry installed ${GJ}, but the ${jJ} binary is not an ELF file.`),Error("Linux binary is not a valid ELF file");let B=F(R);if(await L.putObject({bucket:v,key:"mail-server/smtp-server",body:B,contentType:"application/octet-stream"}),V)$.debug(` Uploaded Zig binary (${(B.length/1024/1024).toFixed(1)}MB) to S3`);let d=["systemctl stop stacks-mail 2>/dev/null || true","systemctl disable stacks-mail 2>/dev/null || true","systemctl stop stacks-smtp 2>/dev/null || true","systemctl disable stacks-smtp 2>/dev/null || true","systemctl stop smtp-server 2>/dev/null || true","mkdir -p /opt/smtp-server /var/lib/smtp-server /var/log/smtp-server /var/spool/mail /etc/smtp-server /var/lib/smtp-server/backups",`aws s3 cp s3://${v}/mail-server/smtp-server /opt/smtp-server/smtp-server --region ${Q}`,"chmod +x /opt/smtp-server/smtp-server",'file /opt/smtp-server/smtp-server | grep -q ELF || { echo "Binary is not ELF"; exit 1; }',_J(p),`cat > /etc/smtp-server/smtp-server.env << 'ENVEOF'
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'
151
919
  SMTP_PROFILE=production
152
920
  SMTP_HOST=0.0.0.0
153
921
  SMTP_PORT=25
154
- SMTP_HOSTNAME=${h}.${D}
922
+ SMTP_HOSTNAME=${mailSubdomain}.${domain}
155
923
  SMTP_ENABLE_TLS=false
156
924
  SMTP_ENABLE_AUTH=true
157
925
  SMTP_DB_PATH=/var/lib/smtp-server/smtp.db
158
- AWS_S3_BUCKET=${v}
159
- AWS_REGION=${Q}
926
+ AWS_S3_BUCKET=${emailBucketName}
927
+ AWS_REGION=${region}
160
928
  SMTP_ENABLE_JSON_LOGGING=true
161
929
  SMTP_LOG_LEVEL=info
162
930
  SMTP_MAILBOX_PATH=/var/spool/mail
@@ -167,7 +935,9 @@ SMTP_MAX_RECIPIENTS=100
167
935
  SMTP_RATE_LIMIT_PER_IP=100
168
936
  SMTP_RATE_LIMIT_PER_USER=200
169
937
  SMTP_IO_MODE=epoll
170
- ENVEOF`,"chmod 600 /etc/smtp-server/smtp-server.env",`cat > /etc/systemd/system/smtp-server.service << 'SVCEOF'
938
+ ENVEOF`,
939
+ "chmod 600 /etc/smtp-server/smtp-server.env",
940
+ `cat > /etc/systemd/system/smtp-server.service << 'SVCEOF'
171
941
  [Unit]
172
942
  Description=Stacks Mail Server (Zig)
173
943
  After=network.target
@@ -185,20 +955,99 @@ SyslogIdentifier=smtp-server
185
955
 
186
956
  [Install]
187
957
  WantedBy=multi-user.target
188
- SVCEOF`,"systemctl daemon-reload","systemctl enable smtp-server","systemctl restart smtp-server","sleep 3",'systemctl is-active smtp-server && echo "Mail server is running" || echo "Mail server failed to start"','ss -tlnp | grep -E "(25|143)" || echo "Warning: mail ports not listening yet"'].join(" && "),$J=(await j.request({service:"ssm",region:Q,method:"POST",path:"/",headers:{"Content-Type":"application/x-amz-json-1.1","X-Amz-Target":"AmazonSSM.SendCommand"},body:JSON.stringify({InstanceIds:[f],DocumentName:"AWS-RunShellScript",Parameters:{commands:[d]},TimeoutSeconds:300})}))?.Command?.CommandId;if($J){let zJ=0;while(zJ<100){await new Promise((u)=>setTimeout(u,3000));try{let u=await j.request({service:"ssm",region:Q,method:"POST",path:"/",headers:{"Content-Type":"application/x-amz-json-1.1","X-Amz-Target":"AmazonSSM.GetCommandInvocation"},body:JSON.stringify({CommandId:$J,InstanceId:f})}),e=u?.Status;if(e==="Success"){if(K.succeed("Zig mail server deployed to EC2"),V&&u?.StandardOutputContent)console.log(u.StandardOutputContent);break}else if(e==="Failed"||e==="Cancelled"||e==="TimedOut"){let qJ=u?.StandardErrorContent||u?.StandardOutputContent||"Unknown error";K.fail(`Zig mail server deploy failed: ${qJ.slice(0,500)}`);break}}catch{}zJ++}if(zJ>=100)K.fail("Zig mail server deploy timed out")}else K.fail("Failed to send SSM command for mail server")}else K.fail(`No Pantry-provided ${jJ} mail binary found. Release ${GJ}, then run the Pantry binary sync for that package.`)}else{let R=E(X.projectPath("storage/framework/core/cloud/src/imap/smtp-server.ts")),B=M(X.projectPath(),".stacks/tmp/smtp-bundle");O(`bun build ${R} --target=bun --outdir=${B}`,{stdio:"pipe"});let d=M(B,"smtp-server.js");if(x(d)){let t=F(d),$J=A.mailboxes||["chris","blake","glenn"],zJ=$J.map((s)=>{let l=`MAIL_PASSWORD_${s.toUpperCase()}`;return` "${s}": { password: process.env["${l}"] || "", email: "${s}@${D}" }`}).join(`,
189
- `),u=`import { SmtpServer } from "./smtp-server.js";
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";
190
1026
  import * as fs from "fs";
191
- const domain = "${D}";
192
- const users = { ${zJ} };
1027
+ const domain = "${domain}";
1028
+ const users = { ${usersEntries} };
193
1029
  const certDir = \`/etc/letsencrypt/live/mail.\${domain}\`;
194
1030
  let tlsConfig = undefined;
195
1031
  if (fs.existsSync(\`\${certDir}/privkey.pem\`)) {
196
1032
  tlsConfig = { key: \`\${certDir}/privkey.pem\`, cert: \`\${certDir}/fullchain.pem\` };
197
1033
  }
198
- const server = new SmtpServer({ port: 587, tlsPort: 465, host: "0.0.0.0", domain, users, tls: tlsConfig, sentBucket: "${G}-production-email", sentPrefix: "sent/" });
1034
+ const server = new SmtpServer({ port: 587, tlsPort: 465, host: "0.0.0.0", domain, users, tls: tlsConfig, sentBucket: "${projectName}-production-email", sentPrefix: "sent/" });
199
1035
  await server.start();
200
- `;await L.putObject({bucket:g,key:"_deploy/smtp-server.js",body:t,contentType:"application/javascript"}),await L.putObject({bucket:g,key:"_deploy/smtp-entry.ts",body:Buffer.from(u),contentType:"application/typescript"});let e=$J.map((s)=>{let l=`MAIL_PASSWORD_${s.toUpperCase()}`;return`Environment=${l}=${T.env[l]||""}`}).join(`
201
- `),qJ=["mkdir -p /var/www/smtp",`aws s3 cp s3://${g}/_deploy/smtp-server.js /var/www/smtp/smtp-server.js`,`aws s3 cp s3://${g}/_deploy/smtp-entry.ts /var/www/smtp/smtp-entry.ts`,_J(p),"if ! command -v certbot &> /dev/null; then dnf install -y certbot || yum install -y certbot || apt-get install -y certbot || true; fi",`if [ ! -d /etc/letsencrypt/live/mail.${D} ]; then certbot certonly --standalone --non-interactive --agree-tos --email admin@${D} -d mail.${D} --http-01-port 80 || echo "Certbot failed"; fi`,`cat > /etc/systemd/system/stacks-smtp.service << 'SERVICEFILE'
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'
202
1051
  [Unit]
203
1052
  Description=Stacks SMTP Proxy Server
204
1053
  After=network.target
@@ -209,11 +1058,115 @@ ExecStart=/root/.bun/bin/bun smtp-entry.ts
209
1058
  Restart=on-failure
210
1059
  RestartSec=5
211
1060
  Environment=NODE_ENV=production
212
- Environment=AWS_REGION=${Q}
213
- ${e}
1061
+ Environment=AWS_REGION=${region}
1062
+ ${envLines}
214
1063
  [Install]
215
1064
  WantedBy=multi-user.target
216
- SERVICEFILE`,"systemctl daemon-reload","systemctl enable stacks-smtp","systemctl restart stacks-smtp"].join(" && "),FJ=(await j.request({service:"ssm",region:Q,method:"POST",path:"/",headers:{"Content-Type":"application/x-amz-json-1.1","X-Amz-Target":"AmazonSSM.SendCommand"},body:JSON.stringify({InstanceIds:[f],DocumentName:"AWS-RunShellScript",Parameters:{commands:[qJ]},TimeoutSeconds:180})}))?.Command?.CommandId;if(FJ){let s=0;while(s<60){await new Promise((l)=>setTimeout(l,3000));try{let l=await j.request({service:"ssm",region:Q,method:"POST",path:"/",headers:{"Content-Type":"application/x-amz-json-1.1","X-Amz-Target":"AmazonSSM.GetCommandInvocation"},body:JSON.stringify({CommandId:FJ,InstanceId:f})}),YJ=l?.Status;if(YJ==="Success"){K.succeed("SMTP proxy deployed to EC2");break}else if(YJ==="Failed"||YJ==="Cancelled"||YJ==="TimedOut"){K.fail(`SMTP proxy deploy failed: ${l?.StandardErrorContent||"Unknown"}`);break}}catch{}s++}if(s>=60)K.fail("SMTP proxy deploy timed out")}else K.fail("Failed to send SSM command for SMTP proxy");try{await L.deleteObject({bucket:g,key:"_deploy/smtp-server.js"}),await L.deleteObject({bucket:g,key:"_deploy/smtp-entry.ts"})}catch{}}else if(K.stop(),V)$.debug("SMTP server bundle not found, skipping")}}else if(K.stop(),V)$.debug("No EC2 instance or bucket found, skipping mail server deploy")}catch(w){if(K.fail(`Mail server deployment failed: ${w.message}`),V)$.debug(`Mail server deploy error: ${w.stack}`)}}}let N=S("Deploying frontend to S3...");N.start();try{let{S3Client:z,CloudFormationClient:q}=await import("@stacksjs/ts-cloud"),{readFileSync:A}=await import("fs"),{buildFrontendStatic:W}=await import("../build/frontend-static.js"),K=new z(Q),w=new q(Q),I=`${G}-cloud`,Y=await w.getStackOutputs(I),F=Y.FrontendBucketName;if(!F)N.stop(),console.log("\u26A0 Frontend bucket not found in stack outputs (add storage.public to infrastructure config)");else{let x=await W();await(async(L,j="")=>{await HJ(L,async(P,C)=>{let f=A(P),g=WJ(P),v=C==="index.html"||C.endsWith("/index.html")?"no-cache, no-store, must-revalidate":"public, max-age=31536000, immutable";if(await a(()=>K.putObject({bucket:F,key:C,body:f,contentType:g,cacheControl:v}),`upload ${C}`),V)$.debug(` Uploaded: ${C}`)})})(x);let M=X.projectPath("bootstrap");if(JJ(M)){let L=A(M);if(await a(()=>K.putObject({bucket:F,key:"install",body:L,contentType:"text/plain",cacheControl:"no-cache, no-store, must-revalidate"}),"upload bootstrap install script"),V)$.debug(" Uploaded bootstrap install script")}let O=Y.publicCloudFrontDistributionId||Y.CloudFrontDistributionId;if(O){if(V)$.debug(" Invalidating CloudFront cache...");let{AWSClient:L}=await import("@stacksjs/ts-cloud");await new L().request({service:"cloudfront",region:"us-east-1",method:"POST",path:`/2020-05-31/distribution/${O}/invalidation`,headers:{"Content-Type":"application/xml"},body:`<?xml version="1.0" encoding="UTF-8"?>
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"?>
217
1170
  <InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
218
1171
  <CallerReference>${Date.now()}</CallerReference>
219
1172
  <Paths>
@@ -222,27 +1175,131 @@ SERVICEFILE`,"systemctl daemon-reload","systemctl enable stacks-smtp","systemctl
222
1175
  <Path>/*</Path>
223
1176
  </Items>
224
1177
  </Paths>
225
- </InvalidationBatch>`})}N.succeed(`Frontend deployed to S3 (${F})`)}}catch(z){if(N.fail(`Frontend deployment failed: ${z.message}`),V)$.debug(`Frontend error: ${z.stack}`)}let QJ=X.projectPath("dist/docs/.bunpress"),{existsSync:UJ}=await import("fs");if(UJ(QJ)){let z=S("Deploying documentation to S3...");z.start();try{let{S3Client:q,CloudFormationClient:A}=await import("@stacksjs/ts-cloud"),{readFileSync:W}=await import("fs"),K=new q(Q),w=new A(Q),I=`${G}-cloud`,Y=await w.getStackOutputs(I),F=Y.DocsBucketName;if(!F)z.stop(),console.log("\u26A0 Docs bucket not found in stack outputs (infrastructure may not be updated yet)");else{await(async(M,O="")=>{await HJ(M,async(L,j)=>{let P=W(L),C=WJ(L),f=j.endsWith(".html")?"no-cache, no-store, must-revalidate":"public, max-age=31536000, immutable";if(await a(()=>K.putObject({bucket:F,key:j,body:P,contentType:C,cacheControl:f}),`upload docs ${j}`),V)$.debug(` Uploaded docs: ${j}`)},"",[".DS_Store",".bunpress"])})(QJ);let E=Y.docsCloudFrontDistributionId||Y.publicCloudFrontDistributionId||Y.CloudFrontDistributionId;if(E){if(V)$.debug(" Invalidating CloudFront cache for docs...");let{AWSClient:M}=await import("@stacksjs/ts-cloud"),O=new M,L=Y.docsCloudFrontDistributionId?["/*"]:["/docs","/docs/*"];await O.request({service:"cloudfront",region:"us-east-1",method:"POST",path:`/2020-05-31/distribution/${E}/invalidation`,headers:{"Content-Type":"application/xml"},body:`<?xml version="1.0" encoding="UTF-8"?>
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"?>
226
1220
  <InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
227
1221
  <CallerReference>docs-${Date.now()}</CallerReference>
228
1222
  <Paths>
229
- <Quantity>${L.length}</Quantity>
1223
+ <Quantity>${docsInvalidationPaths.length}</Quantity>
230
1224
  <Items>
231
- ${L.map((j)=>`<Path>${j}</Path>`).join(`
1225
+ ${docsInvalidationPaths.map((path) => `<Path>${path}</Path>`).join(`
232
1226
  `)}
233
1227
  </Items>
234
1228
  </Paths>
235
- </InvalidationBatch>`})}z.succeed(`Documentation deployed to S3 (${F})`)}}catch(q){if(z.fail(`Documentation deployment failed: ${q.message}`),V)$.debug(`Docs deploy error: ${q.stack}`)}}else if(V)$.debug("No docs build found at dist/docs, skipping docs deployment");let o=X.projectPath("dist/blog"),{existsSync:r}=await import("fs");if(r(o)){let z=S("Deploying blog to S3...");z.start();try{let{S3Client:q,CloudFormationClient:A}=await import("@stacksjs/ts-cloud"),{readFileSync:W}=await import("fs"),K=new q(Q),w=new A(Q),I=`${G}-cloud`,Y=await w.getStackOutputs(I),F=Y.BlogBucketName;if(!F)z.stop(),console.log("\u26A0 Blog bucket not found in stack outputs");else{await HJ(o,async(E,M)=>{let O=W(E),L=WJ(E),j=M.endsWith(".html")?"no-cache, no-store, must-revalidate":"public, max-age=31536000, immutable";if(await a(()=>K.putObject({bucket:F,key:M,body:O,contentType:L,cacheControl:j}),`upload blog ${M}`),V)$.debug(` Uploaded blog: ${M}`)},"",[".DS_Store"]);let x=Y.blogCloudFrontDistributionId||Y.publicCloudFrontDistributionId||Y.CloudFrontDistributionId;if(x){if(V)$.debug(" Invalidating CloudFront cache for blog...");let{AWSClient:E}=await import("@stacksjs/ts-cloud"),M=new E,O=Y.blogCloudFrontDistributionId?["/*"]:["/blog","/blog/*"];await M.request({service:"cloudfront",region:"us-east-1",method:"POST",path:`/2020-05-31/distribution/${x}/invalidation`,headers:{"Content-Type":"application/xml"},body:`<?xml version="1.0" encoding="UTF-8"?>
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"?>
236
1271
  <InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
237
1272
  <CallerReference>blog-${Date.now()}</CallerReference>
238
1273
  <Paths>
239
- <Quantity>${O.length}</Quantity>
1274
+ <Quantity>${blogInvalidationPaths.length}</Quantity>
240
1275
  <Items>
241
- ${O.map((L)=>`<Path>${L}</Path>`).join(`
1276
+ ${blogInvalidationPaths.map((path) => `<Path>${path}</Path>`).join(`
242
1277
  `)}
243
1278
  </Items>
244
1279
  </Paths>
245
- </InvalidationBatch>`})}z.succeed(`Blog deployed to S3 (${F})`)}}catch(q){if(z.fail(`Blog deployment failed: ${q.message}`),V)$.debug(`Blog deploy error: ${q.stack}`)}}else if(V)$.debug("No blog build found at dist/blog, skipping blog deployment");let n=S("Configuring 404 error page...");n.start();try{let{S3Client:z,CloudFormationClient:q,AWSClient:A}=await import("@stacksjs/ts-cloud"),{existsSync:W,readFileSync:K}=await import("fs"),w=new z(Q),I=new q(Q),Y=new A,F=`${G}-cloud`,x=await I.getStackOutputs(F),E=x.FrontendBucketName,M=x.DocsBucketName,O=x.publicCloudFrontDistributionId||x.CloudFrontDistributionId,L=X.projectPath("dist/docs/.bunpress/404.html"),j;if(W(L)){if(j=K(L,"utf-8"),V)$.debug(" Using BunPress-generated 404.html")}else if(j=`<!DOCTYPE html>
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>
246
1303
  <html lang="en">
247
1304
  <head>
248
1305
  <meta charset="UTF-8">
@@ -289,12 +1346,229 @@ SERVICEFILE`,"systemctl daemon-reload","systemctl enable stacks-smtp","systemctl
289
1346
  <div class="action"><a class="link" href="/docs/">Take me home</a></div>
290
1347
  </div>
291
1348
  </body>
292
- </html>`,V)$.debug(" Using default 404.html template");if(E){if(await a(()=>w.putObject({bucket:E,key:"404.html",body:j,contentType:"text/html",cacheControl:"no-cache, no-store, must-revalidate"}),"upload 404.html to frontend bucket"),V)$.debug(` Uploaded 404.html to frontend bucket: ${E}`)}if(M){if(await a(()=>w.putObject({bucket:M,key:"404.html",body:j,contentType:"text/html",cacheControl:"no-cache, no-store, must-revalidate"}),"upload 404.html to docs bucket"),V)$.debug(` Uploaded 404.html to docs bucket: ${M}`)}if(O){if(V)$.debug(" Configuring CloudFront custom error responses...");let P=await Y.request({service:"cloudfront",region:"us-east-1",method:"GET",path:`/2020-05-31/distribution/${O}/config`,returnHeaders:!0}),C=P.headers?.etag||P.headers?.ETag||"",f=P.body?.DistributionConfig||P.DistributionConfig;if(f&&C){if(!((f.CustomErrorResponses?.Quantity||0)>0&&f.CustomErrorResponses?.Items?.CustomErrorResponse?.some?.((m)=>m.ErrorCode===404&&m.ResponsePagePath==="/404.html"))){f.CustomErrorResponses={Quantity:2,Items:{CustomErrorResponse:[{ErrorCode:403,ResponsePagePath:"/404.html",ResponseCode:404,ErrorCachingMinTTL:10},{ErrorCode:404,ResponsePagePath:"/404.html",ResponseCode:404,ErrorCachingMinTTL:10}]}};let m=(D,h,R="")=>{if(h===null||h===void 0)return"";if(typeof h==="boolean")return`${R}<${D}>${h}</${D}>
293
- `;if(typeof h==="number"||typeof h==="string")return`${R}<${D}>${h}</${D}>
294
- `;if(Array.isArray(h))return h.map((B)=>m(D,B,R)).join("");if(typeof h==="object"){if(D.startsWith("@_")||D==="?xml")return"";let B="";for(let[d,t]of Object.entries(h))if(!d.startsWith("@_")&&d!=="#text")B+=m(d,t,`${R} `);if(h["#text"]!==void 0)return`${R}<${D}>${h["#text"]}</${D}>
295
- `;return`${R}<${D}>
296
- ${B}${R}</${D}>
297
- `}return""},p=`<?xml version="1.0" encoding="UTF-8"?>
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"?>
298
1433
  <DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
299
- ${Object.entries(f).filter(([D])=>!D.startsWith("@_")).map(([D,h])=>m(D,h," ")).join("")}</DistributionConfig>
300
- `;if(await Y.request({service:"cloudfront",region:"us-east-1",method:"PUT",path:`/2020-05-31/distribution/${O}/config`,body:p,headers:{"Content-Type":"application/xml","If-Match":C}}),V)$.debug(" CloudFront custom error responses configured")}else if(V)$.debug(" CloudFront custom error responses already configured")}}n.succeed("404 error page configured")}catch(z){if(n.stop(),console.log(`\u26A0 404 page configuration skipped: ${z.message}`),V)$.debug(`Error page config error: ${z.stack}`)}try{let z=await import(X.projectPath("config/cloud")),q=z?.tsCloud?.infrastructure?.tunnel;if(q?.enabled){let A=q.domain||"";if(!A||A==="localtunnel.dev"||A==="api.localtunnel.dev")console.log("\u2139 Tunnel: Using shared localtunnel.dev (no custom tunnel deployment needed)");else{let W=S(`Deploying tunnel server to ${A}...`);W.start();try{let{deployTunnelServer:K}=await import("@stacksjs/tunnel");await K({domain:A,region:q.region||z?.tsCloud?.project?.region||"us-east-1",instanceType:q.instanceType,prefix:q.prefix,enableSsl:q.ssl?.enabled,porkbunApiKey:q.ssl?.porkbunApiKey,porkbunSecretKey:q.ssl?.porkbunSecretKey,verbose:V}),W.succeed(`Tunnel server deployed to ${A}`)}catch(K){if(W.stop(),console.log(`\u26A0 Tunnel deployment skipped: ${K.message}`),V)$.debug(`Tunnel deploy error: ${K.stack}`)}}}}catch{}let VJ=S("Reconciling DNS records...");VJ.start();try{let{Route53Client:z,CloudFormationClient:q}=await import("@stacksjs/ts-cloud"),A=new q(Q),W=`${G}-cloud`,K=await A.getStackOutputs(W),w=await import(X.projectPath("config/cloud")),I=w?.tsCloud?.infrastructure?.dns?.hostedZoneId,Y=w?.tsCloud?.infrastructure?.dns?.domain||"stacksjs.com";if(I){let F=new z(Q),x="Z2FDTNDATAQYW2",E=[{domain:Y,cfDomain:K.publicCloudFrontDomain},{domain:`www.${Y}`,cfDomain:K.publicCloudFrontDomain},{domain:`docs.${Y}`,cfDomain:K.docsCloudFrontDomain},{domain:`blog.${Y}`,cfDomain:K.blogCloudFrontDomain}],M=0;for(let{domain:O,cfDomain:L}of E){if(!L)continue;try{if(!((await F.listResourceRecordSets({HostedZoneId:I,StartRecordName:O,StartRecordType:"A",MaxItems:"1"}))?.ResourceRecordSets||[]).some((f)=>f.Name===`${O}.`&&f.Type==="A")){if(await F.changeResourceRecordSets({HostedZoneId:I,ChangeBatch:{Changes:[{Action:"UPSERT",ResourceRecordSet:{Name:O,Type:"A",AliasTarget:{HostedZoneId:"Z2FDTNDATAQYW2",DNSName:L,EvaluateTargetHealth:!1}}},{Action:"UPSERT",ResourceRecordSet:{Name:O,Type:"AAAA",AliasTarget:{HostedZoneId:"Z2FDTNDATAQYW2",DNSName:L,EvaluateTargetHealth:!1}}}]}}),M++,V)$.debug(` Created DNS records: ${O} -> ${L}`)}else if(V)$.debug(` DNS record exists: ${O}`)}catch(j){if(V)$.debug(` DNS check/create failed for ${O}: ${j.message}`)}}if(M>0)VJ.succeed(`DNS records reconciled (${M} created)`);else VJ.succeed("DNS records verified")}else if(VJ.stop(),V)$.debug("No hosted zone ID configured, skipping DNS reconciliation")}catch(z){if(VJ.stop(),console.log(`\u26A0 DNS reconciliation skipped: ${z.message}`),V)$.debug(`DNS error: ${z.stack}`)}console.log(""),$.success("Deployment completed successfully!")}catch(J){$.error("Deployment failed:",J),T.exit(EJ.FatalError)}
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
+ }