@stacksjs/ts-cloud 0.16.50 → 0.16.52

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 (104) hide show
  1. package/dist/bin/{chunk-7jj0g6jk.js → chunk-0e9yddgc.js} +4 -4
  2. package/dist/bin/{chunk-cw3rhg1c.js → chunk-20dp8658.js} +1 -1
  3. package/dist/bin/{chunk-k9f94hg4.js → chunk-45thdtvf.js} +165 -165
  4. package/dist/bin/{chunk-7cs9nnjy.js → chunk-5c4xh61a.js} +1 -1
  5. package/dist/bin/chunk-67vf8myy.js +1 -0
  6. package/dist/bin/{chunk-e6ewfmbp.js → chunk-83brtkvn.js} +1 -1
  7. package/dist/bin/chunk-87g6jcfm.js +1 -0
  8. package/dist/bin/{chunk-11y7yang.js → chunk-93yw46n7.js} +1 -1
  9. package/dist/bin/{chunk-qsfhb3x7.js → chunk-9qdrv6zp.js} +1 -1
  10. package/dist/bin/{chunk-j2wkxp7d.js → chunk-a7hhzbvm.js} +1 -1
  11. package/dist/bin/{chunk-hdqxyxs0.js → chunk-cjagqdk3.js} +5 -5
  12. package/dist/bin/{chunk-f2t67d76.js → chunk-ddyd6f14.js} +1 -1
  13. package/dist/bin/{chunk-y12vn1h1.js → chunk-f0vedy45.js} +1 -1
  14. package/dist/bin/{chunk-w89fyres.js → chunk-f578z20t.js} +1 -1
  15. package/dist/bin/{chunk-e7pce8jb.js → chunk-f78wmcvb.js} +1 -1
  16. package/dist/bin/{chunk-vavwhhrp.js → chunk-fhe0mdjg.js} +1 -1
  17. package/dist/bin/chunk-g6ph425d.js +1 -0
  18. package/dist/bin/chunk-k6g1c681.js +1 -0
  19. package/dist/bin/{chunk-9ehb4hc6.js → chunk-n5kdne4p.js} +1 -1
  20. package/dist/bin/{chunk-tcj1z34y.js → chunk-ngmewar1.js} +1 -1
  21. package/dist/bin/{chunk-mdkd46c5.js → chunk-nxvtya6v.js} +23 -23
  22. package/dist/bin/{chunk-xaqkj8fn.js → chunk-p6wxv8yq.js} +1 -1
  23. package/dist/bin/{chunk-f4mpkpbf.js → chunk-vpt43nwa.js} +1 -1
  24. package/dist/bin/{chunk-kmpvqby6.js → chunk-wv8060cw.js} +1 -1
  25. package/dist/bin/{chunk-r524t8v8.js → chunk-zywk12mp.js} +1 -1
  26. package/dist/bin/cli.js +16 -16
  27. package/dist/bin/dashboard-server.js +1 -1
  28. package/dist/chunk-2gqm2h84.js +1 -0
  29. package/dist/{chunk-zbx9yehq.js → chunk-6gnmrtp9.js} +1 -1
  30. package/dist/{chunk-85fyadq0.js → chunk-ggtsfsxx.js} +17 -17
  31. package/dist/{chunk-a8hs75xd.js → chunk-p101nhg2.js} +1 -1
  32. package/dist/{chunk-18h7na1g.js → chunk-prtvb2fh.js} +27 -27
  33. package/dist/{chunk-cpba1hz2.js → chunk-pz33w7gv.js} +1 -1
  34. package/dist/{chunk-nnnhq6sx.js → chunk-qcv4twc6.js} +1 -1
  35. package/dist/{chunk-5hgcr7r2.js → chunk-rjaa4epz.js} +2 -2
  36. package/dist/{chunk-nmnf1ha6.js → chunk-x4qye2rc.js} +1 -1
  37. package/dist/{chunk-fqgvackc.js → chunk-xgc6hskd.js} +1 -1
  38. package/dist/deploy/index.js +1 -1
  39. package/dist/drivers/index.js +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/mail/index.js +2 -2
  42. package/dist/operations/index.js +1 -1
  43. package/dist/protection/index.js +1 -1
  44. package/dist/spend/index.js +1 -1
  45. package/dist/ui/404.html +1 -1
  46. package/dist/ui/accept-invitation.html +1 -1
  47. package/dist/ui/access-denied.html +1 -1
  48. package/dist/ui/account/automation.html +5 -5
  49. package/dist/ui/account/security.html +2 -2
  50. package/dist/ui/applications/compose.html +6 -6
  51. package/dist/ui/applications/new.html +2 -2
  52. package/dist/ui/data/backups.html +5 -5
  53. package/dist/ui/data/services.html +6 -6
  54. package/dist/ui/data/volumes.html +6 -6
  55. package/dist/ui/index.html +6 -6
  56. package/dist/ui/infrastructure/topology.html +5 -5
  57. package/dist/ui/integrations.html +2 -2
  58. package/dist/ui/operations/alerts.html +6 -6
  59. package/dist/ui/operations/configuration.html +6 -6
  60. package/dist/ui/operations/jobs.html +6 -6
  61. package/dist/ui/operations/maintenance.html +5 -5
  62. package/dist/ui/operations/observability.html +6 -6
  63. package/dist/ui/operations/previews.html +6 -6
  64. package/dist/ui/operations/queue.html +6 -6
  65. package/dist/ui/operations/regions.html +6 -6
  66. package/dist/ui/operations/releases.html +6 -6
  67. package/dist/ui/operations/spend.html +6 -6
  68. package/dist/ui/operations/workloads.html +6 -6
  69. package/dist/ui/security.html +2 -2
  70. package/dist/ui/server/actions.html +6 -6
  71. package/dist/ui/server/activity.html +2 -2
  72. package/dist/ui/server/capacity.html +6 -6
  73. package/dist/ui/server/database.html +6 -6
  74. package/dist/ui/server/deployments.html +6 -6
  75. package/dist/ui/server/diagnostics.html +2 -2
  76. package/dist/ui/server/firewall.html +6 -6
  77. package/dist/ui/server/fleet.html +6 -6
  78. package/dist/ui/server/logs.html +6 -6
  79. package/dist/ui/server/metrics.html +6 -6
  80. package/dist/ui/server/security.html +2 -2
  81. package/dist/ui/server/services.html +1 -1
  82. package/dist/ui/server/sites.html +6 -6
  83. package/dist/ui/server/ssh-keys.html +6 -6
  84. package/dist/ui/server/team.html +6 -6
  85. package/dist/ui/server/terminal.html +2 -2
  86. package/dist/ui/serverless/alarms.html +6 -6
  87. package/dist/ui/serverless/assets.html +2 -2
  88. package/dist/ui/serverless/cost.html +2 -2
  89. package/dist/ui/serverless/data.html +6 -6
  90. package/dist/ui/serverless/deployments.html +2 -2
  91. package/dist/ui/serverless/firewall.html +1 -1
  92. package/dist/ui/serverless/functions.html +6 -6
  93. package/dist/ui/serverless/logs.html +6 -6
  94. package/dist/ui/serverless/metrics.html +2 -2
  95. package/dist/ui/serverless/queues.html +6 -6
  96. package/dist/ui/serverless/secrets.html +6 -6
  97. package/dist/ui/serverless/traces.html +5 -5
  98. package/dist/ui/serverless.html +5 -5
  99. package/package.json +3 -3
  100. package/dist/bin/chunk-512ecg6y.js +0 -1
  101. package/dist/bin/chunk-7ewbxsan.js +0 -1
  102. package/dist/bin/chunk-q9awn1n3.js +0 -1
  103. package/dist/bin/chunk-v2qk9jeb.js +0 -1
  104. package/dist/chunk-r4kedmas.js +0 -1
