@stacksjs/buddy 0.72.103 → 0.73.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cloud-inventory.d.ts +55 -18
- package/dist/cloud-inventory.js +1 -1
- package/dist/commands/build.js +1 -1
- package/dist/commands/cloud.js +2 -2
- package/dist/commands/deploy-preview.d.ts +10 -21
- package/dist/commands/deploy.d.ts +74 -10
- package/dist/commands/deploy.js +2 -2
- package/dist/commands/dev.js +1 -1
- package/dist/commands/domains.js +2 -2
- package/dist/commands/email.js +3 -3
- package/dist/commands/features.d.ts +1 -1
- package/dist/commands/features.js +1 -1
- package/dist/commands/generate.js +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +1 -1
- package/dist/commands/libs.d.ts +2 -0
- package/dist/commands/libs.js +1 -0
- package/dist/commands/make.js +1 -1
- package/dist/commands/migrate.js +2 -2
- package/dist/commands/sms.js +6 -6
- package/dist/commands/user.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/lazy-commands.js +1 -1
- package/dist/unbacked-data.js +1 -1
- package/package.json +53 -53
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Shape one provider server record into the inventory's own type.
|
|
3
|
-
*
|
|
4
|
-
* Written against the Hetzner server payload (the shape `resolveAttachTargetBox`
|
|
5
|
-
* in the deploy command already reads), but only touches fields any provider
|
|
6
|
-
* listing carries, so an AWS/local-box listing can be mapped onto it too.
|
|
7
|
-
*/
|
|
8
|
-
export declare function toInventoryServer(raw: any): InventoryServer;
|
|
1
|
+
export declare function toInventoryServer(raw: ProviderServerPayload | null | undefined): InventoryServer;
|
|
9
2
|
/**
|
|
10
3
|
* The sites this project declares, in the same terms the box reports.
|
|
11
4
|
*
|
|
@@ -16,15 +9,7 @@ export declare function toInventoryServer(raw: any): InventoryServer;
|
|
|
16
9
|
* partial inventory beats no inventory, and every other field is local.
|
|
17
10
|
*/
|
|
18
11
|
export declare function declaredSites(sites: Record<string, any> | undefined, helpers?: { resolveSiteKind?: (site: any) => string, siteInstallBase?: (slug: string, site: string) => string }, slug?: string): DeclaredSite[];
|
|
19
|
-
|
|
20
|
-
* Flatten the box's registry fragments into one route list.
|
|
21
|
-
*
|
|
22
|
-
* A fragment is `{ slug, ...RpxGatewayConfig }`, so `proxies` carries the
|
|
23
|
-
* routes: `to` is the public host, `path` the prefix it owns, and exactly one
|
|
24
|
-
* of `from` / `static` / `redirect` says where it goes. A fragment written by
|
|
25
|
-
* an older ts-cloud may have no `slug`, which the writer defaults to `app`.
|
|
26
|
-
*/
|
|
27
|
-
export declare function routesFromFragments(fragments: readonly any[]): HostedRoute[];
|
|
12
|
+
export declare function routesFromFragments(fragments: readonly HostRouteFragment[]): HostedRoute[];
|
|
28
13
|
/**
|
|
29
14
|
* Line this project's declared sites up against what the box serves.
|
|
30
15
|
*
|
|
@@ -78,7 +63,7 @@ export declare function buildHostRoutesScript(sitesDir?: string): string;
|
|
|
78
63
|
* take the listing down. The cost is that its routes are invisible here, which
|
|
79
64
|
* is still strictly more than the nothing this command could see before.
|
|
80
65
|
*/
|
|
81
|
-
export declare function parseHostRoutesOutput(stdout: string):
|
|
66
|
+
export declare function parseHostRoutesOutput(stdout: string): HostRouteFragment[];
|
|
82
67
|
/**
|
|
83
68
|
* Ask one box what it serves.
|
|
84
69
|
*
|
|
@@ -181,6 +166,58 @@ export declare interface Reconciliation {
|
|
|
181
166
|
loopback: DeclaredSite[]
|
|
182
167
|
foreign: HostedRoute[]
|
|
183
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Shape one provider server record into the inventory's own type.
|
|
171
|
+
*
|
|
172
|
+
* Written against the Hetzner server payload (the shape `resolveAttachTargetBox`
|
|
173
|
+
* in the deploy command already reads), but only touches fields any provider
|
|
174
|
+
* listing carries, so an AWS/local-box listing can be mapped onto it too.
|
|
175
|
+
*/
|
|
176
|
+
/**
|
|
177
|
+
* One provider server record, as the listing returns it.
|
|
178
|
+
*
|
|
179
|
+
* Written against the Hetzner payload and naming only the fields this function
|
|
180
|
+
* reads, so another provider's listing satisfies it too. Every field is
|
|
181
|
+
* optional and `unknown` because it is JSON from an external API: `text()` is
|
|
182
|
+
* what turns each one into a string or nothing.
|
|
183
|
+
*/
|
|
184
|
+
export declare interface ProviderServerPayload {
|
|
185
|
+
id?: unknown
|
|
186
|
+
name?: unknown
|
|
187
|
+
status?: unknown
|
|
188
|
+
labels?: Record<string, unknown>
|
|
189
|
+
public_net?: { ipv4?: { ip?: unknown }, ipv6?: { ip?: unknown } }
|
|
190
|
+
server_type?: { name?: unknown }
|
|
191
|
+
datacenter?: { name?: unknown, location?: { name?: unknown } }
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Flatten the box's registry fragments into one route list.
|
|
195
|
+
*
|
|
196
|
+
* A fragment is `{ slug, ...RpxGatewayConfig }`, so `proxies` carries the
|
|
197
|
+
* routes: `to` is the public host, `path` the prefix it owns, and exactly one
|
|
198
|
+
* of `from` / `static` / `redirect` says where it goes. A fragment written by
|
|
199
|
+
* an older ts-cloud may have no `slug`, which the writer defaults to `app`.
|
|
200
|
+
*/
|
|
201
|
+
/**
|
|
202
|
+
* One `/etc/rpx/sites.d` fragment, as parsed off the box.
|
|
203
|
+
*
|
|
204
|
+
* Every field is optional and `unknown`, because this is JSON written by
|
|
205
|
+
* another machine and read here defensively - `text()` and the `typeof` tests
|
|
206
|
+
* below are what turn it into something usable. As `any` those checks were
|
|
207
|
+
* indistinguishable from probing for fields that never existed.
|
|
208
|
+
*/
|
|
209
|
+
export declare interface HostRouteFragment {
|
|
210
|
+
slug?: unknown
|
|
211
|
+
proxies?: unknown
|
|
212
|
+
}
|
|
213
|
+
/** One proxy entry inside a fragment, in the same spirit. */
|
|
214
|
+
export declare interface HostRouteProxy {
|
|
215
|
+
to?: unknown
|
|
216
|
+
path?: unknown
|
|
217
|
+
from?: unknown
|
|
218
|
+
static?: unknown
|
|
219
|
+
redirect?: unknown
|
|
220
|
+
}
|
|
184
221
|
export declare interface ProviderListing {
|
|
185
222
|
servers: InventoryServer[]
|
|
186
223
|
failure?: ProviderFailure
|
package/dist/cloud-inventory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const LABEL_PROJECT="ts-cloud/project",LABEL_ENVIRONMENT="ts-cloud/environment",LABEL_ROLE="ts-cloud/role";function text(value){return typeof value==="string"&&value.trim()?value.trim():void 0}export function toInventoryServer(raw){const labels={};for(const[key,value]of Object.entries(raw?.labels??{}))if(typeof value==="string")labels[key]=value;return{id:String(raw?.id??""),name:text(raw?.name)??"(unnamed)",status:text(raw?.status)??"unknown",ipv4:text(raw?.public_net?.ipv4?.ip),ipv6:text(raw?.public_net?.ipv6?.ip),type:text(raw?.server_type?.name),location:text(raw?.datacenter?.location?.name)??text(raw?.datacenter?.name),labels,project:text(labels[LABEL_PROJECT]),environment:text(labels[LABEL_ENVIRONMENT]),role:text(labels[LABEL_ROLE])}}export function declaredSites(sites,helpers,slug){return Object.entries(sites??{}).map(([name,site])=>{const domain=text(site?.domain),port=Number(site?.port);return{name,kind:helpers?.resolveSiteKind?.(site)??"unknown",domain,path:text(site?.path)??"/",port:Number.isFinite(port)&&port>0?port:void 0,installBase:slug&&helpers?.siteInstallBase?helpers.siteInstallBase(slug,name):void 0,loopbackOnly:!domain}})}export function routesFromFragments(fragments){const routes=[];for(const fragment of fragments){const slug=text(fragment?.slug)??"app"
|
|
1
|
+
const LABEL_PROJECT="ts-cloud/project",LABEL_ENVIRONMENT="ts-cloud/environment",LABEL_ROLE="ts-cloud/role";function text(value){return typeof value==="string"&&value.trim()?value.trim():void 0}export function toInventoryServer(raw){const labels={};for(const[key,value]of Object.entries(raw?.labels??{}))if(typeof value==="string")labels[key]=value;return{id:String(raw?.id??""),name:text(raw?.name)??"(unnamed)",status:text(raw?.status)??"unknown",ipv4:text(raw?.public_net?.ipv4?.ip),ipv6:text(raw?.public_net?.ipv6?.ip),type:text(raw?.server_type?.name),location:text(raw?.datacenter?.location?.name)??text(raw?.datacenter?.name),labels,project:text(labels[LABEL_PROJECT]),environment:text(labels[LABEL_ENVIRONMENT]),role:text(labels[LABEL_ROLE])}}export function declaredSites(sites,helpers,slug){return Object.entries(sites??{}).map(([name,site])=>{const domain=text(site?.domain),port=Number(site?.port);return{name,kind:helpers?.resolveSiteKind?.(site)??"unknown",domain,path:text(site?.path)??"/",port:Number.isFinite(port)&&port>0?port:void 0,installBase:slug&&helpers?.siteInstallBase?helpers.siteInstallBase(slug,name):void 0,loopbackOnly:!domain}})}export function routesFromFragments(fragments){const routes=[];for(const fragment of fragments){const slug=text(fragment?.slug)??"app",proxies=Array.isArray(fragment?.proxies)?fragment.proxies:[];for(const proxy of proxies){const host=text(proxy?.to);if(!host)continue;routes.push({slug,host,path:text(proxy?.path)??"/",...describeRouteTarget(proxy)})}}return routes.sort((a,b)=>a.slug.localeCompare(b.slug)||a.host.localeCompare(b.host)||a.path.localeCompare(b.path))}function describeRouteTarget(proxy){const from=proxy?.from;if(typeof from==="string"&&from.trim())return{target:from.trim(),kind:"app"};if(Array.isArray(from)&&from.length)return{target:from.filter((u)=>typeof u==="string").join(", "),kind:"app"};const staticRoute=proxy?.static;if(typeof staticRoute==="string"&&staticRoute.trim())return{target:staticRoute.trim(),kind:"static"};if(staticRoute&&typeof staticRoute==="object"){const dir=text(staticRoute.dir);if(dir)return{target:dir,kind:"static"}}const redirect=text(proxy?.redirect?.to)??text(proxy?.redirect);if(redirect)return{target:redirect,kind:"redirect"};return{target:"(no upstream)",kind:"unknown"}}export function reconcile(declared,routes,slug){const ours=new Set(routes.filter((route)=>route.slug===slug).map((route)=>routeKey(route.host,route.path))),present=[],absent=[],loopback=[];for(const site of declared)if(site.loopbackOnly)loopback.push(site);else if(ours.has(routeKey(site.domain,site.path)))present.push(site);else absent.push(site);return{present,absent,loopback,foreign:routes.filter((route)=>route.slug!==slug)}}function routeKey(host,path){const normalized=path==="/"?"/":path.replace(/\/+$/,"");return`${host.toLowerCase()}${normalized||"/"}`}export function tenantsOf(routes){const bySlug=new Map;for(const route of routes){const bucket=bySlug.get(route.slug);if(bucket)bucket.push(route);else bySlug.set(route.slug,[route])}return[...bySlug.entries()].map(([slug,grouped])=>({slug,routes:grouped})).sort((a,b)=>b.routes.length-a.routes.length||a.slug.localeCompare(b.slug))}export function unaccountedSites(declared,probes,slug){const seen=new Set;for(const probe of probes)for(const route of probe.routes)if(route.slug===slug)seen.add(routeKey(route.host,route.path));return declared.filter((site)=>!site.loopbackOnly&&!seen.has(routeKey(site.domain,site.path)))}export async function listProviderServers(token,fetchImpl=fetch){if(!token)return{servers:[],failure:{kind:"no-token"}};const servers=[];let page=1;while(page>0&&page<=40){let response;try{response=await fetchImpl(`https://api.hetzner.cloud/v1/servers?page=${page}&per_page=50`,{headers:{Authorization:`Bearer ${token}`}})}catch(error){return{servers,failure:{kind:"request-failed",status:0,detail:error instanceof Error?error.message:String(error)}}}if(!response.ok){const body=await response.text().catch(()=>"");return{servers,failure:{kind:"request-failed",status:response.status,detail:body.slice(0,200)||void 0}}}const payload=await response.json().catch(()=>({}));for(const raw of Array.isArray(payload?.servers)?payload.servers:[])servers.push(toInventoryServer(raw));const next=Number(payload?.meta?.pagination?.next_page);page=Number.isFinite(next)&&next>page?next:0}return{servers:servers.sort((a,b)=>a.name.localeCompare(b.name))}}export function describeProviderFailure(failure){if(failure.kind==="no-token")return"No Hetzner API token, so no servers were looked up. Set HCLOUD_TOKEN (or HETZNER_API_TOKEN, or hetzner.apiToken in config/cloud.ts).";return`The Hetzner API ${failure.status>0?`returned HTTP ${failure.status}`:"could not be reached"}, so the server list is incomplete.${failure.detail?` ${failure.detail}`:""}`+(failure.status===401||failure.status===403?" That is an auth failure, not an empty project: check the token is valid for this Hetzner project.":"")}export const HOST_SITES_DIR="/etc/rpx/sites.d";export function buildHostRoutesScript(sitesDir=HOST_SITES_DIR){return`d='${sitesDir}'
|
|
2
2
|
[ -d "$d" ] || exit 0
|
|
3
3
|
find "$d" -maxdepth 1 -type f -name '*.json' | sort | while IFS= read -r f; do
|
|
4
4
|
base64 < "$f" | tr -d '\\n'
|
package/dist/commands/build.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{intro,log,multiselect,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{hasTTY,isCI}from"@stacksjs/env";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";let _runAction;async function runAction(...args){if(!_runAction)_runAction=(await import("@stacksjs/actions")).runAction;return _runAction(...args)}export function build(buddy){const descriptions={build:"Build any of your libraries (packages) for production use",components:"Build your component library",webComponents:"Build your framework agnostic web component library",elements:"An alias to the -w flag",buddy:"Build the Buddy binary",functions:"Build your function library",desktop:"Build the Desktop Application",mobile:"Build the native iOS and Android applications",android:"Build the native Android application",ios:"Build the native iOS application",dmg:"Package the desktop build as a macOS .app inside a .dmg",pages:"Build your frontend",docs:"Build your documentation",framework:"Build Stacks framework",cli:"Automagically build the CLI",server:"Build the Stacks cloud server (Docker image)",frontendStatic:"Build the prerendered marketing/public static site (storage/framework/frontend-dist)",select:"What are you trying to build?",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("build [type]",descriptions.build).option("-c, --components",descriptions.components).option("-w, --web-components",descriptions.webComponents).option("-e, --elements",descriptions.elements).option("-f, --functions",descriptions.functions).option("-k, --desktop",descriptions.desktop).option("-m, --mobile",descriptions.mobile).option("--android",descriptions.android).option("--ios",descriptions.ios).option("-p, --views",descriptions.pages).option("--pages",descriptions.pages).option("-d, --docs",descriptions.docs).option("-b, --buddy",descriptions.buddy,{default:!1}).option("-s, --stacks",descriptions.framework,{default:!1}).option("--project [project]",descriptions.project,{default:!1}).option("--server",descriptions.server,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(server,options)=>{log.debug("Running `buddy build` ...",options);applyBuildTarget(server,options);if(options.mobile){options.android=!0;options.ios=!0}if(hasNoOptions(options)){if(!isCI&&hasTTY&&process.stdin.isTTY){const answers=await multiselect({message:descriptions.select,choices:[{label:"Frontend (views)",value:"views"},{label:"Components",value:"components"},{label:"Web Components",value:"webComponents"},{label:"Functions",value:"functions"},{label:"Desktop application",value:"desktop"},{label:"Mobile applications (iOS + Android)",value:"mobile"},{label:"Android application",value:"android"},{label:"iOS application",value:"ios"},{label:"Documentation",value:"docs"},{label:"Stacks framework",value:"stacks"},{label:"Buddy CLI",value:"buddy"},{label:"Server (Docker image)",value:"server"}]}),selected=new Set(answers);if(selected.has("views"))options.views=!0;if(selected.has("components"))options.components=!0;if(selected.has("webComponents"))options.webComponents=!0;if(selected.has("functions"))options.functions=!0;if(selected.has("desktop"))options.desktop=!0;if(selected.has("mobile")){options.mobile=!0;options.android=!0;options.ios=!0}if(selected.has("android"))options.android=!0;if(selected.has("ios"))options.ios=!0;if(selected.has("docs"))options.docs=!0;if(selected.has("stacks"))options.stacks=!0;if(selected.has("buddy"))options.buddy=!0;if(selected.has("server"))options.server=!0}if(hasNoOptions(options)){options.views=!0;log.info("No build target specified, defaulting to the frontend (views). See `buddy build --help` for all targets.")}}let succeeded=!0;if(options.docs)succeeded=await runBuildAction(Action.BuildDocs,"documentation",options)&&succeeded;if(options.components)succeeded=await runBuildAction(Action.BuildComponentLibs,"component libraries",options)&&succeeded;if(options.webComponents)succeeded=await runBuildAction(Action.BuildWebComponentLib,"web component library",options)&&succeeded;if(options.functions)succeeded=await runBuildAction(Action.BuildFunctionLib,"function library",options)&&succeeded;if(options.desktop)succeeded=await runBuildAction(Action.BuildDesktop,"desktop application",options)&&succeeded;if(options.android)succeeded=await runBuildAction(Action.BuildAndroid,"Android application",options)&&succeeded;if(options.ios)succeeded=await runBuildAction(Action.BuildIos,"iOS application",options)&&succeeded;if(options.views)succeeded=await runBuildAction(Action.BuildViews,"frontend",options)&&succeeded;if(options.stacks)succeeded=await runBuildAction(Action.BuildStacks,"Stacks framework",options)&&succeeded;if(options.buddy)succeeded=await runBuildAction(Action.BuildCli,"Buddy CLI",options)&&succeeded;if(options.server)succeeded=await runBuildAction(Action.BuildServer,"server",options)&&succeeded;if(!succeeded)process.exit(ExitCode.FatalError);process.exit(ExitCode.Success)});buddy.command("build:components","Automagically build component libraries for production use & npm/CDN distribution").alias("prod:components").option("-c, --components",descriptions.components,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:components` ...",options);if(!await runBuildAction(Action.BuildComponentLibs,"component libraries"))process.exit(ExitCode.FatalError)});buddy.command("build:cli",descriptions.cli).alias("prod:cli").option("-b, --buddy",descriptions.buddy,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:cli` ...",options);await runAction(Action.BuildCli,options)});buddy.command("build:server",descriptions.server).alias("prod:server").alias("build:docker").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:server` ...",options);await runAction(Action.BuildServer,options)});buddy.command("build:functions","Automagically build function library for npm/CDN distribution").option("-f, --functions",descriptions.functions,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:functions` ...",options);await runAction(Action.BuildFunctionLib,options)});buddy.command("build:web-components","Automagically build Web Component library for npm/CDN distribution").alias("build:wc").alias("prod:web-components").alias("prod:wc").option("-w, --web-components",descriptions.webComponents,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:web-components` ...",options);if(!await runBuildAction(Action.BuildWebComponentLib,"web component library"))process.exit(ExitCode.FatalError)});buddy.command("build:frontend",descriptions.pages).alias("build:pages").alias("build:views").alias("prod:frontend").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:frontend` ...",options);await runAction(Action.BuildViews,options)});buddy.command("build:docs","Automagically build your documentation site.").alias("prod:docs").alias("build:documentation").alias("prod:documentation").option("-d, --docs",descriptions.docs,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:docs` ...",options);await runAction(Action.BuildDocs,options)});buddy.command("build:frontend-static",descriptions.frontendStatic).alias("build:public").alias("prod:frontend-static").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:frontend-static` ...",options);await runAction(Action.BuildFrontendStatic,options)});buddy.command("build:core","Automagically build the Stacks core.").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:core` ...",options);const startTime=await intro("buddy build:core"),result=await runAction(Action.BuildCore,options);if(resultFailed(result)){log.error("Failed to build the Stacks core.",result.error);process.exit(ExitCode.FatalError)}await outro("Core packages built successfully",{startTime,useSeconds:!0})});buddy.command("build:desktop",descriptions.desktop).alias("prod:desktop").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:desktop` ...",options);const perf=await intro("buddy build:desktop"),result=await runAction(Action.BuildDesktop,options);if(resultFailed(result)){await outro("While running the build:desktop command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("build:mobile",descriptions.mobile).alias("prod:mobile").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:mobile` ...",options);const perf=await intro("buddy build:mobile"),androidSucceeded=await runBuildAction(Action.BuildAndroid,"Android application",options),iosSucceeded=await runBuildAction(Action.BuildIos,"iOS application",options);if(!androidSucceeded||!iosSucceeded){await outro("One or more mobile application builds failed",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}await outro("iOS and Android applications built",{startTime:perf,useSeconds:!0})});buddy.command("build:android",descriptions.android).alias("prod:android").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:android` ...",options);const perf=await intro("buddy build:android"),result=await runAction(Action.BuildAndroid,options);if(resultFailed(result)){await outro("While building the Android application, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Android application built",{startTime:perf,useSeconds:!0})});buddy.command("build:ios",descriptions.ios).alias("prod:ios").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:ios` ...",options);const perf=await intro("buddy build:ios"),result=await runAction(Action.BuildIos,options);if(resultFailed(result)){await outro("While building the iOS application, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("iOS application built",{startTime:perf,useSeconds:!0})});buddy.command("build:dmg",descriptions.dmg).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:dmg` ...",options);const perf=await intro("buddy build:dmg"),result=await runAction(Action.BuildDmg,options);if(resultFailed(result)){await outro("While running the build:dmg command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("build:stacks","Build the Stacks framework.").option("-s, --stacks",descriptions.framework,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:stacks` ...",options);const startTime=await intro("buddy build:stacks"),result=await runAction(Action.BuildStacks,options);if(resultFailed(result)){log.error("Failed to build Stacks.",result.error);process.exit(ExitCode.FatalError)}await outro("Stacks built successfully",{startTime,useSeconds:!0})});onUnknownSubcommand(buddy,"build")}function hasNoOptions(options){return!options.components&&!options.webComponents&&!options.elements&&!options.functions&&!options.desktop&&!options.mobile&&!options.android&&!options.ios&&!options.views&&!options.docs&&!options.stacks&&!options.buddy&&!options.server}export function applyBuildTarget(target,options){switch(target){case"components":options.components=!0;break;case"web-components":options.webComponents=!0;break;case"functions":options.functions=!0;break;case"desktop":options.desktop=!0;break;case"mobile":options.mobile=!0;options.android=!0;options.ios=!0;break;case"android":options.android=!0;break;case"ios":options.ios=!0;break;case"views":options.views=!0;break;case"docs":options.docs=!0;break;case"buddy":case"cli":options.buddy=!0;break;case"stacks":options.stacks=!0;break;case"server":options.server=!0;break}}async function runBuildAction(action,target,options){const result=await runAction(action,options);if(resultFailed(result)){log.error(`Failed to build ${target}.`,result.error);return!1}return!0}
|
|
1
|
+
import process from"node:process";import{intro,log,multiselect,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{hasTTY,isCI}from"@stacksjs/env";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";let _runAction;async function runAction(...args){if(!_runAction)_runAction=(await import("@stacksjs/actions")).runAction;return _runAction(...args)}export function build(buddy){const descriptions={build:"Build any of your libraries (packages) for production use",components:"Build your component library",webComponents:"Build your framework agnostic web component library",elements:"An alias to the -w flag",buddy:"Build the Buddy binary",functions:"Build your function library",libs:"Build every package configured in config/library.ts",desktop:"Build the Desktop Application",mobile:"Build the native iOS and Android applications",android:"Build the native Android application",ios:"Build the native iOS application",dmg:"Package the desktop build as a macOS .app inside a .dmg",pages:"Build your frontend",docs:"Build your documentation",framework:"Build Stacks framework",cli:"Automagically build the CLI",server:"Build the Stacks cloud server (Docker image)",frontendStatic:"Build the prerendered marketing/public static site (storage/framework/frontend-dist)",select:"What are you trying to build?",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("build [type]",descriptions.build).option("-c, --components",descriptions.components).option("-w, --web-components",descriptions.webComponents).option("-e, --elements",descriptions.elements).option("-f, --functions",descriptions.functions).option("-l, --libs",descriptions.libs).option("-k, --desktop",descriptions.desktop).option("-m, --mobile",descriptions.mobile).option("--android",descriptions.android).option("--ios",descriptions.ios).option("-p, --views",descriptions.pages).option("--pages",descriptions.pages).option("-d, --docs",descriptions.docs).option("-b, --buddy",descriptions.buddy,{default:!1}).option("-s, --stacks",descriptions.framework,{default:!1}).option("--project [project]",descriptions.project,{default:!1}).option("--server",descriptions.server,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(server,options)=>{log.debug("Running `buddy build` ...",options);applyBuildTarget(server,options);if(options.mobile){options.android=!0;options.ios=!0}if(hasNoOptions(options)){if(!isCI&&hasTTY&&process.stdin.isTTY){const answers=await multiselect({message:descriptions.select,choices:[{label:"Frontend (views)",value:"views"},{label:"Components",value:"components"},{label:"Web Components",value:"webComponents"},{label:"Functions",value:"functions"},{label:"All library packages",value:"libs"},{label:"Desktop application",value:"desktop"},{label:"Mobile applications (iOS + Android)",value:"mobile"},{label:"Android application",value:"android"},{label:"iOS application",value:"ios"},{label:"Documentation",value:"docs"},{label:"Stacks framework",value:"stacks"},{label:"Buddy CLI",value:"buddy"},{label:"Server (Docker image)",value:"server"}]}),selected=new Set(answers);if(selected.has("views"))options.views=!0;if(selected.has("components"))options.components=!0;if(selected.has("webComponents"))options.webComponents=!0;if(selected.has("functions"))options.functions=!0;if(selected.has("libs"))options.libs=!0;if(selected.has("desktop"))options.desktop=!0;if(selected.has("mobile")){options.mobile=!0;options.android=!0;options.ios=!0}if(selected.has("android"))options.android=!0;if(selected.has("ios"))options.ios=!0;if(selected.has("docs"))options.docs=!0;if(selected.has("stacks"))options.stacks=!0;if(selected.has("buddy"))options.buddy=!0;if(selected.has("server"))options.server=!0}if(hasNoOptions(options)){options.views=!0;log.info("No build target specified, defaulting to the frontend (views). See `buddy build --help` for all targets.")}}let succeeded=!0;if(options.docs)succeeded=await runBuildAction(Action.BuildDocs,"documentation",options)&&succeeded;if(options.components)succeeded=await runBuildAction(Action.BuildComponentLibs,"component libraries",options)&&succeeded;if(options.webComponents)succeeded=await runBuildAction(Action.BuildWebComponentLib,"web component library",options)&&succeeded;if(options.functions)succeeded=await runBuildAction(Action.BuildFunctionLib,"function library",options)&&succeeded;if(options.libs)succeeded=await runBuildAction(Action.BuildLibs,"library packages",options)&&succeeded;if(options.desktop)succeeded=await runBuildAction(Action.BuildDesktop,"desktop application",options)&&succeeded;if(options.android)succeeded=await runBuildAction(Action.BuildAndroid,"Android application",options)&&succeeded;if(options.ios)succeeded=await runBuildAction(Action.BuildIos,"iOS application",options)&&succeeded;if(options.views)succeeded=await runBuildAction(Action.BuildViews,"frontend",options)&&succeeded;if(options.stacks)succeeded=await runBuildAction(Action.BuildStacks,"Stacks framework",options)&&succeeded;if(options.buddy)succeeded=await runBuildAction(Action.BuildCli,"Buddy CLI",options)&&succeeded;if(options.server)succeeded=await runBuildAction(Action.BuildServer,"server",options)&&succeeded;if(!succeeded)process.exit(ExitCode.FatalError);process.exit(ExitCode.Success)});buddy.command("build:components","Automagically build component libraries for production use & npm/CDN distribution").alias("prod:components").option("-c, --components",descriptions.components,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:components` ...",options);if(!await runBuildAction(Action.BuildComponentLibs,"component libraries"))process.exit(ExitCode.FatalError)});buddy.command("build:cli",descriptions.cli).alias("prod:cli").option("-b, --buddy",descriptions.buddy,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:cli` ...",options);await runAction(Action.BuildCli,options)});buddy.command("build:server",descriptions.server).alias("prod:server").alias("build:docker").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:server` ...",options);await runAction(Action.BuildServer,options)});buddy.command("build:functions","Automagically build function library for npm/CDN distribution").option("-f, --functions",descriptions.functions,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:functions` ...",options);if(!await runBuildAction(Action.BuildFunctionLib,"function library",options))process.exit(ExitCode.FatalError)});buddy.command("build:libs",descriptions.libs).alias("build:libraries").alias("prod:libs").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:libs` ...",options);if(!await runBuildAction(Action.BuildLibs,"library packages",options))process.exit(ExitCode.FatalError)});buddy.command("build:web-components","Automagically build Web Component library for npm/CDN distribution").alias("build:wc").alias("prod:web-components").alias("prod:wc").option("-w, --web-components",descriptions.webComponents,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:web-components` ...",options);if(!await runBuildAction(Action.BuildWebComponentLib,"web component library"))process.exit(ExitCode.FatalError)});buddy.command("build:frontend",descriptions.pages).alias("build:pages").alias("build:views").alias("prod:frontend").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:frontend` ...",options);await runAction(Action.BuildViews,options)});buddy.command("build:docs","Automagically build your documentation site.").alias("prod:docs").alias("build:documentation").alias("prod:documentation").option("-d, --docs",descriptions.docs,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:docs` ...",options);await runAction(Action.BuildDocs,options)});buddy.command("build:frontend-static",descriptions.frontendStatic).alias("build:public").alias("prod:frontend-static").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:frontend-static` ...",options);await runAction(Action.BuildFrontendStatic,options)});buddy.command("build:core","Automagically build the Stacks core.").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:core` ...",options);const startTime=await intro("buddy build:core"),result=await runAction(Action.BuildCore,options);if(resultFailed(result)){log.error("Failed to build the Stacks core.",result.error);process.exit(ExitCode.FatalError)}await outro("Core packages built successfully",{startTime,useSeconds:!0})});buddy.command("build:desktop",descriptions.desktop).alias("prod:desktop").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:desktop` ...",options);const perf=await intro("buddy build:desktop"),result=await runAction(Action.BuildDesktop,options);if(resultFailed(result)){await outro("While running the build:desktop command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("build:mobile",descriptions.mobile).alias("prod:mobile").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:mobile` ...",options);const perf=await intro("buddy build:mobile"),androidSucceeded=await runBuildAction(Action.BuildAndroid,"Android application",options),iosSucceeded=await runBuildAction(Action.BuildIos,"iOS application",options);if(!androidSucceeded||!iosSucceeded){await outro("One or more mobile application builds failed",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}await outro("iOS and Android applications built",{startTime:perf,useSeconds:!0})});buddy.command("build:android",descriptions.android).alias("prod:android").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:android` ...",options);const perf=await intro("buddy build:android"),result=await runAction(Action.BuildAndroid,options);if(resultFailed(result)){await outro("While building the Android application, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Android application built",{startTime:perf,useSeconds:!0})});buddy.command("build:ios",descriptions.ios).alias("prod:ios").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:ios` ...",options);const perf=await intro("buddy build:ios"),result=await runAction(Action.BuildIos,options);if(resultFailed(result)){await outro("While building the iOS application, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("iOS application built",{startTime:perf,useSeconds:!0})});buddy.command("build:dmg",descriptions.dmg).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:dmg` ...",options);const perf=await intro("buddy build:dmg"),result=await runAction(Action.BuildDmg,options);if(resultFailed(result)){await outro("While running the build:dmg command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("build:stacks","Build the Stacks framework.").option("-s, --stacks",descriptions.framework,{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy build:stacks` ...",options);const startTime=await intro("buddy build:stacks"),result=await runAction(Action.BuildStacks,options);if(resultFailed(result)){log.error("Failed to build Stacks.",result.error);process.exit(ExitCode.FatalError)}await outro("Stacks built successfully",{startTime,useSeconds:!0})});onUnknownSubcommand(buddy,"build")}function hasNoOptions(options){return!options.components&&!options.webComponents&&!options.elements&&!options.functions&&!options.libs&&!options.desktop&&!options.mobile&&!options.android&&!options.ios&&!options.views&&!options.docs&&!options.stacks&&!options.buddy&&!options.server}export function applyBuildTarget(target,options){switch(target){case"components":options.components=!0;break;case"web-components":options.webComponents=!0;break;case"functions":options.functions=!0;break;case"libs":case"libraries":options.libs=!0;break;case"desktop":options.desktop=!0;break;case"mobile":options.mobile=!0;options.android=!0;options.ios=!0;break;case"android":options.android=!0;break;case"ios":options.ios=!0;break;case"views":options.views=!0;break;case"docs":options.docs=!0;break;case"buddy":case"cli":options.buddy=!0;break;case"stacks":options.stacks=!0;break;case"server":options.server=!0;break}}async function runBuildAction(action,target,options){const result=await runAction(action,options);if(resultFailed(result)){log.error(`Failed to build ${target}.`,result.error);return!1}return!0}
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import process from"node:process";import{intro,italic,log,onUnknownSubcommand,outro,prompts,runCommand,text,underline}from"@stacksjs/cli";import{addJumpBox,deleteCdkRemnants,deleteIamUsers,deleteJumpBox,deleteLogGroups,deleteParameterStore,deleteStacksBuckets,deleteStacksFunctions,deleteSubnets,deleteVpcs,getCloudFrontDistributionId,getJumpBoxInstanceId}from"@stacksjs/cloud";import{hasTTY,isCI}from"@stacksjs/env";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";async function createTemporaryCdkRole(roleName){const{AWSClient}=await import("@stacksjs/ts-cloud"),client=new AWSClient,trustPolicy={Version:"2012-10-17",Statement:[{Effect:"Allow",Principal:{Service:"cloudformation.amazonaws.com"},Action:"sts:AssumeRole"}]};try{try{const getRoleParams=new URLSearchParams({Action:"GetRole",RoleName:roleName,Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:getRoleParams.toString()});log.debug(`Role ${roleName} already exists`);return}catch(e){if(!e.message?.includes("NoSuchEntity")&&!e.message?.includes("cannot be found"))throw e}log.info("Creating temporary IAM role to enable stack deletion...");const createRoleParams=new URLSearchParams({Action:"CreateRole",RoleName:roleName,AssumeRolePolicyDocument:JSON.stringify(trustPolicy),Description:"Temporary role to allow CloudFormation to delete stuck stack",Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:createRoleParams.toString()});log.success("Created IAM role");log.info("Attaching permissions...");const attachPolicyParams=new URLSearchParams({Action:"AttachRolePolicy",RoleName:roleName,PolicyArn:"arn:aws:iam::aws:policy/AdministratorAccess",Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:attachPolicyParams.toString()});log.success("IAM role ready for stack deletion");log.info("Waiting for IAM role to propagate...");await new Promise((resolve)=>setTimeout(resolve,1e4))}catch(error){if(error.message?.includes("EntityAlreadyExists"))log.debug("Role already exists");else throw error}}async function deleteTemporaryCdkRole(roleName){const{AWSClient}=await import("@stacksjs/ts-cloud"),client=new AWSClient;try{const detachPolicyParams=new URLSearchParams({Action:"DetachRolePolicy",RoleName:roleName,PolicyArn:"arn:aws:iam::aws:policy/AdministratorAccess",Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:detachPolicyParams.toString()});const deleteRoleParams=new URLSearchParams({Action:"DeleteRole",RoleName:roleName,Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:deleteRoleParams.toString()});log.success("Cleaned up temporary IAM role")}catch(error){log.debug(`Could not clean up temporary role: ${error.message}`)}}function isResultError(result){return resultFailed(result)}function getResultError(result){if(!result||typeof result!=="object")return"Unknown error";return String(result.error||"Unknown error")}function getResultValue(result){if(!result||typeof result!=="object")return;return result.value}export function cloud(buddy){const descriptions={cloud:"Interact with the Stacks Cloud",ssh:"SSH into the Stacks Cloud",add:"Add a resource to the Stacks Cloud",remove:"Remove the Stacks Cloud. In case it fails, try again",optimizeCost:"Remove certain resources that may be re-applied at a later time",cleanUp:"Remove all resources that were retained during the cloud deletion",invalidateCache:"Invalidate the CloudFront cache",diff:"Show the diff of the current, undeployed cloud changes ",dashboard:"Run the local Stacks Cloud management cockpit (servers, sites, deploys)",sites:"List every server and what each one is hosting, across projects",attach:"Attach this project to a server another project owns, after checking it is safe",attachServer:"Server to attach to, by provider name or by owning project slug",dryRun:"Print the plan and change nothing",sitesEnv:"Environment to take the inventory for",remote:"Skip the SSH read of each box's gateway registry (co-tenants are then not listed)",json:"Emit the inventory as JSON",host:"Host to bind the dashboard to",port:"Port to bind the dashboard to",env:"Environment to manage",paths:"The paths to invalidate",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("cloud",descriptions.cloud).option("--ssh",descriptions.ssh,{default:!1}).option("--connect",descriptions.ssh,{default:!1}).option("--invalidate-cache",descriptions.invalidateCache,{default:!1}).option("--paths [paths]",descriptions.paths).option("--diff",descriptions.diff,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud` ...",options);const startTime=performance.now();if(options.ssh||options.connect){const jumpBoxId=await getJumpBoxInstanceId(),result=await runCommand(`aws ssm start-session --target ${jumpBoxId}`,{...options,cwd:p.projectPath(),stdin:"pipe"});if(isResultError(result)){await outro("While running the cloud command, there was an issue",{startTime,useSeconds:!0},getResultError(result));process.exit(ExitCode.FatalError)}await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}if(options.invalidateCache){const{confirm}=await prompts({name:"confirm",type:"confirm",message:"Would you like to invalidate the CDN (CloudFront) cache?"});if(!confirm){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("Invalidating the CloudFront cache...");const{AWSCloudFrontClient}=await import("@stacksjs/ts-cloud"),cloudfront=new AWSCloudFrontClient,distributionId=await getCloudFrontDistributionId();try{const invalidationId=await cloudfront.invalidateAll(distributionId);log.success(`Invalidation created: ${invalidationId}`);log.info("Status: pending")}catch(err){log.error(`Failed to invalidate CloudFront cache: ${err.message}`)}await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}if(options.diff){try{const{InfrastructureGenerator}=await import("@stacksjs/ts-cloud"),{CloudFormationClient}=await import("@stacksjs/ts-cloud/aws"),{tsCloud:cloudConfig}=await import("~/config/cloud"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production",newTemplate=new InfrastructureGenerator({config:cloudConfig,environment}).generate().toJSON(),stackName=`${cloudConfig.project?.slug||"stacks"}-${environment}`,cfn=new CloudFormationClient(process.env.AWS_REGION||"us-east-1");let currentTemplate="{}";try{currentTemplate=(await cfn.getTemplate(stackName)).TemplateBody}catch{log.info("No deployed stack found. Showing full template as diff.")}if(currentTemplate===newTemplate)log.info("No changes detected.");else{log.info("Changes detected between deployed and local template:");log.info(`Current template: ${currentTemplate.length} bytes`);log.info(`New template: ${newTemplate.length} bytes`)}}catch(error){log.error(`Failed to compute diff: ${error.message}`)}await outro("Cloud diff complete",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("Not implemented yet. Read more about `buddy cloud` here: https://stacksjs.com/docs/cloud");process.exit(ExitCode.Success)});buddy.command("cloud:add",descriptions.add).option("--jump-box","Remove the jump-box",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:add` ...",options);const startTime=await intro("buddy cloud:add");if(options.jumpBox){const{confirm}=await prompts({name:"confirm",type:"confirm",message:"Would you like to add a jump-box to your cloud?"});if(!confirm){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("The jump-box is getting added to your cloud resources...");log.info("This takes a few moments, please be patient.");await new Promise((resolve)=>setTimeout(resolve,2000));const result=await addJumpBox();if(isResultError(result)){await outro("While running the cloud:add command, there was an issue",{startTime,useSeconds:!0},getResultError(result));process.exit(ExitCode.FatalError)}log.info(italic("View the jump-box in the AWS console:"));log.info(underline("https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#Instances:instanceState=running"));log.info(italic("Once it finished initializing, you may SSH into it:"));log.info(underline("buddy cloud --ssh"));await outro("Your jump-box was added.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("This functionality is not yet implemented.");process.exit(ExitCode.Success)});buddy.command("cloud:remove",descriptions.remove).alias("cloud:destroy").alias("cloud:rm").alias("undeploy").option("--jump-box","Remove the jump-box",{default:!1}).option("--force","Force deletion of stack in bad state",{default:!1}).option("--yes","Skip confirmation prompts",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:remove` ...",options);const startTime=await intro("buddy cloud:remove"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production";if(!options.yes&&(isCI||!hasTTY||!process.stdin.isTTY)){log.syncError(`Refusing to remove the "${environment}" cloud infrastructure from a non-interactive shell without confirmation.`);log.syncError(" \u27A1\uFE0F Re-run with `--yes` to confirm (e.g. in CI): `buddy cloud:remove --yes`");await outro("cloud:remove cancelled.",{startTime,useSeconds:!0});await log.flush();process.exit(ExitCode.FatalError)}if(options.jumpBox){if(!options.yes){const{confirm}=await prompts({name:"confirm",type:"confirm",message:"Would you like to remove your jump-box for now?"});if(!confirm){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}}const result=await deleteJumpBox();if(isResultError(result)){await outro("While removing your jump-box, there was an issue",{startTime,useSeconds:!0},getResultError(result));process.exit(ExitCode.FatalError)}await outro("Your jump-box was removed.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}if(!options.yes){log.warning(`This will permanently delete the "${environment}" cloud infrastructure (compute, storage, CDN, and DNS managed by Stacks). This cannot be undone.`);if((await text({message:`Type the environment name "${environment}" to confirm (blank to cancel):`})).trim()!==environment){await outro("cloud:remove cancelled - confirmation did not match.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}}console.log("");console.log("Removing cloud infrastructure...");console.log(` ${italic("This typically takes 2-5 minutes.")}`);console.log("");if(!process.env.AWS_ACCESS_KEY_ID||!process.env.AWS_SECRET_ACCESS_KEY){const{existsSync,readFileSync}=await import("node:fs"),{projectPath}=await import("@stacksjs/path"),envFiles=[projectPath(`.env.${environment}`),projectPath(".env")];for(const envPath of envFiles)if(existsSync(envPath)){const lines=readFileSync(envPath,"utf-8").split(`
|
|
2
|
-
`);for(const line of lines){const trimmed=line.trim();if(trimmed.startsWith("#")||!trimmed.includes("="))continue;const[key,...valueParts]=trimmed.split("="),value=valueParts.join("=").replace(/^["']|["']$/g,"");if(key==="AWS_ACCESS_KEY_ID"||key==="AWS_SECRET_ACCESS_KEY"||key==="AWS_REGION"||key==="AWS_ACCOUNT_ID")process.env[key]=value}break}}delete process.env.AWS_PROFILE;try{const{undeployStack}=await import("../../../actions/deploy"),region=process.env.AWS_REGION||"us-east-1";await undeployStack({environment,region,verbose:options.verbose});await outro("Cloud infrastructure removed",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}catch(error){console.log("");console.error("\u2717 Failed to remove cloud infrastructure");const errorStr=String(error.message||error);if(errorStr.includes("security token")||errorStr.includes("credentials")){console.log("");console.error(" AWS credentials are invalid or expired");console.log(" Check your AWS credentials in .env.production:");console.log(" - AWS_ACCESS_KEY_ID");console.log(" - AWS_SECRET_ACCESS_KEY")}else if(errorStr.includes("region")||errorStr.includes("AWS_REGION")){console.log("");console.error(" AWS Region not configured");console.log(" Add AWS_REGION to your .env.production file")}else if(errorStr.includes("AccessDenied")){console.log("");console.error(" Access denied");console.log(" Your AWS credentials may not have permission to delete stacks")}else console.error(` ${errorStr}`);console.log("");console.log("Troubleshooting:");console.log(" ./buddy cloud:cleanup - Clean up resources manually");console.log(" --verbose - Show detailed error information");console.log("");if(options.verbose)console.error("Error details:",error);await outro("Failed to remove infrastructure",{startTime,useSeconds:!0});process.exit(ExitCode.FatalError)}});buddy.command("cloud:optimize-cost",descriptions.optimizeCost).option("--jump-box","Remove the jump-box",{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:optimize-cost` ...",options);const startTime=await intro("buddy cloud:optimize-cost");if(options.jumpBox){const{confirm}=await prompts({name:"confirm",type:"confirm",message:"Would you like to remove your jump-box to optimize your costs?"});if(!confirm){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}await deleteJumpBox();await outro("Your jump-box was removed & cost optimizations are applied.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}await outro("No cost optimization was applied",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("cloud:cleanup",descriptions.cleanUp).alias("cloud:clean-up").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:cleanup` ...",options);const startTime=await intro("buddy cloud:cleanup");delete process.env.AWS_PROFILE;log.info("Cleaning up your cloud resources will take a while to complete. Please be patient.");await new Promise((resolve)=>setTimeout(resolve,2000));const cleanupSteps=[{label:"jump-boxes",fn:deleteJumpBox,ignoreErrors:["Jump-box not found"]},{label:"retained S3 buckets",fn:deleteStacksBuckets},{label:"retained Lambda functions",fn:deleteStacksFunctions,ignoreErrors:["No stacks functions found"]},{label:"remaining Stacks logs",fn:deleteLogGroups},{label:"stored parameters",fn:deleteParameterStore},{label:"VPCs",fn:deleteVpcs},{label:"Subnets",fn:deleteSubnets},{label:"CDK remnants",fn:deleteCdkRemnants},{label:"IAM users",fn:deleteIamUsers}],errors=[];for(const step of cleanupSteps){log.info(`Removing any ${step.label}...`);try{const result=await step.fn();if(isResultError(result)){const errMsg=getResultError(result);if(!step.ignoreErrors?.includes(errMsg)){log.warn(`${step.label} cleanup issue: ${errMsg}`);errors.push({label:step.label,error:errMsg})}}else{const value=getResultValue(result);if(value)log.info(String(value))}}catch(e){const errMsg=e.message||"AWS SDK error";log.warn(`${step.label} cleanup skipped: ${errMsg}`);errors.push({label:step.label,error:errMsg})}}if(errors.length>0){log.warn(`Cleanup completed with ${errors.length} issue(s):`);for(const{label,error}of errors)log.warn(` - ${label}: ${error}`)}await outro("AWS resources have been removed",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("cloud:invalidate-cache",descriptions.invalidateCache).option("--paths [paths]",descriptions.paths,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:invalidate-cache` ...",options);const startTime=await intro("buddy cloud:invalidate-cache"),{confirm}=await prompts({name:"confirm",type:"confirm",message:"Would you like to invalidate the CloudFront cache?"});if(!confirm){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("Invalidating the CloudFront cache...");const distributionId=await getCloudFrontDistributionId();if(!distributionId){await outro("Could not resolve CloudFront distribution ID",{startTime,useSeconds:!0},"Ensure your cloud stack is deployed before invalidating cache.");process.exit(ExitCode.FatalError)}const paths=options.paths?String(options.paths):"/*",result=await runCommand(`aws cloudfront create-invalidation --distribution-id ${distributionId} --paths ${paths}`,{...options,cwd:p.projectPath(),stdin:"pipe"});if(isResultError(result)){await outro("While running the cloud command, there was an issue",{startTime,useSeconds:!0},getResultError(result));process.exit(ExitCode.FatalError)}await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("cloud:diff",descriptions.diff).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:diff` ...",options);const startTime=await intro("buddy cloud:diff");try{const{InfrastructureGenerator}=await import("@stacksjs/ts-cloud"),{CloudFormationClient}=await import("@stacksjs/ts-cloud/aws"),{tsCloud:cloudConfig}=await import("~/config/cloud"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production",newTemplate=new InfrastructureGenerator({config:cloudConfig,environment}).generate().toJSON(),stackName=`${cloudConfig.project?.slug||"stacks"}-${environment}`,cfn=new CloudFormationClient(process.env.AWS_REGION||"us-east-1");let currentTemplate="{}";try{currentTemplate=(await cfn.getTemplate(stackName)).TemplateBody}catch{log.info("No deployed stack found. Showing full template as diff.")}if(currentTemplate===newTemplate)log.info("No changes detected.");else{log.info("Changes detected between deployed and local template:");log.info(`Current template: ${currentTemplate.length} bytes`);log.info(`New template: ${newTemplate.length} bytes`)}}catch(error){await outro("While running the cloud diff command, there was an issue",{startTime,useSeconds:!0},error.message);process.exit(ExitCode.FatalError)}await outro("Cloud diff complete",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("cloud:dashboard",descriptions.dashboard).alias("cloud:cockpit").option("--host [host]",descriptions.host,{default:"127.0.0.1"}).option("--port [port]",descriptions.port,{default:"7676"}).option("--env [env]",descriptions.env).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:dashboard` ...",options);const startTime=await intro("buddy cloud:dashboard"),tsCloud=await import("@stacksjs/ts-cloud");if(typeof tsCloud.startLocalDashboardServer!=="function"){await outro("The installed @stacksjs/ts-cloud does not provide the local cockpit yet",{startTime,useSeconds:!0},"Update your dependencies (requires @stacksjs/ts-cloud >= 0.5.27).");process.exit(ExitCode.FatalError)}try{const server=await tsCloud.startLocalDashboardServer({host:options.host?String(options.host):void 0,port:options.port?Number(options.port):void 0,environment:options.env,verbose:!!options.verbose});log.success(`Stacks Cloud cockpit running at ${underline(server.url)}`);log.info(italic("Manage servers, sites, SSH keys and deploys. Press Ctrl+C to stop."));await new Promise(()=>{})}catch(error){await outro("While starting the cloud dashboard, there was an issue",{startTime,useSeconds:!0},error?.message??String(error));process.exit(ExitCode.FatalError)}});buddy.command("cloud:sites",descriptions.sites).option("--env [env]",descriptions.sitesEnv).option("--no-remote",descriptions.remote).option("-J, --json",descriptions.json,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:sites` ...",options);const{declaredSites,describeInventory,listProviderServers,probeHostRoutes}=await import("../cloud-inventory"),{loadTsCloudConfig,resolveHetznerApiToken}=await import("./deploy"),environment=String(options.env||process.env.APP_ENV||process.env.NODE_ENV||"production"),tsCloudConfig=await loadTsCloudConfig(options.env?environment:void 0),slug=tsCloudConfig?.project?.slug||"app",provider=tsCloudConfig?.cloud?.provider||process.env.CLOUD_PROVIDER||"aws";if(provider!=="hetzner"){await log.info("The on-box half (the rpx gateway registry) is provider-independent; the server listing is not yet.");await log.exit(`\`buddy cloud:sites\` can only list Hetzner servers, and this project's provider is '${provider}'.`,ExitCode.FatalError)}let helpers={};try{const deployApi=await import("@stacksjs/ts-cloud/deploy");helpers={resolveSiteKind:deployApi.resolveSiteKind,siteInstallBase:deployApi.siteInstallBase}}catch(error){log.debug("Could not load @stacksjs/ts-cloud/deploy for site classification:",error)}const declared=declaredSites(tsCloudConfig?.sites,helpers,slug),listing=await listProviderServers(resolveHetznerApiToken(tsCloudConfig)),probes=[];if(options.remote!==!1){const{sshExec}=await import("@stacksjs/ts-cloud"),batchSize=6;for(let index=0;index<listing.servers.length;index+=batchSize)probes.push(...await Promise.all(listing.servers.slice(index,index+batchSize).map((server)=>probeHostRoutes(server,(host,command)=>sshExec(host,command,{user:"root",connectTimeoutSec:10})))))}const inventory={slug,environment,servers:listing.servers,probes,declared,providerFailure:listing.failure};if(options.json)console.log(JSON.stringify(inventory,null,2));else for(const line of describeInventory(inventory))console.log(line);process.exit(listing.failure&&listing.servers.length===0?ExitCode.FatalError:ExitCode.Success)});buddy.command("cloud:attach",descriptions.attach).option("--server <server>",descriptions.attachServer).option("--env [env]",descriptions.sitesEnv).option("--dry-run",descriptions.dryRun,{default:!1}).option("-J, --json",descriptions.json,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:attach` ...",options);const{declaredSites,listProviderServers,probeHostRoutes}=await import("../cloud-inventory"),{attachConflicts,attachPreconditions,describeAttachPlan,resolveAttachTarget,setAttachTo}=await import("../cloud-attach"),{loadTsCloudConfig,resolveHetznerApiToken}=await import("./deploy"),refuse=async(...messages)=>{for(const message of messages.slice(0,-1))await log.error(message);return log.exit(messages[messages.length-1],ExitCode.FatalError)};if(!options.server)return await refuse("Which server? Pass --server <name|owner-slug>. `buddy cloud:sites` lists them.");const environment=String(options.env||process.env.APP_ENV||process.env.NODE_ENV||"production"),tsCloudConfig=await loadTsCloudConfig(options.env?environment:void 0),slug=tsCloudConfig?.project?.slug||"app",listing=await listProviderServers(resolveHetznerApiToken(tsCloudConfig));if(listing.failure){const{describeProviderFailure}=await import("../cloud-inventory");return await refuse(describeProviderFailure(listing.failure))}const target=resolveAttachTarget(listing.servers,String(options.server),environment);if("problem"in target)return await refuse(target.problem);const server=target.server,preconditions=attachPreconditions(slug,server);if(preconditions.length>0)return await refuse(...preconditions);let helpers={};try{const deployApi=await import("@stacksjs/ts-cloud/deploy");helpers={resolveSiteKind:deployApi.resolveSiteKind,siteInstallBase:deployApi.siteInstallBase}}catch(error){log.debug("Could not load @stacksjs/ts-cloud/deploy for site classification:",error)}const declared=declaredSites(tsCloudConfig?.sites,helpers,slug),{sshExec}=await import("@stacksjs/ts-cloud"),probe=await probeHostRoutes(server,(host,command)=>sshExec(host,command,{user:"root",connectTimeoutSec:10})),conflicts=probe.unavailable?[]:attachConflicts(slug,declared,probe.routes),blocked=Boolean(probe.unavailable)||conflicts.length>0;let edit;if(!blocked){const configPath=p.projectPath("config/cloud.ts"),{readFileSync,writeFileSync}=await import("node:fs");edit=setAttachTo(readFileSync(configPath,"utf8"),server.project);if(!options.dryRun&&"text"in edit&&edit.changed)writeFileSync(configPath,edit.text)}const plan={slug,owner:server.project,server,declared,conflicts,registryRead:!probe.unavailable,registryProblem:probe.unavailable,edit,dryRun:Boolean(options.dryRun)};if(options.json){const edited=!edit?void 0:("problem"in edit)?edit:{changed:edit.changed};console.log(JSON.stringify({...plan,edit:edited},null,2))}else for(const line of describeAttachPlan(plan))console.log(line);process.exit(blocked?ExitCode.FatalError:ExitCode.Success)});onUnknownSubcommand(buddy,"cloud")}
|
|
1
|
+
import process from"node:process";import{intro,italic,log,onUnknownSubcommand,outro,prompts,runCommand,text,underline}from"@stacksjs/cli";import{addJumpBox,deleteCdkRemnants,deleteIamUsers,deleteJumpBox,deleteLogGroups,deleteParameterStore,deleteStacksBuckets,deleteStacksFunctions,deleteSubnets,deleteVpcs,getCloudFrontDistributionId,getJumpBoxInstanceId}from"@stacksjs/cloud";import{hasTTY,isCI}from"@stacksjs/env";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";async function createTemporaryCdkRole(roleName){const{AWSClient}=await import("@stacksjs/ts-cloud"),client=new AWSClient,trustPolicy={Version:"2012-10-17",Statement:[{Effect:"Allow",Principal:{Service:"cloudformation.amazonaws.com"},Action:"sts:AssumeRole"}]};try{try{const getRoleParams=new URLSearchParams({Action:"GetRole",RoleName:roleName,Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:getRoleParams.toString()});log.debug(`Role ${roleName} already exists`);return}catch(e){if(!e.message?.includes("NoSuchEntity")&&!e.message?.includes("cannot be found"))throw e}log.info("Creating temporary IAM role to enable stack deletion...");const createRoleParams=new URLSearchParams({Action:"CreateRole",RoleName:roleName,AssumeRolePolicyDocument:JSON.stringify(trustPolicy),Description:"Temporary role to allow CloudFormation to delete stuck stack",Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:createRoleParams.toString()});log.success("Created IAM role");log.info("Attaching permissions...");const attachPolicyParams=new URLSearchParams({Action:"AttachRolePolicy",RoleName:roleName,PolicyArn:"arn:aws:iam::aws:policy/AdministratorAccess",Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:attachPolicyParams.toString()});log.success("IAM role ready for stack deletion");log.info("Waiting for IAM role to propagate...");await new Promise((resolve)=>setTimeout(resolve,1e4))}catch(error){if(error.message?.includes("EntityAlreadyExists"))log.debug("Role already exists");else throw error}}async function deleteTemporaryCdkRole(roleName){const{AWSClient}=await import("@stacksjs/ts-cloud"),client=new AWSClient;try{const detachPolicyParams=new URLSearchParams({Action:"DetachRolePolicy",RoleName:roleName,PolicyArn:"arn:aws:iam::aws:policy/AdministratorAccess",Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:detachPolicyParams.toString()});const deleteRoleParams=new URLSearchParams({Action:"DeleteRole",RoleName:roleName,Version:"2010-05-08"});await client.request({service:"iam",region:"us-east-1",method:"POST",path:"/",body:deleteRoleParams.toString()});log.success("Cleaned up temporary IAM role")}catch(error){log.debug(`Could not clean up temporary role: ${error.message}`)}}function isResultError(result){return resultFailed(result)}function getResultError(result){if(!result||typeof result!=="object")return"Unknown error";return String(result.error||"Unknown error")}function getResultValue(result){if(!result||typeof result!=="object")return;return result.value}export function cloud(buddy){const descriptions={cloud:"Interact with the Stacks Cloud",ssh:"SSH into the Stacks Cloud",add:"Add a resource to the Stacks Cloud",remove:"Remove the Stacks Cloud. In case it fails, try again",optimizeCost:"Remove certain resources that may be re-applied at a later time",cleanUp:"Remove all resources that were retained during the cloud deletion",invalidateCache:"Invalidate the CloudFront cache",diff:"Show the diff of the current, undeployed cloud changes ",dashboard:"Run the local Stacks Cloud management cockpit (servers, sites, deploys)",sites:"List every server and what each one is hosting, across projects",attach:"Attach this project to a server another project owns, after checking it is safe",attachServer:"Server to attach to, by provider name or by owning project slug",dryRun:"Print the plan and change nothing",sitesEnv:"Environment to take the inventory for",remote:"Skip the SSH read of each box's gateway registry (co-tenants are then not listed)",json:"Emit the inventory as JSON",host:"Host to bind the dashboard to",port:"Port to bind the dashboard to",env:"Environment to manage",paths:"The paths to invalidate",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("cloud",descriptions.cloud).option("--ssh",descriptions.ssh,{default:!1}).option("--connect",descriptions.ssh,{default:!1}).option("--invalidate-cache",descriptions.invalidateCache,{default:!1}).option("--paths [paths]",descriptions.paths).option("--diff",descriptions.diff,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud` ...",options);const startTime=performance.now();if(options.ssh||options.connect){const jumpBoxId=await getJumpBoxInstanceId(),result=await runCommand(`aws ssm start-session --target ${jumpBoxId}`,{...options,cwd:p.projectPath(),stdin:"pipe"});if(isResultError(result)){await outro("While running the cloud command, there was an issue",{startTime,useSeconds:!0},getResultError(result));process.exit(ExitCode.FatalError)}await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}if(options.invalidateCache){if(!await prompts.confirm("Would you like to invalidate the CDN (CloudFront) cache?")){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("Invalidating the CloudFront cache...");const{AWSCloudFrontClient}=await import("@stacksjs/ts-cloud"),cloudfront=new AWSCloudFrontClient,distributionId=await getCloudFrontDistributionId();try{const invalidationId=await cloudfront.invalidateAll(distributionId);log.success(`Invalidation created: ${invalidationId}`);log.info("Status: pending")}catch(err){log.error(`Failed to invalidate CloudFront cache: ${err.message}`)}await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}if(options.diff){try{const{InfrastructureGenerator}=await import("@stacksjs/ts-cloud"),{CloudFormationClient}=await import("@stacksjs/ts-cloud/aws"),{tsCloud:cloudConfig}=await import("~/config/cloud"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production",newTemplate=new InfrastructureGenerator({config:cloudConfig,environment}).generate().toJSON(),stackName=`${cloudConfig.project?.slug||"stacks"}-${environment}`,cfn=new CloudFormationClient(process.env.AWS_REGION||"us-east-1");let currentTemplate="{}";try{currentTemplate=(await cfn.getTemplate(stackName)).TemplateBody}catch{log.info("No deployed stack found. Showing full template as diff.")}if(currentTemplate===newTemplate)log.info("No changes detected.");else{log.info("Changes detected between deployed and local template:");log.info(`Current template: ${currentTemplate.length} bytes`);log.info(`New template: ${newTemplate.length} bytes`)}}catch(error){log.error(`Failed to compute diff: ${error.message}`)}await outro("Cloud diff complete",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("Not implemented yet. Read more about `buddy cloud` here: https://stacksjs.com/docs/cloud");process.exit(ExitCode.Success)});buddy.command("cloud:add",descriptions.add).option("--jump-box","Remove the jump-box",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:add` ...",options);const startTime=await intro("buddy cloud:add");if(options.jumpBox){if(!await prompts.confirm("Would you like to add a jump-box to your cloud?")){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("The jump-box is getting added to your cloud resources...");log.info("This takes a few moments, please be patient.");await new Promise((resolve)=>setTimeout(resolve,2000));const result=await addJumpBox();if(isResultError(result)){await outro("While running the cloud:add command, there was an issue",{startTime,useSeconds:!0},getResultError(result));process.exit(ExitCode.FatalError)}log.info(italic("View the jump-box in the AWS console:"));log.info(underline("https://us-east-1.console.aws.amazon.com/ec2/home?region=us-east-1#Instances:instanceState=running"));log.info(italic("Once it finished initializing, you may SSH into it:"));log.info(underline("buddy cloud --ssh"));await outro("Your jump-box was added.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("This functionality is not yet implemented.");process.exit(ExitCode.Success)});buddy.command("cloud:remove",descriptions.remove).alias("cloud:destroy").alias("cloud:rm").alias("undeploy").option("--jump-box","Remove the jump-box",{default:!1}).option("--force","Force deletion of stack in bad state",{default:!1}).option("--yes","Skip confirmation prompts",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:remove` ...",options);const startTime=await intro("buddy cloud:remove"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production";if(!options.yes&&(isCI||!hasTTY||!process.stdin.isTTY)){log.syncError(`Refusing to remove the "${environment}" cloud infrastructure from a non-interactive shell without confirmation.`);log.syncError(" \u27A1\uFE0F Re-run with `--yes` to confirm (e.g. in CI): `buddy cloud:remove --yes`");await outro("cloud:remove cancelled.",{startTime,useSeconds:!0});await log.flush();process.exit(ExitCode.FatalError)}if(options.jumpBox){if(!options.yes){if(!await prompts.confirm("Would you like to remove your jump-box for now?")){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}}const result=await deleteJumpBox();if(isResultError(result)){await outro("While removing your jump-box, there was an issue",{startTime,useSeconds:!0},getResultError(result));process.exit(ExitCode.FatalError)}await outro("Your jump-box was removed.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}if(!options.yes){log.warning(`This will permanently delete the "${environment}" cloud infrastructure (compute, storage, CDN, and DNS managed by Stacks). This cannot be undone.`);if((await text({message:`Type the environment name "${environment}" to confirm (blank to cancel):`})).trim()!==environment){await outro("cloud:remove cancelled - confirmation did not match.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}}console.log("");console.log("Removing cloud infrastructure...");console.log(` ${italic("This typically takes 2-5 minutes.")}`);console.log("");if(!process.env.AWS_ACCESS_KEY_ID||!process.env.AWS_SECRET_ACCESS_KEY){const{existsSync,readFileSync}=await import("node:fs"),{projectPath}=await import("@stacksjs/path"),envFiles=[projectPath(`.env.${environment}`),projectPath(".env")];for(const envPath of envFiles)if(existsSync(envPath)){const lines=readFileSync(envPath,"utf-8").split(`
|
|
2
|
+
`);for(const line of lines){const trimmed=line.trim();if(trimmed.startsWith("#")||!trimmed.includes("="))continue;const[key,...valueParts]=trimmed.split("="),value=valueParts.join("=").replace(/^["']|["']$/g,"");if(key==="AWS_ACCESS_KEY_ID"||key==="AWS_SECRET_ACCESS_KEY"||key==="AWS_REGION"||key==="AWS_ACCOUNT_ID")process.env[key]=value}break}}delete process.env.AWS_PROFILE;try{const{undeployStack}=await import("../../../actions/deploy"),region=process.env.AWS_REGION||"us-east-1";await undeployStack({environment,region,verbose:options.verbose});await outro("Cloud infrastructure removed",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}catch(error){console.log("");console.error("\u2717 Failed to remove cloud infrastructure");const errorStr=String(error.message||error);if(errorStr.includes("security token")||errorStr.includes("credentials")){console.log("");console.error(" AWS credentials are invalid or expired");console.log(" Check your AWS credentials in .env.production:");console.log(" - AWS_ACCESS_KEY_ID");console.log(" - AWS_SECRET_ACCESS_KEY")}else if(errorStr.includes("region")||errorStr.includes("AWS_REGION")){console.log("");console.error(" AWS Region not configured");console.log(" Add AWS_REGION to your .env.production file")}else if(errorStr.includes("AccessDenied")){console.log("");console.error(" Access denied");console.log(" Your AWS credentials may not have permission to delete stacks")}else console.error(` ${errorStr}`);console.log("");console.log("Troubleshooting:");console.log(" ./buddy cloud:cleanup - Clean up resources manually");console.log(" --verbose - Show detailed error information");console.log("");if(options.verbose)console.error("Error details:",error);await outro("Failed to remove infrastructure",{startTime,useSeconds:!0});process.exit(ExitCode.FatalError)}});buddy.command("cloud:optimize-cost",descriptions.optimizeCost).option("--jump-box","Remove the jump-box",{default:!0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:optimize-cost` ...",options);const startTime=await intro("buddy cloud:optimize-cost");if(options.jumpBox){if(!await prompts.confirm("Would you like to remove your jump-box to optimize your costs?")){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}await deleteJumpBox();await outro("Your jump-box was removed & cost optimizations are applied.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}await outro("No cost optimization was applied",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("cloud:cleanup",descriptions.cleanUp).alias("cloud:clean-up").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:cleanup` ...",options);const startTime=await intro("buddy cloud:cleanup");delete process.env.AWS_PROFILE;log.info("Cleaning up your cloud resources will take a while to complete. Please be patient.");await new Promise((resolve)=>setTimeout(resolve,2000));const cleanupSteps=[{label:"jump-boxes",fn:deleteJumpBox,ignoreErrors:["Jump-box not found"]},{label:"retained S3 buckets",fn:deleteStacksBuckets},{label:"retained Lambda functions",fn:deleteStacksFunctions,ignoreErrors:["No stacks functions found"]},{label:"remaining Stacks logs",fn:deleteLogGroups},{label:"stored parameters",fn:deleteParameterStore},{label:"VPCs",fn:deleteVpcs},{label:"Subnets",fn:deleteSubnets},{label:"CDK remnants",fn:deleteCdkRemnants},{label:"IAM users",fn:deleteIamUsers}],errors=[];for(const step of cleanupSteps){log.info(`Removing any ${step.label}...`);try{const result=await step.fn();if(isResultError(result)){const errMsg=getResultError(result);if(!step.ignoreErrors?.includes(errMsg)){log.warn(`${step.label} cleanup issue: ${errMsg}`);errors.push({label:step.label,error:errMsg})}}else{const value=getResultValue(result);if(value)log.info(String(value))}}catch(e){const errMsg=e.message||"AWS SDK error";log.warn(`${step.label} cleanup skipped: ${errMsg}`);errors.push({label:step.label,error:errMsg})}}if(errors.length>0){log.warn(`Cleanup completed with ${errors.length} issue(s):`);for(const{label,error}of errors)log.warn(` - ${label}: ${error}`)}await outro("AWS resources have been removed",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("cloud:invalidate-cache",descriptions.invalidateCache).option("--paths [paths]",descriptions.paths,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:invalidate-cache` ...",options);const startTime=await intro("buddy cloud:invalidate-cache");if(!await prompts.confirm("Would you like to invalidate the CloudFront cache?")){await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}log.info("Invalidating the CloudFront cache...");const distributionId=await getCloudFrontDistributionId();if(!distributionId){await outro("Could not resolve CloudFront distribution ID",{startTime,useSeconds:!0},"Ensure your cloud stack is deployed before invalidating cache.");process.exit(ExitCode.FatalError)}const paths=options.paths?String(options.paths):"/*",result=await runCommand(`aws cloudfront create-invalidation --distribution-id ${distributionId} --paths ${paths}`,{...options,cwd:p.projectPath(),stdin:"pipe"});if(isResultError(result)){await outro("While running the cloud command, there was an issue",{startTime,useSeconds:!0},getResultError(result));process.exit(ExitCode.FatalError)}await outro("Exited",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("cloud:diff",descriptions.diff).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:diff` ...",options);const startTime=await intro("buddy cloud:diff");try{const{InfrastructureGenerator}=await import("@stacksjs/ts-cloud"),{CloudFormationClient}=await import("@stacksjs/ts-cloud/aws"),{tsCloud:cloudConfig}=await import("~/config/cloud"),environment=process.env.APP_ENV||process.env.NODE_ENV||"production",newTemplate=new InfrastructureGenerator({config:cloudConfig,environment}).generate().toJSON(),stackName=`${cloudConfig.project?.slug||"stacks"}-${environment}`,cfn=new CloudFormationClient(process.env.AWS_REGION||"us-east-1");let currentTemplate="{}";try{currentTemplate=(await cfn.getTemplate(stackName)).TemplateBody}catch{log.info("No deployed stack found. Showing full template as diff.")}if(currentTemplate===newTemplate)log.info("No changes detected.");else{log.info("Changes detected between deployed and local template:");log.info(`Current template: ${currentTemplate.length} bytes`);log.info(`New template: ${newTemplate.length} bytes`)}}catch(error){await outro("While running the cloud diff command, there was an issue",{startTime,useSeconds:!0},error.message);process.exit(ExitCode.FatalError)}await outro("Cloud diff complete",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("cloud:dashboard",descriptions.dashboard).alias("cloud:cockpit").option("--host [host]",descriptions.host,{default:"127.0.0.1"}).option("--port [port]",descriptions.port,{default:"7676"}).option("--env [env]",descriptions.env).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:dashboard` ...",options);const startTime=await intro("buddy cloud:dashboard"),tsCloud=await import("@stacksjs/ts-cloud");if(typeof tsCloud.startLocalDashboardServer!=="function"){await outro("The installed @stacksjs/ts-cloud does not provide the local cockpit yet",{startTime,useSeconds:!0},"Update your dependencies (requires @stacksjs/ts-cloud >= 0.5.27).");process.exit(ExitCode.FatalError)}try{const server=await tsCloud.startLocalDashboardServer({host:options.host?String(options.host):void 0,port:options.port?Number(options.port):void 0,environment:options.env,verbose:!!options.verbose});log.success(`Stacks Cloud cockpit running at ${underline(server.url)}`);log.info(italic("Manage servers, sites, SSH keys and deploys. Press Ctrl+C to stop."));await new Promise(()=>{})}catch(error){await outro("While starting the cloud dashboard, there was an issue",{startTime,useSeconds:!0},error?.message??String(error));process.exit(ExitCode.FatalError)}});buddy.command("cloud:sites",descriptions.sites).option("--env [env]",descriptions.sitesEnv).option("--no-remote",descriptions.remote).option("-J, --json",descriptions.json,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:sites` ...",options);const{declaredSites,describeInventory,listProviderServers,probeHostRoutes}=await import("../cloud-inventory"),{loadTsCloudConfig,resolveHetznerApiToken}=await import("./deploy"),environment=String(options.env||process.env.APP_ENV||process.env.NODE_ENV||"production"),tsCloudConfig=await loadTsCloudConfig(options.env?environment:void 0),slug=tsCloudConfig?.project?.slug||"app",provider=tsCloudConfig?.cloud?.provider||process.env.CLOUD_PROVIDER||"aws";if(provider!=="hetzner"){await log.info("The on-box half (the rpx gateway registry) is provider-independent; the server listing is not yet.");await log.exit(`\`buddy cloud:sites\` can only list Hetzner servers, and this project's provider is '${provider}'.`,ExitCode.FatalError)}let helpers={};try{const deployApi=await import("@stacksjs/ts-cloud/deploy");helpers={resolveSiteKind:deployApi.resolveSiteKind,siteInstallBase:deployApi.siteInstallBase}}catch(error){log.debug("Could not load @stacksjs/ts-cloud/deploy for site classification:",error)}const declared=declaredSites(tsCloudConfig?.sites,helpers,slug),listing=await listProviderServers(resolveHetznerApiToken(tsCloudConfig)),probes=[];if(options.remote!==!1){const{sshExec}=await import("@stacksjs/ts-cloud"),batchSize=6;for(let index=0;index<listing.servers.length;index+=batchSize)probes.push(...await Promise.all(listing.servers.slice(index,index+batchSize).map((server)=>probeHostRoutes(server,(host,command)=>sshExec(host,command,{user:"root",connectTimeoutSec:10})))))}const inventory={slug,environment,servers:listing.servers,probes,declared,providerFailure:listing.failure};if(options.json)console.log(JSON.stringify(inventory,null,2));else for(const line of describeInventory(inventory))console.log(line);process.exit(listing.failure&&listing.servers.length===0?ExitCode.FatalError:ExitCode.Success)});buddy.command("cloud:attach",descriptions.attach).option("--server <server>",descriptions.attachServer).option("--env [env]",descriptions.sitesEnv).option("--dry-run",descriptions.dryRun,{default:!1}).option("-J, --json",descriptions.json,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy cloud:attach` ...",options);const{declaredSites,listProviderServers,probeHostRoutes}=await import("../cloud-inventory"),{attachConflicts,attachPreconditions,describeAttachPlan,resolveAttachTarget,setAttachTo}=await import("../cloud-attach"),{loadTsCloudConfig,resolveHetznerApiToken}=await import("./deploy"),refuse=async(...messages)=>{for(const message of messages.slice(0,-1))await log.error(message);return log.exit(messages[messages.length-1],ExitCode.FatalError)};if(!options.server)return await refuse("Which server? Pass --server <name|owner-slug>. `buddy cloud:sites` lists them.");const environment=String(options.env||process.env.APP_ENV||process.env.NODE_ENV||"production"),tsCloudConfig=await loadTsCloudConfig(options.env?environment:void 0),slug=tsCloudConfig?.project?.slug||"app",listing=await listProviderServers(resolveHetznerApiToken(tsCloudConfig));if(listing.failure){const{describeProviderFailure}=await import("../cloud-inventory");return await refuse(describeProviderFailure(listing.failure))}const target=resolveAttachTarget(listing.servers,String(options.server),environment);if("problem"in target)return await refuse(target.problem);const server=target.server,preconditions=attachPreconditions(slug,server);if(preconditions.length>0)return await refuse(...preconditions);let helpers={};try{const deployApi=await import("@stacksjs/ts-cloud/deploy");helpers={resolveSiteKind:deployApi.resolveSiteKind,siteInstallBase:deployApi.siteInstallBase}}catch(error){log.debug("Could not load @stacksjs/ts-cloud/deploy for site classification:",error)}const declared=declaredSites(tsCloudConfig?.sites,helpers,slug),{sshExec}=await import("@stacksjs/ts-cloud"),probe=await probeHostRoutes(server,(host,command)=>sshExec(host,command,{user:"root",connectTimeoutSec:10})),conflicts=probe.unavailable?[]:attachConflicts(slug,declared,probe.routes),blocked=Boolean(probe.unavailable)||conflicts.length>0;let edit;if(!blocked){const configPath=p.projectPath("config/cloud.ts"),{readFileSync,writeFileSync}=await import("node:fs");edit=setAttachTo(readFileSync(configPath,"utf8"),server.project);if(!options.dryRun&&"text"in edit&&edit.changed)writeFileSync(configPath,edit.text)}const plan={slug,owner:server.project,server,declared,conflicts,registryRead:!probe.unavailable,registryProblem:probe.unavailable,edit,dryRun:Boolean(options.dryRun)};if(options.json){const edited=!edit?void 0:("problem"in edit)?edit:{changed:edit.changed};console.log(JSON.stringify({...plan,edit:edited},null,2))}else for(const line of describeAttachPlan(plan))console.log(line);process.exit(blocked?ExitCode.FatalError:ExitCode.Success)});onUnknownSubcommand(buddy,"cloud")}
|
|
@@ -1,30 +1,10 @@
|
|
|
1
1
|
import type { DeploymentPreview, DeploymentSiteKind } from '@stacksjs/types';
|
|
2
|
+
import type { TsCloudConfig } from './deploy';
|
|
2
3
|
export declare function resolveDeploymentEnvironment(options: ResolveDeploymentEnvironmentOptions): string;
|
|
3
4
|
export declare function applyDeploymentDomainOverride<T extends DeploymentPreviewConfig>(config: T, domain?: unknown): T;
|
|
4
5
|
export declare function createDeploymentPreview(options: CreateDeploymentPreviewOptions): DeploymentPreview;
|
|
5
6
|
export declare function formatDeploymentPreview(plan: DeploymentPreview): string;
|
|
6
7
|
export declare const deploymentPreviewJsonPrefix: 'STACKS_DEPLOY_PREVIEW_JSON=';
|
|
7
|
-
declare interface DeploymentPreviewConfig {
|
|
8
|
-
project?: {
|
|
9
|
-
name?: string
|
|
10
|
-
slug?: string
|
|
11
|
-
region?: string
|
|
12
|
-
}
|
|
13
|
-
cloud?: {
|
|
14
|
-
provider?: string
|
|
15
|
-
attachTo?: string
|
|
16
|
-
}
|
|
17
|
-
infrastructure?: {
|
|
18
|
-
compute?: {
|
|
19
|
-
size?: string
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
environments?: Record<string, {
|
|
23
|
-
region?: string
|
|
24
|
-
}>
|
|
25
|
-
mode?: string
|
|
26
|
-
sites?: Record<string, Record<string, unknown> | null | undefined>
|
|
27
|
-
}
|
|
28
8
|
export declare interface CreateDeploymentPreviewOptions {
|
|
29
9
|
config?: DeploymentPreviewConfig
|
|
30
10
|
environment: string
|
|
@@ -50,3 +30,12 @@ export declare interface ResolveDeploymentEnvironmentOptions {
|
|
|
50
30
|
staging?: boolean
|
|
51
31
|
development?: boolean
|
|
52
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The ts-cloud config this command previews.
|
|
35
|
+
*
|
|
36
|
+
* An alias for the one description in `deploy.ts` rather than a second partial
|
|
37
|
+
* copy of it: this file used to declare its own, listing `project.name` and
|
|
38
|
+
* `mode` where the other listed `hetzner` and `sites[].port`, so the two
|
|
39
|
+
* disagreed about the same file and neither was wrong enough to notice.
|
|
40
|
+
*/
|
|
41
|
+
declare type DeploymentPreviewConfig = TsCloudConfig;
|
|
@@ -7,7 +7,7 @@ export declare function runDeployRollback(site: string | undefined, options: Dep
|
|
|
7
7
|
* Returns undefined if the project has no ts-cloud config (older projects /
|
|
8
8
|
* pure AWS setups that only export the legacy `CloudConfig`).
|
|
9
9
|
*/
|
|
10
|
-
export declare function loadTsCloudConfig(envName?: string): Promise<
|
|
10
|
+
export declare function loadTsCloudConfig(envName?: string): Promise<TsCloudConfig | undefined>;
|
|
11
11
|
/**
|
|
12
12
|
* Why the last attempt failed, as one line fit for an error message.
|
|
13
13
|
*
|
|
@@ -357,7 +357,7 @@ export declare function applyPreMigrationBackup(sites: Record<string, any>, back
|
|
|
357
357
|
* without duplicating site blocks. Only `//<host>` URL occurrences are rewritten;
|
|
358
358
|
* bare `user@host` (e.g. mail identities) is left alone. Production is untouched.
|
|
359
359
|
*/
|
|
360
|
-
export declare function applyEnvironmentToSites(sites: Record<string,
|
|
360
|
+
export declare function applyEnvironmentToSites(sites: Record<string, TsCloudSite | null | undefined>, environment: string, config: TsCloudConfig): Record<string, TsCloudSite | null | undefined>;
|
|
361
361
|
/**
|
|
362
362
|
* Resolve the app's direct ts-cloud dependency before Buddy's own dependency.
|
|
363
363
|
* Package managers may retain a stale nested copy below Buddy even after the
|
|
@@ -386,15 +386,15 @@ export declare function loadTsCloudDeployApi(): Promise<typeof import('@stacksjs
|
|
|
386
386
|
* gateway's route table (which proxies to 127.0.0.1:port on-box), so its
|
|
387
387
|
* port declaration is left alone.
|
|
388
388
|
*/
|
|
389
|
-
export declare function shouldInjectManagementDashboard(tsCloudConfig:
|
|
390
|
-
export declare function reconcilePartialDeployManagementDashboards(tsCloudConfig:
|
|
389
|
+
export declare function shouldInjectManagementDashboard(tsCloudConfig: TsCloudConfig): boolean;
|
|
390
|
+
export declare function reconcilePartialDeployManagementDashboards(tsCloudConfig: TsCloudConfig, livePorts: Record<string, number>): { preserved: string[], removed: string[] };
|
|
391
391
|
/**
|
|
392
392
|
* Read a tenant's existing shared-server pin without requiring provider API
|
|
393
393
|
* credentials. The pin contains only compute metadata and is sufficient for
|
|
394
394
|
* an SSH release to a box that the owner project already provisioned.
|
|
395
395
|
*/
|
|
396
396
|
export declare function resolvePersistedAttachTargetBox(tsCloudConfig: any, environment: string, cwd?: unknown): AttachedComputeBox | null;
|
|
397
|
-
export declare function scrubLoopbackSitePortsForFirewall(tsCloudConfig:
|
|
397
|
+
export declare function scrubLoopbackSitePortsForFirewall(tsCloudConfig: TsCloudConfig): TsCloudConfig;
|
|
398
398
|
/**
|
|
399
399
|
* Mail tenancy is an explicit deployment capability. The merged Stacks config
|
|
400
400
|
* always contains framework email defaults, so checking `emailConfig` alone
|
|
@@ -402,7 +402,7 @@ export declare function scrubLoopbackSitePortsForFirewall(tsCloudConfig: any): a
|
|
|
402
402
|
* does not provide `config/email.ts`.
|
|
403
403
|
*/
|
|
404
404
|
export declare function hasExplicitEmailConfig(projectRoot?: unknown): boolean;
|
|
405
|
-
export declare function mailServerOwnerFromConfig(config:
|
|
405
|
+
export declare function mailServerOwnerFromConfig(config: { server?: { attachTo?: unknown } } | null | undefined): string | undefined;
|
|
406
406
|
/**
|
|
407
407
|
* The Hetzner token, resolved the same way everywhere.
|
|
408
408
|
*
|
|
@@ -413,7 +413,7 @@ export declare function mailServerOwnerFromConfig(config: any): string | undefin
|
|
|
413
413
|
* and then failed attach resolution with no request made and no reason given
|
|
414
414
|
* (stacksjs/stacks#2344). One resolver, so the two cannot drift again.
|
|
415
415
|
*/
|
|
416
|
-
export declare function resolveHetznerApiToken(tsCloudConfig?:
|
|
416
|
+
export declare function resolveHetznerApiToken(tsCloudConfig?: TsCloudConfig): string | undefined;
|
|
417
417
|
/**
|
|
418
418
|
* What to tell the operator when no box came back.
|
|
419
419
|
*
|
|
@@ -452,7 +452,11 @@ export declare function provisionMailTenant(ip: string, logger: typeof log): Pro
|
|
|
452
452
|
* is about who actually administers the zone — it is not a preference to be
|
|
453
453
|
* weighed against whatever credentials happen to be in the environment.
|
|
454
454
|
*/
|
|
455
|
-
|
|
455
|
+
// Accepts an absent config, which is what both callers pass: they load it with
|
|
456
|
+
// `.catch(() => undefined)` because a project without one still needs DNS
|
|
457
|
+
// guidance. The body always read it with `config?.`; only the signature had
|
|
458
|
+
// never said so, and `any` is what let the two disagree.
|
|
459
|
+
export declare function declaredDnsProvider(config: TsCloudConfig | null | undefined): string | undefined;
|
|
456
460
|
/**
|
|
457
461
|
* Build the DNS provider credentials to try, in priority order.
|
|
458
462
|
*
|
|
@@ -469,12 +473,12 @@ export declare function declaredDnsProvider(config: any): string | undefined;
|
|
|
469
473
|
* falling through to a different registrar. Writing DNS into the wrong zone
|
|
470
474
|
* is not a lesser failure than writing none.
|
|
471
475
|
*/
|
|
472
|
-
export declare function dnsProviderConfigsFromEnv(declared?: string):
|
|
476
|
+
export declare function dnsProviderConfigsFromEnv(declared?: string): DnsProviderConfig[];
|
|
473
477
|
/**
|
|
474
478
|
* Why a declared provider produced no usable credentials, phrased as the fix.
|
|
475
479
|
* Returns undefined when nothing is wrong.
|
|
476
480
|
*/
|
|
477
|
-
export declare function declaredDnsProviderProblem(declared: string | undefined, configs:
|
|
481
|
+
export declare function declaredDnsProviderProblem(declared: string | undefined, configs: readonly DnsProviderConfig[]): string | undefined;
|
|
478
482
|
/**
|
|
479
483
|
* The DMARC policy to publish, from `email.server.dmarc.policy`.
|
|
480
484
|
*
|
|
@@ -615,6 +619,46 @@ export declare interface DeployRollbackOptions {
|
|
|
615
619
|
dryRun?: boolean
|
|
616
620
|
verbose?: boolean
|
|
617
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* The project's ts-cloud config, as this command reads it.
|
|
624
|
+
*
|
|
625
|
+
* Written out rather than left as `any`, with every field optional because the
|
|
626
|
+
* file is user-authored and each read below already guards for absence. The
|
|
627
|
+
* index signature keeps any other key a project carries legal.
|
|
628
|
+
*
|
|
629
|
+
* The point is not to constrain what a config may contain. It is that
|
|
630
|
+
* `config.infrastructure.dsn` - a typo for `dns` - used to typecheck and answer
|
|
631
|
+
* `undefined`, which is indistinguishable from a provider that was never
|
|
632
|
+
* declared, and this command chooses a DNS provider off exactly that read.
|
|
633
|
+
*/
|
|
634
|
+
export declare interface TsCloudSite {
|
|
635
|
+
port?: number
|
|
636
|
+
domain?: string | string[]
|
|
637
|
+
env?: Record<string, string>
|
|
638
|
+
[key: string]: unknown
|
|
639
|
+
}
|
|
640
|
+
export declare interface TsCloudInfrastructure {
|
|
641
|
+
compute?: {
|
|
642
|
+
runtime?: unknown
|
|
643
|
+
size?: unknown
|
|
644
|
+
proxy?: {
|
|
645
|
+
autoWww?: unknown
|
|
646
|
+
cdn?: { provider?: string }
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
dns?: { provider?: string, hostedZoneId?: string }
|
|
650
|
+
}
|
|
651
|
+
export declare interface TsCloudConfig {
|
|
652
|
+
project?: { name?: string, slug?: string, region?: string }
|
|
653
|
+
cloud?: { attachTo?: string, retiredDomains?: unknown, provider?: string }
|
|
654
|
+
hetzner?: { apiToken?: string, location?: string }
|
|
655
|
+
infrastructure?: TsCloudInfrastructure
|
|
656
|
+
sites?: Record<string, TsCloudSite | null | undefined>
|
|
657
|
+
environments?: Record<string, { domainPrefix?: string, region?: string } | undefined>
|
|
658
|
+
mode?: string
|
|
659
|
+
tsCloud?: { infrastructure?: TsCloudInfrastructure }
|
|
660
|
+
[key: string]: unknown
|
|
661
|
+
}
|
|
618
662
|
declare interface AttachedComputeBox {
|
|
619
663
|
serverId: number
|
|
620
664
|
serverName: string
|
|
@@ -647,6 +691,26 @@ export declare interface AttachLookupResult {
|
|
|
647
691
|
box: AttachTargetBox | null
|
|
648
692
|
failure?: AttachLookupFailure
|
|
649
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Candidate DNS provider configs, built from whatever credentials the
|
|
696
|
+
* environment carries. Shared by every DNS path in a deploy so they agree on
|
|
697
|
+
* which registrars are usable — mail DNS used to read `PORKBUN_API_KEY`
|
|
698
|
+
* directly and was therefore the one path that could not publish to a Route53,
|
|
699
|
+
* Cloudflare or GoDaddy zone.
|
|
700
|
+
*/
|
|
701
|
+
/**
|
|
702
|
+
* One registrar's credentials, as this deploy builds them from the
|
|
703
|
+
* environment. The credential fields differ per provider, so each is optional
|
|
704
|
+
* and `provider` is what says which of them to expect.
|
|
705
|
+
*/
|
|
706
|
+
export declare interface DnsProviderConfig {
|
|
707
|
+
provider: 'porkbun' | 'cloudflare' | 'godaddy' | 'route53'
|
|
708
|
+
apiKey?: string
|
|
709
|
+
secretKey?: string
|
|
710
|
+
apiToken?: string
|
|
711
|
+
apiSecret?: string
|
|
712
|
+
environment?: string
|
|
713
|
+
}
|
|
650
714
|
/** A name+type this deploy expects to end up holding exactly one record. */
|
|
651
715
|
export declare interface MailDnsExpectation {
|
|
652
716
|
label: string
|