@@ -1,10 +1,10 @@
1
- import{p}from"./chunk-z06jkvvr.js";import{ca,A,w,ne,Ur,da,Vo,le,B,T,Jt,Fr}from"./chunk-k9f94hg4.js";import{v}from"./chunk-hhmqsrfc.js";import{Q}from"./chunk-te52z1wk.js";import{g}from"./chunk-dv2he3vc.js";import{J,Ri,Gs,I,U,Dr,Ct,rt,_o,Ii,$o,Tt,Gc,Lo,Mo,ki,Uo,Ht,Xs,Wc,Ni,Qs,Ys,Zs,ea,Fo,fn,ta,hn,ra,Bo}from"./chunk-hdqxyxs0.js";import{Io,Po,$s}from"./chunk-9ehb4hc6.js";import{fs}from"./chunk-m3xap663.js";import{sn}from"./chunk-ytzcsskk.js";import{Er}from"./chunk-mcg4wg4j.js";class We{client;region;constructor(e="us-east-1",t){this.region=e,this.client=new p}async putParameter(e){let t={Name:e.Name,Value:e.Value};if(e.Type)t.Type=e.Type;if(e.Description)t.Description=e.Description;if(e.KeyId)t.KeyId=e.KeyId;if(e.Overwrite!==void 0)t.Overwrite=e.Overwrite;if(e.AllowedPattern)t.AllowedPattern=e.AllowedPattern;if(e.Tags&&e.Tags.length>0)t.Tags=e.Tags;if(e.Tier)t.Tier=e.Tier;if(e.DataType)t.DataType=e.DataType;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.PutParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Version:r.Version,Tier:r.Tier}}async getParameter(e){let t={Name:e.Name};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameter:r.Parameter?this.parseParameter(r.Parameter):void 0}}async getParameters(e){let t={Names:e.Names};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),InvalidParameters:r.InvalidParameters}}async getParametersByPath(e){let t={Path:e.Path};if(e.Recursive!==void 0)t.Recursive=e.Recursive;if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;if(e.MaxResults)t.MaxResults=e.MaxResults;if(e.NextToken)t.NextToken=e.NextToken;if(e.ParameterFilters&&e.ParameterFilters.length>0)t.ParameterFilters=e.ParameterFilters;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParametersByPath","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),NextToken:r.NextToken}}async deleteParameter(e){let t={Name:e.Name};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DeleteParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async deleteParameters(e){let t={Names:e},r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DeleteParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{DeletedParameters:r.DeletedParameters,InvalidParameters:r.InvalidParameters}}async describeParameters(e){let t={};if(e?.Filters&&e.Filters.length>0)t.Filters=e.Filters;if(e?.ParameterFilters&&e.ParameterFilters.length>0)t.ParameterFilters=e.ParameterFilters;if(e?.MaxResults)t.MaxResults=e.MaxResults;if(e?.NextToken)t.NextToken=e.NextToken;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DescribeParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),NextToken:r.NextToken}}async getParameterHistory(e){let t={Name:e.Name};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;if(e.MaxResults)t.MaxResults=e.MaxResults;if(e.NextToken)t.NextToken=e.NextToken;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameterHistory","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>({Name:n.Name,Type:n.Type,KeyId:n.KeyId,LastModifiedDate:n.LastModifiedDate,LastModifiedUser:n.LastModifiedUser,Description:n.Description,Value:n.Value,Version:n.Version,Labels:n.Labels,Tier:n.Tier})),NextToken:r.NextToken}}async labelParameterVersion(e){let t={Name:e.Name,Labels:e.Labels};if(e.ParameterVersion!==void 0)t.ParameterVersion=e.ParameterVersion;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.LabelParameterVersion","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{InvalidLabels:r.InvalidLabels,ParameterVersion:r.ParameterVersion}}async addTagsToResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId,Tags:e.Tags};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.AddTagsToResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async removeTagsFromResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId,TagKeys:e.TagKeys};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.RemoveTagsFromResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async listTagsForResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId};return{TagList:(await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.ListTagsForResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})).TagList}}async setString(e,t,r){return this.putParameter({Name:e,Value:t,Type:"String",Description:r?.description,Overwrite:r?.overwrite??!0,Tags:r?.tags})}async setSecureString(e,t,r){return this.putParameter({Name:e,Value:t,Type:"SecureString",Description:r?.description,Overwrite:r?.overwrite??!0,KeyId:r?.kmsKeyId,Tags:r?.tags})}async getValue(e){return(await this.getParameter({Name:e,WithDecryption:!0})).Parameter?.Value}async getAllByPath(e,t=!0){let r=[],n;do{let s=await this.getParametersByPath({Path:e,Recursive:t,WithDecryption:!0,NextToken:n});if(s.Parameters)r.push(...s.Parameters);n=s.NextToken}while(n);return r}parseParameter(e){return{Name:e.Name,Type:e.Type,Value:e.Value,Version:e.Version,LastModifiedDate:e.LastModifiedDate,ARN:e.ARN,DataType:e.DataType,Description:e.Description,AllowedPattern:e.AllowedPattern,KeyId:e.KeyId,Tier:e.Tier}}async sendCommand(e){if(!e.InstanceIds&&!e.Targets)throw Error("SendCommand requires either InstanceIds or Targets");let t={DocumentName:e.DocumentName};if(e.InstanceIds)t.InstanceIds=e.InstanceIds;if(e.Targets)t.Targets=e.Targets;if(e.Parameters)t.Parameters=e.Parameters;if(e.TimeoutSeconds)t.TimeoutSeconds=e.TimeoutSeconds;if(e.Comment)t.Comment=e.Comment;if(e.OutputS3BucketName)t.OutputS3BucketName=e.OutputS3BucketName;if(e.OutputS3KeyPrefix)t.OutputS3KeyPrefix=e.OutputS3KeyPrefix;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.SendCommand","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{CommandId:r.Command?.CommandId,Status:r.Command?.Status,StatusDetails:r.Command?.StatusDetails}}async getCommandInvocation(e){let t={CommandId:e.CommandId,InstanceId:e.InstanceId},r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetCommandInvocation","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Status:r.Status,StatusDetails:r.StatusDetails,StandardOutputContent:r.StandardOutputContent,StandardErrorContent:r.StandardErrorContent,ResponseCode:r.ResponseCode}}async runShellCommand(e,t,r){let n=await this.sendCommand({InstanceIds:[e],DocumentName:"AWS-RunShellScript",Parameters:{commands:t},TimeoutSeconds:r?.timeoutSeconds||600});if(!n.CommandId)return{success:!1,error:"Failed to send command"};if(r?.waitForCompletion===!1)return{success:!0,status:"Pending"};let s=r?.pollIntervalMs||2000,i=r?.maxWaitMs||300000,o=Date.now();while(Date.now()-o<i){await new Promise((a)=>setTimeout(a,s));try{let a=await this.getCommandInvocation({CommandId:n.CommandId,InstanceId:e});if(a.Status==="Success")return{success:!0,output:a.StandardOutputContent,error:a.StandardErrorContent,status:a.Status};if(a.Status==="Failed"||a.Status==="Cancelled"||a.Status==="TimedOut")return{success:!1,output:a.StandardOutputContent,error:a.StandardErrorContent||a.StatusDetails,status:a.Status}}catch(a){if(!a.message?.includes("InvocationDoesNotExist"))return{success:!1,error:a.message}}}return{success:!1,error:"Command timed out waiting for completion"}}async listCommandInvocations(e){let t={CommandId:e.CommandId};if(e.Details)t.Details=!0;return((await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.ListCommandInvocations","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})).CommandInvocations||[]).map((s)=>({InstanceId:s.InstanceId,Status:s.Status,StatusDetails:s.StatusDetails,StandardOutputContent:s.CommandPlugins?.[0]?.Output,StandardErrorContent:s.CommandPlugins?.[0]?.StandardErrorUrl}))}async sendCommandByTags(e){let t=Object.entries(e.tags).map(([c,l])=>({Key:`tag:${c}`,Values:[l]})),r=await this.sendCommand({Targets:t,DocumentName:"AWS-RunShellScript",Parameters:{commands:e.commands},TimeoutSeconds:e.timeoutSeconds||600,Comment:e.comment});if(!r.CommandId)return{success:!1,instanceCount:0,perInstance:[],error:"Failed to send command"};let n=e.pollIntervalMs||3000,s=e.maxWaitMs||600000,i=Date.now(),o=new Set(["Success","Failed","Cancelled","TimedOut"]),a=[];while(Date.now()-i<s){await new Promise((c)=>setTimeout(c,n));try{if(a=await this.listCommandInvocations({CommandId:r.CommandId,Details:!0}),a.length===0)continue;if(a.every((l)=>o.has(l.Status||""))){let l=a.map((d)=>({instanceId:d.InstanceId,status:d.Status||"Unknown",output:d.StandardOutputContent,error:d.StandardErrorContent||d.StatusDetails})),u=a.every((d)=>d.Status==="Success");return{success:u,instanceCount:a.length,perInstance:l,error:u?void 0:"One or more instances reported a non-success status"}}}catch(c){if(!c.message?.includes("InvocationDoesNotExist"))return{success:!1,instanceCount:0,perInstance:[],error:c.message}}}return{success:!1,instanceCount:a.length,perInstance:a.map((c)=>({instanceId:c.InstanceId,status:c.Status||"Unknown"})),error:"Command timed out waiting for all instances to complete"}}}function os(e={}){let t=new Set;for(let r of Object.values(e)){if(!r.domain)continue;if(!(r.redirect||r.deploy==="server"||r.start))continue;t.add(r.domain);for(let n of r.aliases??[]){let s=String(n).trim().toLowerCase();if(s)t.add(s)}}return t}function sr(e){return e.replace(/\.$/,"").toLowerCase()}function Ao(e,t,r){let n=sr(e.name),s=sr(t),i=sr(r),o=i===s?"@":i.endsWith(`.${s}`)?i.slice(0,-(s.length+1)):i;return n===i||n===o||o==="@"&&(n===""||n===s)}async function No(e,t,r,n,s="A"){let i=await e.listRecords(t);if(!i.success)return[`could not list ${s} records: ${i.message||"unknown provider error"}`];let o=i.records.filter((l)=>l.type===s&&Ao(l,t,r)),a=o.findIndex((l)=>l.content===n);if(o.length<=1||a===-1)return[];let c=[];for(let[l,u]of o.entries()){if(l===a)continue;let d=await e.deleteRecord(t,u);if(!d.success)c.push(`could not remove stale ${u.name} ${s} ${u.content}: ${d.message||"unknown provider error"}`)}return c}function me(e){if(!e)return;let t=e.trim();if(!t)return;if(!t.includes("/"))return t;let[r]=t.split("/");if(!r.endsWith("::"))return r||void 0;return`${r}1`}var Ho=new Set(["mail","smtp","imap","mx"]);function Ji(e){return!Ho.has(e.split(".")[0]??"")}async function di(e){let{provider:t,zone:r,fqdn:n,ipv4:s,ipv6:i,ttl:o=600}=e,a={published:[],warnings:[]},c=[{type:"A",content:s}];if(i&&Ji(n))c.push({type:"AAAA",content:i});for(let{type:l,content:u}of c){let d=await t.upsertRecord(r,{name:n,type:l,content:u,ttl:o});if(d?.success===!1){a.warnings.push(`${n} → ${u} failed: ${d.error||d.message||"unknown error"}`);continue}if(a.warnings.push(...(await No(t,r,n,u,l)).map((m)=>`${n} cleanup: ${m}`)),await Xi(t,r,n,u,l))a.published.push({fqdn:n,type:l,content:u});else a.warnings.push(`${n} → ${u} reported success at ${t.name} but no matching ${l} record exists — create it manually: ${l} ${n} → ${u}`)}return a}async function Xi(e,t,r,n,s){try{if(typeof e?.listRecords!=="function")return!0;let i=await e.listRecords(t);if(!i?.success||!Array.isArray(i.records))return!0;return i.records.some((o)=>{let a=typeof o?.name==="string"?o.name.replace(/\.$/,""):"";return o?.type===s&&a===r&&o?.content===n})}catch{return!0}}import{readFileSync as is}from"node:fs";import{join as Na}from"node:path";function ir(e){return(Buffer.isBuffer(e)?e.toString("utf8"):typeof e==="string"?e:"").replace(/(^|\n)(\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*=).*$/gm,"$1$2[redacted]").replace(/encrypted:[A-Za-z0-9+/=]+/g,"encrypted:[redacted]").trim().slice(-8000)}function ot(e,t){let r=ir(e?.stderr),n=/line (\d+): syntax error/.exec(r);if(!n)return"";let s=Number(n[1]),i=t.split(`
1
+ import{p}from"./chunk-z06jkvvr.js";import{ca,A,w,ne,Ur,da,Vo,le,B,T,Jt,Fr}from"./chunk-45thdtvf.js";import{v}from"./chunk-hhmqsrfc.js";import{Q}from"./chunk-te52z1wk.js";import{g}from"./chunk-dv2he3vc.js";import{J,Ri,Gs,I,U,Dr,Ct,rt,_o,Ii,$o,Tt,Gc,Lo,Mo,ki,Uo,Ht,Xs,Wc,Ni,Qs,Ys,Zs,ea,Fo,fn,ta,hn,ra,Bo}from"./chunk-cjagqdk3.js";import{Io,Po,$s}from"./chunk-n5kdne4p.js";import{fs}from"./chunk-m3xap663.js";import{sn}from"./chunk-ytzcsskk.js";import{Er}from"./chunk-mcg4wg4j.js";class We{client;region;constructor(e="us-east-1",t){this.region=e,this.client=new p}async putParameter(e){let t={Name:e.Name,Value:e.Value};if(e.Type)t.Type=e.Type;if(e.Description)t.Description=e.Description;if(e.KeyId)t.KeyId=e.KeyId;if(e.Overwrite!==void 0)t.Overwrite=e.Overwrite;if(e.AllowedPattern)t.AllowedPattern=e.AllowedPattern;if(e.Tags&&e.Tags.length>0)t.Tags=e.Tags;if(e.Tier)t.Tier=e.Tier;if(e.DataType)t.DataType=e.DataType;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.PutParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Version:r.Version,Tier:r.Tier}}async getParameter(e){let t={Name:e.Name};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameter:r.Parameter?this.parseParameter(r.Parameter):void 0}}async getParameters(e){let t={Names:e.Names};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),InvalidParameters:r.InvalidParameters}}async getParametersByPath(e){let t={Path:e.Path};if(e.Recursive!==void 0)t.Recursive=e.Recursive;if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;if(e.MaxResults)t.MaxResults=e.MaxResults;if(e.NextToken)t.NextToken=e.NextToken;if(e.ParameterFilters&&e.ParameterFilters.length>0)t.ParameterFilters=e.ParameterFilters;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParametersByPath","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),NextToken:r.NextToken}}async deleteParameter(e){let t={Name:e.Name};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DeleteParameter","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async deleteParameters(e){let t={Names:e},r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DeleteParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{DeletedParameters:r.DeletedParameters,InvalidParameters:r.InvalidParameters}}async describeParameters(e){let t={};if(e?.Filters&&e.Filters.length>0)t.Filters=e.Filters;if(e?.ParameterFilters&&e.ParameterFilters.length>0)t.ParameterFilters=e.ParameterFilters;if(e?.MaxResults)t.MaxResults=e.MaxResults;if(e?.NextToken)t.NextToken=e.NextToken;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.DescribeParameters","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>this.parseParameter(n)),NextToken:r.NextToken}}async getParameterHistory(e){let t={Name:e.Name};if(e.WithDecryption!==void 0)t.WithDecryption=e.WithDecryption;if(e.MaxResults)t.MaxResults=e.MaxResults;if(e.NextToken)t.NextToken=e.NextToken;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetParameterHistory","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Parameters:r.Parameters?.map((n)=>({Name:n.Name,Type:n.Type,KeyId:n.KeyId,LastModifiedDate:n.LastModifiedDate,LastModifiedUser:n.LastModifiedUser,Description:n.Description,Value:n.Value,Version:n.Version,Labels:n.Labels,Tier:n.Tier})),NextToken:r.NextToken}}async labelParameterVersion(e){let t={Name:e.Name,Labels:e.Labels};if(e.ParameterVersion!==void 0)t.ParameterVersion=e.ParameterVersion;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.LabelParameterVersion","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{InvalidLabels:r.InvalidLabels,ParameterVersion:r.ParameterVersion}}async addTagsToResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId,Tags:e.Tags};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.AddTagsToResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async removeTagsFromResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId,TagKeys:e.TagKeys};await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.RemoveTagsFromResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})}async listTagsForResource(e){let t={ResourceType:e.ResourceType,ResourceId:e.ResourceId};return{TagList:(await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.ListTagsForResource","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})).TagList}}async setString(e,t,r){return this.putParameter({Name:e,Value:t,Type:"String",Description:r?.description,Overwrite:r?.overwrite??!0,Tags:r?.tags})}async setSecureString(e,t,r){return this.putParameter({Name:e,Value:t,Type:"SecureString",Description:r?.description,Overwrite:r?.overwrite??!0,KeyId:r?.kmsKeyId,Tags:r?.tags})}async getValue(e){return(await this.getParameter({Name:e,WithDecryption:!0})).Parameter?.Value}async getAllByPath(e,t=!0){let r=[],n;do{let s=await this.getParametersByPath({Path:e,Recursive:t,WithDecryption:!0,NextToken:n});if(s.Parameters)r.push(...s.Parameters);n=s.NextToken}while(n);return r}parseParameter(e){return{Name:e.Name,Type:e.Type,Value:e.Value,Version:e.Version,LastModifiedDate:e.LastModifiedDate,ARN:e.ARN,DataType:e.DataType,Description:e.Description,AllowedPattern:e.AllowedPattern,KeyId:e.KeyId,Tier:e.Tier}}async sendCommand(e){if(!e.InstanceIds&&!e.Targets)throw Error("SendCommand requires either InstanceIds or Targets");let t={DocumentName:e.DocumentName};if(e.InstanceIds)t.InstanceIds=e.InstanceIds;if(e.Targets)t.Targets=e.Targets;if(e.Parameters)t.Parameters=e.Parameters;if(e.TimeoutSeconds)t.TimeoutSeconds=e.TimeoutSeconds;if(e.Comment)t.Comment=e.Comment;if(e.OutputS3BucketName)t.OutputS3BucketName=e.OutputS3BucketName;if(e.OutputS3KeyPrefix)t.OutputS3KeyPrefix=e.OutputS3KeyPrefix;let r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.SendCommand","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{CommandId:r.Command?.CommandId,Status:r.Command?.Status,StatusDetails:r.Command?.StatusDetails}}async getCommandInvocation(e){let t={CommandId:e.CommandId,InstanceId:e.InstanceId},r=await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.GetCommandInvocation","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)});return{Status:r.Status,StatusDetails:r.StatusDetails,StandardOutputContent:r.StandardOutputContent,StandardErrorContent:r.StandardErrorContent,ResponseCode:r.ResponseCode}}async runShellCommand(e,t,r){let n=await this.sendCommand({InstanceIds:[e],DocumentName:"AWS-RunShellScript",Parameters:{commands:t},TimeoutSeconds:r?.timeoutSeconds||600});if(!n.CommandId)return{success:!1,error:"Failed to send command"};if(r?.waitForCompletion===!1)return{success:!0,status:"Pending"};let s=r?.pollIntervalMs||2000,i=r?.maxWaitMs||300000,o=Date.now();while(Date.now()-o<i){await new Promise((a)=>setTimeout(a,s));try{let a=await this.getCommandInvocation({CommandId:n.CommandId,InstanceId:e});if(a.Status==="Success")return{success:!0,output:a.StandardOutputContent,error:a.StandardErrorContent,status:a.Status};if(a.Status==="Failed"||a.Status==="Cancelled"||a.Status==="TimedOut")return{success:!1,output:a.StandardOutputContent,error:a.StandardErrorContent||a.StatusDetails,status:a.Status}}catch(a){if(!a.message?.includes("InvocationDoesNotExist"))return{success:!1,error:a.message}}}return{success:!1,error:"Command timed out waiting for completion"}}async listCommandInvocations(e){let t={CommandId:e.CommandId};if(e.Details)t.Details=!0;return((await this.client.request({service:"ssm",region:this.region,method:"POST",path:"/",headers:{"X-Amz-Target":"AmazonSSM.ListCommandInvocations","Content-Type":"application/x-amz-json-1.1"},body:JSON.stringify(t)})).CommandInvocations||[]).map((s)=>({InstanceId:s.InstanceId,Status:s.Status,StatusDetails:s.StatusDetails,StandardOutputContent:s.CommandPlugins?.[0]?.Output,StandardErrorContent:s.CommandPlugins?.[0]?.StandardErrorUrl}))}async sendCommandByTags(e){let t=Object.entries(e.tags).map(([c,l])=>({Key:`tag:${c}`,Values:[l]})),r=await this.sendCommand({Targets:t,DocumentName:"AWS-RunShellScript",Parameters:{commands:e.commands},TimeoutSeconds:e.timeoutSeconds||600,Comment:e.comment});if(!r.CommandId)return{success:!1,instanceCount:0,perInstance:[],error:"Failed to send command"};let n=e.pollIntervalMs||3000,s=e.maxWaitMs||600000,i=Date.now(),o=new Set(["Success","Failed","Cancelled","TimedOut"]),a=[];while(Date.now()-i<s){await new Promise((c)=>setTimeout(c,n));try{if(a=await this.listCommandInvocations({CommandId:r.CommandId,Details:!0}),a.length===0)continue;if(a.every((l)=>o.has(l.Status||""))){let l=a.map((d)=>({instanceId:d.InstanceId,status:d.Status||"Unknown",output:d.StandardOutputContent,error:d.StandardErrorContent||d.StatusDetails})),u=a.every((d)=>d.Status==="Success");return{success:u,instanceCount:a.length,perInstance:l,error:u?void 0:"One or more instances reported a non-success status"}}}catch(c){if(!c.message?.includes("InvocationDoesNotExist"))return{success:!1,instanceCount:0,perInstance:[],error:c.message}}}return{success:!1,instanceCount:a.length,perInstance:a.map((c)=>({instanceId:c.InstanceId,status:c.Status||"Unknown"})),error:"Command timed out waiting for all instances to complete"}}}function os(e={}){let t=new Set;for(let r of Object.values(e)){if(!r.domain)continue;if(!(r.redirect||r.deploy==="server"||r.start))continue;t.add(r.domain);for(let n of r.aliases??[]){let s=String(n).trim().toLowerCase();if(s)t.add(s)}}return t}function sr(e){return e.replace(/\.$/,"").toLowerCase()}function Ao(e,t,r){let n=sr(e.name),s=sr(t),i=sr(r),o=i===s?"@":i.endsWith(`.${s}`)?i.slice(0,-(s.length+1)):i;return n===i||n===o||o==="@"&&(n===""||n===s)}async function No(e,t,r,n,s="A"){let i=await e.listRecords(t);if(!i.success)return[`could not list ${s} records: ${i.message||"unknown provider error"}`];let o=i.records.filter((l)=>l.type===s&&Ao(l,t,r)),a=o.findIndex((l)=>l.content===n);if(o.length<=1||a===-1)return[];let c=[];for(let[l,u]of o.entries()){if(l===a)continue;let d=await e.deleteRecord(t,u);if(!d.success)c.push(`could not remove stale ${u.name} ${s} ${u.content}: ${d.message||"unknown provider error"}`)}return c}function me(e){if(!e)return;let t=e.trim();if(!t)return;if(!t.includes("/"))return t;let[r]=t.split("/");if(!r.endsWith("::"))return r||void 0;return`${r}1`}var Ho=new Set(["mail","smtp","imap","mx"]);function Ji(e){return!Ho.has(e.split(".")[0]??"")}async function di(e){let{provider:t,zone:r,fqdn:n,ipv4:s,ipv6:i,ttl:o=600}=e,a={published:[],warnings:[]},c=[{type:"A",content:s}];if(i&&Ji(n))c.push({type:"AAAA",content:i});for(let{type:l,content:u}of c){let d=await t.upsertRecord(r,{name:n,type:l,content:u,ttl:o});if(d?.success===!1){a.warnings.push(`${n} → ${u} failed: ${d.error||d.message||"unknown error"}`);continue}if(a.warnings.push(...(await No(t,r,n,u,l)).map((m)=>`${n} cleanup: ${m}`)),await Xi(t,r,n,u,l))a.published.push({fqdn:n,type:l,content:u});else a.warnings.push(`${n} → ${u} reported success at ${t.name} but no matching ${l} record exists — create it manually: ${l} ${n} → ${u}`)}return a}async function Xi(e,t,r,n,s){try{if(typeof e?.listRecords!=="function")return!0;let i=await e.listRecords(t);if(!i?.success||!Array.isArray(i.records))return!0;return i.records.some((o)=>{let a=typeof o?.name==="string"?o.name.replace(/\.$/,""):"";return o?.type===s&&a===r&&o?.content===n})}catch{return!0}}import{readFileSync as is}from"node:fs";import{join as Na}from"node:path";function ir(e){return(Buffer.isBuffer(e)?e.toString("utf8"):typeof e==="string"?e:"").replace(/(^|\n)(\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*=).*$/gm,"$1$2[redacted]").replace(/encrypted:[A-Za-z0-9+/=]+/g,"encrypted:[redacted]").trim().slice(-8000)}function ot(e,t){let r=ir(e?.stderr),n=/line (\d+): syntax error/.exec(r);if(!n)return"";let s=Number(n[1]),i=t.split(`
2
2
  `),o=Math.max(1,s-12),a=Math.min(i.length,s+2),c=i.slice(o-1,a).map((l,u)=>{let d=o+u,m=/^[A-Z_][A-Z0-9_]*=/.test(l)?l.replace(/=.*/,"=<redacted>"):l;return` ${String(d).padStart(4)}${d===s?" >":" "} ${m}`});return["",`The deploy script bash rejected is ${i.length} lines; it stopped at ${s}.`,...c].join(`
3
- `)}function Ee(e){let t=e,r=typeof t?.status==="number"?` (exit ${t.status})`:"",n=t?.signal?` (signal ${t.signal})`:"",s=[ir(t?.stderr),ir(t?.stdout)].filter(Boolean).join(`
3
+ `)}function Oe(e){let t=e,r=typeof t?.status==="number"?` (exit ${t.status})`:"",n=t?.signal?` (signal ${t.signal})`:"",s=[ir(t?.stderr),ir(t?.stdout)].filter(Boolean).join(`
4
4
  `);return`Remote SSH command failed${r}${n}${s?`
5
5
  ${s}`:""}`}function Ue(e){for(let t of e.split(`
6
6
  `)){let r=t.trim();if(!r.startsWith("[ts-cloud]"))continue;console.warn(r)}}var Dn=4000,ko=3;function zo(e){return e.length>Dn?`…
7
- ${e.slice(-Dn)}`:e}function Oe(e,t){let r=e.filter((o)=>o.status!=="Success");if(r.length===0)return t;let n=r.slice(0,ko).map((o)=>{let a=(o.error||o.output||"").trim(),c=`${o.instanceId}: ${o.status}`;return a?`${c}
7
+ ${e.slice(-Dn)}`:e}function De(e,t){let r=e.filter((o)=>o.status!=="Success");if(r.length===0)return t;let n=r.slice(0,ko).map((o)=>{let a=(o.error||o.output||"").trim(),c=`${o.instanceId}: ${o.status}`;return a?`${c}
8
8
  ${zo(a)}`:c}),s=r.length-n.length,i=s>0?[`(+${s} more instance${s===1?"":"s"} failed)`]:[];return[t,...n,...i].join(`
9
9
  `)}function Ko(e){return`/etc/ts-cloud/${e}-upgrade.env`}function jo(e){return`${e}-upgrade`}function Qi(e){if(!e||e.length===0)return[];let t=["mkdir -p /etc/ts-cloud"],r=[];for(let n of e){if(n.enabled===!1)continue;if(!n.service||!n.binary)continue;let{service:s,binary:i,command:o="upgrade",channel:a="stable",schedule:c="daily",randomizedDelay:l="4h",args:u=[]}=n,d=jo(s),m=Ko(s),f=[`--path ${i}`,`--service ${s}`,...a==="canary"?["--canary"]:[],...u].join(" ");t.push(`if [ ! -f ${m} ]; then`,` cat > ${m} <<'TS_CLOUD_APPUPD_ENV_EOF'`,"# Set to false to pause automatic updates without disabling the timer.","ENABLED=true","# Extra flags appended to the update command.","ARGS=","TS_CLOUD_APPUPD_ENV_EOF","fi",`chmod 644 ${m}`,`cat > /etc/systemd/system/${d}.service <<'TS_CLOUD_APPUPD_SVC_EOF'`,"[Unit]",`Description=Check for and install ${s} updates`,"After=network-online.target","Wants=network-online.target","","[Service]","Type=oneshot",`EnvironmentFile=-${m}`,`ExecStart=/bin/sh -c 'if [ "\${ENABLED:-true}" != "true" ]; then echo "auto-update disabled via ${m}"; exit 0; fi; exec ${i} ${o} ${f} $ARGS'`,"TS_CLOUD_APPUPD_SVC_EOF",`cat > /etc/systemd/system/${d}.timer <<'TS_CLOUD_APPUPD_TMR_EOF'`,"[Unit]",`Description=Scheduled ${s} update check`,"","[Timer]",`OnCalendar=${c}`,`RandomizedDelaySec=${l}`,"Persistent=true","","[Install]","WantedBy=timers.target","TS_CLOUD_APPUPD_TMR_EOF"),r.push(`systemctl enable --now ${d}.timer`)}if(r.length===0)return[];return[...t,"systemctl daemon-reload",...r]}function ye(e=!0){if(!e)return[];return["export DEBIAN_FRONTEND=noninteractive","dpkg-query -W -f='${Status}' unattended-upgrades 2>/dev/null | grep -q 'ok installed' || apt-get install -y unattended-upgrades","cat > /etc/apt/apt.conf.d/20auto-upgrades <<'TS_CLOUD_AUTOUPD_EOF'",'APT::Periodic::Update-Package-Lists "1";','APT::Periodic::Unattended-Upgrade "1";','APT::Periodic::Download-Upgradeable-Packages "1";','APT::Periodic::AutocleanInterval "7";',"TS_CLOUD_AUTOUPD_EOF","systemctl enable --now unattended-upgrades 2>/dev/null || true"]}var In=[["nginx",80],["https",443],["phpFpm",9074],["mysql",3306],["postgres",5432],["redis",6379],["meilisearch",7700],["typesense",8108],["smtp",25]];function Wo(e=!0){let t=typeof e==="object"?e:{},r=typeof e==="boolean"?e:e.enabled!==!1,n=Number(t.alerts?.bandwidthTb??0);return{enabled:r,cpuLoadPerCore:t.alerts?.cpuLoadPerCore??2,memPercent:t.alerts?.memPercent??90,diskPercent:t.alerts?.diskPercent??90,bandwidthTb:Number.isFinite(n)&&n>0?n:0,bandwidthPercent:t.alerts?.bandwidthPercent??80}}function ve(e=!0){let{enabled:t,cpuLoadPerCore:r,memPercent:n,diskPercent:s,bandwidthTb:i,bandwidthPercent:o}=Wo(e);if(!t)return[];let a=Math.round(i*1000000000000),c=In.map(([u,d])=>`SVC_${u.toUpperCase()}=$(probe ${d})`),l=In.map(([u])=>`"${u}":"$SVC_${u.toUpperCase()}"`).join(",");return["if ! command -v sadf >/dev/null 2>&1; then export DEBIAN_FRONTEND=noninteractive; apt-get update -qq; apt-get install -y -qq sysstat; fi","systemctl enable --now sysstat.service 2>/dev/null || true","mkdir -p /var/lib/ts-cloud","cat > /usr/local/bin/ts-cloud-metrics.sh <<'TS_CLOUD_METRICS_EOF'","#!/bin/bash","set -uo pipefail","LOAD=$(cut -d' ' -f1 /proc/loadavg)","CPUS=$(nproc)",`read MEM_TOTAL MEM_USED SWAP_TOTAL SWAP_USED <<EOF
10
10
  $(free -m | awk '/^Mem:/{total=$2; used=$3} /^Swap:/{print total, used, $2, $3}')
@@ -141,7 +141,7 @@ writeFileSync(candidatePath, JSON.stringify(candidate, null, 2) + '\\n', { mode:
141
141
  `);return[`mkdir -p ${n}`,...e.tenant?[]:[`(cd ${G} && ${i} add @stacksjs/tlsx@${o}) || true`],...Fe(f,_,"TS_CLOUD_RENEW_EOF"),`chmod +x ${f}`,...Fe(`/etc/systemd/system/${h}`,P,"TS_CLOUD_RENEW_SVC_EOF"),...Fe(`/etc/systemd/system/${R}`,S,"TS_CLOUD_RENEW_TIMER_EOF"),"systemctl daemon-reload",`systemctl enable --now ${R} || true`,`${f} || true`]}function pe(e){let{config:t,proxy:r}=e,n=e.bunBin??"/usr/local/bin/bun",s=r.version??"latest",i=t.productionCerts.certsDir,o=(e.slug||"app").replace(/[^a-z0-9._-]+/gi,"-"),a=Fn(t,{slug:o}),c=Ta(X,i);if(e.tenant)return["set -euo pipefail",`if [ ! -x ${Me} ] || [ ! -d ${G}/node_modules/@stacksjs/rpx ]; then`,` echo "ts-cloud: no rpx gateway on this box. This project attaches to another project's compute, so it does not provision one - deploy the owning project first." >&2`," exit 1","fi",`mkdir -p ${X} ${i}`,`__tsc_rpx_before="$(sha256sum ${X}/${o}.json 2>/dev/null | cut -d' ' -f1 || true)"`,...Sr(`${X}/${o}.json`,a,"TS_CLOUD_RPX_FRAGMENT_EOF",{bunBin:n,preserveManagementDashboardRoutes:e.preserveManagementDashboardRoutes}),`__tsc_rpx_after="$(sha256sum ${X}/${o}.json 2>/dev/null | cut -d' ' -f1 || true)"`,`if [ "$__tsc_rpx_before" != "$__tsc_rpx_after" ] || ! systemctl is-active --quiet ${ue}; then`,` systemctl restart ${ue}`,"else",` echo "[ts-cloud] rpx routes for ${o} unchanged - leaving the shared gateway alone"`,"fi",...Hn(e)];let u=[`Environment=RPX_UPSTREAM_TIMEOUT=${r.upstreamTimeout??60}`];if(typeof r.maxUpstreamConns==="number")u.push(`Environment=RPX_MAX_UPSTREAM_CONNS=${r.maxUpstreamConns}`);let d=e.profile==="raspberry-pi"?Ca:Pa,m=r.memoryHigh??d.high,f=r.memoryMax??d.max;return["set -euo pipefail",`mkdir -p ${ct} ${X} ${i} ${G}`,`rm -rf ${G}.next ${G}.prev`,`mkdir -p ${G}.next`,`(cd ${G}.next && ${n} add @stacksjs/rpx@${s})`,`mv ${G} ${G}.prev`,`mv ${G}.next ${G}`,`rm -rf ${G}.prev`,`ln -sfn ${G}/node_modules ${ct}/node_modules`,...Sr(`${X}/${o}.json`,a,"TS_CLOUD_RPX_FRAGMENT_EOF",{bunBin:n,preserveManagementDashboardRoutes:e.preserveManagementDashboardRoutes}),...Fe(dr,c,"TS_CLOUD_RPX_EOF"),`${n} build --production --compile --outfile ${Me}.next ${dr}`,`chmod 0755 ${Me}.next`,`mv -f ${Me}.next ${Me}`,...Fe(`/etc/systemd/system/${ue}`,["[Unit]","Description=rpx reverse-proxy gateway (managed by ts-cloud)","After=network.target network-online.target","Wants=network-online.target","","[Service]","Type=simple",`ExecStart=${Me}`,`WorkingDirectory=${G}`,"Environment=BUN_INSTALL=/root/.bun","Environment=APP_ENV=production","Environment=NODE_ENV=production",...u,"MemoryAccounting=true",`MemoryHigh=${m}`,`MemoryMax=${f}`,"CPUWeight=500","IOWeight=500","OOMPolicy=stop","Restart=always","RestartSec=5","StartLimitIntervalSec=120","StartLimitBurst=5",`ExecStartPre=/bin/sh -c 'for p in 80 443; do fuser -k -n tcp "$p" 2>/dev/null || true; done; sleep 1'`,"LimitNOFILE=1048576","AmbientCapabilities=CAP_NET_BIND_SERVICE","","[Install]","WantedBy=multi-user.target"].join(`
142
142
  `),"TS_CLOUD_RPX_UNIT_EOF"),"systemctl daemon-reload","systemctl disable --now bun-gateway.service 2>/dev/null || true","systemctl disable --now bun-gateway-renew.timer bun-gateway-renew.service 2>/dev/null || true","rm -f /etc/systemd/system/bun-gateway-renew.timer /etc/systemd/system/bun-gateway-renew.service","systemctl disable --now ts-cloud-nginx.service 2>/dev/null || true",`for timer in /etc/systemd/system/rpx-cert-renew-*.timer; do [ -e "$timer" ] || continue; unit="$(basename "$timer")"; tenant="\${unit#rpx-cert-renew-}"; tenant="\${tenant%.timer}"; [ -f "${X}/$tenant.json" ] && continue; systemctl disable --now "$unit" 2>/dev/null || true; rm -f "$timer" "/etc/systemd/system/rpx-cert-renew-$tenant.service" "${ct}/renew-certs-$tenant.sh"; done`,`systemctl enable ${ue}`,`systemctl restart ${ue}`,...Hn(e)]}function Ke(e){let t=(e.slug||"app").replace(/[^a-z0-9._-]+/gi,"-"),r=Fn(e.config,{slug:t});return["set -euo pipefail",`mkdir -p ${X}`,...Sr(`${X}/${t}.json`,r,"TS_CLOUD_RPX_FRAGMENT_EOF",{bunBin:"/usr/local/bin/bun",preserveManagementDashboardRoutes:e.preserveManagementDashboardRoutes}),`systemctl restart ${ue}`]}function Be(e=[],t={}){let r=t.path??"/root/.ssh/authorized_keys",s=[`mkdir -p ${r.replace(/\/[^/]*$/,"")}`,`touch ${r}`,`sed -i '/^${Kn("# >>> ts-cloud managed keys >>>")}$/,/^${Kn("# <<< ts-cloud managed keys <<<")}$/d' ${r}`];if(e.length>0){let i=["# >>> ts-cloud managed keys >>>",...e.map((o)=>`${o.publicKey.trim()} ${o.name}`),"# <<< ts-cloud managed keys <<<"].join(`
143
143
  `);s.push(`cat >> ${r} <<'TS_CLOUD_KEYS_EOF'`,i,"TS_CLOUD_KEYS_EOF",`chmod 600 ${r}`)}return s}function Kn(e){return e.replace(/[.*[\]\\/^$]/g,"\\$&")}function Wn(e,t){let r=`/tmp/ts-cloud-${e}.sh`,n=`TS_CLOUD_${e.toUpperCase().replace(/[^A-Z0-9]/g,"_")}_EOF`;return[`cat > ${r} <<'${n}'`,t.replace(/\n$/,""),n,`chmod +x ${r}`,`bash ${r} || echo "ts-cloud: ${e} setup did not complete; continuing" >&2`,`rm -f ${r}`]}function Gn(e,t){if(e===!1)return;if(e===!0||e===void 0)return t;return e}var jn={enabled:!0,ports:[80,443]},Ra={mode:"detection",paranoiaLevel:1};function Ea(e=!0,t=[]){let r=Gn(e,jn);if(!r||r.enabled===!1)return[];let n=[...new Set([...r.ports??jn.ports??[],...t])].sort((s,i)=>s-i);return Wn("ddos",Er({...r,ports:n}))}function Oa(e=!0){let t=Gn(e,Ra);if(!t||t.mode==="off")return[];return Wn("waf",fs(t))}function Vn(e={},t=[]){return[...Ea(e.ddos,t),...Oa(e.waf)]}function Da(e){return`${e}-sftp`}function qn(e){return e.port??2222}function Ia(e){let t=e.sftp.storage;if(t?.type==="efs"&&t.path)return t.path;return`/var/sftp/${e.slug}`}function je(e){return`'${e.split("'").join("'\\''")}'`}function Xn(e,t){if(t==="aws")return;let r=e.storage;if(r?.type==="s3"||!r&&!!e.bucket)throw Error(`sftp: bucket-backed storage is only available on the aws provider (Transfer Family). On ${t}, use storage: { type: 'efs' } to serve a directory on the server.`);for(let[s,i]of Object.entries(e.users))if(!i.sshPublicKeys?.length)throw Error(`sftp: user ${s} needs at least one SSH public key`)}function Yn(e){let{slug:t,sftp:r}=e,n=Da(t),s=Ia(e),i=qn(r),o="/etc/ts-sftp/host_key",a="/etc/ts-sftp/users",c=r.serviceUser??"ts-sftp",l=[`# --- ts-sftp (${n}) ---`,`id -u ${c} >/dev/null 2>&1 || useradd --system --home-dir ${je(s)} --shell /usr/sbin/nologin ${c}`,'export HOME="${HOME:-/root}"','command -v bun >/dev/null 2>&1 || (curl -fsSL https://bun.sh/install | bash && ln -sf "$HOME/.bun/bin/bun" /usr/local/bin/bun)',`mkdir -p ${je(s)} /etc/ts-sftp /etc/ts-sftp/users /opt/ts-sftp`,"chmod 750 /etc/ts-sftp",`cd /opt/ts-sftp && [ -f package.json ] || printf '{"name":"ts-sftp-host","private":true}' > package.json`,`cd /opt/ts-sftp && bun add ts-sftp@${r.version??"latest"} --no-save --production || bun add ts-sftp@${r.version??"latest"}`,`[ -f /etc/ts-sftp/host_key ] || bun /opt/ts-sftp/node_modules/ts-sftp/dist/bin/cli.js keygen --out /etc/ts-sftp/host_key --comment ${je(`${t}-sftp`)}`,"chmod 600 /etc/ts-sftp/host_key",'BUN_REAL=$(readlink -f "$(command -v bun)")','case "$BUN_REAL" in /root/*) install -m 0755 "$BUN_REAL" /usr/local/bin/ts-sftp-bun; BUN_BIN=/usr/local/bin/ts-sftp-bun;; *) BUN_BIN="$BUN_REAL";; esac'],u=[];for(let[d,m]of Object.entries(r.users)){let f=(m.homeDirectory??d).replace(/^\/+|\/+$/g,"");if(!f||f.split("/").includes(".."))throw Error(`sftp: invalid homeDirectory for user ${d}`);let h=`/etc/ts-sftp/users/${d}.pub`;l.push(`mkdir -p ${je(`${s}/${f}`)}`,`printf '%s\\n' ${je(m.sshPublicKeys.join(`
144
- `))} > ${h}`,`chmod 644 ${h}`),u.push(`--user ${d}:${h}`)}return l.push(`chown -R ${c}:${c} ${je(s)} /etc/ts-sftp`,`cat > /etc/systemd/system/${n}.service <<TSCLOUD_SFTP_UNIT`,"[Unit]","Description=ts-sftp file transfer server","After=network.target","","[Service]",`User=${c}`,`ExecStart=$BUN_BIN /opt/ts-sftp/node_modules/ts-sftp/dist/bin/cli.js serve --root ${s} --host-key /etc/ts-sftp/host_key --port ${i}${r.readOnly?" --read-only":""} ${u.join(" ")}`,"Restart=always","RestartSec=2","NoNewPrivileges=true","PrivateTmp=true","ProtectSystem=strict",`ReadWritePaths=${s}`,"","[Install]","WantedBy=multi-user.target","TSCLOUD_SFTP_UNIT","systemctl daemon-reload",`systemctl enable ${n}.service`,`systemctl restart ${n}.service`),l}function Jn(e){return e?[qn(e)]:[]}var Aa=[80,443];function Ot(e={}){if(e.enabled===!1)return[];let t=[...new Set([...Aa,...e.allowedPorts||[]])].filter((n)=>Number.isInteger(n)&&n>=1&&n<=65535).sort((n,s)=>n-s),r=["export DEBIAN_FRONTEND=noninteractive","command -v ufw >/dev/null 2>&1 || apt-get install -y ufw","ufw --force reset","ufw default deny incoming","ufw default allow outgoing","ufw allow OpenSSH"];for(let n of t)r.push(`ufw allow ${n}/tcp`);return r.push("ufw --force enable"),r}function De(e,t){let r=e.infrastructure?.compute??{},n=r.runtime==="php"||!!r.php,s=ne(e,{environment:t?.environment}),i=n||!!r.managedServices,o=i?Dr():[],a=!Z({...r,proxy:le(e,{environment:t?.environment})}),c=n?[...o,...xt({versions:r.php?.versions,default:r.php?.default,extensions:r.php?.extensions,installNginx:a,optimizeForProduction:r.php?.optimizeForProduction,ini:r.php?.ini}),...a?cr():[]]:void 0,l=[],u=w(e);if(!n&&i)l.push(...o);if(l.push(...ke(e.notifications)),r.managedServices)l.push(...$o(r.managedServices,{mail:s.enabled?s:void 0}),...Tt(u,r.managedServices));let d=e.infrastructure?.sftp;if(d)Xn(d,B(e)),l.push(...Yn({slug:e.project.slug,sftp:d}));let m=r.firewall??(n?{enabled:!0}:{enabled:!1}),f=Ur(s);if(l.push(...Ot(f.length>0?{...m,allowedPorts:[...m.allowedPorts??[],...f]}:m)),l.push(...Vn({ddos:r.ddos,waf:r.waf},[...r.firewall?.allowedPorts??[],...f])),l.push(...ye(r.autoUpdates??n)),l.push(...Qi(r.appUpdates)),l.push(...ve(r.monitoring??n)),l.push(...Be(r.sshKeys,{path:t?.authorizedKeysPath})),r.backups?.enabled)l.push(...Lo({database:u,backups:r.backups}));return{runtime:r.runtime||"bun",runtimeVersion:r.runtimeVersion||"latest",phpBox:n,phpProvision:c,servicesProvision:l.length>0?l:void 0}}function Y(e={}){let{runtime:t="bun",runtimeVersion:r="latest",systemPackages:n=[],database:s,phpProvision:i,servicesProvision:o,caddyfile:a,rpxProvision:c,baked:l=!1,swapGb:u=2}=e,d=new Set(n);if(s==="sqlite")d.add("sqlite3");else if(s==="mysql")d.add("mysql-client");else if(s==="postgres")d.add("postgresql-client");let m=`#!/bin/bash
144
+ `))} > ${h}`,`chmod 644 ${h}`),u.push(`--user ${d}:${h}`)}return l.push(`chown -R ${c}:${c} ${je(s)} /etc/ts-sftp`,`cat > /etc/systemd/system/${n}.service <<TSCLOUD_SFTP_UNIT`,"[Unit]","Description=ts-sftp file transfer server","After=network.target","","[Service]",`User=${c}`,`ExecStart=$BUN_BIN /opt/ts-sftp/node_modules/ts-sftp/dist/bin/cli.js serve --root ${s} --host-key /etc/ts-sftp/host_key --port ${i}${r.readOnly?" --read-only":""} ${u.join(" ")}`,"Restart=always","RestartSec=2","NoNewPrivileges=true","PrivateTmp=true","ProtectSystem=strict",`ReadWritePaths=${s}`,"","[Install]","WantedBy=multi-user.target","TSCLOUD_SFTP_UNIT","systemctl daemon-reload",`systemctl enable ${n}.service`,`systemctl restart ${n}.service`),l}function Jn(e){return e?[qn(e)]:[]}var Aa=[80,443];function Ot(e={}){if(e.enabled===!1)return[];let t=[...new Set([...Aa,...e.allowedPorts||[]])].filter((n)=>Number.isInteger(n)&&n>=1&&n<=65535).sort((n,s)=>n-s),r=["export DEBIAN_FRONTEND=noninteractive","command -v ufw >/dev/null 2>&1 || apt-get install -y ufw","ufw --force reset","ufw default deny incoming","ufw default allow outgoing","ufw allow OpenSSH"];for(let n of t)r.push(`ufw allow ${n}/tcp`);return r.push("ufw --force enable"),r}function Ie(e,t){let r=e.infrastructure?.compute??{},n=r.runtime==="php"||!!r.php,s=ne(e,{environment:t?.environment}),i=n||!!r.managedServices,o=i?Dr():[],a=!Z({...r,proxy:le(e,{environment:t?.environment})}),c=n?[...o,...xt({versions:r.php?.versions,default:r.php?.default,extensions:r.php?.extensions,installNginx:a,optimizeForProduction:r.php?.optimizeForProduction,ini:r.php?.ini}),...a?cr():[]]:void 0,l=[],u=w(e);if(!n&&i)l.push(...o);if(l.push(...ke(e.notifications)),r.managedServices)l.push(...$o(r.managedServices,{mail:s.enabled?s:void 0}),...Tt(u,r.managedServices));let d=e.infrastructure?.sftp;if(d)Xn(d,B(e)),l.push(...Yn({slug:e.project.slug,sftp:d}));let m=r.firewall??(n?{enabled:!0}:{enabled:!1}),f=Ur(s);if(l.push(...Ot(f.length>0?{...m,allowedPorts:[...m.allowedPorts??[],...f]}:m)),l.push(...Vn({ddos:r.ddos,waf:r.waf},[...r.firewall?.allowedPorts??[],...f])),l.push(...ye(r.autoUpdates??n)),l.push(...Qi(r.appUpdates)),l.push(...ve(r.monitoring??n)),l.push(...Be(r.sshKeys,{path:t?.authorizedKeysPath})),r.backups?.enabled)l.push(...Lo({database:u,backups:r.backups}));return{runtime:r.runtime||"bun",runtimeVersion:r.runtimeVersion||"latest",phpBox:n,phpProvision:c,servicesProvision:l.length>0?l:void 0}}function Y(e={}){let{runtime:t="bun",runtimeVersion:r="latest",systemPackages:n=[],database:s,phpProvision:i,servicesProvision:o,caddyfile:a,rpxProvision:c,baked:l=!1,swapGb:u=2}=e,d=new Set(n);if(s==="sqlite")d.add("sqlite3");else if(s==="mysql")d.add("mysql-client");else if(s==="postgres")d.add("postgresql-client");let m=`#!/bin/bash
145
145
  set -euo pipefail
146
146
  `,f=Math.floor(u);if(f>0)m+=`
147
147
  # Swap: a swapless box under memory pressure lets the kernel OOM killer choose
@@ -251,10 +251,10 @@ ${h.join(`
251
251
  `)}
252
252
  `}return m+=`
253
253
  echo "ts-cloud bootstrap complete — instance is ready for site deploys"
254
- `,m}var Qn="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function Zn(e,t){let r=[{port:22,protocol:"tcp",cidr:"0.0.0.0/0"},{port:80,protocol:"tcp",cidr:"0.0.0.0/0"},{port:443,protocol:"tcp",cidr:"0.0.0.0/0"}],n=new Set;for(let s of Object.values(e.sites||{}))if(s&&typeof s.port==="number"&&![22,80,443].includes(s.port))n.add(s.port);for(let s of Ur(ne(e,{environment:t})))n.add(s);for(let s of n)r.push({port:s,protocol:"tcp",cidr:"0.0.0.0/0"});return r}function es(e,t){let r={...e.infrastructure?.compute,proxy:le(e,{environment:t})},n=De(e,{environment:t}),s=r.proxy?.engine==="rpx"?pe({proxy:r.proxy,config:Te(e.sites??{},{proxy:r.proxy,slug:e.project.slug,mail:ne(e,{environment:t})}),slug:e.project.slug,bunBin:r.runtime==="node"||r.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0;return Y({runtime:n.runtime,runtimeVersion:n.runtimeVersion,systemPackages:r.systemPackages,database:e.infrastructure?.database,phpProvision:n.phpProvision,servicesProvision:n.servicesProvision,rpxProvision:s,baked:r.bakedImage===!0,swapGb:r.swapGb})}function ts(e){return Buffer.from(e,"utf8").toString("base64")}function rs(e){return e.infrastructure?.compute?.image??null}function Dt(e,t){return`${e}-${t}-app-sg`}function Tr(e,t,r="app"){return[{Key:"Project",Value:e},{Key:"Environment",Value:t},{Key:"Role",Value:r},{Key:"ManagedBy",Value:"ts-cloud"}]}function La(e,t,r){if(!e?.length)return!1;let n=(s)=>e.find((i)=>i.Key===s)?.Value;return n("Project")===t&&n("Environment")===r}function ns(e,t){let r=Dt(t.slug,t.environment);return e.find((n)=>{if(n.GroupName!==r)return!1;if(t.vpcId&&n.VpcId&&n.VpcId!==t.vpcId)return!1;if(!!n.Tags?.some((i)=>i.Key==="Project"))return La(n.Tags,t.slug,t.environment);return!!t.vpcId&&n.VpcId===t.vpcId})}function Ha(e){try{let t=is(Na(process.cwd(),"storage/cloud/state",`${e}.json`),"utf8"),r=JSON.parse(t);return typeof r.instanceId==="string"&&r.instanceId.length>0?r.instanceId:null}catch{return null}}class It{name="aws";usesCloudFormation=!0;region;ec2Client;cfnClient;constructor(e={}){this.region=e.region||"us-east-1",this.ec2Client=e.ec2Client??((t)=>new Q(t)),this.cfnClient=e.cfnClient??((t)=>new v(t))}resolveRegion(e){return e.project.region||this.region}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=this.resolveRegion(t),s=t.project.slug,i=t.infrastructure?.compute;if(!i)throw Error("infrastructure.compute is required to provision AWS compute");let o=this.ec2Client(n),a=await this.findComputeTargets({slug:s,environment:r,role:"app",region:n});if(a.length>0){let C=a[0];return{appInstanceId:C.id,appPublicIp:C.publicIp,appPublicIpv6:C.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}let c=rs(t);if(!c){if(c=(await new We(n).getParameter({Name:Qn})).Parameter?.Value??null,!c)throw Error("Could not resolve the Ubuntu 24.04 AMI from SSM")}let l=await o.describeVpcs(),u=(l.Vpcs||[]).find((C)=>C.IsDefault)||(l.Vpcs||[])[0];if(!u?.VpcId)throw Error("No VPC found to launch the instance into");let d=(await o.describeSubnets({Filters:[{Name:"vpc-id",Values:[u.VpcId]}]})).Subnets||[],m=d.find((C)=>C.MapPublicIpOnLaunch)||d[0],f=m?.SubnetId;if(!m?.MapPublicIpOnLaunch)console.warn("ts-cloud: no public subnet found; the instance may not get a public IP (deploys/SSL need one).");let h=Dt(s,r),x=(await o.describeSecurityGroups({Filters:[{Name:"group-name",Values:[h]},{Name:"vpc-id",Values:[u.VpcId]}]})).SecurityGroups?.[0]?.GroupId;if(!x)x=(await o.createSecurityGroup({GroupName:h,Description:`ts-cloud ${s}/${r} app`,VpcId:u.VpcId,TagSpecifications:[{ResourceType:"security-group",Tags:Tr(s,r)}]})).GroupId;let b=Zn(t,r);await o.authorizeSecurityGroupIngress({GroupId:x,IpPermissions:b.map((C)=>({IpProtocol:C.protocol,FromPort:C.port,ToPort:C.port,IpRanges:[{CidrIp:C.cidr}]}))}).catch((C)=>{if(!/InvalidPermission\.Duplicate/.test(C instanceof Error?C.message:""))throw C});let _=ts(es(t,r)),P=i.server?.instanceType||"t3.small",E=(await o.runInstances({ImageId:c,InstanceType:P,MinCount:1,MaxCount:1,SecurityGroupIds:x?[x]:void 0,SubnetId:f,UserData:_,IamInstanceProfile:i.server?.iamInstanceProfile?{Name:i.server.iamInstanceProfile}:void 0,TagSpecifications:[{ResourceType:"instance",Tags:[{Key:"Name",Value:`${s}-${r}-app`},...Tr(s,r)]}]})).Instances?.[0]?.InstanceId;if(!E)throw Error("RunInstances did not return an instance id");let y=await o.waitForInstanceState(E,"running");if(!y)throw Error(`Instance ${E} did not reach 'running' before timeout`);return{appInstanceId:E,appPublicIp:y.PublicIpAddress,appPublicIpv6:me(y.Ipv6Address),sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=this.resolveRegion(t),i=this.ec2Client(s),o=[],c=(await this.findComputeTargets({slug:n,environment:r,role:"app",region:s})).map((h)=>h.id),l;if(c.length>0)l=(await i.describeInstances({InstanceIds:c}).catch(()=>({Reservations:[]}))).Reservations?.flatMap((R)=>R.Instances??[]).find((R)=>R.VpcId)?.VpcId;if(c.length>0)await i.terminateInstances(c),o.push(...c.map((h)=>`instance ${h}`)),await Promise.all(c.map((h)=>i.waitForInstanceState(h,"terminated").catch(()=>{return})));let u=Dt(n,r),d=[{Name:"group-name",Values:[u]}];if(l)d.push({Name:"vpc-id",Values:[l]});let m=await i.describeSecurityGroups({Filters:d}).catch(()=>({SecurityGroups:[]})),f=ns(m.SecurityGroups??[],{slug:n,environment:r,vpcId:l});if(f?.GroupId)for(let h=0;h<10;h++)try{await i.deleteSecurityGroup(f.GroupId),o.push(`security group ${u}`);break}catch{await new Promise((R)=>setTimeout(R,3000))}return{destroyed:o}}async getComputeOutputs(e){let t=this.resolveRegion(e.config),r=T(e.config,e.environment),n=this.cfnClient(t);try{let s=await n.getStackOutputs(r);return{deployBucketName:s.deployBucketName,appInstanceId:s.appInstanceId,appPublicIp:s.appPublicIp,sshUser:"ec2-user"}}catch(s){if(!/does not exist|ValidationError/i.test(s instanceof Error?s.message:""))throw s;let o=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",region:t}))[0];return{appInstanceId:o?.id,appPublicIp:o?.publicIp,appPublicIpv6:o?.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}}async uploadRelease(e){let t=this.resolveRegion(e.config),n=(await this.getComputeOutputs({config:e.config,environment:e.environment})).deployBucketName;if(!n)throw Error("No deployBucketName in stack outputs. Re-deploy infrastructure to add the staging bucket.");return await new g(t).putObject({bucket:n,key:e.remoteKey,body:is(e.localPath),contentType:"application/gzip"}),{artifactRef:`s3://${n}/${e.remoteKey}`}}async findComputeTargets(e){let t=e.region??this.region,r=this.ec2Client(t),n=[{Name:"tag:Project",Values:[e.slug]},{Name:"tag:Environment",Values:[e.environment]},{Name:"tag:Role",Values:[e.role||"app"]},{Name:"instance-state-name",Values:["running","pending"]}],s=await r.describeInstances({Filters:n}),i=this.reservationsToTargets(s.Reservations);if(i.length>0||(e.role||"app")!=="app")return i;let o=e.stackName??`${e.slug}-${e.environment}`,a=Ha(o);if(!a)try{a=(await this.cfnClient(t).getStackOutputs(o)).appInstanceId??null}catch{a=null}if(!a)return[];try{let c=await r.describeInstances({InstanceIds:[a],Filters:[{Name:"instance-state-name",Values:["running","pending"]}]});return this.reservationsToTargets(c.Reservations)}catch{return[]}}reservationsToTargets(e){let t=[];for(let r of e||[])for(let n of r.Instances||[]){if(!n.InstanceId)continue;let s=n.Tags?.find((i)=>i.Key==="Name")?.Value;t.push({id:n.InstanceId,name:s,publicIp:n.PublicIpAddress,publicIpv6:me(n.Ipv6Address),privateIp:n.PrivateIpAddress,status:n.State?.Name})}return t}bashWrap(e){return["bash <<'TS_CLOUD_BASH_EOF'",e.join(`
255
- `),"TS_CLOUD_BASH_EOF"]}async runRemoteDeploy(e){let t=this.region,r=new We(t);if(e.targets.length>0){let s=await r.sendCommand({InstanceIds:e.targets.map((i)=>i.id),DocumentName:"AWS-RunShellScript",Parameters:{commands:this.bashWrap(e.commands)},TimeoutSeconds:e.timeoutSeconds||600,Comment:e.comment});if(!s.CommandId)return{success:!1,instanceCount:0,perInstance:[],error:"Failed to send SSM command"};return this.pollSsmCommand(r,s.CommandId,e.targets.length,((e.timeoutSeconds||600)+30)*1000)}if(!e.tags||Object.keys(e.tags).length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets or tags provided for AWS deploy"};let n=await r.sendCommandByTags({tags:e.tags,commands:this.bashWrap(e.commands),timeoutSeconds:e.timeoutSeconds||600,comment:e.comment});return{success:n.success,instanceCount:n.instanceCount,perInstance:n.perInstance.map((s)=>({instanceId:s.instanceId,status:s.status,output:s.output,error:s.error})),error:n.error}}async pollSsmCommand(e,t,r,n=600000){let i=Date.now(),o=new Set(["Success","Failed","Cancelled","TimedOut"]),a=[];while(Date.now()-i<n){await new Promise((u)=>setTimeout(u,3000));try{if(a=await e.listCommandInvocations({CommandId:t,Details:!0}),a.length>=r&&a.every((d)=>o.has(d.Status||"")))break}catch{}}let c=a.map((u)=>({instanceId:u.InstanceId,status:u.Status||"Unknown",output:u.StandardOutputContent,error:u.StandardErrorContent})),l=c.length>0&&c.every((u)=>u.status==="Success");return{success:l,instanceCount:c.length,perInstance:c,error:l?void 0:Oe(c,"One or more SSM command invocations failed")}}}function gs(e={},t="ssh"){let r=[];if(e.identityFile)r.push("-i",e.identityFile);if(e.port)r.push(t==="scp"?"-P":"-p",String(e.port));return r.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR","-o",`ConnectTimeout=${e.connectTimeoutSec??10}`),r}async function $r(e,t,r={}){let n=Bun.spawn(["ssh",...gs(r),`${r.user??"root"}@${e}`,t],{stdout:"pipe",stderr:"pipe"}),[s,i]=await Promise.all([new Response(n.stdout).text(),new Response(n.stderr).text()]);return{code:await n.exited,stdout:s,stderr:i}}async function _e(e,t,r={}){let n=await $r(e,t,r);if(n.code!==0)throw Error(`ssh \`${t}\` on ${e} failed (${n.code}): ${n.stderr.trim()||n.stdout.trim()}`);return n.stdout}async function Cr(e,t={}){let r=t.timeoutMs??180000,n=t.pollIntervalMs??5000,s=Date.now();while(Date.now()-s<r){let i=await $r(e,"echo ready",t);if(i.code===0&&i.stdout.includes("ready"))return;await new Promise((o)=>setTimeout(o,n))}throw Error(`SSH not ready on ${e} after ${r}ms`)}function lt(e={}){return["set -e","ts_cloud_script=$(mktemp /tmp/ts-cloud-deploy.XXXXXXXX)",`trap 'rm -f "$ts_cloud_script"' EXIT`,'cat > "$ts_cloud_script"',e.sudo?'sudo -n -H bash "$ts_cloud_script" < /dev/null':'bash "$ts_cloud_script" < /dev/null'].join(`
254
+ `,m}var Qn="/aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id";function Zn(e,t){let r=[{port:22,protocol:"tcp",cidr:"0.0.0.0/0"},{port:80,protocol:"tcp",cidr:"0.0.0.0/0"},{port:443,protocol:"tcp",cidr:"0.0.0.0/0"}],n=new Set;for(let s of Object.values(e.sites||{}))if(s&&typeof s.port==="number"&&![22,80,443].includes(s.port))n.add(s.port);for(let s of Ur(ne(e,{environment:t})))n.add(s);for(let s of n)r.push({port:s,protocol:"tcp",cidr:"0.0.0.0/0"});return r}function es(e,t){let r={...e.infrastructure?.compute,proxy:le(e,{environment:t})},n=Ie(e,{environment:t}),s=r.proxy?.engine==="rpx"?pe({proxy:r.proxy,config:Te(e.sites??{},{proxy:r.proxy,slug:e.project.slug,mail:ne(e,{environment:t})}),slug:e.project.slug,bunBin:r.runtime==="node"||r.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0;return Y({runtime:n.runtime,runtimeVersion:n.runtimeVersion,systemPackages:r.systemPackages,database:e.infrastructure?.database,phpProvision:n.phpProvision,servicesProvision:n.servicesProvision,rpxProvision:s,baked:r.bakedImage===!0,swapGb:r.swapGb})}function ts(e){return Buffer.from(e,"utf8").toString("base64")}function rs(e){return e.infrastructure?.compute?.image??null}function Dt(e,t){return`${e}-${t}-app-sg`}function Tr(e,t,r="app"){return[{Key:"Project",Value:e},{Key:"Environment",Value:t},{Key:"Role",Value:r},{Key:"ManagedBy",Value:"ts-cloud"}]}function La(e,t,r){if(!e?.length)return!1;let n=(s)=>e.find((i)=>i.Key===s)?.Value;return n("Project")===t&&n("Environment")===r}function ns(e,t){let r=Dt(t.slug,t.environment);return e.find((n)=>{if(n.GroupName!==r)return!1;if(t.vpcId&&n.VpcId&&n.VpcId!==t.vpcId)return!1;if(!!n.Tags?.some((i)=>i.Key==="Project"))return La(n.Tags,t.slug,t.environment);return!!t.vpcId&&n.VpcId===t.vpcId})}function Ha(e){try{let t=is(Na(process.cwd(),"storage/cloud/state",`${e}.json`),"utf8"),r=JSON.parse(t);return typeof r.instanceId==="string"&&r.instanceId.length>0?r.instanceId:null}catch{return null}}class It{name="aws";usesCloudFormation=!0;region;ec2Client;cfnClient;constructor(e={}){this.region=e.region||"us-east-1",this.ec2Client=e.ec2Client??((t)=>new Q(t)),this.cfnClient=e.cfnClient??((t)=>new v(t))}resolveRegion(e){return e.project.region||this.region}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=this.resolveRegion(t),s=t.project.slug,i=t.infrastructure?.compute;if(!i)throw Error("infrastructure.compute is required to provision AWS compute");let o=this.ec2Client(n),a=await this.findComputeTargets({slug:s,environment:r,role:"app",region:n});if(a.length>0){let C=a[0];return{appInstanceId:C.id,appPublicIp:C.publicIp,appPublicIpv6:C.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}let c=rs(t);if(!c){if(c=(await new We(n).getParameter({Name:Qn})).Parameter?.Value??null,!c)throw Error("Could not resolve the Ubuntu 24.04 AMI from SSM")}let l=await o.describeVpcs(),u=(l.Vpcs||[]).find((C)=>C.IsDefault)||(l.Vpcs||[])[0];if(!u?.VpcId)throw Error("No VPC found to launch the instance into");let d=(await o.describeSubnets({Filters:[{Name:"vpc-id",Values:[u.VpcId]}]})).Subnets||[],m=d.find((C)=>C.MapPublicIpOnLaunch)||d[0],f=m?.SubnetId;if(!m?.MapPublicIpOnLaunch)console.warn("ts-cloud: no public subnet found; the instance may not get a public IP (deploys/SSL need one).");let h=Dt(s,r),x=(await o.describeSecurityGroups({Filters:[{Name:"group-name",Values:[h]},{Name:"vpc-id",Values:[u.VpcId]}]})).SecurityGroups?.[0]?.GroupId;if(!x)x=(await o.createSecurityGroup({GroupName:h,Description:`ts-cloud ${s}/${r} app`,VpcId:u.VpcId,TagSpecifications:[{ResourceType:"security-group",Tags:Tr(s,r)}]})).GroupId;let b=Zn(t,r);await o.authorizeSecurityGroupIngress({GroupId:x,IpPermissions:b.map((C)=>({IpProtocol:C.protocol,FromPort:C.port,ToPort:C.port,IpRanges:[{CidrIp:C.cidr}]}))}).catch((C)=>{if(!/InvalidPermission\.Duplicate/.test(C instanceof Error?C.message:""))throw C});let _=ts(es(t,r)),P=i.server?.instanceType||"t3.small",E=(await o.runInstances({ImageId:c,InstanceType:P,MinCount:1,MaxCount:1,SecurityGroupIds:x?[x]:void 0,SubnetId:f,UserData:_,IamInstanceProfile:i.server?.iamInstanceProfile?{Name:i.server.iamInstanceProfile}:void 0,TagSpecifications:[{ResourceType:"instance",Tags:[{Key:"Name",Value:`${s}-${r}-app`},...Tr(s,r)]}]})).Instances?.[0]?.InstanceId;if(!E)throw Error("RunInstances did not return an instance id");let y=await o.waitForInstanceState(E,"running");if(!y)throw Error(`Instance ${E} did not reach 'running' before timeout`);return{appInstanceId:E,appPublicIp:y.PublicIpAddress,appPublicIpv6:me(y.Ipv6Address),sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=this.resolveRegion(t),i=this.ec2Client(s),o=[],c=(await this.findComputeTargets({slug:n,environment:r,role:"app",region:s})).map((h)=>h.id),l;if(c.length>0)l=(await i.describeInstances({InstanceIds:c}).catch(()=>({Reservations:[]}))).Reservations?.flatMap((R)=>R.Instances??[]).find((R)=>R.VpcId)?.VpcId;if(c.length>0)await i.terminateInstances(c),o.push(...c.map((h)=>`instance ${h}`)),await Promise.all(c.map((h)=>i.waitForInstanceState(h,"terminated").catch(()=>{return})));let u=Dt(n,r),d=[{Name:"group-name",Values:[u]}];if(l)d.push({Name:"vpc-id",Values:[l]});let m=await i.describeSecurityGroups({Filters:d}).catch(()=>({SecurityGroups:[]})),f=ns(m.SecurityGroups??[],{slug:n,environment:r,vpcId:l});if(f?.GroupId)for(let h=0;h<10;h++)try{await i.deleteSecurityGroup(f.GroupId),o.push(`security group ${u}`);break}catch{await new Promise((R)=>setTimeout(R,3000))}return{destroyed:o}}async getComputeOutputs(e){let t=this.resolveRegion(e.config),r=T(e.config,e.environment),n=this.cfnClient(t);try{let s=await n.getStackOutputs(r);return{deployBucketName:s.deployBucketName,appInstanceId:s.appInstanceId,appPublicIp:s.appPublicIp,sshUser:"ec2-user"}}catch(s){if(!/does not exist|ValidationError/i.test(s instanceof Error?s.message:""))throw s;let o=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",region:t}))[0];return{appInstanceId:o?.id,appPublicIp:o?.publicIp,appPublicIpv6:o?.publicIpv6,sshUser:"ubuntu",deployStoragePath:"/var/ts-cloud/staging"}}}async uploadRelease(e){let t=this.resolveRegion(e.config),n=(await this.getComputeOutputs({config:e.config,environment:e.environment})).deployBucketName;if(!n)throw Error("No deployBucketName in stack outputs. Re-deploy infrastructure to add the staging bucket.");return await new g(t).putObject({bucket:n,key:e.remoteKey,body:is(e.localPath),contentType:"application/gzip"}),{artifactRef:`s3://${n}/${e.remoteKey}`}}async findComputeTargets(e){let t=e.region??this.region,r=this.ec2Client(t),n=[{Name:"tag:Project",Values:[e.slug]},{Name:"tag:Environment",Values:[e.environment]},{Name:"tag:Role",Values:[e.role||"app"]},{Name:"instance-state-name",Values:["running","pending"]}],s=await r.describeInstances({Filters:n}),i=this.reservationsToTargets(s.Reservations);if(i.length>0||(e.role||"app")!=="app")return i;let o=e.stackName??`${e.slug}-${e.environment}`,a=Ha(o);if(!a)try{a=(await this.cfnClient(t).getStackOutputs(o)).appInstanceId??null}catch{a=null}if(!a)return[];try{let c=await r.describeInstances({InstanceIds:[a],Filters:[{Name:"instance-state-name",Values:["running","pending"]}]});return this.reservationsToTargets(c.Reservations)}catch{return[]}}reservationsToTargets(e){let t=[];for(let r of e||[])for(let n of r.Instances||[]){if(!n.InstanceId)continue;let s=n.Tags?.find((i)=>i.Key==="Name")?.Value;t.push({id:n.InstanceId,name:s,publicIp:n.PublicIpAddress,publicIpv6:me(n.Ipv6Address),privateIp:n.PrivateIpAddress,status:n.State?.Name})}return t}bashWrap(e){return["bash <<'TS_CLOUD_BASH_EOF'",e.join(`
255
+ `),"TS_CLOUD_BASH_EOF"]}async runRemoteDeploy(e){let t=this.region,r=new We(t);if(e.targets.length>0){let s=await r.sendCommand({InstanceIds:e.targets.map((i)=>i.id),DocumentName:"AWS-RunShellScript",Parameters:{commands:this.bashWrap(e.commands)},TimeoutSeconds:e.timeoutSeconds||600,Comment:e.comment});if(!s.CommandId)return{success:!1,instanceCount:0,perInstance:[],error:"Failed to send SSM command"};return this.pollSsmCommand(r,s.CommandId,e.targets.length,((e.timeoutSeconds||600)+30)*1000)}if(!e.tags||Object.keys(e.tags).length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets or tags provided for AWS deploy"};let n=await r.sendCommandByTags({tags:e.tags,commands:this.bashWrap(e.commands),timeoutSeconds:e.timeoutSeconds||600,comment:e.comment});return{success:n.success,instanceCount:n.instanceCount,perInstance:n.perInstance.map((s)=>({instanceId:s.instanceId,status:s.status,output:s.output,error:s.error})),error:n.error}}async pollSsmCommand(e,t,r,n=600000){let i=Date.now(),o=new Set(["Success","Failed","Cancelled","TimedOut"]),a=[];while(Date.now()-i<n){await new Promise((u)=>setTimeout(u,3000));try{if(a=await e.listCommandInvocations({CommandId:t,Details:!0}),a.length>=r&&a.every((d)=>o.has(d.Status||"")))break}catch{}}let c=a.map((u)=>({instanceId:u.InstanceId,status:u.Status||"Unknown",output:u.StandardOutputContent,error:u.StandardErrorContent})),l=c.length>0&&c.every((u)=>u.status==="Success");return{success:l,instanceCount:c.length,perInstance:c,error:l?void 0:De(c,"One or more SSM command invocations failed")}}}function gs(e={},t="ssh"){let r=[];if(e.identityFile)r.push("-i",e.identityFile);if(e.port)r.push(t==="scp"?"-P":"-p",String(e.port));return r.push("-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR","-o",`ConnectTimeout=${e.connectTimeoutSec??10}`),r}async function $r(e,t,r={}){let n=Bun.spawn(["ssh",...gs(r),`${r.user??"root"}@${e}`,t],{stdout:"pipe",stderr:"pipe"}),[s,i]=await Promise.all([new Response(n.stdout).text(),new Response(n.stderr).text()]);return{code:await n.exited,stdout:s,stderr:i}}async function _e(e,t,r={}){let n=await $r(e,t,r);if(n.code!==0)throw Error(`ssh \`${t}\` on ${e} failed (${n.code}): ${n.stderr.trim()||n.stdout.trim()}`);return n.stdout}async function Cr(e,t={}){let r=t.timeoutMs??180000,n=t.pollIntervalMs??5000,s=Date.now();while(Date.now()-s<r){let i=await $r(e,"echo ready",t);if(i.code===0&&i.stdout.includes("ready"))return;await new Promise((o)=>setTimeout(o,n))}throw Error(`SSH not ready on ${e} after ${r}ms`)}function lt(e={}){return["set -e","ts_cloud_script=$(mktemp /tmp/ts-cloud-deploy.XXXXXXXX)",`trap 'rm -f "$ts_cloud_script"' EXIT`,'cat > "$ts_cloud_script"',e.sudo?'sudo -n -H bash "$ts_cloud_script" < /dev/null':'bash "$ts_cloud_script" < /dev/null'].join(`
256
256
  `)}var Pr=lt();import{execFileSync as sc,execSync as Ns}from"node:child_process";import{createHash as ic,randomUUID as Hs}from"node:crypto";import{createReadStream as oc,existsSync as ac,readFileSync as cc}from"node:fs";function At(e){return Object.entries(e).map(([t,r])=>`${t}=${Ua(String(r))}`).join(`
257
- `)}function Ua(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t")}"`}var ka=new Set(["sqlite","sqlite3"]),Ma=new Set([":memory:","memory"]);function Fa(e){let t=e?.DB_CONNECTION?.trim().toLowerCase();return t!=null&&ka.has(t)}function za(e){let t=e.trim().replace(/^\.\//,"");if(t===""||t.startsWith("/")||t.startsWith("~"))return;if(t.split("/").includes(".."))return;return t.replace(/\/+$/,"")}function xr(e,t=[]){if(!Fa(e))return{};let r=e?.DB_DATABASE_PATH?.trim()||e?.DB_DATABASE?.trim();if(r&&Ma.has(r.toLowerCase()))return{};if(!r)return{warning:"DB_CONNECTION is sqlite but neither DB_DATABASE_PATH nor DB_DATABASE names a file, so ts-cloud cannot tell where the database lives. If it is written inside the release directory, the next deploy discards it. Set DB_DATABASE_PATH, or list the file in the site's `sharedPaths`."};let n=za(r);if(!n)return{};if(t.some((s)=>Xs(s)===n))return{};return{path:n}}function Lt(e,t=[]){let{path:r}=xr(e,t);return r?[r]:[]}var Ge="/var/ts-cloud/artifacts";var ws=Object.freeze({stagingMaxAgeMinutes:60,artifactMaxAgeDays:2,artifactUploadMaxAgeMinutes:60,bunCacheMaxAgeDays:7,journalMaxAgeDays:14,journalMaxSizeMb:512,containerImageMaxAgeHours:168}),Ts=Object.freeze({artifactMaxAgeDays:0,bunCacheMaxAgeDays:1,journalMaxAgeDays:3,journalMaxSizeMb:256,containerImageMaxAgeHours:24});function hs(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Ae(e,t,r=0){if(!Number.isInteger(t)||t<r)throw Error(`hostCleanup: ${e} must be a whole number >= ${r} (got ${t})`)}function Ss(e,t){for(let[r,n]of Object.entries(t))Ae(`${e}.${r}`,n);Ae(`${e}.stagingMaxAgeMinutes`,t.stagingMaxAgeMinutes,15),Ae(`${e}.artifactUploadMaxAgeMinutes`,t.artifactUploadMaxAgeMinutes,15),Ae(`${e}.journalMaxSizeMb`,t.journalMaxSizeMb,1)}function Ba(e,t){let r=`hostCleanup.paths[${t}]`,n=e.path?.replace(/\/+$/,"")??"";if(!n.startsWith("/")||n===""||/[\s'"`$\\]/.test(n)||n.split("/").includes(".."))throw Error(`${r}.path must be an absolute directory with no spaces, quotes or '..' (got ${JSON.stringify(e.path)})`);if(n==="/var/www"||n.startsWith("/var/www/"))throw Error(`${r}.path must not be under /var/www: releases are pruned by keepReleases`);if(!e.pattern||/^[*?]+$/.test(e.pattern)||e.pattern.includes("/")||/[\n'"`]/.test(e.pattern))throw Error(`${r}.pattern must be a file-name glob naming something, not a bare wildcard (got ${JSON.stringify(e.pattern)})`);if(Ae(`${r}.maxAgeDays`,e.maxAgeDays),e.type!==void 0&&e.type!=="file"&&e.type!=="directory")throw Error(`${r}.type must be 'file' or 'directory'`)}function Nt(e){let t=typeof e==="object"&&e!==null?e:{},r=e!==!1,n={...ws,...ys(t.retention)};Ss("hostCleanup.retention",n);let s=null;if(t.pressure!==!1){let a=typeof t.pressure==="object"?t.pressure:{},c=a.lowWaterPercent??50,l=a.highWaterPercent??85;if(Ae("hostCleanup.pressure.lowWaterPercent",c),Ae("hostCleanup.pressure.highWaterPercent",l,1),l>100||c>=l)throw Error(`hostCleanup.pressure: need 0 <= lowWaterPercent < highWaterPercent <= 100 (got ${c} and ${l})`);let u={...Ts,...ys(a.escalated)},d=Object.fromEntries(Object.keys(n).map((m)=>[m,Math.min(n[m],u[m]??n[m])]));Ss("hostCleanup.pressure.escalated",d),s={lowWaterPercent:c,highWaterPercent:l,escalated:d}}let i=t.schedule??"daily";if(!/^[\w*:,./~ -]+$/.test(i))throw Error(`hostCleanup.schedule must be a systemd OnCalendar expression (got ${JSON.stringify(i)})`);let o=t.paths??[];return o.forEach(Ba),{enabled:r,timer:r&&t.timer!==!1,schedule:i,retention:n,pressure:s,paths:o}}function ys(e){return Object.fromEntries(Object.entries(e??{}).filter(([,t])=>t!==void 0))}var ut={stagingMaxAgeMinutes:"TS_CLOUD_HC_STAGING_MIN",artifactMaxAgeDays:"TS_CLOUD_HC_ARTIFACT_DAYS",artifactUploadMaxAgeMinutes:"TS_CLOUD_HC_UPLOAD_MIN",bunCacheMaxAgeDays:"TS_CLOUD_HC_BUN_DAYS",journalMaxAgeDays:"TS_CLOUD_HC_JOURNAL_DAYS",journalMaxSizeMb:"TS_CLOUD_HC_JOURNAL_MB",containerImageMaxAgeHours:"TS_CLOUD_HC_IMAGE_HOURS"};function Rr(e){return Object.keys(ut).map((t)=>`${ut[t]}=${e[t]}`).join(" ")}var Ie=(e)=>`"$${ut[e]}"`;function vs(e){return[`find /var/ts-cloud/staging -xdev -maxdepth 1 -type f -mmin +${Ie("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /tmp -xdev -maxdepth 1 -type f -name "*-release.tar.gz" -mmin +${Ie("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name "*.tar.gz" -mtime +${Ie("artifactMaxAgeDays")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name ".*.tmp" -mmin +${Ie("artifactUploadMaxAgeMinutes")} -delete 2>/dev/null || true`,...e.map(Ka),`journalctl --vacuum-time=${Ie("journalMaxAgeDays")}d --vacuum-size=${Ie("journalMaxSizeMb")}M >/dev/null 2>&1 || true`]}function Ka(e){let t=hs(e.path.replace(/\/+$/,"")),r=hs(e.pattern);return e.type==="directory"?`find ${t} -xdev -mindepth 1 -maxdepth 1 -type d -name ${r} -mtime +${e.maxAgeDays} -exec rm -rf -- {} + 2>/dev/null || true`:`find ${t} -xdev -mindepth 1 -maxdepth 1 -type f -name ${r} -mtime +${e.maxAgeDays} -delete 2>/dev/null || true`}function bs(){return[`find /root/.bun/install/cache -xdev -type f -mtime +${Ie("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,"find /root/.bun/install/cache -xdev -depth -type d -empty -delete 2>/dev/null || true",`if command -v docker >/dev/null 2>&1; then docker image prune --all --force --filter "until=\${${ut.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,`if command -v podman >/dev/null 2>&1; then podman image prune --all --force --filter "until=\${${ut.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,"if command -v apt-get >/dev/null 2>&1; then apt-get clean >/dev/null 2>&1 || true; fi"]}function _s(e){return[`${e}="$(df -P / 2>/dev/null | awk 'NR==2 {sub(/%/, "", $5); print $5}' || true)"`,`case "$${e}" in ''|*[!0-9]*) ${e}=-1 ;; esac`]}function Ir(e){let{retention:t,pressure:r,paths:n}=Nt(e),s='echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',i='echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"';if(!r)return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',Rr(t),...vs(n),...bs(),'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"'];let{lowWaterPercent:o,highWaterPercent:a,escalated:c}=r;return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',..._s("TS_CLOUD_HC_BEFORE"),"TS_CLOUD_HC_TIER=normal",Rr(t),`if [ "$TS_CLOUD_HC_BEFORE" -ge 0 ] && [ "$TS_CLOUD_HC_BEFORE" -lt ${o} ]; then`," TS_CLOUD_HC_TIER=light",` echo "[ts-cloud] host cleanup: / at \${TS_CLOUD_HC_BEFORE}% (under ${o}%), skipping cache and image pruning"`,`elif [ "$TS_CLOUD_HC_BEFORE" -ge ${a} ]; then`," TS_CLOUD_HC_TIER=pressure",` ${Rr(c)}`,` echo "[ts-cloud] warning: / at \${TS_CLOUD_HC_BEFORE}% (at or above ${a}%), host cleanup is using shortened retention windows" >&2`,"fi",...vs(n),'if [ "$TS_CLOUD_HC_TIER" != light ]; then',...bs().map((l)=>` ${l}`),"fi",'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"',..._s("TS_CLOUD_HC_AFTER"),`echo "[ts-cloud] host-cleanup {\\"event\\":\\"host-cleanup\\",\\"tier\\":\\"$TS_CLOUD_HC_TIER\\",\\"diskBeforePercent\\":$TS_CLOUD_HC_BEFORE,\\"diskAfterPercent\\":$TS_CLOUD_HC_AFTER,\\"lowWaterPercent\\":${o},\\"highWaterPercent\\":${a}}"`,"mkdir -p /var/lib/ts-cloud 2>/dev/null || true",'TS_CLOUD_HC_PREV="$(cat /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || echo ok)"',`if [ "$TS_CLOUD_HC_AFTER" -ge ${a} ]; then`,` echo "[ts-cloud] warning: / still at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%); something outside ts-cloud's retention rules is filling the disk" >&2`,` if [ "$TS_CLOUD_HC_PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): / at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%)" >/dev/null 2>&1 || true; fi`," echo alert > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","else",` if [ "$TS_CLOUD_HC_PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "✅ $(hostname): / back under ${a}% after host cleanup (\${TS_CLOUD_HC_AFTER}%)" >/dev/null 2>&1 || true; fi`," echo ok > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","fi"]}function Cs(e){return["#!/usr/bin/env bash","# Managed by ts-cloud: rewritten on every deploy. Configure compute.hostCleanup instead of editing.","set -uo pipefail","TS_CLOUD_HC_LOCK=/run/lock/ts-cloud-host-cleanup.lock","[ -d /run/lock ] || TS_CLOUD_HC_LOCK=/tmp/ts-cloud-host-cleanup.lock",'if command -v flock >/dev/null 2>&1 && exec 9>"$TS_CLOUD_HC_LOCK"; then',' if ! flock -n 9; then echo "[ts-cloud] host cleanup already running; skipping"; exit 0; fi',"fi",...Ir(e),"exit 0"]}function Or(e,t,r,n,s){return[`cat > ${e}.ts-cloud-new <<'${r}' || true`,...t,r,`if [ -s ${e}.ts-cloud-new ] && ! cmp -s ${e}.ts-cloud-new ${e}; then chmod ${n} ${e}.ts-cloud-new && mv -f ${e}.ts-cloud-new ${e}${s?` && ${s}=1`:""} || true; else rm -f ${e}.ts-cloud-new; fi`]}function Ps(e){let t=Nt(e),r="/etc/systemd/system/ts-cloud-host-cleanup",n=["if [ -f /etc/systemd/system/ts-cloud-host-cleanup.timer ]; then"," systemctl disable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true"," rm -f /etc/systemd/system/ts-cloud-host-cleanup.timer /etc/systemd/system/ts-cloud-host-cleanup.service"," systemctl daemon-reload >/dev/null 2>&1 || true","fi"];if(!t.enabled)return[...n,"rm -f /usr/local/bin/ts-cloud-host-cleanup.sh"];let s=Or("/usr/local/bin/ts-cloud-host-cleanup.sh",Cs(e),"TS_CLOUD_HOST_CLEANUP_EOF","755");if(!t.timer)return[...s,...n];return[...s,"TS_CLOUD_HC_UNITS_CHANGED=0","if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then",...Or("/etc/systemd/system/ts-cloud-host-cleanup.service",["[Unit]","Description=ts-cloud host cleanup (bounded disk retention)","After=local-fs.target","","[Service]","Type=oneshot","ExecStart=/usr/local/bin/ts-cloud-host-cleanup.sh","Nice=19","IOSchedulingClass=idle"],"TS_CLOUD_HOST_CLEANUP_SVC_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),...Or("/etc/systemd/system/ts-cloud-host-cleanup.timer",["[Unit]","Description=Run ts-cloud host cleanup on a schedule, independent of deploys","","[Timer]",`OnCalendar=${t.schedule}`,"RandomizedDelaySec=1h","Persistent=true","","[Install]","WantedBy=timers.target"],"TS_CLOUD_HOST_CLEANUP_TMR_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),' if [ "$TS_CLOUD_HC_UNITS_CHANGED" -eq 1 ]; then systemctl daemon-reload >/dev/null 2>&1 || true; fi'," systemctl enable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true","fi"]}function Ar(e){let t=Ps(e);if(!Nt(e).enabled)return t;return[...t,'if [ -x /usr/local/bin/ts-cloud-host-cleanup.sh ]; then /usr/local/bin/ts-cloud-host-cleanup.sh < /dev/null || echo "[ts-cloud] warning: host cleanup exited $?" >&2; fi']}var ja=/\.(?:[cm]?[jt]sx?)$/;function Hr(e,t,r){let n=t==="bun"?"/usr/local/bin/bun":t==="deno"?"/usr/local/bin/deno":"/usr/local/bin/node",s=e.trim();if(/^(?:bun|node|deno)\s+/.test(s))return`${n} ${s.replace(/^(?:bun|node|deno)\s+/,"")}`;let[i="",...o]=s.split(/\s+/);if(i&&!ja.test(i)){let a=i.startsWith("/")?i:r&&i.startsWith("./")?`${r.replace(/\/+$/,"")}/${i.replace(/^\.\//,"")}`:void 0;if(a)return[a,...o].join(" ")}return`${n} ${s}`}function xs(e){return["TS_CLOUD_MEM_MB=$(awk '/^MemTotal:/{print int($2/1024)}' /proc/meminfo)","TS_CLOUD_HIGH_MB=$((TS_CLOUD_MEM_MB / 8))",'[ "$TS_CLOUD_HIGH_MB" -lt 512 ] && TS_CLOUD_HIGH_MB=512','[ "$TS_CLOUD_HIGH_MB" -gt 4096 ] && TS_CLOUD_HIGH_MB=4096',`mkdir -p /etc/systemd/system/${e}.d`,`printf '# Written by ts-cloud: memoryHigh resolved from this box.
257
+ `)}function Ua(e){return`"${e.replace(/\\/g,"\\\\").replace(/"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t")}"`}var ka=new Set(["sqlite","sqlite3"]),Ma=new Set([":memory:","memory"]);function Fa(e){let t=e?.DB_CONNECTION?.trim().toLowerCase();return t!=null&&ka.has(t)}function za(e){let t=e.trim().replace(/^\.\//,"");if(t===""||t.startsWith("/")||t.startsWith("~"))return;if(t.split("/").includes(".."))return;return t.replace(/\/+$/,"")}function xr(e,t=[]){if(!Fa(e))return{};let r=e?.DB_DATABASE_PATH?.trim()||e?.DB_DATABASE?.trim();if(r&&Ma.has(r.toLowerCase()))return{};if(!r)return{warning:"DB_CONNECTION is sqlite but neither DB_DATABASE_PATH nor DB_DATABASE names a file, so ts-cloud cannot tell where the database lives. If it is written inside the release directory, the next deploy discards it. Set DB_DATABASE_PATH, or list the file in the site's `sharedPaths`."};let n=za(r);if(!n)return{};if(t.some((s)=>Xs(s)===n))return{};return{path:n}}function Lt(e,t=[]){let{path:r}=xr(e,t);return r?[r]:[]}var Ge="/var/ts-cloud/artifacts";var ws=Object.freeze({stagingMaxAgeMinutes:60,artifactMaxAgeDays:2,artifactUploadMaxAgeMinutes:60,bunCacheMaxAgeDays:7,journalMaxAgeDays:14,journalMaxSizeMb:512,containerImageMaxAgeHours:168}),Ts=Object.freeze({artifactMaxAgeDays:0,bunCacheMaxAgeDays:1,journalMaxAgeDays:3,journalMaxSizeMb:256,containerImageMaxAgeHours:24});function hs(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Ae(e,t,r=0){if(!Number.isInteger(t)||t<r)throw Error(`hostCleanup: ${e} must be a whole number >= ${r} (got ${t})`)}function Ss(e,t){for(let[r,n]of Object.entries(t))Ae(`${e}.${r}`,n);Ae(`${e}.stagingMaxAgeMinutes`,t.stagingMaxAgeMinutes,15),Ae(`${e}.artifactUploadMaxAgeMinutes`,t.artifactUploadMaxAgeMinutes,15),Ae(`${e}.journalMaxSizeMb`,t.journalMaxSizeMb,1)}function Ba(e,t){let r=`hostCleanup.paths[${t}]`,n=e.path?.replace(/\/+$/,"")??"";if(!n.startsWith("/")||n===""||/[\s'"`$\\]/.test(n)||n.split("/").includes(".."))throw Error(`${r}.path must be an absolute directory with no spaces, quotes or '..' (got ${JSON.stringify(e.path)})`);if(n==="/var/www"||n.startsWith("/var/www/"))throw Error(`${r}.path must not be under /var/www: releases are pruned by keepReleases`);if(!e.pattern||/^[*?]+$/.test(e.pattern)||e.pattern.includes("/")||/[\n'"`]/.test(e.pattern))throw Error(`${r}.pattern must be a file-name glob naming something, not a bare wildcard (got ${JSON.stringify(e.pattern)})`);if(Ae(`${r}.maxAgeDays`,e.maxAgeDays),e.type!==void 0&&e.type!=="file"&&e.type!=="directory")throw Error(`${r}.type must be 'file' or 'directory'`)}function Nt(e){let t=typeof e==="object"&&e!==null?e:{},r=e!==!1,n={...ws,...ys(t.retention)};Ss("hostCleanup.retention",n);let s=null;if(t.pressure!==!1){let a=typeof t.pressure==="object"?t.pressure:{},c=a.lowWaterPercent??50,l=a.highWaterPercent??85;if(Ae("hostCleanup.pressure.lowWaterPercent",c),Ae("hostCleanup.pressure.highWaterPercent",l,1),l>100||c>=l)throw Error(`hostCleanup.pressure: need 0 <= lowWaterPercent < highWaterPercent <= 100 (got ${c} and ${l})`);let u={...Ts,...ys(a.escalated)},d=Object.fromEntries(Object.keys(n).map((m)=>[m,Math.min(n[m],u[m]??n[m])]));Ss("hostCleanup.pressure.escalated",d),s={lowWaterPercent:c,highWaterPercent:l,escalated:d}}let i=t.schedule??"daily";if(!/^[\w*:,./~ -]+$/.test(i))throw Error(`hostCleanup.schedule must be a systemd OnCalendar expression (got ${JSON.stringify(i)})`);let o=t.paths??[];return o.forEach(Ba),{enabled:r,timer:r&&t.timer!==!1,schedule:i,retention:n,pressure:s,paths:o}}function ys(e){return Object.fromEntries(Object.entries(e??{}).filter(([,t])=>t!==void 0))}var ut={stagingMaxAgeMinutes:"TS_CLOUD_HC_STAGING_MIN",artifactMaxAgeDays:"TS_CLOUD_HC_ARTIFACT_DAYS",artifactUploadMaxAgeMinutes:"TS_CLOUD_HC_UPLOAD_MIN",bunCacheMaxAgeDays:"TS_CLOUD_HC_BUN_DAYS",journalMaxAgeDays:"TS_CLOUD_HC_JOURNAL_DAYS",journalMaxSizeMb:"TS_CLOUD_HC_JOURNAL_MB",containerImageMaxAgeHours:"TS_CLOUD_HC_IMAGE_HOURS"};function Rr(e){return Object.keys(ut).map((t)=>`${ut[t]}=${e[t]}`).join(" ")}var we=(e)=>`"$${ut[e]}"`;function vs(e){return[`find /var/ts-cloud/staging -xdev -maxdepth 1 -type f -mmin +${we("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /tmp -xdev -maxdepth 1 -type f -name "*-release.tar.gz" -mmin +${we("stagingMaxAgeMinutes")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name "*.tar.gz" -mtime +${we("artifactMaxAgeDays")} -delete 2>/dev/null || true`,`find /var/ts-cloud/artifacts -xdev -maxdepth 1 -type f -name ".*.tmp" -mmin +${we("artifactUploadMaxAgeMinutes")} -delete 2>/dev/null || true`,...e.map(Ka),`journalctl --vacuum-time=${we("journalMaxAgeDays")}d --vacuum-size=${we("journalMaxSizeMb")}M >/dev/null 2>&1 || true`]}function Ka(e){let t=hs(e.path.replace(/\/+$/,"")),r=hs(e.pattern);return e.type==="directory"?`find ${t} -xdev -mindepth 1 -maxdepth 1 -type d -name ${r} -mtime +${e.maxAgeDays} -exec rm -rf -- {} + 2>/dev/null || true`:`find ${t} -xdev -mindepth 1 -maxdepth 1 -type f -name ${r} -mtime +${e.maxAgeDays} -delete 2>/dev/null || true`}function bs(){return[`find /root/.bun/install/cache -xdev -type f -mtime +${we("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`find /root/.bun/install/cache -xdev -depth -type d -empty -mtime +${we("bunCacheMaxAgeDays")} -delete 2>/dev/null || true`,`if command -v docker >/dev/null 2>&1; then docker image prune --all --force --filter "until=\${${ut.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,`if command -v podman >/dev/null 2>&1; then podman image prune --all --force --filter "until=\${${ut.containerImageMaxAgeHours}}h" >/dev/null 2>&1 || true; fi`,"if command -v apt-get >/dev/null 2>&1; then apt-get clean >/dev/null 2>&1 || true; fi"]}function _s(e){return[`${e}="$(df -P / 2>/dev/null | awk 'NR==2 {sub(/%/, "", $5); print $5}' || true)"`,`case "$${e}" in ''|*[!0-9]*) ${e}=-1 ;; esac`]}function Ir(e){let{retention:t,pressure:r,paths:n}=Nt(e),s='echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',i='echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"';if(!r)return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',Rr(t),...vs(n),...bs(),'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"'];let{lowWaterPercent:o,highWaterPercent:a,escalated:c}=r;return['echo "[ts-cloud] host cleanup (disk before): $(df -h / | tail -1)"',..._s("TS_CLOUD_HC_BEFORE"),"TS_CLOUD_HC_TIER=normal",Rr(t),`if [ "$TS_CLOUD_HC_BEFORE" -ge 0 ] && [ "$TS_CLOUD_HC_BEFORE" -lt ${o} ]; then`," TS_CLOUD_HC_TIER=light",` echo "[ts-cloud] host cleanup: / at \${TS_CLOUD_HC_BEFORE}% (under ${o}%), skipping cache and image pruning"`,`elif [ "$TS_CLOUD_HC_BEFORE" -ge ${a} ]; then`," TS_CLOUD_HC_TIER=pressure",` ${Rr(c)}`,` echo "[ts-cloud] warning: / at \${TS_CLOUD_HC_BEFORE}% (at or above ${a}%), host cleanup is using shortened retention windows" >&2`,"fi",...vs(n),'if [ "$TS_CLOUD_HC_TIER" != light ]; then',...bs().map((l)=>` ${l}`),"fi",'echo "[ts-cloud] host cleanup (disk after): $(df -h / | tail -1)"',..._s("TS_CLOUD_HC_AFTER"),`echo "[ts-cloud] host-cleanup {\\"event\\":\\"host-cleanup\\",\\"tier\\":\\"$TS_CLOUD_HC_TIER\\",\\"diskBeforePercent\\":$TS_CLOUD_HC_BEFORE,\\"diskAfterPercent\\":$TS_CLOUD_HC_AFTER,\\"lowWaterPercent\\":${o},\\"highWaterPercent\\":${a}}"`,"mkdir -p /var/lib/ts-cloud 2>/dev/null || true",'TS_CLOUD_HC_PREV="$(cat /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || echo ok)"',`if [ "$TS_CLOUD_HC_AFTER" -ge ${a} ]; then`,` echo "[ts-cloud] warning: / still at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%); something outside ts-cloud's retention rules is filling the disk" >&2`,` if [ "$TS_CLOUD_HC_PREV" != alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "⚠️ $(hostname): / at \${TS_CLOUD_HC_AFTER}% after host cleanup (high-water ${a}%)" >/dev/null 2>&1 || true; fi`," echo alert > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","else",` if [ "$TS_CLOUD_HC_PREV" = alert ] && [ -x /usr/local/bin/ts-cloud-notify ]; then /usr/local/bin/ts-cloud-notify "✅ $(hostname): / back under ${a}% after host cleanup (\${TS_CLOUD_HC_AFTER}%)" >/dev/null 2>&1 || true; fi`," echo ok > /var/lib/ts-cloud/host-cleanup.alert 2>/dev/null || true","fi"]}function Cs(e){return["#!/usr/bin/env bash","# Managed by ts-cloud: rewritten on every deploy. Configure compute.hostCleanup instead of editing.","set -uo pipefail","TS_CLOUD_HC_LOCK=/run/lock/ts-cloud-host-cleanup.lock","[ -d /run/lock ] || TS_CLOUD_HC_LOCK=/tmp/ts-cloud-host-cleanup.lock",'if command -v flock >/dev/null 2>&1 && exec 9>"$TS_CLOUD_HC_LOCK"; then',' if ! flock -n 9; then echo "[ts-cloud] host cleanup already running; skipping"; exit 0; fi',"fi",...Ir(e),"exit 0"]}function Or(e,t,r,n,s){return[`cat > ${e}.ts-cloud-new <<'${r}' || true`,...t,r,`if [ -s ${e}.ts-cloud-new ] && ! cmp -s ${e}.ts-cloud-new ${e}; then chmod ${n} ${e}.ts-cloud-new && mv -f ${e}.ts-cloud-new ${e}${s?` && ${s}=1`:""} || true; else rm -f ${e}.ts-cloud-new; fi`]}function Ps(e){let t=Nt(e),r="/etc/systemd/system/ts-cloud-host-cleanup",n=["if [ -f /etc/systemd/system/ts-cloud-host-cleanup.timer ]; then"," systemctl disable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true"," rm -f /etc/systemd/system/ts-cloud-host-cleanup.timer /etc/systemd/system/ts-cloud-host-cleanup.service"," systemctl daemon-reload >/dev/null 2>&1 || true","fi"];if(!t.enabled)return[...n,"rm -f /usr/local/bin/ts-cloud-host-cleanup.sh"];let s=Or("/usr/local/bin/ts-cloud-host-cleanup.sh",Cs(e),"TS_CLOUD_HOST_CLEANUP_EOF","755");if(!t.timer)return[...s,...n];return[...s,"TS_CLOUD_HC_UNITS_CHANGED=0","if command -v systemctl >/dev/null 2>&1 && [ -d /etc/systemd/system ]; then",...Or("/etc/systemd/system/ts-cloud-host-cleanup.service",["[Unit]","Description=ts-cloud host cleanup (bounded disk retention)","After=local-fs.target","","[Service]","Type=oneshot","ExecStart=/usr/local/bin/ts-cloud-host-cleanup.sh","Nice=19","IOSchedulingClass=idle"],"TS_CLOUD_HOST_CLEANUP_SVC_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),...Or("/etc/systemd/system/ts-cloud-host-cleanup.timer",["[Unit]","Description=Run ts-cloud host cleanup on a schedule, independent of deploys","","[Timer]",`OnCalendar=${t.schedule}`,"RandomizedDelaySec=1h","Persistent=true","","[Install]","WantedBy=timers.target"],"TS_CLOUD_HOST_CLEANUP_TMR_EOF","644","TS_CLOUD_HC_UNITS_CHANGED"),' if [ "$TS_CLOUD_HC_UNITS_CHANGED" -eq 1 ]; then systemctl daemon-reload >/dev/null 2>&1 || true; fi'," systemctl enable --now ts-cloud-host-cleanup.timer >/dev/null 2>&1 || true","fi"]}function Ar(e){let t=Ps(e);if(!Nt(e).enabled)return t;return[...t,'if [ -x /usr/local/bin/ts-cloud-host-cleanup.sh ]; then /usr/local/bin/ts-cloud-host-cleanup.sh < /dev/null || echo "[ts-cloud] warning: host cleanup exited $?" >&2; fi']}var ja=/\.(?:[cm]?[jt]sx?)$/;function Hr(e,t,r){let n=t==="bun"?"/usr/local/bin/bun":t==="deno"?"/usr/local/bin/deno":"/usr/local/bin/node",s=e.trim();if(/^(?:bun|node|deno)\s+/.test(s))return`${n} ${s.replace(/^(?:bun|node|deno)\s+/,"")}`;let[i="",...o]=s.split(/\s+/);if(i&&!ja.test(i)){let a=i.startsWith("/")?i:r&&i.startsWith("./")?`${r.replace(/\/+$/,"")}/${i.replace(/^\.\//,"")}`:void 0;if(a)return[a,...o].join(" ")}return`${n} ${s}`}function xs(e){return["TS_CLOUD_MEM_MB=$(awk '/^MemTotal:/{print int($2/1024)}' /proc/meminfo)","TS_CLOUD_HIGH_MB=$((TS_CLOUD_MEM_MB / 8))",'[ "$TS_CLOUD_HIGH_MB" -lt 512 ] && TS_CLOUD_HIGH_MB=512','[ "$TS_CLOUD_HIGH_MB" -gt 4096 ] && TS_CLOUD_HIGH_MB=4096',`mkdir -p /etc/systemd/system/${e}.d`,`printf '# Written by ts-cloud: memoryHigh resolved from this box.
258
258
  # An eighth of %sMB RAM. Set memoryHigh in config to override.
259
259
  [Service]
260
260
  MemoryAccounting=true
@@ -273,16 +273,16 @@ ${As(t.before)}write_files:
273
273
  ${s}
274
274
  runcmd:
275
275
  - [ bash, ${r} ]
276
- ${As(t.after)}`}function tn(e){let t=new Set([80,443,...e.sitePorts,...e.allowedPorts??[]]);if(e.allowSsh)t.add(22);return[...t].map((r)=>({direction:"in",protocol:"tcp",port:String(r),source_ips:["0.0.0.0/0","::/0"],description:`ts-cloud port ${r}`}))}var Wr={micro:"cpx11",small:"cx23",medium:"cx33",large:"cx43",xlarge:"cx53","2xlarge":"ccx33"};function pt(e){if(!e)return Wr.micro;if(e in Wr)return Wr[e];return e}var j="ts-cloud";function W(e,t,r="app"){return{[`${j}/project`]:e,[`${j}/environment`]:t,[`${j}/role`]:r,[`${j}/managed-by`]:"ts-cloud"}}function ee(e,t,r,n="app"){return Gr(e,t,r)&&e[`${j}/role`]===n}function Gr(e,t,r){if(!e)return!1;return e[`${j}/project`]===t&&e[`${j}/environment`]===r}import{mkdir as ec,readFile as tc,rename as rc,writeFile as nc}from"node:fs/promises";import{join as Ls}from"node:path";var Vr="storage/cloud/state";function we(e=process.cwd()){return ca()?A(e,"state"):Ls(e,Vr)}function Ye(e,t=process.cwd()){return Ls(we(t),`${e}.json`)}async function Ne(e,t=process.cwd()){try{let r=await tc(Ye(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function Qe(e,t,r=process.cwd()){let n=Ye(e,r);await ec(we(r),{recursive:!0});let s=`${n}.${process.pid}.tmp`;await nc(s,`${JSON.stringify(t,null,2)}
277
- `,"utf8"),await rc(s,n)}async function ge(e){return Ne(e)}async function mt(e,t){await Qe(e,t)}var qr=268435456;function vt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function lc(e){let t=ic("sha256");for await(let r of oc(e))t.update(r);return t.digest("hex")}class $e{name="hetzner";usesCloudFormation=!1;client;sshPrivateKeyPath;sshPublicKeyPath;sshUser;location;stateOnly;waitForBoot;bootWait;constructor(e={}){let t=ht(e.apiToken);if(this.stateOnly=!e.client&&!t&&e.allowStateOnly===!0,!e.client&&!t&&!this.stateOnly)throw Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");this.client=e.client??new Je({apiToken:t??""});let r=ur(void 0,{sshPrivateKeyPath:e.sshPrivateKeyPath,sshPublicKeyPath:e.sshPublicKeyPath,sshUser:e.sshUser,location:e.location});this.sshPrivateKeyPath=r.sshPrivateKeyPath,this.sshPublicKeyPath=r.sshPublicKeyPath,this.sshUser=r.sshUser,this.location=r.location,this.waitForBoot=e.waitForBoot??!0,this.bootWait={sshIntervalMs:e.bootWait?.sshIntervalMs??5000,sshTimeoutMs:e.bootWait?.sshTimeoutMs??300000,cloudInitIntervalMs:e.bootWait?.cloudInitIntervalMs??5000,cloudInitTimeoutMs:e.bootWait?.cloudInitTimeoutMs??600000}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=t.project.slug,s=t.infrastructure?.compute;if(!s)throw Error("infrastructure.compute is required to provision Hetzner compute");if(t.cloud?.attachTo)return this.attachToComputeInfrastructure(t.cloud.attachTo,t,r);let i=T(t,r),o=`${n}-${r}-app`,a=s.runtime==="php"||!!s.php,c=Os(s);if(c.dedicatedServices||c.appServers>1)return a?this.provisionFleet(e,c):this.provisionBunFleet(e,c);let l=W(n,r,"app"),u=t.sites||{},d=Z(s)?[]:this.collectUpstreamPorts(u),m=`${n}-${r}-app-fw`,{firewall:f}=await this.ensureFirewall(m,l,tn({allowSsh:s.allowSsh!==!1,sitePorts:d,allowedPorts:[...s.firewall?.allowedPorts??[],...Jn(t.infrastructure?.sftp),...Ur(ne(t,{environment:r}))]})),h=await this.ensureSshKey(n,r,l),R=await ge(i);if(R?.serverId){let k=await this.tryGetServer(R.serverId);if(k&&k.status!=="off")return this.outputsFromState(R,k)}let x=await this.findExistingServer(n,r,o);if(x&&x.status!=="off"){let k={provider:"hetzner",stackName:i,serverId:x.id,serverName:x.name,publicIp:x.public_net.ipv4?.ip,publicIpv6:me(x.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(i,k),this.outputsFromState(k,x)}let b=le(t,{environment:r}),_=b?.engine==="rpx"?pe({proxy:b,config:Te(u,{proxy:b,slug:t.project.slug,mail:ne(t,{environment:r})}),slug:t.project.slug,bunBin:s.runtime==="node"||s.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,P=De(t,{environment:r}),S=s.bakedImage===!0,E=Y({runtime:P.runtime,runtimeVersion:P.runtimeVersion,systemPackages:s.systemPackages,database:t.infrastructure?.database,phpProvision:P.phpProvision,servicesProvision:P.servicesProvision,rpxProvision:_,baked:S,swapGb:s.swapGb}),y=de(E),C=pt(s.size),H=St(t),{server:te,action:oe}=await this.client.createServer({name:o,serverType:C,image:H,location:t.hetzner?.location||this.location,userData:y,labels:l,sshKeys:h?[h]:void 0,firewalls:[{firewall:f.id}]});await this.client.waitForAction(oe.id);let N=await this.client.waitForServerRunning(te.id),q={provider:"hetzner",stackName:i,serverId:N.id,serverName:N.name,firewallId:f.id,publicIp:N.public_net.ipv4?.ip,publicIpv6:me(N.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};await mt(i,q);let K=N.public_net.ipv4?.ip;if(K&&this.waitForBoot)await this.waitForSshReady(K),await this.waitForCloudInit(K);return this.outputsFromState(q,N)}async provisionFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=pt(i.size),c=St(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=await ge(o);if(d?.loadBalancerId){let M=(await this.client.listLoadBalancers().catch(()=>[])).find((D)=>D.id===d.loadBalancerId);if(M)return{appPublicIp:M.public_net?.ipv4?.ip,loadBalancerIp:M.public_net?.ipv4?.ip,servicesPrivateIp:d.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}let m=await this.ensureSshKey(s,n,W(s,n,"app")),f=`${s}-${n}-net`,R=(await this.client.listNetworks().catch(()=>[])).find((L)=>L.name===f)??await this.client.createNetwork({name:f,labels:W(s,n,"app")}),{firewall:x}=await this.ensureFirewall(`${s}-${n}-app-fw`,W(s,n,"app"),tn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),{firewall:b}=await this.ensureFirewall(`${s}-${n}-services-fw`,W(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[R.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[R.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[R.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[R.ip_range]}]),_=Kr(r),P=de(Y({runtime:"php",servicesProvision:_,baked:u,swapGb:i.swapGb})),S=await this.client.listServers().catch(()=>[]),E=[],y=S.find((L)=>ee(L.labels,s,n,"services"));if(!y){let{server:L,action:M}=await this.client.createServer({name:`${s}-${n}-services`,serverType:pt(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:P,labels:W(s,n,"services"),sshKeys:m?[m]:void 0,firewalls:[{firewall:b.id}],networks:[R.id]});await this.client.waitForAction(M.id),y=await this.client.waitForServerRunning(L.id),E.push(L.id)}let C=y.id,H=y.private_net?.[0]?.ip??(await this.client.getServer(C)).private_net?.[0]?.ip,te=[...ye(!0),...ve(!0),...Be(i.sshKeys),...ke(r.notifications)],oe=xt({versions:i.php?.versions,default:i.php?.default,extensions:i.php?.extensions,installNginx:!Z(i),optimizeForProduction:i.php?.optimizeForProduction,ini:i.php?.ini}),N=de(Y({runtime:"php",phpProvision:oe,servicesProvision:te,baked:u,swapGb:i.swapGb})),q=S.filter((L)=>ee(L.labels,s,n,"app")),K=q.map((L)=>L.id);if(q.length>t.appServers)for(let L of q.slice(t.appServers))await this.client.deleteServer(L.id).catch(()=>{}),K.splice(K.indexOf(L.id),1);for(let L=q.length;L<t.appServers;L++){let{server:M,action:D}=await this.client.createServer({name:`${s}-${n}-app-${L+1}`,serverType:a,image:c,location:l,userData:N,labels:W(s,n,"app"),sshKeys:m?[m]:void 0,firewalls:[{firewall:x.id}],networks:[R.id]});await this.client.waitForAction(D.id),K.push(M.id),E.push(M.id)}if(this.waitForBoot&&E.length>0)await Promise.all(E.map(async(L)=>{let D=(await this.client.waitForServerRunning(L)).public_net.ipv4?.ip;if(D)await this.waitForSshReady(D),await this.waitForCloudInit(D)}));let k=`${s}-${n}-lb`,ae,he;if(t.loadBalancer){let M=(await this.client.listLoadBalancers().catch(()=>[])).find((D)=>D.name===k)??await this.client.createLoadBalancer({name:k,location:l,network:R.id,labels:W(s,n,"lb"),labelSelector:`ts-cloud/project=${s},ts-cloud/environment=${n},ts-cloud/role=app`,services:[{listenPort:80,destinationPort:80},{listenPort:443,destinationPort:443}]});he=M.id,ae=M.public_net?.ipv4?.ip}let xe=ae??(await this.client.getServer(K[0]).catch(()=>{return}))?.public_net.ipv4?.ip,re={provider:"hetzner",stackName:o,networkId:R.id,loadBalancerId:he,servicesServerId:C,servicesPrivateIp:H,publicIp:xe,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(o,re),{appPublicIp:xe,loadBalancerIp:ae,servicesPrivateIp:H,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async provisionBunFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=pt(i.size),c=St(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=r.sites||{},m=await ge(o);if(m?.lbServerId){let D=await this.tryGetServer(m.lbServerId);if(D&&D.status!=="off"){let F=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"},z=await this.findComputeTargets({slug:s,environment:n,role:"app",stackName:o});return this.refreshLbRoutes(D,d,z.map((ce)=>({privateIp:ce.privateIp,publicIp:ce.publicIp})),F,s),{appPublicIp:D.public_net.ipv4?.ip??m.publicIp,loadBalancerIp:D.public_net.ipv4?.ip??m.publicIp,servicesPrivateIp:m.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}}let f=await this.ensureSshKey(s,n,W(s,n,"app")),h=`${s}-${n}-net`,x=(await this.client.listNetworks().catch(()=>[])).find((D)=>D.name===h)??await this.client.createNetwork({name:h,labels:W(s,n,"app")}),b=this.collectUpstreamPorts(d),{firewall:_}=await this.ensureFirewall(`${s}-${n}-app-fw`,W(s,n,"app"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},...b.map((D)=>({direction:"in",protocol:"tcp",port:String(D),source_ips:[x.ip_range],description:`ts-cloud port ${D} (private, LB-only)`}))]),{firewall:P}=await this.ensureFirewall(`${s}-${n}-lb-fw`,W(s,n,"lb"),tn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),S=await this.client.listServers().catch(()=>[]),E=[],y,C,H=!!i.servicesServer||!!i.managedServices,te=t.dedicatedServices&&H;if(te){let{firewall:D}=await this.ensureFirewall(`${s}-${n}-services-fw`,W(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[x.ip_range]}]),F=Kr(r),z=de(Y({runtime:"bun",servicesProvision:F,baked:u,swapGb:i.swapGb})),ce=S.find((Se)=>ee(Se.labels,s,n,"services"));if(!ce){let{server:Se,action:st}=await this.client.createServer({name:`${s}-${n}-services`,serverType:pt(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:z,labels:W(s,n,"services"),sshKeys:f?[f]:void 0,firewalls:[{firewall:D.id}],networks:[x.id]});await this.client.waitForAction(st.id),ce=await this.client.waitForServerRunning(Se.id),E.push(Se.id)}y=ce.id,C=ce.private_net?.[0]?.ip??(await this.client.getServer(y)).private_net?.[0]?.ip}let oe=te?{...i,managedServices:void 0,backups:void 0}:i,N=De({...r,infrastructure:{...r.infrastructure,compute:oe}},{environment:n}),q=de(Y({runtime:N.runtime,runtimeVersion:N.runtimeVersion,systemPackages:i.systemPackages,database:r.infrastructure?.database,servicesProvision:N.servicesProvision,baked:u,swapGb:i.swapGb})),K=S.filter((D)=>ee(D.labels,s,n,"app")),k=K.map((D)=>D.id);if(K.length>t.appServers)for(let D of K.slice(t.appServers))await this.client.deleteServer(D.id).catch(()=>{}),k.splice(k.indexOf(D.id),1);for(let D=K.length;D<t.appServers;D++){let{server:F,action:z}=await this.client.createServer({name:`${s}-${n}-app-${D+1}`,serverType:a,image:c,location:l,userData:q,labels:W(s,n,"app"),sshKeys:f?[f]:void 0,firewalls:[{firewall:_.id}],networks:[x.id]});await this.client.waitForAction(z.id),k.push(F.id),E.push(F.id)}if(this.waitForBoot&&E.length>0)await Promise.all(E.map(async(D)=>{let z=(await this.client.waitForServerRunning(D)).public_net.ipv4?.ip;if(z)await this.waitForSshReady(z),await this.waitForCloudInit(z)}));let ae=[];for(let D of k){let F=S.find((z)=>z.id===D);if(!F||!F.private_net?.[0]?.ip)F=await this.client.getServer(D).catch(()=>F);ae.push({privateIp:F?.private_net?.[0]?.ip,publicIp:F?.public_net.ipv4?.ip})}let he=`${s}-${n}-lb`,xe,re;if(t.loadBalancer){let D=S.find((z)=>ee(z.labels,s,n,"lb")),F=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"};if(!D){let z=pe({proxy:F,config:ze(d,ae,{proxy:F,slug:s}),slug:s,bunBin:N.runtime==="node"||N.runtime==="deno"?void 0:"/usr/local/bin/bun"}),ce=de(Y({runtime:"bun",rpxProvision:z,baked:!1})),Se=typeof i.loadBalancer==="object"?i.loadBalancer.size:void 0,{server:st,action:nr}=await this.client.createServer({name:he,serverType:pt(Se??"micro"),image:c,location:l,userData:ce,labels:W(s,n,"lb"),sshKeys:f?[f]:void 0,firewalls:[{firewall:P.id}],networks:[x.id]});if(await this.client.waitForAction(nr.id),D=await this.client.waitForServerRunning(st.id),this.waitForBoot){let it=D.public_net.ipv4?.ip;if(it)await this.waitForSshReady(it),await this.waitForCloudInit(it)}}else this.refreshLbRoutes(D,d,ae,F,s);xe=D.id,re=D.public_net.ipv4?.ip}let L=re??(await this.client.getServer(k[0]).catch(()=>{return}))?.public_net.ipv4?.ip,M={provider:"hetzner",stackName:o,networkId:x.id,lbServerId:xe,appServerIds:k,servicesServerId:y,servicesPrivateIp:C,publicIp:L,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(o,M),{appPublicIp:L,loadBalancerIp:re,servicesPrivateIp:C,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=T(t,r),i=await ge(s),o=[],a=`${n}-${r}-lb`,l=(await this.client.listLoadBalancers().catch(()=>[])).find((_)=>_.name===a);if(l)try{await this.client.deleteLoadBalancer(l.id),o.push(`load balancer ${a}`)}catch{}let u=await this.client.listServers().catch(()=>[]),d=new Set;for(let _ of u)if(ee(_.labels,n,r,"app")||ee(_.labels,n,r,"services")||ee(_.labels,n,r,"lb"))d.add(_.id);if(i?.serverId)d.add(i.serverId);if(i?.servicesServerId)d.add(i.servicesServerId);if(i?.lbServerId)d.add(i.lbServerId);for(let _ of i?.appServerIds??[])d.add(_);for(let _ of d)try{await this.client.deleteServer(_),o.push(`server ${_}`)}catch{}let m=await this.client.listFirewalls().catch(()=>[]);for(let _ of[`${n}-${r}-app-fw`,`${n}-${r}-services-fw`,`${n}-${r}-lb-fw`]){let P=m.find((S)=>S.name===_);if(!P)continue;for(let S=0;S<12;S++)try{await this.client.deleteFirewall(P.id),o.push(`firewall ${_}`);break}catch{await this.sleep(3000)}}let f=`${n}-${r}-net`,R=(await this.client.listNetworks().catch(()=>[])).find((_)=>_.name===f);if(R)for(let _=0;_<12;_++)try{await this.client.deleteNetwork(R.id),o.push(`network ${f}`);break}catch{await this.sleep(3000)}let b=(await this.client.listSshKeys().catch(()=>[])).filter((_)=>Gr(_.labels,n,r));for(let _ of b)try{await this.client.deleteSshKey(_.id),o.push(`ssh key ${_.name}`)}catch{}return await mt(s,{provider:"hetzner",stackName:s}).catch(()=>{}),{destroyed:o}}async getComputeOutputs(e){let t=T(e.config,e.environment),r=await ge(t);if(r?.serverId){let i=await this.tryGetServer(r.serverId);if(i)return this.outputsFromState(r,i)}if(r?.lbServerId){let o=(await this.tryGetServer(r.lbServerId))?.public_net.ipv4?.ip??r.publicIp;return{...this.outputsFromState(r),appPublicIp:o,loadBalancerIp:o}}if(r?.loadBalancerId||r?.servicesPrivateIp)return this.outputsFromState(r);let s=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)}))[0];return{deployStoragePath:"/var/ts-cloud/staging",appInstanceId:s?.id,appPublicIp:s?.publicIp,sshUser:this.sshUser}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)});if(t.length===0)throw Error("No Hetzner compute targets found for release upload");let s=`/var/ts-cloud/staging/${`${e.remoteKey.replace(/^releases\//,"").replace(/\.tar\.gz$/,"").replace(/\//g,"-")}-${Hs()}.tar.gz`}`,i=await lc(e.localPath),o=Ge,a=`${o}/${i}.tar.gz`;for(let c of t){if(!c.publicIp)throw Error(`Target ${c.id} has no public IP for SCP upload`);let l=["set -euo pipefail",`mkdir -p ${vt(o)} /var/ts-cloud/staging`,`test -s ${vt(a)}`,`cp -- ${vt(a)} ${vt(s)}`].join(`
276
+ ${As(t.after)}`}function tn(e){let t=new Set([80,443,...e.sitePorts,...e.allowedPorts??[]]);if(e.allowSsh)t.add(22);return[...t].map((r)=>({direction:"in",protocol:"tcp",port:String(r),source_ips:["0.0.0.0/0","::/0"],description:`ts-cloud port ${r}`}))}var Wr={micro:"cpx11",small:"cx23",medium:"cx33",large:"cx43",xlarge:"cx53","2xlarge":"ccx33"};function pt(e){if(!e)return Wr.micro;if(e in Wr)return Wr[e];return e}var j="ts-cloud";function W(e,t,r="app"){return{[`${j}/project`]:e,[`${j}/environment`]:t,[`${j}/role`]:r,[`${j}/managed-by`]:"ts-cloud"}}function ee(e,t,r,n="app"){return Gr(e,t,r)&&e[`${j}/role`]===n}function Gr(e,t,r){if(!e)return!1;return e[`${j}/project`]===t&&e[`${j}/environment`]===r}import{mkdir as ec,readFile as tc,rename as rc,writeFile as nc}from"node:fs/promises";import{join as Ls}from"node:path";var Vr="storage/cloud/state";function $e(e=process.cwd()){return ca()?A(e,"state"):Ls(e,Vr)}function Ye(e,t=process.cwd()){return Ls($e(t),`${e}.json`)}async function Ne(e,t=process.cwd()){try{let r=await tc(Ye(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function Qe(e,t,r=process.cwd()){let n=Ye(e,r);await ec($e(r),{recursive:!0});let s=`${n}.${process.pid}.tmp`;await nc(s,`${JSON.stringify(t,null,2)}
277
+ `,"utf8"),await rc(s,n)}async function ge(e){return Ne(e)}async function mt(e,t){await Qe(e,t)}var qr=268435456;function vt(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function lc(e){let t=ic("sha256");for await(let r of oc(e))t.update(r);return t.digest("hex")}class Ce{name="hetzner";usesCloudFormation=!1;client;sshPrivateKeyPath;sshPublicKeyPath;sshUser;location;stateOnly;waitForBoot;bootWait;constructor(e={}){let t=ht(e.apiToken);if(this.stateOnly=!e.client&&!t&&e.allowStateOnly===!0,!e.client&&!t&&!this.stateOnly)throw Error("Hetzner API token required. Set hetzner.apiToken in cloud.config.ts or HCLOUD_TOKEN / HETZNER_API_TOKEN.");this.client=e.client??new Je({apiToken:t??""});let r=ur(void 0,{sshPrivateKeyPath:e.sshPrivateKeyPath,sshPublicKeyPath:e.sshPublicKeyPath,sshUser:e.sshUser,location:e.location});this.sshPrivateKeyPath=r.sshPrivateKeyPath,this.sshPublicKeyPath=r.sshPublicKeyPath,this.sshUser=r.sshUser,this.location=r.location,this.waitForBoot=e.waitForBoot??!0,this.bootWait={sshIntervalMs:e.bootWait?.sshIntervalMs??5000,sshTimeoutMs:e.bootWait?.sshTimeoutMs??300000,cloudInitIntervalMs:e.bootWait?.cloudInitIntervalMs??5000,cloudInitTimeoutMs:e.bootWait?.cloudInitTimeoutMs??600000}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e,n=t.project.slug,s=t.infrastructure?.compute;if(!s)throw Error("infrastructure.compute is required to provision Hetzner compute");if(t.cloud?.attachTo)return this.attachToComputeInfrastructure(t.cloud.attachTo,t,r);let i=T(t,r),o=`${n}-${r}-app`,a=s.runtime==="php"||!!s.php,c=Os(s);if(c.dedicatedServices||c.appServers>1)return a?this.provisionFleet(e,c):this.provisionBunFleet(e,c);let l=W(n,r,"app"),u=t.sites||{},d=Z(s)?[]:this.collectUpstreamPorts(u),m=`${n}-${r}-app-fw`,{firewall:f}=await this.ensureFirewall(m,l,tn({allowSsh:s.allowSsh!==!1,sitePorts:d,allowedPorts:[...s.firewall?.allowedPorts??[],...Jn(t.infrastructure?.sftp),...Ur(ne(t,{environment:r}))]})),h=await this.ensureSshKey(n,r,l),R=await ge(i);if(R?.serverId){let k=await this.tryGetServer(R.serverId);if(k&&k.status!=="off")return this.outputsFromState(R,k)}let x=await this.findExistingServer(n,r,o);if(x&&x.status!=="off"){let k={provider:"hetzner",stackName:i,serverId:x.id,serverName:x.name,publicIp:x.public_net.ipv4?.ip,publicIpv6:me(x.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(i,k),this.outputsFromState(k,x)}let b=le(t,{environment:r}),_=b?.engine==="rpx"?pe({proxy:b,config:Te(u,{proxy:b,slug:t.project.slug,mail:ne(t,{environment:r})}),slug:t.project.slug,bunBin:s.runtime==="node"||s.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,P=Ie(t,{environment:r}),S=s.bakedImage===!0,E=Y({runtime:P.runtime,runtimeVersion:P.runtimeVersion,systemPackages:s.systemPackages,database:t.infrastructure?.database,phpProvision:P.phpProvision,servicesProvision:P.servicesProvision,rpxProvision:_,baked:S,swapGb:s.swapGb}),y=de(E),C=pt(s.size),H=St(t),{server:te,action:oe}=await this.client.createServer({name:o,serverType:C,image:H,location:t.hetzner?.location||this.location,userData:y,labels:l,sshKeys:h?[h]:void 0,firewalls:[{firewall:f.id}]});await this.client.waitForAction(oe.id);let N=await this.client.waitForServerRunning(te.id),q={provider:"hetzner",stackName:i,serverId:N.id,serverName:N.name,firewallId:f.id,publicIp:N.public_net.ipv4?.ip,publicIpv6:me(N.public_net.ipv6?.ip),deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};await mt(i,q);let K=N.public_net.ipv4?.ip;if(K&&this.waitForBoot)await this.waitForSshReady(K),await this.waitForCloudInit(K);return this.outputsFromState(q,N)}async provisionFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=pt(i.size),c=St(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=await ge(o);if(d?.loadBalancerId){let M=(await this.client.listLoadBalancers().catch(()=>[])).find((D)=>D.id===d.loadBalancerId);if(M)return{appPublicIp:M.public_net?.ipv4?.ip,loadBalancerIp:M.public_net?.ipv4?.ip,servicesPrivateIp:d.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}let m=await this.ensureSshKey(s,n,W(s,n,"app")),f=`${s}-${n}-net`,R=(await this.client.listNetworks().catch(()=>[])).find((L)=>L.name===f)??await this.client.createNetwork({name:f,labels:W(s,n,"app")}),{firewall:x}=await this.ensureFirewall(`${s}-${n}-app-fw`,W(s,n,"app"),tn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),{firewall:b}=await this.ensureFirewall(`${s}-${n}-services-fw`,W(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[R.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[R.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[R.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[R.ip_range]}]),_=Kr(r),P=de(Y({runtime:"php",servicesProvision:_,baked:u,swapGb:i.swapGb})),S=await this.client.listServers().catch(()=>[]),E=[],y=S.find((L)=>ee(L.labels,s,n,"services"));if(!y){let{server:L,action:M}=await this.client.createServer({name:`${s}-${n}-services`,serverType:pt(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:P,labels:W(s,n,"services"),sshKeys:m?[m]:void 0,firewalls:[{firewall:b.id}],networks:[R.id]});await this.client.waitForAction(M.id),y=await this.client.waitForServerRunning(L.id),E.push(L.id)}let C=y.id,H=y.private_net?.[0]?.ip??(await this.client.getServer(C)).private_net?.[0]?.ip,te=[...ye(!0),...ve(!0),...Be(i.sshKeys),...ke(r.notifications)],oe=xt({versions:i.php?.versions,default:i.php?.default,extensions:i.php?.extensions,installNginx:!Z(i),optimizeForProduction:i.php?.optimizeForProduction,ini:i.php?.ini}),N=de(Y({runtime:"php",phpProvision:oe,servicesProvision:te,baked:u,swapGb:i.swapGb})),q=S.filter((L)=>ee(L.labels,s,n,"app")),K=q.map((L)=>L.id);if(q.length>t.appServers)for(let L of q.slice(t.appServers))await this.client.deleteServer(L.id).catch(()=>{}),K.splice(K.indexOf(L.id),1);for(let L=q.length;L<t.appServers;L++){let{server:M,action:D}=await this.client.createServer({name:`${s}-${n}-app-${L+1}`,serverType:a,image:c,location:l,userData:N,labels:W(s,n,"app"),sshKeys:m?[m]:void 0,firewalls:[{firewall:x.id}],networks:[R.id]});await this.client.waitForAction(D.id),K.push(M.id),E.push(M.id)}if(this.waitForBoot&&E.length>0)await Promise.all(E.map(async(L)=>{let D=(await this.client.waitForServerRunning(L)).public_net.ipv4?.ip;if(D)await this.waitForSshReady(D),await this.waitForCloudInit(D)}));let k=`${s}-${n}-lb`,ae,he;if(t.loadBalancer){let M=(await this.client.listLoadBalancers().catch(()=>[])).find((D)=>D.name===k)??await this.client.createLoadBalancer({name:k,location:l,network:R.id,labels:W(s,n,"lb"),labelSelector:`ts-cloud/project=${s},ts-cloud/environment=${n},ts-cloud/role=app`,services:[{listenPort:80,destinationPort:80},{listenPort:443,destinationPort:443}]});he=M.id,ae=M.public_net?.ipv4?.ip}let Re=ae??(await this.client.getServer(K[0]).catch(()=>{return}))?.public_net.ipv4?.ip,re={provider:"hetzner",stackName:o,networkId:R.id,loadBalancerId:he,servicesServerId:C,servicesPrivateIp:H,publicIp:Re,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(o,re),{appPublicIp:Re,loadBalancerIp:ae,servicesPrivateIp:H,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async provisionBunFleet(e,t){let{config:r,environment:n}=e,s=r.project.slug,i=r.infrastructure.compute,o=T(r,n),a=pt(i.size),c=St(r),l=r.hetzner?.location||this.location,u=i.bakedImage===!0,d=r.sites||{},m=await ge(o);if(m?.lbServerId){let D=await this.tryGetServer(m.lbServerId);if(D&&D.status!=="off"){let F=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"},z=await this.findComputeTargets({slug:s,environment:n,role:"app",stackName:o});return this.refreshLbRoutes(D,d,z.map((ce)=>({privateIp:ce.privateIp,publicIp:ce.publicIp})),F,s),{appPublicIp:D.public_net.ipv4?.ip??m.publicIp,loadBalancerIp:D.public_net.ipv4?.ip??m.publicIp,servicesPrivateIp:m.servicesPrivateIp,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}}let f=await this.ensureSshKey(s,n,W(s,n,"app")),h=`${s}-${n}-net`,x=(await this.client.listNetworks().catch(()=>[])).find((D)=>D.name===h)??await this.client.createNetwork({name:h,labels:W(s,n,"app")}),b=this.collectUpstreamPorts(d),{firewall:_}=await this.ensureFirewall(`${s}-${n}-app-fw`,W(s,n,"app"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},...b.map((D)=>({direction:"in",protocol:"tcp",port:String(D),source_ips:[x.ip_range],description:`ts-cloud port ${D} (private, LB-only)`}))]),{firewall:P}=await this.ensureFirewall(`${s}-${n}-lb-fw`,W(s,n,"lb"),tn({allowSsh:!0,sitePorts:[],allowedPorts:i.firewall?.allowedPorts})),S=await this.client.listServers().catch(()=>[]),E=[],y,C,H=!!i.servicesServer||!!i.managedServices,te=t.dedicatedServices&&H;if(te){let{firewall:D}=await this.ensureFirewall(`${s}-${n}-services-fw`,W(s,n,"services"),[{direction:"in",protocol:"tcp",port:"22",source_ips:["0.0.0.0/0","::/0"]},{direction:"in",protocol:"tcp",port:"3306",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"5432",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"6379",source_ips:[x.ip_range]},{direction:"in",protocol:"tcp",port:"7700",source_ips:[x.ip_range]}]),F=Kr(r),z=de(Y({runtime:"bun",servicesProvision:F,baked:u,swapGb:i.swapGb})),ce=S.find((Se)=>ee(Se.labels,s,n,"services"));if(!ce){let{server:Se,action:st}=await this.client.createServer({name:`${s}-${n}-services`,serverType:pt(typeof i.servicesServer==="object"?i.servicesServer.size:i.size),image:c,location:l,userData:z,labels:W(s,n,"services"),sshKeys:f?[f]:void 0,firewalls:[{firewall:D.id}],networks:[x.id]});await this.client.waitForAction(st.id),ce=await this.client.waitForServerRunning(Se.id),E.push(Se.id)}y=ce.id,C=ce.private_net?.[0]?.ip??(await this.client.getServer(y)).private_net?.[0]?.ip}let oe=te?{...i,managedServices:void 0,backups:void 0}:i,N=Ie({...r,infrastructure:{...r.infrastructure,compute:oe}},{environment:n}),q=de(Y({runtime:N.runtime,runtimeVersion:N.runtimeVersion,systemPackages:i.systemPackages,database:r.infrastructure?.database,servicesProvision:N.servicesProvision,baked:u,swapGb:i.swapGb})),K=S.filter((D)=>ee(D.labels,s,n,"app")),k=K.map((D)=>D.id);if(K.length>t.appServers)for(let D of K.slice(t.appServers))await this.client.deleteServer(D.id).catch(()=>{}),k.splice(k.indexOf(D.id),1);for(let D=K.length;D<t.appServers;D++){let{server:F,action:z}=await this.client.createServer({name:`${s}-${n}-app-${D+1}`,serverType:a,image:c,location:l,userData:q,labels:W(s,n,"app"),sshKeys:f?[f]:void 0,firewalls:[{firewall:_.id}],networks:[x.id]});await this.client.waitForAction(z.id),k.push(F.id),E.push(F.id)}if(this.waitForBoot&&E.length>0)await Promise.all(E.map(async(D)=>{let z=(await this.client.waitForServerRunning(D)).public_net.ipv4?.ip;if(z)await this.waitForSshReady(z),await this.waitForCloudInit(z)}));let ae=[];for(let D of k){let F=S.find((z)=>z.id===D);if(!F||!F.private_net?.[0]?.ip)F=await this.client.getServer(D).catch(()=>F);ae.push({privateIp:F?.private_net?.[0]?.ip,publicIp:F?.public_net.ipv4?.ip})}let he=`${s}-${n}-lb`,Re,re;if(t.loadBalancer){let D=S.find((z)=>ee(z.labels,s,n,"lb")),F=i.proxy?.engine==="rpx"?i.proxy:{engine:"rpx"};if(!D){let z=pe({proxy:F,config:ze(d,ae,{proxy:F,slug:s}),slug:s,bunBin:N.runtime==="node"||N.runtime==="deno"?void 0:"/usr/local/bin/bun"}),ce=de(Y({runtime:"bun",rpxProvision:z,baked:!1})),Se=typeof i.loadBalancer==="object"?i.loadBalancer.size:void 0,{server:st,action:nr}=await this.client.createServer({name:he,serverType:pt(Se??"micro"),image:c,location:l,userData:ce,labels:W(s,n,"lb"),sshKeys:f?[f]:void 0,firewalls:[{firewall:P.id}],networks:[x.id]});if(await this.client.waitForAction(nr.id),D=await this.client.waitForServerRunning(st.id),this.waitForBoot){let it=D.public_net.ipv4?.ip;if(it)await this.waitForSshReady(it),await this.waitForCloudInit(it)}}else this.refreshLbRoutes(D,d,ae,F,s);Re=D.id,re=D.public_net.ipv4?.ip}let L=re??(await this.client.getServer(k[0]).catch(()=>{return}))?.public_net.ipv4?.ip,M={provider:"hetzner",stackName:o,networkId:x.id,lbServerId:Re,appServerIds:k,servicesServerId:y,servicesPrivateIp:C,publicIp:L,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(o,M),{appPublicIp:L,loadBalancerIp:re,servicesPrivateIp:C,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser}}async destroyCompute(e){let{config:t,environment:r}=e,n=t.project.slug,s=T(t,r),i=await ge(s),o=[],a=`${n}-${r}-lb`,l=(await this.client.listLoadBalancers().catch(()=>[])).find((_)=>_.name===a);if(l)try{await this.client.deleteLoadBalancer(l.id),o.push(`load balancer ${a}`)}catch{}let u=await this.client.listServers().catch(()=>[]),d=new Set;for(let _ of u)if(ee(_.labels,n,r,"app")||ee(_.labels,n,r,"services")||ee(_.labels,n,r,"lb"))d.add(_.id);if(i?.serverId)d.add(i.serverId);if(i?.servicesServerId)d.add(i.servicesServerId);if(i?.lbServerId)d.add(i.lbServerId);for(let _ of i?.appServerIds??[])d.add(_);for(let _ of d)try{await this.client.deleteServer(_),o.push(`server ${_}`)}catch{}let m=await this.client.listFirewalls().catch(()=>[]);for(let _ of[`${n}-${r}-app-fw`,`${n}-${r}-services-fw`,`${n}-${r}-lb-fw`]){let P=m.find((S)=>S.name===_);if(!P)continue;for(let S=0;S<12;S++)try{await this.client.deleteFirewall(P.id),o.push(`firewall ${_}`);break}catch{await this.sleep(3000)}}let f=`${n}-${r}-net`,R=(await this.client.listNetworks().catch(()=>[])).find((_)=>_.name===f);if(R)for(let _=0;_<12;_++)try{await this.client.deleteNetwork(R.id),o.push(`network ${f}`);break}catch{await this.sleep(3000)}let b=(await this.client.listSshKeys().catch(()=>[])).filter((_)=>Gr(_.labels,n,r));for(let _ of b)try{await this.client.deleteSshKey(_.id),o.push(`ssh key ${_.name}`)}catch{}return await mt(s,{provider:"hetzner",stackName:s}).catch(()=>{}),{destroyed:o}}async getComputeOutputs(e){let t=T(e.config,e.environment),r=await ge(t);if(r?.serverId){let i=await this.tryGetServer(r.serverId);if(i)return this.outputsFromState(r,i)}if(r?.lbServerId){let o=(await this.tryGetServer(r.lbServerId))?.public_net.ipv4?.ip??r.publicIp;return{...this.outputsFromState(r),appPublicIp:o,loadBalancerIp:o}}if(r?.loadBalancerId||r?.servicesPrivateIp)return this.outputsFromState(r);let s=(await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)}))[0];return{deployStoragePath:"/var/ts-cloud/staging",appInstanceId:s?.id,appPublicIp:s?.publicIp,sshUser:this.sshUser}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)});if(t.length===0)throw Error("No Hetzner compute targets found for release upload");let s=`/var/ts-cloud/staging/${`${e.remoteKey.replace(/^releases\//,"").replace(/\.tar\.gz$/,"").replace(/\//g,"-")}-${Hs()}.tar.gz`}`,i=await lc(e.localPath),o=Ge,a=`${o}/${i}.tar.gz`;for(let c of t){if(!c.publicIp)throw Error(`Target ${c.id} has no public IP for SCP upload`);let l=["set -euo pipefail",`mkdir -p ${vt(o)} /var/ts-cloud/staging`,`test -s ${vt(a)}`,`cp -- ${vt(a)} ${vt(s)}`].join(`
278
278
  `);try{this.sshExec(c.publicIp,l);continue}catch{}let u=`${o}/.${i}-${Hs()}.tmp`;try{this.scpToHost(c.publicIp,e.localPath,u),this.sshExec(c.publicIp,kt(u,a,s))}catch(d){try{this.sshExec(c.publicIp,Mt(u))}catch{}throw d}}return{artifactRef:s}}async findComputeTargets(e){let t=e.role||"app",r=(i)=>({id:String(i.id),name:i.name,publicIp:i.public_net.ipv4?.ip,publicIpv6:me(i.public_net.ipv6?.ip),privateIp:i.private_net?.[0]?.ip,status:i.status});if(t==="app"||t==="lb"||t==="services"){let i=await ge(e.stackName??`${e.slug}-${e.environment}`),o=[...new Set(t==="app"?[i?.serverId,...i?.appServerIds??[]]:t==="lb"?[i?.lbServerId]:[i?.servicesServerId])].filter((l)=>typeof l==="number");if(this.stateOnly){if(o.length>0&&i?.publicIp)return o.map((l)=>({id:String(l),name:l===i.serverId?i.serverName:void 0,publicIp:i.publicIp,publicIpv6:i.publicIpv6,status:"running"}));if(t!=="app")return[];throw Error(`Hetzner API token required because '${e.stackName??`${e.slug}-${e.environment}`}' has no complete local compute state pin.`)}let a=await this.client.listServers();if(o.length>0){let l=[];for(let u of o){let d=a.find((m)=>m.id===u)??await this.tryGetServer(u);if(u===i?.serverId&&i.serverName&&d?.name!==i.serverName)continue;if(d&&d.status!=="off")l.push(d)}if(l.length>0)return l.map(r)}let c=a.filter((l)=>ee(l.labels,e.slug,e.environment,t));if(c.length>0)return c.map(r);if(t==="app"){let l=a.filter((u)=>u.status!=="off"&&u.labels?.[`${j}/managed-by`]==="ts-cloud"&&u.labels?.[`${j}/environment`]===e.environment&&u.labels?.[`${j}/role`]==="app");if(l.length===1)return l.map(r)}return[]}if(this.stateOnly)throw Error(`Hetzner API token required to discover '${t}' compute targets.`);let s=(await this.client.listServers()).filter((i)=>ee(i.labels,e.slug,e.environment,t));if(s.length>0)return s.map(r);return[]}async attachToComputeInfrastructure(e,t,r){let n=await this.client.listServers(),s=(m)=>n.filter((f)=>f.status!=="off"&&ee(f.labels,e,r,m)),i=s("app");if(i.length===0)throw Error(`Cannot attach '${t.project.slug}' to '${e}': no running Hetzner app server is labeled for ${e}/${r}. Deploy the owner project first.`);let o=s("lb")[0],a=s("services")[0],c=i[0],l=o??c,u=T(t,r),d={provider:"hetzner",stackName:u,serverId:c.id,serverName:c.name,appServerIds:i.map((m)=>m.id),lbServerId:o?.id,servicesServerId:a?.id,servicesPrivateIp:a?.private_net?.[0]?.ip,publicIp:l.public_net.ipv4?.ip,deployStoragePath:"/var/ts-cloud/staging",sshUser:this.sshUser};return await mt(u,d),this.outputsFromState(d)}async listReachableResources(){return(await this.client.listServers()).map((t)=>({name:t.name,labels:t.labels}))}async runRemoteDeploy(e){if(e.targets.length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets provided"};let t=e.commands.join(`
279
- `),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing public IP"});continue}try{let i=this.sshExec(s.publicIp,t);Ue(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:Oe(r,"One or more SSH deploy commands failed")}}async ensureSshKey(e,t,r){if(!ac(this.sshPublicKeyPath))throw Error(`SSH public key not found at ${this.sshPublicKeyPath}. ts-cloud deploys to Hetzner over SSH and needs a public key to authorize on the server. Generate one (\`ssh-keygen -t ed25519\`) or set hetzner.sshPrivateKeyPath / HCLOUD_SSH_PUBLIC_KEY.`);let n=cc(this.sshPublicKeyPath,"utf8").trim(),s=yt(n),o=(await this.client.listSshKeys()).find((c)=>yt(c.public_key)===s);if(o)return o.id;return(await this.client.createSshKey({name:`${e}-${t}-deploy`,publicKey:n,labels:r})).id}async tryGetServer(e){try{return await this.client.getServer(e)}catch{return null}}refreshLbRoutes(e,t,r,n,s){let i=e.public_net.ipv4?.ip;if(!i){console.warn(`[ts-cloud] LB server '${e.name}' has no public IP — skipping the rpx route refresh; its routes may be stale.`);return}let o=r.filter((c)=>c.privateIp||c.publicIp);if(o.length===0){console.warn(`[ts-cloud] No routable app boxes found for LB '${e.name}' — leaving its current rpx routes untouched.`);return}let a=ze(t,o,{proxy:n,slug:s});this.sshExec(i,Ke({config:a,slug:s}).join(`
279
+ `),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing public IP"});continue}try{let i=this.sshExec(s.publicIp,t);Ue(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:De(r,"One or more SSH deploy commands failed")}}async ensureSshKey(e,t,r){if(!ac(this.sshPublicKeyPath))throw Error(`SSH public key not found at ${this.sshPublicKeyPath}. ts-cloud deploys to Hetzner over SSH and needs a public key to authorize on the server. Generate one (\`ssh-keygen -t ed25519\`) or set hetzner.sshPrivateKeyPath / HCLOUD_SSH_PUBLIC_KEY.`);let n=cc(this.sshPublicKeyPath,"utf8").trim(),s=yt(n),o=(await this.client.listSshKeys()).find((c)=>yt(c.public_key)===s);if(o)return o.id;return(await this.client.createSshKey({name:`${e}-${t}-deploy`,publicKey:n,labels:r})).id}async tryGetServer(e){try{return await this.client.getServer(e)}catch{return null}}refreshLbRoutes(e,t,r,n,s){let i=e.public_net.ipv4?.ip;if(!i){console.warn(`[ts-cloud] LB server '${e.name}' has no public IP — skipping the rpx route refresh; its routes may be stale.`);return}let o=r.filter((c)=>c.privateIp||c.publicIp);if(o.length===0){console.warn(`[ts-cloud] No routable app boxes found for LB '${e.name}' — leaving its current rpx routes untouched.`);return}let a=ze(t,o,{proxy:n,slug:s});this.sshExec(i,Ke({config:a,slug:s}).join(`
280
280
  `))}async findExistingServer(e,t,r){let n=await this.client.listServers(),s=n.find((o)=>ee(o.labels,e,t,"app")||o.name===r);if(s)return s;let i=n.filter((o)=>o.status!=="off"&&o.labels?.[`${j}/managed-by`]==="ts-cloud"&&o.labels?.[`${j}/environment`]===t&&o.labels?.[`${j}/role`]==="app");if(i.length===1){let o=i[0],a=o.labels?.[`${j}/project`]??"unknown";return console.warn(`[ts-cloud] No server named '${r}' found; adopting existing ts-cloud app server '${o.name}' (project label '${a}') for project '${e}' — `+"updating it in place instead of provisioning a new server."),o}return}async ensureFirewall(e,t,r){let s=(await this.client.listFirewalls()).find((o)=>o.name===e);if(s)return await this.client.setFirewallRules(s.id,r),{firewall:s};let{firewall:i}=await this.client.createFirewall({name:e,labels:t,rules:r});return{firewall:i}}collectUpstreamPorts(e){let t=new Set;for(let r of Object.values(e))if(typeof r.port==="number")t.add(r.port);return[...t].filter((r)=>![80,443].includes(r))}async sleep(e){await new Promise((t)=>setTimeout(t,e))}async waitForSshReady(e){let{sshIntervalMs:t,sshTimeoutMs:r}=this.bootWait,n=Date.now(),s;while(Date.now()-n<r)try{Ns(`ssh ${this.sshBaseArgs(e,["-o","ConnectTimeout=5"]).map(vt).join(" ")} true`,{stdio:"pipe",maxBuffer:qr});return}catch(i){s=i,await this.sleep(t)}throw Error(`Timed out waiting for SSH on ${e} after ${r}ms: ${s?.message??"unknown error"}`)}async waitForCloudInit(e){let{cloudInitIntervalMs:t,cloudInitTimeoutMs:r}=this.bootWait,n=Date.now();while(Date.now()-n<r){try{let s=this.sshExec(e,"if command -v cloud-init >/dev/null 2>&1; then cloud-init status --long 2>/dev/null || true; else echo 'status: done'; fi");if(/status:\s*error/.test(s))throw Error(`cloud-init reported an error on ${e}:
281
- ${s}`);if(/status:\s*(?:done|degraded)/.test(s))return}catch(s){if(s instanceof Error&&/cloud-init reported an error/.test(s.message))throw s}await this.sleep(t)}throw Error(`Timed out waiting for cloud-init to finish on ${e} after ${r}ms`)}outputsFromState(e,t){return{deployStoragePath:e.deployStoragePath||"/var/ts-cloud/staging",appInstanceId:e.serverId?String(e.serverId):void 0,appPublicIp:t?.public_net.ipv4?.ip||e.publicIp,appPublicIpv6:me(t?.public_net.ipv6?.ip)||e.publicIpv6,sshUser:e.sshUser||this.sshUser,servicesPrivateIp:e.servicesPrivateIp}}static SSH_HOST_KEY_OPTS=["-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"];static SSH_KEEPALIVE_OPTS=["-o","ConnectTimeout=30","-o","ServerAliveInterval=15","-o","ServerAliveCountMax=4"];sshBaseArgs(e,t=[]){return["-i",this.sshPrivateKeyPath,...$e.SSH_HOST_KEY_OPTS,...$e.SSH_KEEPALIVE_OPTS,"-o","BatchMode=yes",...t,`${this.sshUser}@${e}`]}scpToHost(e,t,r){let n=["scp","-i",this.sshPrivateKeyPath,...$e.SSH_HOST_KEY_OPTS,...$e.SSH_KEEPALIVE_OPTS,"-o","BatchMode=yes",t,`${this.sshUser}@${e}:${r}`].map((i)=>`"${i.replace(/"/g,"\\\"")}"`).join(" "),s=3;for(let i=1;i<=3;i++)try{Ns(n,{stdio:"pipe",maxBuffer:qr});return}catch(o){if(i===3)throw Error(Ee(o))}}sshExec(e,t){try{return sc("ssh",[...this.sshBaseArgs(e),Pr],{encoding:"utf8",input:t,stdio:["pipe","pipe","pipe"],maxBuffer:qr})}catch(r){throw Error(`${Ee(r)}${ot(r,t)}`)}}}class Kt{name="ssh";usesCloudFormation=!1;async getComputeOutputs(){return{appPublicIp:"127.0.0.1",sshUser:"root"}}async uploadRelease(e){return{artifactRef:e.localPath}}async findComputeTargets(e){if((e.role||"app")!=="app")return[];return[{id:"localhost",name:"localhost",publicIp:"127.0.0.1",status:"running"}]}async runRemoteDeploy(e){let t=e.commands.join(`
281
+ ${s}`);if(/status:\s*(?:done|degraded)/.test(s))return}catch(s){if(s instanceof Error&&/cloud-init reported an error/.test(s.message))throw s}await this.sleep(t)}throw Error(`Timed out waiting for cloud-init to finish on ${e} after ${r}ms`)}outputsFromState(e,t){return{deployStoragePath:e.deployStoragePath||"/var/ts-cloud/staging",appInstanceId:e.serverId?String(e.serverId):void 0,appPublicIp:t?.public_net.ipv4?.ip||e.publicIp,appPublicIpv6:me(t?.public_net.ipv6?.ip)||e.publicIpv6,sshUser:e.sshUser||this.sshUser,servicesPrivateIp:e.servicesPrivateIp}}static SSH_HOST_KEY_OPTS=["-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"];static SSH_KEEPALIVE_OPTS=["-o","ConnectTimeout=30","-o","ServerAliveInterval=15","-o","ServerAliveCountMax=4"];sshBaseArgs(e,t=[]){return["-i",this.sshPrivateKeyPath,...Ce.SSH_HOST_KEY_OPTS,...Ce.SSH_KEEPALIVE_OPTS,"-o","BatchMode=yes",...t,`${this.sshUser}@${e}`]}scpToHost(e,t,r){let n=["scp","-i",this.sshPrivateKeyPath,...Ce.SSH_HOST_KEY_OPTS,...Ce.SSH_KEEPALIVE_OPTS,"-o","BatchMode=yes",t,`${this.sshUser}@${e}:${r}`].map((i)=>`"${i.replace(/"/g,"\\\"")}"`).join(" "),s=3;for(let i=1;i<=3;i++)try{Ns(n,{stdio:"pipe",maxBuffer:qr});return}catch(o){if(i===3)throw Error(Oe(o))}}sshExec(e,t){try{return sc("ssh",[...this.sshBaseArgs(e),Pr],{encoding:"utf8",input:t,stdio:["pipe","pipe","pipe"],maxBuffer:qr})}catch(r){throw Error(`${Oe(r)}${ot(r,t)}`)}}}class Kt{name="ssh";usesCloudFormation=!1;async getComputeOutputs(){return{appPublicIp:"127.0.0.1",sshUser:"root"}}async uploadRelease(e){return{artifactRef:e.localPath}}async findComputeTargets(e){if((e.role||"app")!=="app")return[];return[{id:"localhost",name:"localhost",publicIp:"127.0.0.1",status:"running"}]}async runRemoteDeploy(e){let t=e.commands.join(`
282
282
  `);try{let r=Bun.spawn(["bash","-c",t],{stdout:"pipe",stderr:"pipe",env:process.env}),[n,s,i]=await Promise.all([new Response(r.stdout).text(),new Response(r.stderr).text(),r.exited]);return{success:i===0,instanceCount:1,perInstance:[{instanceId:"localhost",status:i===0?"Success":"Failed",output:n,error:s}]}}catch(r){return{success:!1,instanceCount:1,perInstance:[{instanceId:"localhost",status:"Failed",error:r?.message??String(r)}],error:r?.message??String(r)}}}}function Xr(){let e=process.env.TS_CLOUD_DASHBOARD_BOX;return e!=null&&e!==""&&e!=="0"&&e.toLowerCase()!=="false"}import{createHash as xc,randomUUID as gi}from"node:crypto";import{createReadStream as Rc}from"node:fs";import{isIP as Ec}from"node:net";import{createHash as uc}from"node:crypto";import{appendFile as pc,mkdir as dc,readFile as mc}from"node:fs/promises";import{dirname as Sc}from"node:path";var Us=268435456,ks=async(e,t)=>{let r=Bun.spawn(e,{stdin:t===void 0?"ignore":Buffer.from(t),stdout:"pipe",stderr:"pipe"}),[n,s,i]=await Promise.all([r.exited,new Response(r.stdout).text(),new Response(r.stderr).text()]);return{code:n,signal:r.signalCode,stdout:s.slice(-Us),stderr:i.slice(-Us)}},Ms=["-o","StrictHostKeyChecking=no","-o","UserKnownHostsFile=/dev/null","-o","LogLevel=ERROR"],Fs=["-o","ConnectTimeout=30","-o","ServerAliveInterval=15","-o","ServerAliveCountMax=4"];function zs(e){if(e.hostKey==="insecure")return[...Ms];return["-o",`StrictHostKeyChecking=${e.hostKey.policy==="pin"?"yes":"accept-new"}`,"-o",`UserKnownHostsFile=${e.hostKey.knownHostsFile}`,"-o","LogLevel=ERROR"]}function Bs(e){let t=[...Fs];if(e.connectTimeoutSec!==void 0)t[1]=`ConnectTimeout=${e.connectTimeoutSec}`;return t}function Yr(e,t,r=[]){return["-i",e.identityFile,"-p",String(e.port??22),...zs(e),...Bs(e),"-o","BatchMode=yes",...r,`${e.user}@${t}`]}function Ks(e,t,r,n){return["-i",e.identityFile,"-P",String(e.port??22),...zs(e),...Bs(e),"-o","BatchMode=yes",r,`${e.user}@${t}:${n}`]}function mr(e){return`SHA256:${uc("sha256").update(Buffer.from(e,"base64")).digest("base64").replace(/=+$/,"")}`}async function eo(e,t=22,r=ks){let n=await r(["ssh-keyscan","-T","10","-p",String(t),"-t","ed25519",e]),s=n.stdout.split(`
283
283
  `).find((i)=>i&&!i.startsWith("#"))?.trim().split(/\s+/);if(n.code!==0||!s?.[1]||!s[2])throw Error(`SSH host-key scan failed: ${n.stderr.trim()||"no key returned"}`);return{algorithm:s[1],key:s[2]}}function Jr(e,t=22){return t===22?e:`[${e}]:${t}`}function js(e,t,r){return`${Jr(e,t)} ${r.algorithm} ${r.key}`}function Ws(e,t,r=22){let n=Jr(t,r);for(let s of e.split(`
284
284
  `)){let i=s.trim();if(!i||i.startsWith("#")||i.startsWith("@"))continue;let[o,a,c]=i.split(/\s+/);if(!o||!a||!c)continue;if(o.split(",").includes(n))return{algorithm:a,key:c}}return null}function Qr(e=process.cwd()){return A(e,"ssh","known_hosts")}async function yc(e){try{return await mc(e,"utf8")}catch{return""}}async function vc(e,t){await dc(Sc(e),{recursive:!0}),await pc(e,`${t}
285
- `,{mode:384})}class jt{options;run;scan;readKnownHosts;appendKnownHosts;constructor(e,t={}){this.options=e;this.run=t.run??ks,this.scan=t.scan??eo,this.readKnownHosts=t.readKnownHosts??yc,this.appendKnownHosts=t.appendKnownHosts??vc}runnerFor(e={}){if(e.sudo!==void 0)return lt({sudo:e.sudo});return this.options.runner??lt()}async exec(e,t,r={}){let n=await this.run(["ssh",...Yr(this.options,e),this.runnerFor(r)],t);if(n.code!==0){let s={status:n.code,signal:n.signal,stdout:n.stdout,stderr:n.stderr};throw Error(`${Ee(s)}${ot(s,t)}`)}return n.stdout}async scp(e,t,r){let n=["scp",...Ks(this.options,e,t,r)],s=3;for(let i=1;i<=3;i++){let o=await this.run(n);if(o.code===0)return;if(i===3)throw Error(Ee({status:o.code,signal:o.signal,stdout:o.stdout,stderr:o.stderr}))}}async probe(e){return(await this.run(["ssh",...Yr(this.options,e,["-o","ConnectTimeout=5"]),"true"])).code===0}async ensureHostKey(e){let t=this.options.port??22;if(this.options.hostKey==="insecure")try{return{fingerprint:mr((await this.scan(e,t,this.run)).key),pinnedNow:!1}}catch{return{fingerprint:"",pinnedNow:!1}}let{policy:r,knownHostsFile:n}=this.options.hostKey,s=Ws(await this.readKnownHosts(n),e,t);if(s)return{fingerprint:mr(s.key),pinnedNow:!1};let i=await this.scan(e,t,this.run);if(r==="pin")return await this.appendKnownHosts(n,js(e,t,i)),{fingerprint:mr(i.key),pinnedNow:!0};return{fingerprint:mr(i.key),pinnedNow:!1}}}import{isIP as bc}from"node:net";var Ce=1,an="/var/lib/ts-cloud";function qs(e=Ce){return`${an}/bootstrap.v${e}`}var Js=1;function _c(e){return e==="aarch64"||e==="arm64"}function Vs(e){let t=e.lanIp?.trim()||e.facts?.lanIp?.trim();return t&&bc(t)!==0?t:void 0}function Wt(e,t,r){if(!Ii(e.infrastructure?.compute?.managedServices?.vitess))return;if(t==="raspberry-pi"||_c(r))throw Error(`managedServices.vitess is enabled, but Vitess publishes only x86_64 release tarballs and this host is ${r??"a Raspberry Pi (arm64)"}. Drop managedServices.vitess or deploy it to an x86_64 host.`)}function He(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function wc(){return`
285
+ `,{mode:384})}class jt{options;run;scan;readKnownHosts;appendKnownHosts;constructor(e,t={}){this.options=e;this.run=t.run??ks,this.scan=t.scan??eo,this.readKnownHosts=t.readKnownHosts??yc,this.appendKnownHosts=t.appendKnownHosts??vc}runnerFor(e={}){if(e.sudo!==void 0)return lt({sudo:e.sudo});return this.options.runner??lt()}async exec(e,t,r={}){let n=await this.run(["ssh",...Yr(this.options,e),this.runnerFor(r)],t);if(n.code!==0){let s={status:n.code,signal:n.signal,stdout:n.stdout,stderr:n.stderr};throw Error(`${Oe(s)}${ot(s,t)}`)}return n.stdout}async scp(e,t,r){let n=["scp",...Ks(this.options,e,t,r)],s=3;for(let i=1;i<=3;i++){let o=await this.run(n);if(o.code===0)return;if(i===3)throw Error(Oe({status:o.code,signal:o.signal,stdout:o.stdout,stderr:o.stderr}))}}async probe(e){return(await this.run(["ssh",...Yr(this.options,e,["-o","ConnectTimeout=5"]),"true"])).code===0}async ensureHostKey(e){let t=this.options.port??22;if(this.options.hostKey==="insecure")try{return{fingerprint:mr((await this.scan(e,t,this.run)).key),pinnedNow:!1}}catch{return{fingerprint:"",pinnedNow:!1}}let{policy:r,knownHostsFile:n}=this.options.hostKey,s=Ws(await this.readKnownHosts(n),e,t);if(s)return{fingerprint:mr(s.key),pinnedNow:!1};let i=await this.scan(e,t,this.run);if(r==="pin")return await this.appendKnownHosts(n,js(e,t,i)),{fingerprint:mr(i.key),pinnedNow:!0};return{fingerprint:mr(i.key),pinnedNow:!1}}}import{isIP as bc}from"node:net";var Pe=1,an="/var/lib/ts-cloud";function qs(e=Pe){return`${an}/bootstrap.v${e}`}var Js=1;function _c(e){return e==="aarch64"||e==="arm64"}function Vs(e){let t=e.lanIp?.trim()||e.facts?.lanIp?.trim();return t&&bc(t)!==0?t:void 0}function Wt(e,t,r){if(!Ii(e.infrastructure?.compute?.managedServices?.vitess))return;if(t==="raspberry-pi"||_c(r))throw Error(`managedServices.vitess is enabled, but Vitess publishes only x86_64 release tarballs and this host is ${r??"a Raspberry Pi (arm64)"}. Drop managedServices.vitess or deploy it to an x86_64 host.`)}function He(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function wc(){return`
286
286
  # A Pi 5 has no battery-backed RTC and boots in 1970 until NTP answers. apt
287
287
  # rejects Release files "from the future" and ACME rejects a skewed clock, so
288
288
  # wait (bounded) for systemd-timesyncd before anything touches the network
@@ -313,16 +313,16 @@ systemctl restart systemd-journald
313
313
  # The deploy user uploads releases as itself (scp), while the scripts run as
314
314
  # root through sudo. setgid group-writable directories let both write.
315
315
  install -d -m 2775 -g "$(id -gn ${He(e)})" /var/ts-cloud/artifacts /var/ts-cloud/staging
316
- `}function Cc(e){let t=He(e),r=`/home/${e}`;return[`chown -R ${t}:"$(id -gn ${t})" ${He(`${r}/.ssh`)}`,`chmod 700 ${He(`${r}/.ssh`)}`]}function gr(e){let{config:t,environment:r,profile:n,sudoUser:s}=e;Wt(t,n,e.facts?.arch);let i={...t.infrastructure?.compute,proxy:le(t,{environment:e.environment,environmentType:e.environment})},o=t.sites??{},a=t.project.slug,c=e.lan?{...e.lan.hostname?{hostname:e.lan.hostname}:{},tls:e.lan.tls??"local-ca",...Vs(e)?{ip:Vs(e)}:{}}:void 0,l=i.proxy?.engine==="rpx"?Te(o,{proxy:i.proxy,slug:a,lan:c,mail:ne(t,{environment:e.environment,environmentType:e.environment})}):void 0,u=i.proxy?.engine==="rpx"&&l?pe({proxy:i.proxy,config:l,slug:a,profile:n,bunBin:i.runtime==="node"||i.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,d=i.proxy?.engine==="rpx"&&l?Ke({config:l,slug:a}):void 0,m=s?`/home/${s}/.ssh/authorized_keys`:void 0,f=De(t,{environment:r,authorizedKeysPath:m}),h=s?[...f.servicesProvision??[],...Cc(s)]:f.servicesProvision,R=[...new Set([...i.systemPackages??[],"psmisc","ca-certificates"])],x=i.swapGb??(n==="raspberry-pi"?Js:void 0),b=Y({runtime:f.runtime,runtimeVersion:f.runtimeVersion,systemPackages:R,database:t.infrastructure?.database,phpProvision:f.phpProvision,servicesProvision:h,rpxProvision:u,swapGb:x}).replace(/^#!\/bin\/bash\nset -euo pipefail\n/,""),_=qs(),P=l?.localCa?` lan=${l.localCa.hosts.join(",")} tls=local-ca`:c?` lan=${c.hostname??`${a}.local`} tls=${c.tls??"local-ca"}`:"",S=`#!/bin/bash
316
+ `}function Cc(e){let t=He(e),r=`/home/${e}`;return[`chown -R ${t}:"$(id -gn ${t})" ${He(`${r}/.ssh`)}`,`chmod 700 ${He(`${r}/.ssh`)}`]}function gr(e){let{config:t,environment:r,profile:n,sudoUser:s}=e;Wt(t,n,e.facts?.arch);let i={...t.infrastructure?.compute,proxy:le(t,{environment:e.environment,environmentType:e.environment})},o=t.sites??{},a=t.project.slug,c=e.lan?{...e.lan.hostname?{hostname:e.lan.hostname}:{},tls:e.lan.tls??"local-ca",...Vs(e)?{ip:Vs(e)}:{}}:void 0,l=i.proxy?.engine==="rpx"?Te(o,{proxy:i.proxy,slug:a,lan:c,mail:ne(t,{environment:e.environment,environmentType:e.environment})}):void 0,u=i.proxy?.engine==="rpx"&&l?pe({proxy:i.proxy,config:l,slug:a,profile:n,bunBin:i.runtime==="node"||i.runtime==="deno"?void 0:"/usr/local/bin/bun"}):void 0,d=i.proxy?.engine==="rpx"&&l?Ke({config:l,slug:a}):void 0,m=s?`/home/${s}/.ssh/authorized_keys`:void 0,f=Ie(t,{environment:r,authorizedKeysPath:m}),h=s?[...f.servicesProvision??[],...Cc(s)]:f.servicesProvision,R=[...new Set([...i.systemPackages??[],"psmisc","ca-certificates"])],x=i.swapGb??(n==="raspberry-pi"?Js:void 0),b=Y({runtime:f.runtime,runtimeVersion:f.runtimeVersion,systemPackages:R,database:t.infrastructure?.database,phpProvision:f.phpProvision,servicesProvision:h,rpxProvision:u,swapGb:x}).replace(/^#!\/bin\/bash\nset -euo pipefail\n/,""),_=qs(),P=l?.localCa?` lan=${l.localCa.hosts.join(",")} tls=local-ca`:c?` lan=${c.hostname??`${a}.local`} tls=${c.tls??"local-ca"}`:"",S=`#!/bin/bash
317
317
  set -euo pipefail
318
- # ts-cloud ssh bootstrap v${Ce} profile=${n}${P}
318
+ # ts-cloud ssh bootstrap v${Pe} profile=${n}${P}
319
319
 
320
320
  # Idempotence guard. This script may run again (a cloud-init rerun, a repeated
321
321
  # adopt, a deploy): once this version has been applied, only the gateway's
322
322
  # route fragment is refreshed, so a rerun cannot re-download the runtime or
323
323
  # re-create swap on every boot.
324
324
  if [ -e ${He(_)} ]; then
325
- echo "[ts-cloud] bootstrap v${Ce} already applied; refreshing the gateway fragment only"
325
+ echo "[ts-cloud] bootstrap v${Pe} already applied; refreshing the gateway fragment only"
326
326
  `;if(d){let E=d[0]==="set -euo pipefail"?d.slice(1):d;S+=`${E.join(`
327
327
  `)}
328
328
  `}if(S+=` exit 0
@@ -331,9 +331,9 @@ fi
331
331
  ${b}`,S+=`
332
332
  install -d -m 0755 ${He(an)}
333
333
  date -u +%Y-%m-%dT%H:%M:%SZ > ${He(_)}
334
- echo "[ts-cloud] bootstrap v${Ce} applied (${n}); gateway fragment at ${X}/${a}.json"
334
+ echo "[ts-cloud] bootstrap v${Pe} applied (${n}); gateway fragment at ${X}/${a}.json"
335
335
  `,l?.localCa)S+=`echo "[ts-cloud] LAN certificate authority for ${l.localCa.hosts.join(", ")} at ${Et(l.localCa.dir)} (copy it to a client to trust this box)"
336
- `;return S}var Pe={user:"root",port:22,privateKeyPath:"~/.ssh/id_ed25519",hostKey:"pin",profile:"generic",publicIp:"auto"},ei=["pin","accept-new","insecure"],ti=["raspberry-pi","generic"];function Ze(...e){for(let t of e){let r=process.env[t]?.trim();if(r)return r}return}function bt(...e){for(let t of e){let r=t?.trim();if(r)return r}return}function ri(e,t){if(t?.length)return t;if(e?.ssh?.hosts?.length)return e.ssh.hosts;let r=Ze("TS_CLOUD_SSH_HOST");if(r)return[{host:r}];throw Error("No ssh host configured. Set ssh.hosts in cloud.config.ts or TS_CLOUD_SSH_HOST.")}function ni(e,t){return bt(t,e?.user,Ze("TS_CLOUD_SSH_USER"))??Pe.user}function si(e,t){let r=[{source:"the explicit port",value:t},{source:`ssh.hosts[].port for ${e?.host??"the host"}`,value:e?.port},{source:"TS_CLOUD_SSH_PORT",value:Ze("TS_CLOUD_SSH_PORT")}];for(let{source:n,value:s}of r){if(s===void 0||s==="")continue;let i=typeof s==="number"?s:Number(s);if(!Number.isInteger(i)||i<1||i>65535)throw Error(`Invalid SSH port from ${n}: ${String(s)}`);return i}return Pe.port}function ii(e,t){return en(bt(t,e?.privateKeyPath,Ze("TS_CLOUD_SSH_KEY"))??Pe.privateKeyPath)}function oi(e,t){let r=bt(t,e?.ssh?.hostKey,Ze("TS_CLOUD_SSH_HOST_KEY"))??Pe.hostKey;if(!ei.includes(r))throw Error(`Invalid ssh.hostKey '${r}'. Use one of: ${ei.join(", ")}.`);return r}function ai(e,t=Pe.user,r){return r??e?.ssh?.sudo??t!=="root"}function ci(e,t){let r=bt(t,e?.ssh?.profile,Ze("TS_CLOUD_SSH_PROFILE"))??Pe.profile;if(!ti.includes(r))throw Error(`Invalid ssh.profile '${r}'. Use one of: ${ti.join(", ")}.`);return r}function li(e,t){return bt(t,e?.ssh?.publicIp)??Pe.publicIp}function fi(e,t){return t??e?.ssh?.lan}function fr(e,t={}){let r=ri(e,t.hosts).map((n)=>({host:n.host,user:ni(n,t.user),port:si(n,t.port),privateKeyPath:ii(n,t.privateKeyPath),role:n.role??"app"}));return{hosts:r,hostKey:oi(e,t.hostKey),sudo:ai(e,r[0]?.user,t.sudo),profile:ci(e,t.profile),publicIp:li(e,t.publicIp),lan:fi(e,t.lan)}}var cn=`set -u
336
+ `;return S}var xe={user:"root",port:22,privateKeyPath:"~/.ssh/id_ed25519",hostKey:"pin",profile:"generic",publicIp:"auto"},ei=["pin","accept-new","insecure"],ti=["raspberry-pi","generic"];function Ze(...e){for(let t of e){let r=process.env[t]?.trim();if(r)return r}return}function bt(...e){for(let t of e){let r=t?.trim();if(r)return r}return}function ri(e,t){if(t?.length)return t;if(e?.ssh?.hosts?.length)return e.ssh.hosts;let r=Ze("TS_CLOUD_SSH_HOST");if(r)return[{host:r}];throw Error("No ssh host configured. Set ssh.hosts in cloud.config.ts or TS_CLOUD_SSH_HOST.")}function ni(e,t){return bt(t,e?.user,Ze("TS_CLOUD_SSH_USER"))??xe.user}function si(e,t){let r=[{source:"the explicit port",value:t},{source:`ssh.hosts[].port for ${e?.host??"the host"}`,value:e?.port},{source:"TS_CLOUD_SSH_PORT",value:Ze("TS_CLOUD_SSH_PORT")}];for(let{source:n,value:s}of r){if(s===void 0||s==="")continue;let i=typeof s==="number"?s:Number(s);if(!Number.isInteger(i)||i<1||i>65535)throw Error(`Invalid SSH port from ${n}: ${String(s)}`);return i}return xe.port}function ii(e,t){return en(bt(t,e?.privateKeyPath,Ze("TS_CLOUD_SSH_KEY"))??xe.privateKeyPath)}function oi(e,t){let r=bt(t,e?.ssh?.hostKey,Ze("TS_CLOUD_SSH_HOST_KEY"))??xe.hostKey;if(!ei.includes(r))throw Error(`Invalid ssh.hostKey '${r}'. Use one of: ${ei.join(", ")}.`);return r}function ai(e,t=xe.user,r){return r??e?.ssh?.sudo??t!=="root"}function ci(e,t){let r=bt(t,e?.ssh?.profile,Ze("TS_CLOUD_SSH_PROFILE"))??xe.profile;if(!ti.includes(r))throw Error(`Invalid ssh.profile '${r}'. Use one of: ${ti.join(", ")}.`);return r}function li(e,t){return bt(t,e?.ssh?.publicIp)??xe.publicIp}function fi(e,t){return t??e?.ssh?.lan}function fr(e,t={}){let r=ri(e,t.hosts).map((n)=>({host:n.host,user:ni(n,t.user),port:si(n,t.port),privateKeyPath:ii(n,t.privateKeyPath),role:n.role??"app"}));return{hosts:r,hostKey:oi(e,t.hostKey),sudo:ai(e,r[0]?.user,t.sudo),profile:ci(e,t.profile),publicIp:li(e,t.publicIp),lan:fi(e,t.lan)}}var cn=`set -u
337
337
  . /etc/os-release 2>/dev/null || true
338
338
  # Strings are stripped of quotes, backslashes and newlines rather than escaped:
339
339
  # nothing reported here legitimately contains them, and a probe that cannot
@@ -371,9 +371,9 @@ ${e.trim().slice(-2000)}`);try{return JSON.parse(t)}catch(r){throw Error(`Prefli
371
371
  ${" ".repeat(t+10)}${r.remediation}`:n}).join(`
372
372
  `)}var wt="/var/ts-cloud/staging",pn=Ge,hi="TS_CLOUD_SSH_SKIP_BOOTSTRAP",Oc=["size","instances","appServers","servicesServer","loadBalancer","fleet","image","bakedImage","allowSsh"];function _t(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function Dc(e){let t=xc("sha256");for await(let r of Rc(e))t.update(r);return t.digest("hex")}function Si(e,t){if(e.cloud?.attachTo)throw Error("cloud.attachTo is not supported by the ssh provider: it deploys to the hosts in ssh.hosts and nothing else.");if(t.hosts.length>1)throw Error(`ssh.hosts lists ${t.hosts.length} hosts; multi-host ssh fleets are not supported yet.`);if(Ii(e.infrastructure?.compute?.managedServices?.vitess))Wt(e,t.profile);let r=e.infrastructure?.compute??{};return Oc.filter((n)=>r[n]!==void 0).map((n)=>`infrastructure.compute.${n} has no effect with the ssh provider; the host is what it is.`)}class yr{name="ssh";usesCloudFormation=!1;settings;cwd;bootWait;transportInstance;constructor(e={}){this.settings=fr(void 0,{hosts:e.hosts,user:e.user,port:e.port,privateKeyPath:e.privateKeyPath,hostKey:e.hostKey,sudo:e.sudo,profile:e.profile,publicIp:e.publicIp,lan:e.lan}),this.cwd=e.cwd??process.cwd(),this.transportInstance=e.transport,this.bootWait={sshIntervalMs:e.bootWait?.sshIntervalMs??5000,sshTimeoutMs:e.bootWait?.sshTimeoutMs??300000,cloudInitIntervalMs:e.bootWait?.cloudInitIntervalMs??5000,cloudInitTimeoutMs:e.bootWait?.cloudInitTimeoutMs??600000}}get host(){if(this.settings.hosts.length>1)throw Error(`ssh.hosts lists ${this.settings.hosts.length} hosts; multi-host ssh fleets are not supported yet.`);return this.settings.hosts[0]}get transport(){if(!this.transportInstance){let e=this.host;this.transportInstance=new jt({user:e.user,port:e.port,identityFile:e.privateKeyPath,hostKey:this.settings.hostKey==="insecure"?"insecure":{policy:this.settings.hostKey,knownHostsFile:Qr(this.cwd)}})}return this.transportInstance}exec(e,t){return this.transport.exec(e,t,{sudo:this.settings.sudo})}async findComputeTargets(e){if((e.role||"app")!=="app")return[];let{host:t}=this.host;return[{id:t,name:t,publicIp:t,status:"running"}]}async getComputeOutputs(){let{host:e,user:t}=this.host;return{appInstanceId:e,appPublicIp:e,sshUser:t,deployStoragePath:wt}}async uploadRelease(e){let t=e.targets?.length?e.targets:await this.findComputeTargets({slug:e.config.project.slug,environment:e.environment,role:"app",stackName:T(e.config,e.environment)});if(t.length===0)throw Error("No ssh compute targets found for release upload");let n=`${e.remoteKey.replace(/^releases\//,"").replace(/\.tar\.gz$/,"").replace(/\//g,"-")}-${gi()}.tar.gz`,s=`${wt}/${n}`,i=await Dc(e.localPath),o=`${pn}/${i}.tar.gz`;for(let a of t){if(!a.publicIp)throw Error(`Target ${a.id} has no address for SCP upload`);let c=["set -euo pipefail",`mkdir -p ${_t(pn)} ${_t(wt)}`,`test -s ${_t(o)}`,`cp -- ${_t(o)} ${_t(s)}`].join(`
373
373
  `);try{await this.exec(a.publicIp,c);continue}catch{}let l=`${pn}/.${i}-${gi()}.tmp`;try{await this.transport.scp(a.publicIp,e.localPath,l),await this.exec(a.publicIp,kt(l,o,s))}catch(u){throw await this.exec(a.publicIp,Mt(l)).catch(()=>{}),u}}return{artifactRef:s}}async runRemoteDeploy(e){if(e.targets.length===0)return{success:!1,instanceCount:0,perInstance:[],error:"No targets provided"};let t=e.commands.join(`
374
- `),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing address"});continue}try{let i=await this.exec(s.publicIp,t);Ue(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:Oe(r,"One or more SSH deploy commands failed")}}targetHost(){return this.host.host}ensureHostKey(e=this.host.host){return this.transport.ensureHostKey(e)}async preflight(e=this.host.host){let t=await this.transport.exec(e,cn,{sudo:!1}),r=ln(t);return{facts:r,findings:un(r,{user:this.host.user,profile:this.settings.profile})}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e;for(let h of Si(t,this.settings))console.warn(`[ts-cloud] ${h}`);let n=this.host,s=T(t,r),i=await Ne(s,this.cwd),o=process.env[hi]==="1"&&i?.provider==="ssh"&&i.host===n.host&&i.bootstrapVersion===Ce,a=await this.ensureHostKey(n.host);if(a.pinnedNow)console.warn(`[ts-cloud] pinned ${n.host} host key ${a.fingerprint}`);await this.waitForSshReady(n.host);let{facts:c,findings:l}=await this.preflight(n.host);if(hr(l))throw Error(`Preflight failed for ${n.host}; refusing to bootstrap it.
375
- ${rn(l)}`);for(let h of l)console.warn(`[ts-cloud] ${h.severity}: ${h.message}`);if(c.cloudInitPresent&&/running/i.test(c.cloudInitStatus??""))await this.waitForCloudInit(n.host);let u=c.lanIp?.trim()||(Ec(n.host)!==0?n.host:void 0),d=i?.provider==="ssh"?i.bootstrappedAt:void 0;if(!o){let h=gr({config:t,environment:r,profile:this.settings.profile,facts:c,sudoUser:n.user!=="root"?n.user:void 0,lan:this.settings.lan,lanIp:u});Ue(await this.exec(n.host,h)),d=new Date().toISOString()}let m={provider:"ssh",stackName:s,host:n.host,sshUser:n.user,sshPort:n.port,hostKeyFingerprint:a.fingerprint||void 0,publicIp:this.settings.publicIp==="auto"?void 0:this.settings.publicIp,lanIp:c.lanIp||void 0,deployStoragePath:wt,profile:this.settings.profile,bootstrapVersion:Ce,bootstrappedAt:d};await Qe(s,m,this.cwd);let f=await this.getComputeOutputs();if(Z(t.infrastructure?.compute)&&br(this.settings.lan)==="local-ca")f.lanCaCertPath=Et(),console.warn(`[ts-cloud] LAN certificate authority on ${n.host} at ${f.lanCaCertPath}. Fetch it with: scp ${n.user}@${n.host}:${f.lanCaCertPath} ./rpx-root-ca.crt`);return f}async destroyCompute(e){let t=T(e.config,e.environment),r=this.host;return console.warn(`[ts-cloud] forgot ${r.host} for ${t}; nothing was removed on the host (stop the systemd units and remove /var/ts-cloud and /etc/rpx by hand if you want it clean).`),await Qe(t,{provider:"ssh",stackName:t,host:r.host,sshUser:r.user,sshPort:r.port},this.cwd).catch(()=>{}),{destroyed:[`local state for ${t}`]}}async sleep(e){await new Promise((t)=>setTimeout(t,e))}async waitForSshReady(e){let{sshIntervalMs:t,sshTimeoutMs:r}=this.bootWait,n=Date.now();do{if(await this.transport.probe(e))return;await this.sleep(t)}while(Date.now()-n<r);throw Error(`Timed out waiting for SSH on ${e} after ${r}ms`)}async waitForCloudInit(e){let{cloudInitIntervalMs:t,cloudInitTimeoutMs:r}=this.bootWait,n=Date.now();while(Date.now()-n<r){try{let s=await this.transport.exec(e,"if command -v cloud-init >/dev/null 2>&1; then cloud-init status --long 2>/dev/null || true; else echo 'status: done'; fi",{sudo:!1});if(/status:\s*error/.test(s))throw Error(`cloud-init reported an error on ${e}:
376
- ${s}`);if(/status:\s*(?:done|degraded)/.test(s))return}catch(s){if(s instanceof Error&&/cloud-init reported an error/.test(s.message))throw s}await this.sleep(t)}throw Error(`Timed out waiting for cloud-init to finish on ${e} after ${r}ms`)}}function O(e){if(Xr())return new Kt;switch(e.provider??B(e.config)){case"aws":return new It({region:e.config.project.region});case"hetzner":return new $e({apiToken:e.config.hetzner?.apiToken,allowStateOnly:Boolean(e.config.cloud?.attachTo),sshPrivateKeyPath:e.config.hetzner?.sshPrivateKeyPath,sshPublicKeyPath:e.config.hetzner?.sshPublicKeyPath,sshUser:e.config.hetzner?.sshUser,location:e.config.hetzner?.location});case"ssh":return new yr({hosts:e.config.ssh?.hosts,hostKey:e.config.ssh?.hostKey,sudo:e.config.ssh?.sudo,profile:e.config.ssh?.profile,publicIp:e.config.ssh?.publicIp,lan:e.config.ssh?.lan});case"fly":throw Error("Fly.io has no box to provision or reach over SSH: deploy it with deployToFly() from @stacksjs/ts-cloud.");default:throw Error(`Unknown cloud provider: ${e.provider??B(e.config)}`)}}class yi{drivers=new Map;getDriver(e,t){let r=t??B(e),n=`${r}:${e.project.slug}:${e.project.region||"default"}`,s=this.drivers.get(n);if(s)return s;let i=O({config:e,provider:r});return this.drivers.set(n,i),i}}var dm=new yi;var vi="/var/lib/ts-cloud/bootstrap.sh",to={"raspberry-pi-os":"bootfs",ubuntu:"system-boot"},Ic=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/,Ac=/^[a-z_][a-z0-9_-]{0,31}$/,Lc=/^[A-Z]{2}$/;function se(e){return JSON.stringify(e)}function Nc(e){if(!Ic.test(e.hostname))throw Error(`Invalid hostname '${e.hostname}': use lowercase letters, digits and hyphens.`);if(!Ac.test(e.user))throw Error(`Invalid user '${e.user}': use a lowercase Unix user name.`);let t=e.publicKey.trim();if(!t||t.includes(`
374
+ `),r=[];for(let s of e.targets){if(!s.publicIp){r.push({instanceId:s.id,status:"Failed",error:"Missing address"});continue}try{let i=await this.exec(s.publicIp,t);Ue(i),r.push({instanceId:s.id,status:"Success",output:i})}catch(i){r.push({instanceId:s.id,status:"Failed",error:i.message})}}let n=r.every((s)=>s.status==="Success");return{success:n,instanceCount:e.targets.length,perInstance:r,error:n?void 0:De(r,"One or more SSH deploy commands failed")}}targetHost(){return this.host.host}ensureHostKey(e=this.host.host){return this.transport.ensureHostKey(e)}async preflight(e=this.host.host){let t=await this.transport.exec(e,cn,{sudo:!1}),r=ln(t);return{facts:r,findings:un(r,{user:this.host.user,profile:this.settings.profile})}}async provisionComputeInfrastructure(e){let{config:t,environment:r}=e;for(let h of Si(t,this.settings))console.warn(`[ts-cloud] ${h}`);let n=this.host,s=T(t,r),i=await Ne(s,this.cwd),o=process.env[hi]==="1"&&i?.provider==="ssh"&&i.host===n.host&&i.bootstrapVersion===Pe,a=await this.ensureHostKey(n.host);if(a.pinnedNow)console.warn(`[ts-cloud] pinned ${n.host} host key ${a.fingerprint}`);await this.waitForSshReady(n.host);let{facts:c,findings:l}=await this.preflight(n.host);if(hr(l))throw Error(`Preflight failed for ${n.host}; refusing to bootstrap it.
375
+ ${rn(l)}`);for(let h of l)console.warn(`[ts-cloud] ${h.severity}: ${h.message}`);if(c.cloudInitPresent&&/running/i.test(c.cloudInitStatus??""))await this.waitForCloudInit(n.host);let u=c.lanIp?.trim()||(Ec(n.host)!==0?n.host:void 0),d=i?.provider==="ssh"?i.bootstrappedAt:void 0;if(!o){let h=gr({config:t,environment:r,profile:this.settings.profile,facts:c,sudoUser:n.user!=="root"?n.user:void 0,lan:this.settings.lan,lanIp:u});Ue(await this.exec(n.host,h)),d=new Date().toISOString()}let m={provider:"ssh",stackName:s,host:n.host,sshUser:n.user,sshPort:n.port,hostKeyFingerprint:a.fingerprint||void 0,publicIp:this.settings.publicIp==="auto"?void 0:this.settings.publicIp,lanIp:c.lanIp||void 0,deployStoragePath:wt,profile:this.settings.profile,bootstrapVersion:Pe,bootstrappedAt:d};await Qe(s,m,this.cwd);let f=await this.getComputeOutputs();if(Z(t.infrastructure?.compute)&&br(this.settings.lan)==="local-ca")f.lanCaCertPath=Et(),console.warn(`[ts-cloud] LAN certificate authority on ${n.host} at ${f.lanCaCertPath}. Fetch it with: scp ${n.user}@${n.host}:${f.lanCaCertPath} ./rpx-root-ca.crt`);return f}async destroyCompute(e){let t=T(e.config,e.environment),r=this.host;return console.warn(`[ts-cloud] forgot ${r.host} for ${t}; nothing was removed on the host (stop the systemd units and remove /var/ts-cloud and /etc/rpx by hand if you want it clean).`),await Qe(t,{provider:"ssh",stackName:t,host:r.host,sshUser:r.user,sshPort:r.port},this.cwd).catch(()=>{}),{destroyed:[`local state for ${t}`]}}async sleep(e){await new Promise((t)=>setTimeout(t,e))}async waitForSshReady(e){let{sshIntervalMs:t,sshTimeoutMs:r}=this.bootWait,n=Date.now();do{if(await this.transport.probe(e))return;await this.sleep(t)}while(Date.now()-n<r);throw Error(`Timed out waiting for SSH on ${e} after ${r}ms`)}async waitForCloudInit(e){let{cloudInitIntervalMs:t,cloudInitTimeoutMs:r}=this.bootWait,n=Date.now();while(Date.now()-n<r){try{let s=await this.transport.exec(e,"if command -v cloud-init >/dev/null 2>&1; then cloud-init status --long 2>/dev/null || true; else echo 'status: done'; fi",{sudo:!1});if(/status:\s*error/.test(s))throw Error(`cloud-init reported an error on ${e}:
376
+ ${s}`);if(/status:\s*(?:done|degraded)/.test(s))return}catch(s){if(s instanceof Error&&/cloud-init reported an error/.test(s.message))throw s}await this.sleep(t)}throw Error(`Timed out waiting for cloud-init to finish on ${e} after ${r}ms`)}}function O(e){if(Xr())return new Kt;switch(e.provider??B(e.config)){case"aws":return new It({region:e.config.project.region});case"hetzner":return new Ce({apiToken:e.config.hetzner?.apiToken,allowStateOnly:Boolean(e.config.cloud?.attachTo),sshPrivateKeyPath:e.config.hetzner?.sshPrivateKeyPath,sshPublicKeyPath:e.config.hetzner?.sshPublicKeyPath,sshUser:e.config.hetzner?.sshUser,location:e.config.hetzner?.location});case"ssh":return new yr({hosts:e.config.ssh?.hosts,hostKey:e.config.ssh?.hostKey,sudo:e.config.ssh?.sudo,profile:e.config.ssh?.profile,publicIp:e.config.ssh?.publicIp,lan:e.config.ssh?.lan});case"fly":throw Error("Fly.io has no box to provision or reach over SSH: deploy it with deployToFly() from @stacksjs/ts-cloud.");default:throw Error(`Unknown cloud provider: ${e.provider??B(e.config)}`)}}class yi{drivers=new Map;getDriver(e,t){let r=t??B(e),n=`${r}:${e.project.slug}:${e.project.region||"default"}`,s=this.drivers.get(n);if(s)return s;let i=O({config:e,provider:r});return this.drivers.set(n,i),i}}var dm=new yi;var vi="/var/lib/ts-cloud/bootstrap.sh",to={"raspberry-pi-os":"bootfs",ubuntu:"system-boot"},Ic=/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/,Ac=/^[a-z_][a-z0-9_-]{0,31}$/,Lc=/^[A-Z]{2}$/;function se(e){return JSON.stringify(e)}function Nc(e){if(!Ic.test(e.hostname))throw Error(`Invalid hostname '${e.hostname}': use lowercase letters, digits and hyphens.`);if(!Ac.test(e.user))throw Error(`Invalid user '${e.user}': use a lowercase Unix user name.`);let t=e.publicKey.trim();if(!t||t.includes(`
377
377
  `)||!/^(?:ssh|ecdsa|sk)-/.test(t))throw Error("publicKey must be one public key line, e.g. the contents of ~/.ssh/id_ed25519.pub.");if(e.wifi){if(!e.wifi.ssid)throw Error("wifi.ssid is required.");if(!e.wifi.passphrase)throw Error("wifi.passphrase is required.");if(!Lc.test(e.wifi.country))throw Error(`wifi.country must be a two-letter regulatory domain such as DE or US, not '${e.wifi.country}'.`)}}function Hc(e,t){let r=[`hostname: ${se(e.hostname)}`,"manage_etc_hosts: true","users:",` - name: ${se(e.user)}`," ssh_authorized_keys:",` - ${se(e.publicKey.trim())}`,` sudo: ${se("ALL=(ALL) NOPASSWD:ALL")}`," shell: /bin/bash"," lock_passwd: true","ssh_pwauth: false"];if(e.timezone)r.push(`timezone: ${se(e.timezone)}`);if(e.locale)r.push(`locale: ${se(e.locale)}`);if(r.push("package_update: true"),t==="raspberry-pi-os")r.push("rpi:"," enable_ssh: true"," interfaces:"," spi: false"," i2c: false"," onewire: false"," remote_gpio: false"," serial:"," console: false"," hardware: false");return`${r.join(`
378
378
  `)}
379
379
  `}function Uc(e){let t=["version: 2","ethernets:"," eth0:"," match:",` name: ${se("e*")}`," dhcp4: true"," dhcp6: true"," optional: true"];if(e.wifi)t.push("wifis:"," wlan0:"," dhcp4: true"," dhcp6: true"," optional: true",` regulatory-domain: ${se(e.wifi.country)}`," access-points:",` ${se(e.wifi.ssid)}:`,` password: ${se(e.wifi.passphrase)}`);return`${t.join(`
@@ -518,15 +518,15 @@ console.log(JSON.stringify({
518
518
  publicTcpPorts: [...new Set(publicTcpPorts)].sort((a, b) => a - b),
519
519
  }))
520
520
  `;function tl(e){return`printf %s ${Buffer.from(e).toString("base64")} | base64 -d | bun -`}async function ls(e,t){let r=Ti(e).join(`
521
- `),n=`printf %s ${Buffer.from(r).toString("base64")} | base64 -d | bash`;await _e(t.host,n,t)}async function ds(e){let t=await _e(e.host,tl(el),e);return JSON.parse(t)}function us(e,t){let r=[],n=new Set(t.firewallPorts),s=e.firewallPorts.filter((o)=>!n.has(o)),i=t.firewallPorts.filter((o)=>!e.firewallPorts.includes(o));if(!t.firewallActive)r.push("UFW is not active");if(s.length>0)r.push(`UFW is missing ports: ${s.join(", ")}`);if(i.length>0)r.push(`UFW has unexpected ports: ${i.join(", ")}`);if(t.passwordAuthentication)r.push("SSH password authentication remains enabled");if(t.rootPasswordLogin)r.push("SSH root password login remains enabled");if(!t.fail2banActive)r.push("fail2ban is not active");if(e.autoUpdates&&!t.unattendedUpgradesActive)r.push("unattended upgrades are not active");if(e.monitoring&&!t.metricsTimerActive)r.push("metrics timer is not active");if(e.monitoring&&!t.metricsSnapshotFresh)r.push("metrics snapshot is stale");if(e.swapGb>0&&t.swapBytes<e.swapGb*1073741824*0.9)r.push("configured swap is missing");if(t.failedUnits.length>0)r.push(`failed services: ${t.failedUnits.join(", ")}`);return r}import{mkdir as qt,readFile as rl,rename as $i,rmdir as nl,stat as sl,unlink as il,writeFile as Ci}from"node:fs/promises";import{join as Xt}from"node:path";function mn(e){return Xt(we(),`${e}-resize.json`)}function Pi(e){return Xt(we(),`${e}-resize.lock`)}async function ps(e){try{return JSON.parse(await rl(mn(e),"utf8"))}catch{return null}}async function gt(e){let t=mn(e.stackName);await qt(we(),{recursive:!0});let r=`${t}.${process.pid}.tmp`;await Ci(r,`${JSON.stringify(e,null,2)}
522
- `,"utf8"),await $i(r,t)}async function ms(e,t=1800000){let r=Pi(e);await qt(we(),{recursive:!0});try{await qt(r)}catch(n){if(Date.now()-(await sl(r)).mtimeMs<=t)throw Error(`Another resize process holds ${r}.`);await $i(r,`${r}.stale-${Date.now()}`),await qt(r)}return await Ci(Xt(r,"owner.json"),`${JSON.stringify({pid:process.pid,acquiredAt:new Date().toISOString()},null,2)}
521
+ `),n=`printf %s ${Buffer.from(r).toString("base64")} | base64 -d | bash`;await _e(t.host,n,t)}async function ds(e){let t=await _e(e.host,tl(el),e);return JSON.parse(t)}function us(e,t){let r=[],n=new Set(t.firewallPorts),s=e.firewallPorts.filter((o)=>!n.has(o)),i=t.firewallPorts.filter((o)=>!e.firewallPorts.includes(o));if(!t.firewallActive)r.push("UFW is not active");if(s.length>0)r.push(`UFW is missing ports: ${s.join(", ")}`);if(i.length>0)r.push(`UFW has unexpected ports: ${i.join(", ")}`);if(t.passwordAuthentication)r.push("SSH password authentication remains enabled");if(t.rootPasswordLogin)r.push("SSH root password login remains enabled");if(!t.fail2banActive)r.push("fail2ban is not active");if(e.autoUpdates&&!t.unattendedUpgradesActive)r.push("unattended upgrades are not active");if(e.monitoring&&!t.metricsTimerActive)r.push("metrics timer is not active");if(e.monitoring&&!t.metricsSnapshotFresh)r.push("metrics snapshot is stale");if(e.swapGb>0&&t.swapBytes<e.swapGb*1073741824*0.9)r.push("configured swap is missing");if(t.failedUnits.length>0)r.push(`failed services: ${t.failedUnits.join(", ")}`);return r}import{mkdir as qt,readFile as rl,rename as $i,rmdir as nl,stat as sl,unlink as il,writeFile as Ci}from"node:fs/promises";import{join as Xt}from"node:path";function mn(e){return Xt($e(),`${e}-resize.json`)}function Pi(e){return Xt($e(),`${e}-resize.lock`)}async function ps(e){try{return JSON.parse(await rl(mn(e),"utf8"))}catch{return null}}async function gt(e){let t=mn(e.stackName);await qt($e(),{recursive:!0});let r=`${t}.${process.pid}.tmp`;await Ci(r,`${JSON.stringify(e,null,2)}
522
+ `,"utf8"),await $i(r,t)}async function ms(e,t=1800000){let r=Pi(e);await qt($e(),{recursive:!0});try{await qt(r)}catch(n){if(Date.now()-(await sl(r)).mtimeMs<=t)throw Error(`Another resize process holds ${r}.`);await $i(r,`${r}.stale-${Date.now()}`),await qt(r)}return await Ci(Xt(r,"owner.json"),`${JSON.stringify({pid:process.pid,acquiredAt:new Date().toISOString()},null,2)}
523
523
  `,"utf8"),async()=>{await il(Xt(r,"owner.json")).catch(()=>{}),await nl(r).catch(()=>{})}}import{copyFileSync as Al}from"node:fs";import{tmpdir as Ll}from"node:os";import{join as Nl}from"node:path";var ul=`${j}/project`;function xi(e,t){let r=t.ownerSlug.trim(),n=t.selfSlug?.trim(),s={total:e.length,owner:[],self:[],others:new Map,unmanaged:[]};for(let i of e){let o=i.labels?.[ul]?.trim();if(!o){s.unmanaged.push(i.name);continue}if(o===r){s.owner.push(i.name);continue}if(n&&o===n){s.self.push(i.name);continue}let a=s.others.get(o);if(a)a.push(i.name);else s.others.set(o,[i.name])}return s}function gn(e){let t=e.unmanaged.length;for(let r of e.others.values())t+=r.length;return t}function Ei(e,t){let r=gn(e),n=[];if(n.push(`Attaching to '${t.ownerSlug}' shares one provider project, so this deploy's credential can write to all ${e.total} server(s) it can see.`),r===0)return n.push("Nothing outside the two projects being joined is reachable with it."),n;n.push(`${r} of them belong to neither project:`);for(let s of[...e.others.keys()].sort())n.push(` ${s}: ${[...e.others.get(s)].sort().join(", ")}`);if(e.unmanaged.length>0)n.push(` not managed by ts-cloud: ${[...e.unmanaged].sort().join(", ")}`);return n.push("A compromised CI run or a mistargeted teardown in this project now reaches those. Keep the app in its own provider project instead if that is not acceptable, which rules out attaching."),n}var pl=new Set(["laravel","php","statamic","wordpress"]);function on(e){if(e.framework)return e.framework;if(e.type&&pl.has(e.type))return"laravel";return"stacks"}var Oi=`eval "$(cd ${U} && pantry env 2>/dev/null)"`,Di="/usr/local/bin/bun",dl="node_modules/@stacksjs/ts-cloud/dist/bin/stacks-scheduler.js",ml="node_modules/@stacksjs/ts-cloud/dist/bin/stacks-queue-worker.js",Ai={id:"stacks",wrapExec:(e)=>e,execEnv:{PATH:"/usr/local/bin:/usr/bin:/bin",BUN_INSTALL:"/root/.bun",APP_ENV:"production",NODE_ENV:"production"},schedulerMode:"daemon",schedulerCommand:()=>`${Di} ${dl}`,queueWorkerCommand:(e,{current:t})=>{let r=[`--queue=${e.queue||"default"}`,`--sleep=${e.sleep??3}`,`--tries=${e.tries??3}`,`--timeout=${e.timeout??60}`];return`${Di} ${t}/${ml} ${r.join(" ")}`}},fl={id:"laravel",wrapExec:(e)=>`/bin/sh -lc '${Oi}; exec ${e}'`,schedulerMode:"cron",schedulerCommand:({current:e})=>`cd ${e} && ${Oi} && php artisan schedule:run`,queueWorkerCommand:(e,{current:t})=>{let r=`${t}/artisan`;if(e.horizon)return`php ${r} horizon`;let n=[e.connection||"default",`--queue=${e.queue||"default"}`,`--sleep=${e.sleep??3}`,`--tries=${e.tries??3}`,`--timeout=${e.timeout??60}`,`--memory=${e.memory??128}`];if(e.maxJobs)n.push(`--max-jobs=${e.maxJobs}`);if(e.maxTime)n.push(`--max-time=${e.maxTime}`);return`php ${r} queue:work ${n.join(" ")}`}},gl={stacks:Ai,laravel:fl};function Li(e){return gl[e]??Ai}function Hi(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function hl(e){return`'${e.split("'").join("'\\''")}'`}function Ui(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unnamed"}function Sl(e,t,r){return`${e}-${t}-queue-${r}`}function yl(e,t,r,n){return`${e}-${t}-daemon-${r.name?Ui(r.name):Ui(r.command).slice(0,32)||String(n)}`}function Sn(e){let t=["[Unit]",`Description=${e.description}`,"After=network.target","","[Service]","Type=simple",`WorkingDirectory=${e.workingDir}`,...Object.entries(e.environment??{}).map(([r,n])=>`Environment="${r}=${n}"`),`ExecStart=${e.execStart}`,`Restart=${e.restart||"always"}`,"RestartSec=5","MemoryAccounting=true",`MemoryHigh=${e.memoryHigh??"2G"}`,...e.memoryMax?[`MemoryMax=${e.memoryMax}`]:[]];if(e.user)t.push(`User=${e.user}`);if(typeof e.stopWaitSecs==="number")t.push(`TimeoutStopSec=${e.stopWaitSecs}`);return t.push("","[Install]","WantedBy=multi-user.target"),`${t.join(`
524
524
  `)}
525
525
  `}function yn(e,t){return[`cat > /etc/systemd/system/${e}.service <<'TS_CLOUD_UNIT_EOF'`,t.replace(/\n$/,""),"TS_CLOUD_UNIT_EOF"]}function vl(e,t){return`/etc/cron.d/${e}-${t}-scheduler`}function vn(e){let{slug:t,siteName:r,site:n}=e,i=`${e.appBase??`/var/www/${r}`}/current`,o=Li(on(n)),a={current:i},c=[],l=[];(n.queues||[]).forEach((b,_)=>{let P=Math.max(1,b.processes??1);for(let S=0;S<P;S++){let E=Sl(t,r,l.length);l.push(E),c.push(...yn(E,Sn({description:`${r} queue worker ${_}.${S} (managed by ts-cloud)`,workingDir:i,execStart:o.wrapExec(o.queueWorkerCommand(b,a)),environment:o.execEnv,stopWaitSecs:b.stopWaitSecs??90})))}}),(n.daemons||[]).forEach((b,_)=>{let P=Math.max(1,b.processes??1);for(let S=0;S<P;S++){let E=`${yl(t,r,b,_)}-${S}`;l.push(E),c.push(...yn(E,Sn({description:`${r} daemon ${b.name||b.command} (managed by ts-cloud)`,workingDir:b.directory||i,execStart:o.wrapExec(b.command),environment:o.execEnv,restart:b.restart,user:b.user})))}});let m=n.scheduler,f=m===!0||typeof m==="object"&&m!==null,h=`${t}-${r}-scheduler`;if(f&&o.schedulerMode==="daemon"){l.push(h);let b=typeof m==="object"&&m!==null?m:{};c.push(...yn(h,Sn({description:`${r} scheduler (managed by ts-cloud)`,workingDir:i,execStart:o.wrapExec(o.schedulerCommand(a)),environment:o.execEnv,memoryHigh:b.memoryHigh,memoryMax:b.memoryMax})),...Ut(`${h}.service`,{auto:!1,high:Boolean(b.memoryHigh),max:Boolean(b.memoryMax)}))}let R=l.map((b)=>`${b}.service`).join(" ");c.push("systemctl daemon-reload",`TS_CLOUD_DESIRED="${R}"`,`for unit in $(ls /etc/systemd/system/ 2>/dev/null | grep -E '^${Hi(t)}-${Hi(r)}-((queue|daemon)-.*|scheduler)\\.service$' || true); do`,' case " $TS_CLOUD_DESIRED " in',' *" $unit "*) ;;',' *) systemctl stop "$unit" 2>/dev/null || true; systemctl disable "$unit" 2>/dev/null || true; rm -f "/etc/systemd/system/$unit" ;;'," esac","done","systemctl daemon-reload");for(let b of l)c.push(`systemctl enable ${b}.service`,`systemctl restart ${b}.service`);let x=vl(t,r);if(f&&o.schedulerMode==="cron"){let b=typeof m==="object"&&m!==null?m:void 0,_=`${o.schedulerCommand(a)} >> /dev/null 2>&1`;if(b?.heartbeatUrl){let S=b.heartbeatMethod||"GET",E=S==="GET"?"":`-X ${S} `;_+=` && curl -fsS -m 10 ${E}${hl(b.heartbeatUrl)} >/dev/null 2>&1`}let P=`* * * * * root ${_}
526
526
  `.replace(/%/g,"\\%");c.push(`cat > ${x} <<'TS_CLOUD_CRON_EOF'`,P.replace(/\n$/,""),"TS_CLOUD_CRON_EOF",`chmod 644 ${x}`)}else c.push(`rm -f ${x}`);return c}function bn(e){return!!(e.queues?.length||e.daemons?.length||e.scheduler)}function _n(e){if(e.ssl?.provider)return e.ssl.provider;return e.domain?"letsencrypt":"none"}var Mi={cloudflare:{pkg:"python3-certbot-dns-cloudflare",plugin:"dns-cloudflare"},route53:{pkg:"python3-certbot-dns-route53",plugin:"dns-route53"},digitalocean:{pkg:"python3-certbot-dns-digitalocean",plugin:"dns-digitalocean"},google:{pkg:"python3-certbot-dns-google",plugin:"dns-google"}};function Fi(e){return`/etc/letsencrypt/ts-cloud-${e}.ini`}function bl(e){let t=e?Mi[e.provider]:void 0,r=["certbot","python3-certbot-nginx",...t?[t.pkg]:[]].join(" ");return["export DEBIAN_FRONTEND=noninteractive",t?`apt-get update -y && apt-get install -y ${r}`:`command -v certbot >/dev/null 2>&1 || { apt-get update -y && apt-get install -y ${r}; }`,"mkdir -p /etc/letsencrypt/renewal-hooks/deploy","cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh <<'TS_CLOUD_HOOK_EOF'","#!/bin/sh","systemctl reload ts-cloud-nginx 2>/dev/null || systemctl reload nginx 2>/dev/null || true","TS_CLOUD_HOOK_EOF","chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh","systemctl enable certbot.timer 2>/dev/null || true","systemctl start certbot.timer 2>/dev/null || true"]}function _l(e){if(!e.credentials||Object.keys(e.credentials).length===0)return[];let t=Fi(e.provider),r=Object.entries(e.credentials).map(([n,s])=>`${n} = ${s}`).join(`
527
527
  `);return[`cat > ${t} <<'TS_CLOUD_DNSCREDS_EOF'`,r,"TS_CLOUD_DNSCREDS_EOF",`chmod 600 ${t}`]}function wl(e){let t=e.dns,r=e.wildcard?[`*.${e.domain}`,e.domain]:[e.domain,...e.aliases||[]].filter(Boolean),n=["certbot","--non-interactive","--agree-tos","--keep-until-expiring"];if(t){let s=Mi[t.provider].plugin;if(n.push(`--${s}`),t.provider!=="route53"&&t.credentials)n.push(`--${s}-credentials ${Fi(t.provider)}`);if(typeof t.propagationSeconds==="number"&&t.provider!=="route53")n.push(`--${s}-propagation-seconds ${t.propagationSeconds}`);n.push("certonly")}else n.push("--nginx",`--nginx-ctl ${be}`,e.redirect===!1?"--no-redirect":"--redirect");if(e.email)n.push(`-m ${e.email}`);else n.push("--register-unsafely-without-email");for(let s of r)n.push(`-d ${s}`);return[n.join(" ")]}function wn(e){if(_n(e)!=="letsencrypt"||!e.domain)return[];let t=e.ssl,r=t?.dns,n=t?.wildcard===!0;if(n&&!r)return[];return[...bl(r),...r?_l(r):[],...wl({domain:e.domain,aliases:e.aliases,email:t?.email,wildcard:n,dns:r})]}function zi(e){let{repository:t,releaseDir:r,commit:n}=e,s=t.branch||"main",i=t.url,o=[`rm -rf ${r}`,`mkdir -p ${r}`];if(t.strategy==="tag")if(t.tag)o.push(`git clone -q --depth 1 --branch ${ie(t.tag)} ${ie(i)} ${r}`),o.push(`printf '%s' ${ie(t.tag)} > ${r}/.ts-cloud-tag`);else{let a=t.tagPattern||"v*";o.push(`TS_CLOUD_TAG="$(git ls-remote --tags --refs --sort=-v:refname ${ie(i)} ${ie(`refs/tags/${a}`)} | head -n1 | sed 's#.*refs/tags/##')"`,`test -n "$TS_CLOUD_TAG" || { echo "no tags matching" ${ie(a)} "found in" ${ie(i)} >&2; exit 1; }`,`git clone -q --depth 1 --branch "$TS_CLOUD_TAG" ${ie(i)} ${r}`,`printf '%s' "$TS_CLOUD_TAG" > ${r}/.ts-cloud-tag`)}else if(n)o.push(`git -C ${r} init -q`,`git -C ${r} remote add origin ${ie(i)}`,`git -C ${r} fetch -q --depth 1 origin ${ie(n)}`,`git -C ${r} checkout -q FETCH_HEAD`);else o.push(`git clone -q --depth 1 --branch ${ie(s)} ${ie(i)} ${r}`);return o.push(`git -C ${r} rev-parse HEAD > ${r}/.ts-cloud-sha`),o}function ie(e){return`'${e.split("'").join("'\\''")}'`}var Yt="$CREATE_RELEASE",Qt="$ACTIVATE_RELEASE",ji="$RESTART_QUEUES",Bi="composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev",Tl=["php artisan migrate --force","php artisan config:cache","php artisan route:cache","php artisan view:cache","php artisan event:cache","php artisan storage:link"];function $l(e){switch(e){case"laravel":case"statamic":return[Yt,Bi,...Tl,Qt,ji];case"php":case"wordpress":return[Yt,`${Bi} || true`,Qt];case"static":case"spa":return[Yt,Qt]}}function Cl(e,t){return e.replace(/^php\s+/,`${t} `).replace(/(\s)php\s+artisan\s+/g,`$1${t} artisan `)}function Pl(e,t){let r=At(t);return[`cat > ${e} <<'TS_CLOUD_ENV_EOF'`,r,"TS_CLOUD_ENV_EOF",`chmod 600 ${e}`]}function Ki(e,t,r){return[`cat > ${e} <<'${r}'`,t.replace(/\n$/,""),r,`chmod 600 ${e}`]}function xl(e,t){if(!t)return[];let r=[];if(t.composerAuth){let n=typeof t.composerAuth==="string"?t.composerAuth:JSON.stringify(t.composerAuth,null,2);r.push(...Ki(`${e}/auth.json`,n,"TS_CLOUD_AUTHJSON_EOF"))}if(t.npmrc)r.push(...Ki(`${e}/.npmrc`,t.npmrc,"TS_CLOUD_NPMRC_EOF"));return r}function Wi(e){let t=e.healthCheck?.path;if(!t||!e.domain)return[];let n=`http://127.0.0.1${t.startsWith("/")?t:`/${t}`}`;return[`echo "[ts-cloud] health check ${n} (Host: ${e.domain})"`,"TS_CLOUD_HC_OK=0",`for i in $(seq 1 10); do TS_CLOUD_HC=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: ${e.domain}" -m 10 "${n}" 2>/dev/null || echo 000); case "$TS_CLOUD_HC" in 2*|3*) TS_CLOUD_HC_OK=1; break ;; esac; sleep 3; done`,'[ "$TS_CLOUD_HC_OK" = 1 ] && echo "health check passed ($TS_CLOUD_HC)" || { echo "health check FAILED (last=$TS_CLOUD_HC)" >&2; exit 1; }']}function Gi(e){let{siteName:t,site:r,releaseId:n,commit:s}=e;if(!r.repository?.url)throw Error(`Site '${t}' is a PHP/git site but has no repository.url to clone`);let i=e.appBase??`/var/www/${t}`,o="php",a=Ht(i,n),c=r.sharedPaths??Mo,l=[...c,...Lt(r.env,c)],u=r.keepReleases??ki,d=r.deployScript?.length?r.deployScript:$l(r.type??"laravel"),m=["set -euo pipefail",'export HOME="${HOME:-/root}"','export COMPOSER_HOME="${COMPOSER_HOME:-/root/.composer}"',"export COMPOSER_ALLOW_SUPERUSER=1",Ct()];if(m.push(...Bo(i,{releaseId:n,commit:s,branch:r.repository.branch,keepLogs:u})),m.push(...Ni(a,l)),r.env&&Object.keys(r.env).length>0)m.push(...Pl(`${a.shared}/.env`,r.env));for(let f of d){let h=f.trim();if(h===Yt)m.push(...zi({repository:r.repository,releaseDir:a.release,commit:s})),m.push(...Fo(a,l)),m.push(`cd ${a.release}`),m.push(...xl(a.release,r.credentials)),m.push(`chown -R www-data:www-data ${a.shared}/storage 2>/dev/null || true`,`[ -d ${a.release}/bootstrap/cache ] && chown -R www-data:www-data ${a.release}/bootstrap/cache 2>/dev/null || true`,`chmod -R ug+rwX ${a.shared}/storage 2>/dev/null || true`);else if(h===Qt)m.push(...fn(a)),m.push(...hn(a,u)),m.push(`(cd ${U} && pantry restart php-fpm) 2>/dev/null || true`);else if(h===ji)m.push(`${o} artisan queue:restart || true`);else m.push(Cl(f,o))}return m}var Zt={mysql:{port:3306,binaries:["mysqld","mysql"]},mariadb:{port:3306,binaries:["mariadbd","mariadb","mysqld"]},postgres:{port:5432,binaries:["postgres","psql"]},redis:{port:6379,binaries:["redis-server","redis-cli"]},memcached:{port:11211,binaries:["memcached"]},meilisearch:{port:7700,binaries:["meilisearch"]},vitess:{port:15306,binaries:["vtgate"]}},Pt="ts-cloud-service:";function Rl(e){return e===!0||typeof e==="object"&&e!=null}function Vi(e){if(!e)return[];return Object.keys(Zt).filter((t)=>Rl(e[t]))}function qi(e){if(e.length===0)return[];return[Ct(),"ts_cloud_port_open() {",' if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]" && return 0; fi',' if command -v netstat >/dev/null 2>&1; then netstat -ltn 2>/dev/null | grep -qE "[:.]$1[[:space:]]" && return 0; fi'," (exec 3<>/dev/tcp/127.0.0.1/$1) 2>/dev/null && { exec 3<&- 3>&-; return 0; }"," return 1","}","ts_cloud_probe() {",' TS_CLOUD_SVC="$1"; TS_CLOUD_PORT="$2"; shift 2',' for TS_CLOUD_BIN in "$@"; do',` if command -v "$TS_CLOUD_BIN" >/dev/null 2>&1; then echo "${Pt}$TS_CLOUD_SVC:present"; return 0; fi`," done",` if ts_cloud_port_open "$TS_CLOUD_PORT"; then echo "${Pt}$TS_CLOUD_SVC:present"; return 0; fi`,` echo "${Pt}$TS_CLOUD_SVC:missing"`," return 0","}",...e.filter((t)=>Zt[t]).map((t)=>`ts_cloud_probe ${t} ${Zt[t].port} ${Zt[t].binaries.join(" ")}`),"exit 0"]}function so(e,t){if(!e)return[];let r=new Map;for(let n of e.split(`
528
528
  `)){let s=n.indexOf(Pt);if(s===-1)continue;let[i,o]=n.slice(s+Pt.length).trim().split(":");if(i)r.set(i,o)}return t.filter((n)=>r.get(n)==="missing")}function ao(e,t,r){let n=r?` on ${r}`:"",s=e.map((o)=>`managedServices.${o}`).join(", "),i=e.length===1?`no ${e[0]}`:`none of ${e.join(", ")}`;return`This project declares ${s}, but '${t}' has ${i}${n}. Attach mode does not provision services on the owner's box, so nothing will install ${e.length===1?"it":"them"}. Point the app at an external service, or ask the owner of '${t}' to add ${e.length===1?"it":"them"} to their own config and re-provision.`}var co="/var/lib/pantry/php-fpm/pool.d",El=9100,Ol=400;function er(e){return(e.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"")||"site").slice(0,28)}function lo(e){return`web_${er(e)}`}function Dl(e){let t=er(e),r=0;for(let n=0;n<t.length;n++)r=r*31+t.charCodeAt(n)>>>0;return El+r%Ol}function Tn(e){return`127.0.0.1:${Dl(e)}`}function Il(e){let t=er(e.siteName),r=lo(e.siteName),n=e.maxChildren??10;return[`; ts-cloud-managed pool for ${e.siteName} (site isolation)`,`[${t}]`,`user = ${r}`,`group = ${r}`,`listen = ${Tn(e.siteName)}`,"pm = dynamic",`pm.max_children = ${n}`,"pm.start_servers = 2","pm.min_spare_servers = 1","pm.max_spare_servers = 3","pm.max_requests = 500","catch_workers_output = yes",`php_admin_value[open_basedir] = ${e.appBase}:/tmp`,""].join(`
529
- `)}function uo(e){let t=lo(e.siteName),r=er(e.siteName),n=`${co}/${r}.conf`;return[`getent group ${t} >/dev/null || groupadd --system ${t}`,`id -u ${t} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin -g ${t} ${t}`,`usermod -aG ${t} www-data 2>/dev/null || true`,`chown -R ${t}:${t} ${e.appBase} || true`,`chmod -R g+rX ${e.appBase} || true`,`chmod 600 ${e.appBase}/shared/.env 2>/dev/null || true`,`mkdir -p ${co}`,`cat > ${n} <<'TS_CLOUD_FPMPOOL_EOF'`,Il(e).replace(/\n$/,""),"TS_CLOUD_FPMPOOL_EOF",`(cd ${U} && pantry restart php-fpm) 2>/dev/null || true`]}var rr={info:()=>{},warn:()=>{},error:()=>{},step:()=>{},success:()=>{}};function Hl(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function mo(e,t,r=rr){let{config:n,environment:s,siteName:i,site:o,slug:a,sha:c,runtime:l,localTarballPath:u}=t,d=`releases/${i}/${c}.tar.gz`,m=T(n,s),f=await e.getComputeOutputs({config:n,environment:s}),h=await e.findComputeTargets({slug:a,environment:s,role:"app",stackName:m});if(h.length===0)return{success:!1,error:e.name==="aws"?`Stack '${m}' has no EC2 instances tagged Project=${a} Environment=${s} Role=app, and storage/cloud/state/${m}.json pins no live instance. For a shared box, record its instanceId there.`:`No Hetzner servers labeled ts-cloud/project=${a} ts-cloud/environment=${s} ts-cloud/role=app, and storage/cloud/state/${m}.json pins no live server. For a shared box, record its serverId there.`};let R=f.servicesPrivateIp?Ds(f.servicesPrivateIp,w(n)):Gc(w(n)),x=da(ne(n,{environment:s})),b=n.infrastructure?.compute,_=Ar(b?.hostCleanup),P=Z({...b,proxy:le(n,{environment:s})})?{HOST:"127.0.0.1"}:{},S=s==="production"?{APP_ENV:"production",NODE_ENV:"production"}:{},E={...R,...x,...P,...o.env||{},...S},y=xr(E,o.sharedPaths??[]);if(y.warning)r.warn(`Site '${i}': ${y.warning}`);else if(y.path)r.info(`Site '${i}': sharing '${y.path}' so the SQLite database survives deploys.`);if(Ri(o)){let L=o.phpVersion??b?.php?.default??b?.php?.versions?.[0],M=J(a,i),D=Object.keys(R).length>0?{...o,env:E}:o,F=Gi({siteName:i,site:D,releaseId:c,appBase:M,defaultPhpVersion:L}),z=!Z(b),Se=_n(o)==="custom"&&o.ssl?.certPath&&o.ssl?.keyPath?{certPath:o.ssl.certPath,keyPath:o.ssl.keyPath}:void 0,st=o.isolation?uo({siteName:i,appBase:M}):[],nr=z?ar({siteName:i,domain:o.domain||i,aliases:o.aliases,type:o.type,appDir:`${M}/current`,webDirectory:o.webDirectory,phpVersion:L,fastcgiPass:o.isolation?Tn(i):void 0,redirects:o.redirects,ssl:Se,auth:o.auth&&o.auth.enabled!==!1&&o.auth.password?{username:o.auth.username||"admin",password:o.auth.password,realm:o.auth.realm}:void 0,serverSnippet:or(o.nginx,b?.nginxTemplates),clientMaxBodySize:o.nginx?.clientMaxBodySize,hsts:o.ssl?.hsts,tlsProtocols:o.ssl?.tlsProtocols,security:o.security}):[],it=z?wn(o):[],Eo=bn(o)?vn({slug:a,siteName:i,site:o,phpVersion:L,appBase:M}):[],Oo=z?Wi(o):[];r.step(`Deploying PHP site '${i}' to ${h.length} target(s)...`);let Re=await e.runRemoteDeploy({targets:h,commands:[...ra(M,a),...F,...st,...nr,...it,...Eo,...Oo,..._],comment:`ts-cloud deploy ${a}/${i}@${c}`,tags:{Project:a,Environment:s,Role:"app"}}),On=Nn(n.notifications,o.notifications);if(!Re.success)return await po(e,h,_,r),await lr(On,"deploy-failed",`❌ Deploy of ${a}/${i}@${c} failed: ${Re.error||"unknown error"}`),{success:!1,error:Re.error||"Remote PHP deploy failed",instanceCount:Re.instanceCount,perInstance:Re.perInstance};let Do=o.domain?` → https://${o.domain}`:"";return await lr(On,"deploy",`✅ Deployed ${a}/${i}@${c}${Do}`),{success:!0,instanceCount:Re.instanceCount,perInstance:Re.perInstance}}if(!u)return{success:!1,error:`Site '${i}' requires a release tarball but none was provided`};let C=await e.uploadRelease({config:n,environment:s,localPath:u,remoteKey:d,targets:h}),H=ft(a,i,c),te=e.name==="aws"?zr(f.deployBucketName,d,n.project.region||"us-east-1",H):Br(C.artifactRef,H),oe=I(o),N=J(a,i),q=oe==="server-static"?Mr({siteName:i,slug:a,appDir:N,artifactFetch:te,releaseId:c,preStartCommands:o.preStart}):kr({siteName:i,slug:a,appDir:N,artifactFetch:te,releaseId:c,execStart:Hr(o.start,l,`${N.replace(/\/+$/,"")}/releases/%i`),envEntries:E,port:o.port,preStartCommands:o.preStart,sharedPaths:o.sharedPaths,zeroDowntime:o.zeroDowntime!==!1,healthCheckPath:o.healthCheck?.path,liveness:o.liveness,memoryHigh:o.memoryHigh,memoryMax:o.memoryMax,cpuWeight:o.cpuWeight,ioWeight:o.ioWeight,tasksMax:o.tasksMax,stopTimeout:o.stopTimeout}),K=oe==="server-static"&&!Z(b)&&!!o.domain,k=K?ar({siteName:i,domain:o.domain,aliases:o.aliases,type:o.type==="spa"?"spa":"static",appDir:`${N}/current`,webDirectory:"",redirects:o.redirects,auth:o.auth&&o.auth.enabled!==!1&&o.auth.password?{username:o.auth.username||"admin",password:o.auth.password,realm:o.auth.realm}:void 0,serverSnippet:or(o.nginx,b?.nginxTemplates),clientMaxBodySize:o.nginx?.clientMaxBodySize,hsts:o.ssl?.hsts,tlsProtocols:o.ssl?.tlsProtocols,security:o.security}):[],ae=K?wn(o):[],he=bn(o)?vn({slug:a,siteName:i,site:o,appBase:N}):[],xe=[...Bo(N,{releaseId:c,commit:c,branch:o.repository?.branch??"main"}),...ra(N,a),...q,...k,...ae,...he,..._];r.step(`Deploying to ${h.length} target(s)...`);let re=await e.runRemoteDeploy({targets:h,commands:xe,comment:`ts-cloud deploy ${a}/${i}@${c}`,tags:{Project:a,Environment:s,Role:"app"}});if(!re.success)return await po(e,h,_,r),{success:!1,error:re.error||"Remote deploy failed",instanceCount:re.instanceCount,perInstance:re.perInstance};return{success:!0,instanceCount:re.instanceCount,perInstance:re.perInstance}}async function po(e,t,r,n){if(r.length===0)return;try{let s=await e.runRemoteDeploy({targets:t,commands:r,comment:"ts-cloud host cleanup after failed deploy"});if(!s.success)n.warn(`Host cleanup after the failed deploy did not complete: ${s.error||"unknown error"}`)}catch(s){n.warn(`Host cleanup after the failed deploy did not run: ${s.message}`)}}function tr(e){return`'${e.replaceAll("'","'\\''")}'`}function Ul(e,t,r=!1,n=[]){let s=`${e}-dashboard-`,i=t.map((a)=>`${e}-${a}.service`),o=r?[`export TS_CLOUD_DASHBOARD_KEEP_DOMAINS=${tr(JSON.stringify(n))}`,"rm -f /run/ts-cloud-dashboard-routes-changed",`/usr/local/bin/bun --bun -e ${tr(`
529
+ `)}function uo(e){let t=lo(e.siteName),r=er(e.siteName),n=`${co}/${r}.conf`;return[`getent group ${t} >/dev/null || groupadd --system ${t}`,`id -u ${t} >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin -g ${t} ${t}`,`usermod -aG ${t} www-data 2>/dev/null || true`,`chown -R ${t}:${t} ${e.appBase} || true`,`chmod -R g+rX ${e.appBase} || true`,`chmod 600 ${e.appBase}/shared/.env 2>/dev/null || true`,`mkdir -p ${co}`,`cat > ${n} <<'TS_CLOUD_FPMPOOL_EOF'`,Il(e).replace(/\n$/,""),"TS_CLOUD_FPMPOOL_EOF",`(cd ${U} && pantry restart php-fpm) 2>/dev/null || true`]}var rr={info:()=>{},warn:()=>{},error:()=>{},step:()=>{},success:()=>{}};function Hl(e){return`'${e.replace(/'/g,`'"'"'`)}'`}async function mo(e,t,r=rr){let{config:n,environment:s,siteName:i,site:o,slug:a,sha:c,runtime:l,localTarballPath:u}=t,d=`releases/${i}/${c}.tar.gz`,m=T(n,s),f=await e.getComputeOutputs({config:n,environment:s}),h=await e.findComputeTargets({slug:a,environment:s,role:"app",stackName:m});if(h.length===0)return{success:!1,error:e.name==="aws"?`Stack '${m}' has no EC2 instances tagged Project=${a} Environment=${s} Role=app, and storage/cloud/state/${m}.json pins no live instance. For a shared box, record its instanceId there.`:`No Hetzner servers labeled ts-cloud/project=${a} ts-cloud/environment=${s} ts-cloud/role=app, and storage/cloud/state/${m}.json pins no live server. For a shared box, record its serverId there.`};let R=f.servicesPrivateIp?Ds(f.servicesPrivateIp,w(n)):Gc(w(n)),x=da(ne(n,{environment:s})),b=n.infrastructure?.compute,_=Ar(b?.hostCleanup),P=Z({...b,proxy:le(n,{environment:s})})?{HOST:"127.0.0.1"}:{},S=s==="production"?{APP_ENV:"production",NODE_ENV:"production"}:{},E={...R,...x,...P,...o.env||{},...S},y=xr(E,o.sharedPaths??[]);if(y.warning)r.warn(`Site '${i}': ${y.warning}`);else if(y.path)r.info(`Site '${i}': sharing '${y.path}' so the SQLite database survives deploys.`);if(Ri(o)){let L=o.phpVersion??b?.php?.default??b?.php?.versions?.[0],M=J(a,i),D=Object.keys(R).length>0?{...o,env:E}:o,F=Gi({siteName:i,site:D,releaseId:c,appBase:M,defaultPhpVersion:L}),z=!Z(b),Se=_n(o)==="custom"&&o.ssl?.certPath&&o.ssl?.keyPath?{certPath:o.ssl.certPath,keyPath:o.ssl.keyPath}:void 0,st=o.isolation?uo({siteName:i,appBase:M}):[],nr=z?ar({siteName:i,domain:o.domain||i,aliases:o.aliases,type:o.type,appDir:`${M}/current`,webDirectory:o.webDirectory,phpVersion:L,fastcgiPass:o.isolation?Tn(i):void 0,redirects:o.redirects,ssl:Se,auth:o.auth&&o.auth.enabled!==!1&&o.auth.password?{username:o.auth.username||"admin",password:o.auth.password,realm:o.auth.realm}:void 0,serverSnippet:or(o.nginx,b?.nginxTemplates),clientMaxBodySize:o.nginx?.clientMaxBodySize,hsts:o.ssl?.hsts,tlsProtocols:o.ssl?.tlsProtocols,security:o.security}):[],it=z?wn(o):[],Eo=bn(o)?vn({slug:a,siteName:i,site:o,phpVersion:L,appBase:M}):[],Oo=z?Wi(o):[];r.step(`Deploying PHP site '${i}' to ${h.length} target(s)...`);let Ee=await e.runRemoteDeploy({targets:h,commands:[...ra(M,a),...F,...st,...nr,...it,...Eo,...Oo,..._],comment:`ts-cloud deploy ${a}/${i}@${c}`,tags:{Project:a,Environment:s,Role:"app"}}),On=Nn(n.notifications,o.notifications);if(!Ee.success)return await po(e,h,_,r),await lr(On,"deploy-failed",`❌ Deploy of ${a}/${i}@${c} failed: ${Ee.error||"unknown error"}`),{success:!1,error:Ee.error||"Remote PHP deploy failed",instanceCount:Ee.instanceCount,perInstance:Ee.perInstance};let Do=o.domain?` → https://${o.domain}`:"";return await lr(On,"deploy",`✅ Deployed ${a}/${i}@${c}${Do}`),{success:!0,instanceCount:Ee.instanceCount,perInstance:Ee.perInstance}}if(!u)return{success:!1,error:`Site '${i}' requires a release tarball but none was provided`};let C=await e.uploadRelease({config:n,environment:s,localPath:u,remoteKey:d,targets:h}),H=ft(a,i,c),te=e.name==="aws"?zr(f.deployBucketName,d,n.project.region||"us-east-1",H):Br(C.artifactRef,H),oe=I(o),N=J(a,i),q=oe==="server-static"?Mr({siteName:i,slug:a,appDir:N,artifactFetch:te,releaseId:c,preStartCommands:o.preStart}):kr({siteName:i,slug:a,appDir:N,artifactFetch:te,releaseId:c,execStart:Hr(o.start,l,`${N.replace(/\/+$/,"")}/releases/%i`),envEntries:E,port:o.port,preStartCommands:o.preStart,sharedPaths:o.sharedPaths,zeroDowntime:o.zeroDowntime!==!1,healthCheckPath:o.healthCheck?.path,liveness:o.liveness,memoryHigh:o.memoryHigh,memoryMax:o.memoryMax,cpuWeight:o.cpuWeight,ioWeight:o.ioWeight,tasksMax:o.tasksMax,stopTimeout:o.stopTimeout}),K=oe==="server-static"&&!Z(b)&&!!o.domain,k=K?ar({siteName:i,domain:o.domain,aliases:o.aliases,type:o.type==="spa"?"spa":"static",appDir:`${N}/current`,webDirectory:"",redirects:o.redirects,auth:o.auth&&o.auth.enabled!==!1&&o.auth.password?{username:o.auth.username||"admin",password:o.auth.password,realm:o.auth.realm}:void 0,serverSnippet:or(o.nginx,b?.nginxTemplates),clientMaxBodySize:o.nginx?.clientMaxBodySize,hsts:o.ssl?.hsts,tlsProtocols:o.ssl?.tlsProtocols,security:o.security}):[],ae=K?wn(o):[],he=bn(o)?vn({slug:a,siteName:i,site:o,appBase:N}):[],Re=[...Bo(N,{releaseId:c,commit:c,branch:o.repository?.branch??"main"}),...ra(N,a),...q,...k,...ae,...he,..._];r.step(`Deploying to ${h.length} target(s)...`);let re=await e.runRemoteDeploy({targets:h,commands:Re,comment:`ts-cloud deploy ${a}/${i}@${c}`,tags:{Project:a,Environment:s,Role:"app"}});if(!re.success)return await po(e,h,_,r),{success:!1,error:re.error||"Remote deploy failed",instanceCount:re.instanceCount,perInstance:re.perInstance};return{success:!0,instanceCount:re.instanceCount,perInstance:re.perInstance}}async function po(e,t,r,n){if(r.length===0)return;try{let s=await e.runRemoteDeploy({targets:t,commands:r,comment:"ts-cloud host cleanup after failed deploy"});if(!s.success)n.warn(`Host cleanup after the failed deploy did not complete: ${s.error||"unknown error"}`)}catch(s){n.warn(`Host cleanup after the failed deploy did not run: ${s.message}`)}}function tr(e){return`'${e.replaceAll("'","'\\''")}'`}function Ul(e,t,r=!1,n=[]){let s=`${e}-dashboard-`,i=t.map((a)=>`${e}-${a}.service`),o=r?[`export TS_CLOUD_DASHBOARD_KEEP_DOMAINS=${tr(JSON.stringify(n))}`,"rm -f /run/ts-cloud-dashboard-routes-changed",`/usr/local/bin/bun --bun -e ${tr(`
530
530
  const { chmodSync, existsSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } = require('node:fs')
531
531
  const { basename, join } = require('node:path')
532
532
  const root = '/etc/rpx/sites.d'