@stacksjs/buddy 0.70.263 → 0.70.265

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/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
- import process from"node:process";import{cli,log}from"@stacksjs/cli";import{path as p}from"@stacksjs/path";import{registerGlobalOptions}from"./global-options";import{shouldSkipAppKeyCheck}from"./project-setup";try{const{isSupportedBunVersion,minimumBunVersion}=await import("@stacksjs/utils"),currentBunVersion=typeof Bun<"u"?Bun.version:process.versions.bun;if(currentBunVersion&&!isSupportedBunVersion(currentBunVersion)){console.error(`[buddy] Bun v${minimumBunVersion} or later is required (current: v${currentBunVersion}). Run: bun upgrade`);process.exit(1)}}catch{}const args=process.argv.slice(2),requestedCommand=args[0]||"help",isHelpFlag=args.includes("--help")||args.includes("-h"),isVersionOnly=["--version","-V","version"].includes(requestedCommand),isHelpMode=requestedCommand==="help"||isHelpFlag&&args.length<=2,skipAppKeyCheck=shouldSkipAppKeyCheck(requestedCommand,{isHelpFlag,isHelpMode}),needsFullSetup=!isVersionOnly;if(needsFullSetup){const reportFatal=(label,error)=>{log.debug(`Buddy ${label}`);try{process.stderr.write(`
2
+ import process from"node:process";import{cli,log}from"@stacksjs/cli";import{path as p}from"@stacksjs/path";import{registerGlobalOptions}from"./global-options";import{shouldSkipAppKeyCheck}from"./project-setup";import{resultFailed}from"./result";try{const{isSupportedBunVersion,minimumBunVersion}=await import("@stacksjs/utils"),currentBunVersion=typeof Bun<"u"?Bun.version:process.versions.bun;if(currentBunVersion&&!isSupportedBunVersion(currentBunVersion)){console.error(`[buddy] Bun v${minimumBunVersion} or later is required (current: v${currentBunVersion}). Run: bun upgrade`);process.exit(1)}}catch{}const args=process.argv.slice(2),requestedCommand=args[0]||"help",isHelpFlag=args.includes("--help")||args.includes("-h"),isVersionOnly=["--version","-V","version"].includes(requestedCommand),isHelpMode=requestedCommand==="help"||isHelpFlag&&args.length<=2,skipAppKeyCheck=shouldSkipAppKeyCheck(requestedCommand,{isHelpFlag,isHelpMode}),needsFullSetup=!isVersionOnly;if(needsFullSetup){const reportFatal=(label,error)=>{log.debug(`Buddy ${label}`);try{process.stderr.write(`
3
3
  [buddy] ${label}: ${error?.stack??String(error)}
4
- `)}catch{}log.error(error);return process.exit(1)};process.on("uncaughtException",(error)=>reportFatal("uncaughtException",error));process.on("unhandledRejection",(error)=>reportFatal("unhandledRejection",error))}async function main(){const buddy=cli("buddy");registerGlobalOptions(buddy,{version:requestedCommand!=="upgrade"&&requestedCommand!=="update"});const configPath="./buddy.config.js";try{await Bun.file(configPath).text();const{applyBuddyConfig}=await import("./config.js");await applyBuddyConfig(buddy)}catch{}if(needsFullSetup){p.ensureRuntimeDirectories();const{loadCommands,getCommandsToLoad,markLoaded}=await import("./lazy-commands.js"),{setup}=await import("./commands/setup.js");setup(buddy);markLoaded(buddy,"setup");if(!skipAppKeyCheck){const{runAction}=await import("@stacksjs/actions"),{Action}=await import("@stacksjs/enums"),{ensureProjectIsInitialized}=await import("@stacksjs/utils");if(!await ensureProjectIsInitialized()){log.info("Your `APP_KEY` is not yet set");log.info("Generating application key...");const result=await runAction(Action.KeyGenerate);if(result.isErr){log.error("Failed to set random application key.",result.error);process.exit(1)}}}const commandsToLoad=getCommandsToLoad(args);await loadCommands(commandsToLoad,buddy);await dynamicImports(buddy)}else{const{loadCommand}=await import("./lazy-commands.js");await loadCommand("version",buddy)}buddy.help();if(args.length===0&&process.stdin.isTTY&&!buddy.isNoInteraction)await showInteractiveMenu(buddy);else await parseOrExit(buddy)}async function showInteractiveMenu(buddy){const{bold,green,intro}=await import("@stacksjs/cli"),{select}=await import("@stacksjs/cli");await intro("buddy");console.log(bold(green("What would you like to do?")));console.log("");const choice=await select({message:"Select a command:",choices:[{value:"dev",label:"Start development server"},{value:"build",label:"Build for production"},{value:"test",label:"Run tests"},{value:"list",label:"List all commands"},{value:"doctor",label:"Run health checks"},{value:"about",label:"Show system information"},{value:"help",label:"Show help"},{value:"exit",label:"Exit"}],initial:0});console.log("");if(choice==="exit")process.exit(0);else if(choice==="help")buddy.outputHelp();else{process.argv=["bun","buddy",choice];await parseOrExit(buddy)}}async function parseOrExit(buddy){try{await buddy.parse()}catch(error){if(!(error?.name==="ClappError"||error?.isUsageError===!0))throw error;process.stderr.write(`${error.message}
4
+ `)}catch{}log.error(error);return process.exit(1)};process.on("uncaughtException",(error)=>reportFatal("uncaughtException",error));process.on("unhandledRejection",(error)=>reportFatal("unhandledRejection",error))}async function main(){const buddy=cli("buddy");registerGlobalOptions(buddy,{version:requestedCommand!=="upgrade"&&requestedCommand!=="update"});const configPath="./buddy.config.js";try{await Bun.file(configPath).text();const{applyBuddyConfig}=await import("./config.js");await applyBuddyConfig(buddy)}catch{}if(needsFullSetup){p.ensureRuntimeDirectories();const{loadCommands,getCommandsToLoad,markLoaded}=await import("./lazy-commands.js"),{setup}=await import("./commands/setup.js");setup(buddy);markLoaded(buddy,"setup");if(!skipAppKeyCheck){const{runAction}=await import("@stacksjs/actions"),{Action}=await import("@stacksjs/enums"),{ensureProjectIsInitialized}=await import("@stacksjs/utils");if(!await ensureProjectIsInitialized()){log.info("Your `APP_KEY` is not yet set");log.info("Generating application key...");const result=await runAction(Action.KeyGenerate);if(resultFailed(result)){log.error("Failed to set random application key.",result.error);process.exit(1)}}}const commandsToLoad=getCommandsToLoad(args);await loadCommands(commandsToLoad,buddy);await dynamicImports(buddy)}else{const{loadCommand}=await import("./lazy-commands.js");await loadCommand("version",buddy)}buddy.help();if(args.length===0&&process.stdin.isTTY&&!buddy.isNoInteraction)await showInteractiveMenu(buddy);else await parseOrExit(buddy)}async function showInteractiveMenu(buddy){const{bold,green,intro}=await import("@stacksjs/cli"),{select}=await import("@stacksjs/cli");await intro("buddy");console.log(bold(green("What would you like to do?")));console.log("");const choice=await select({message:"Select a command:",choices:[{value:"dev",label:"Start development server"},{value:"build",label:"Build for production"},{value:"test",label:"Run tests"},{value:"list",label:"List all commands"},{value:"doctor",label:"Run health checks"},{value:"about",label:"Show system information"},{value:"help",label:"Show help"},{value:"exit",label:"Exit"}],initial:0});console.log("");if(choice==="exit")process.exit(0);else if(choice==="help")buddy.outputHelp();else{process.argv=["bun","buddy",choice];await parseOrExit(buddy)}}async function parseOrExit(buddy){try{await buddy.parse()}catch(error){if(!(error?.name==="ClappError"||error?.isUsageError===!0))throw error;process.stderr.write(`${error.message}
5
5
  `);if(error.usage)process.stderr.write(`${error.usage}
6
6
  `);process.exit(typeof error.exitCode==="number"?error.exitCode:2)}}await main();export function applyAliases(buddy,signature,aliases){const name=signature.trim().split(/\s+/)[0],command=buddy.commands?.find((c)=>c.name===name);if(!command||typeof command.alias!=="function"){log.debug(`Could not alias '${name}': no matching command was registered.`);return}for(const alias of aliases)command.alias(alias)}async function dynamicImports(buddy){const{fs}=await import("@stacksjs/storage"),commandsDir=p.appPath("Commands");try{const registry=(await import(p.appPath("Commands.ts"))).default||{};for(const[signature,config]of Object.entries(registry)){const commandConfig=typeof config==="string"?{file:config,enabled:!0}:config;if(commandConfig.enabled===!1)continue;const commandPath=`${commandsDir}/${commandConfig.file}.ts`;if(!fs.existsSync(commandPath)){log.debug(`Command file not found: ${commandPath} (registered as '${signature}')`);continue}try{const dynamicImport=await import(commandPath);if(typeof dynamicImport.default==="function"){dynamicImport.default(buddy);if(commandConfig.aliases?.length)applyAliases(buddy,signature,commandConfig.aliases)}else log.error(`Expected a default export function in ${commandConfig.file}.ts, but got:`,dynamicImport.default)}catch(error){log.error(`Failed to load command ${commandConfig.file}:`,error)}}}catch{if(!fs.existsSync(commandsDir)){log.debug("app/Commands directory not found, skipping user commands");return}log.debug("Commands.ts not found, using auto-discovery");const commandFiles=fs.readdirSync(commandsDir).filter((file)=>file.endsWith(".ts"));for(const file of commandFiles){const commandPath=`${commandsDir}/${file}`;try{const dynamicImport=await import(commandPath);if(typeof dynamicImport.default==="function")dynamicImport.default(buddy);else log.debug(`Skipping ${file} \u2014 no default export function`)}catch(error){log.error(`Failed to load command ${file}:`,error)}}}try{const listenerImport=await import(p.listenersPath("Console.ts"));if(typeof listenerImport.default==="function")listenerImport.default(buddy)}catch{}}
@@ -1 +1 @@
1
- import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function auth(buddy){const descriptions={setup:"Set up authentication (migrations + personal access client)",token:"Create a personal access client token",client:"Create a new OAuth client",prune:"Prune expired and revoked tokens",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("auth:setup",descriptions.setup).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy auth:setup` ...",options);const perf=await intro("buddy auth:setup"),result=await runAction(Action.AuthSetup,options);if(result.isErr){await outro("While setting up authentication, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Authentication setup completed successfully.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("auth:token",descriptions.token).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy auth:token` ...",options);const perf=await intro("buddy auth:token"),result=await runAction(Action.CreatePersonalAccessClient,options);if(result.isErr){await outro("While creating the personal access client token, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Personal access client token created successfully.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("auth:client",descriptions.client).option("-n, --name [name]","Client name",{default:"OAuth Client"}).option("-r, --redirect [redirect]","Redirect URI",{default:"http://localhost"}).option("--personal","Create a personal access client",{default:!1}).option("--password","Create a password grant client",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy auth:client` ...",options);const perf=await intro("buddy auth:client"),result=await runAction(Action.AuthClient,options);if(result.isErr){await outro("While creating the OAuth client, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("OAuth client created successfully.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("auth:prune",descriptions.prune).option("--expired","Prune expired tokens (default: true)",{default:!0}).option("--revoked","Prune revoked tokens (default: true)",{default:!0}).option("-d, --days [days]","Prune revoked tokens older than N days",{default:7}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy auth:prune` ...",options);const perf=await intro("buddy auth:prune"),result=await runAction(Action.AuthPrune,options);if(result.isErr){await outro("While pruning tokens, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Token pruning completed successfully.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"auth")}
1
+ import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function auth(buddy){const descriptions={setup:"Set up authentication (migrations + personal access client)",token:"Create a personal access client token",client:"Create a new OAuth client",prune:"Prune expired and revoked tokens",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("auth:setup",descriptions.setup).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy auth:setup` ...",options);const perf=await intro("buddy auth:setup"),result=await runAction(Action.AuthSetup,options);if(resultFailed(result)){await outro("While setting up authentication, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Authentication setup completed successfully.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("auth:token",descriptions.token).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy auth:token` ...",options);const perf=await intro("buddy auth:token"),result=await runAction(Action.CreatePersonalAccessClient,options);if(resultFailed(result)){await outro("While creating the personal access client token, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Personal access client token created successfully.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("auth:client",descriptions.client).option("-n, --name [name]","Client name",{default:"OAuth Client"}).option("-r, --redirect [redirect]","Redirect URI",{default:"http://localhost"}).option("--personal","Create a personal access client",{default:!1}).option("--password","Create a password grant client",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy auth:client` ...",options);const perf=await intro("buddy auth:client"),result=await runAction(Action.AuthClient,options);if(resultFailed(result)){await outro("While creating the OAuth client, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("OAuth client created successfully.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("auth:prune",descriptions.prune).option("--expired","Prune expired tokens (default: true)",{default:!0}).option("--revoked","Prune revoked tokens (default: true)",{default:!0}).option("-d, --days [days]","Prune revoked tokens older than N days",{default:7}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy auth:prune` ...",options);const perf=await intro("buddy auth:prune"),result=await runAction(Action.AuthPrune,options);if(resultFailed(result)){await outro("While pruning tokens, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Token pruning completed successfully.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"auth")}
@@ -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";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",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("-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(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:"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("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")&&succeeded;if(options.components)succeeded=await runBuildAction(Action.BuildComponentLibs,"component libraries")&&succeeded;if(options.webComponents)succeeded=await runBuildAction(Action.BuildWebComponentLib,"web component library")&&succeeded;if(options.functions)succeeded=await runBuildAction(Action.BuildFunctionLib,"function library")&&succeeded;if(options.desktop)succeeded=await runBuildAction(Action.BuildDesktop,"desktop application")&&succeeded;if(options.views)succeeded=await runBuildAction(Action.BuildViews,"frontend")&&succeeded;if(options.stacks)succeeded=await runBuildAction(Action.BuildStacks,"Stacks framework")&&succeeded;if(options.buddy)succeeded=await runBuildAction(Action.BuildCli,"Buddy CLI")&&succeeded;if(options.server)succeeded=await runBuildAction(Action.BuildServer,"server")&&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(result.isErr){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(result.isErr){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: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(result.isErr){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(result.isErr){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.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"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){const result=await runAction(action);if(result.isErr){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",desktop:"Build the Desktop 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("-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(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:"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("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")&&succeeded;if(options.components)succeeded=await runBuildAction(Action.BuildComponentLibs,"component libraries")&&succeeded;if(options.webComponents)succeeded=await runBuildAction(Action.BuildWebComponentLib,"web component library")&&succeeded;if(options.functions)succeeded=await runBuildAction(Action.BuildFunctionLib,"function library")&&succeeded;if(options.desktop)succeeded=await runBuildAction(Action.BuildDesktop,"desktop application")&&succeeded;if(options.views)succeeded=await runBuildAction(Action.BuildViews,"frontend")&&succeeded;if(options.stacks)succeeded=await runBuildAction(Action.BuildStacks,"Stacks framework")&&succeeded;if(options.buddy)succeeded=await runBuildAction(Action.BuildCli,"Buddy CLI")&&succeeded;if(options.server)succeeded=await runBuildAction(Action.BuildServer,"server")&&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: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.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"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){const result=await runAction(action);if(resultFailed(result)){log.error(`Failed to build ${target}.`,result.error);return!1}return!0}
@@ -1 +1 @@
1
- import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function changelog(buddy){const descriptions={changelog:"Create a CHANGELOG.md file",quiet:"Minimal output",dryRun:"Do not write the file, just output the changes",from:"Start git revision for generated changelog",project:"Target a specific project",to:"End git revision for generated changelog",verbose:"Enable verbose output",version:"Version heading to use for the generated changelog entry"};buddy.command("changelog",descriptions.changelog).option("-q, --quiet",descriptions.quiet,{default:!1}).option("-d, --dry-run",descriptions.dryRun,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--from <revision>",descriptions.from).option("--to <revision>",descriptions.to).option("--version <version>",descriptions.version).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy changelog` ...",options);const perf=await intro("buddy changelog"),result=await runAction(Action.Changelog,options);if(result.isErr){await outro("While running the changelog command, there was an issue",{...options,startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Generated CHANGELOG.md",{...options,startTime:perf,useSeconds:!0,type:"success"})});onUnknownSubcommand(buddy,"changelog")}
1
+ import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function changelog(buddy){const descriptions={changelog:"Create a CHANGELOG.md file",quiet:"Minimal output",dryRun:"Do not write the file, just output the changes",from:"Start git revision for generated changelog",project:"Target a specific project",to:"End git revision for generated changelog",verbose:"Enable verbose output",version:"Version heading to use for the generated changelog entry"};buddy.command("changelog",descriptions.changelog).option("-q, --quiet",descriptions.quiet,{default:!1}).option("-d, --dry-run",descriptions.dryRun,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--from <revision>",descriptions.from).option("--to <revision>",descriptions.to).option("--version <version>",descriptions.version).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy changelog` ...",options);const perf=await intro("buddy changelog"),result=await runAction(Action.Changelog,options);if(resultFailed(result)){await outro("While running the changelog command, there was an issue",{...options,startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Generated CHANGELOG.md",{...options,startTime:perf,useSeconds:!0,type:"success"})});onUnknownSubcommand(buddy,"changelog")}
@@ -1 +1 @@
1
- import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{hasTTY,isCI}from"@stacksjs/env";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function clean(buddy){const descriptions={clean:"Removes all node_modules & lock files",project:"Target a specific project",force:"Skip the confirmation prompt (required in CI/non-interactive shells)",verbose:"Enable verbose output"};buddy.command("clean",descriptions.clean).option("-p, --project [project]",descriptions.project,{default:!1}).option("-f, --force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy clean` ...",options);const skipConfirm=options.force===!0||Boolean(buddy.isForce)||Boolean(buddy.isNoInteraction);if(!skipConfirm&&(isCI||!hasTTY||!process.stdin.isTTY)){log.syncError("Refusing to run `buddy clean` from a non-interactive shell without confirmation.");log.fatal(" \u27A1\uFE0F Re-run with `--force` to proceed: `buddy clean --force`")}if(!skipConfirm){const{confirm}=await import("@stacksjs/cli");if(!await confirm({message:"This will remove all node_modules and lock files. Continue?",initial:!1})){log.info("Clean cancelled");process.exit(ExitCode.Success)}}const perf=await intro("buddy clean"),result=await runAction(Action.Clean,options);if(result.isErr){await outro("While running the clean command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Cleaned up",{startTime:perf,useSeconds:!0,message:"Cleaned up"});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"clean")}
1
+ import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{hasTTY,isCI}from"@stacksjs/env";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function clean(buddy){const descriptions={clean:"Removes all node_modules & lock files",project:"Target a specific project",force:"Skip the confirmation prompt (required in CI/non-interactive shells)",verbose:"Enable verbose output"};buddy.command("clean",descriptions.clean).option("-p, --project [project]",descriptions.project,{default:!1}).option("-f, --force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy clean` ...",options);const skipConfirm=options.force===!0||Boolean(buddy.isForce)||Boolean(buddy.isNoInteraction);if(!skipConfirm&&(isCI||!hasTTY||!process.stdin.isTTY)){log.syncError("Refusing to run `buddy clean` from a non-interactive shell without confirmation.");log.fatal(" \u27A1\uFE0F Re-run with `--force` to proceed: `buddy clean --force`")}if(!skipConfirm){const{confirm}=await import("@stacksjs/cli");if(!await confirm({message:"This will remove all node_modules and lock files. Continue?",initial:!1})){log.info("Clean cancelled");process.exit(ExitCode.Success)}}const perf=await intro("buddy clean"),result=await runAction(Action.Clean,options);if(resultFailed(result)){await outro("While running the clean command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Cleaned up",{startTime:perf,useSeconds:!0,message:"Cleaned up"});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"clean")}
@@ -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";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){if(!result||typeof result!=="object")return!1;const r=result;if(typeof r.isErr==="function")return r.isErr();return!!r.isErr}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)",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(`
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)",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
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)}});onUnknownSubcommand(buddy,"cloud")}
@@ -1,5 +1,5 @@
1
- import process from"node:process";import{log,onUnknownSubcommand,outro,runCommand}from"@stacksjs/cli";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";export function configure(buddy){const descriptions={configure:"Configure options",aws:"Configure the AWS connection",project:"Target a specific project",profile:"The AWS profile to use",verbose:"Enable verbose output"};buddy.command("configure",descriptions.configure).option("--aws",descriptions.aws,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy configure` ...",options);if(options?.aws){await configureAws(options);process.exit(ExitCode.Success)}log.info("Not implemented yet. Please use the --aws flag to configure AWS.");process.exit(ExitCode.Success)});buddy.command("configure:aws",descriptions.aws).option("-p, --project [project]",descriptions.project,{default:!1}).option("--profile",descriptions.profile,{default:process.env.AWS_PROFILE}).option("--verbose",descriptions.verbose,{default:!1}).option("--access-key-id","The AWS access key").option("--secret-access-key","The AWS secret access key").option("--region","The AWS region").option("--output","The AWS output format").option("--quiet","Suppress output").action(async(options)=>{log.debug("Running `buddy configure:aws` ...",options);await configureAws(options);process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"configure")}async function configureAws(options){const startTime=performance.now(),awsAccessKeyId=options?.accessKeyId??process.env.AWS_ACCESS_KEY_ID,awsSecretAccessKey=options?.secretAccessKey??process.env.AWS_SECRET_ACCESS_KEY,defaultRegion="us-east-1",defaultOutputFormat=options?.output??"json",profile=process.env.AWS_PROFILE??options?.profile,command=profile?`aws configure --profile ${profile}`:"aws configure",input=`${awsAccessKeyId}
1
+ import process from"node:process";import{log,onUnknownSubcommand,outro,runCommand}from"@stacksjs/cli";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function configure(buddy){const descriptions={configure:"Configure options",aws:"Configure the AWS connection",project:"Target a specific project",profile:"The AWS profile to use",verbose:"Enable verbose output"};buddy.command("configure",descriptions.configure).option("--aws",descriptions.aws,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy configure` ...",options);if(options?.aws){await configureAws(options);process.exit(ExitCode.Success)}log.info("Not implemented yet. Please use the --aws flag to configure AWS.");process.exit(ExitCode.Success)});buddy.command("configure:aws",descriptions.aws).option("-p, --project [project]",descriptions.project,{default:!1}).option("--profile",descriptions.profile,{default:process.env.AWS_PROFILE}).option("--verbose",descriptions.verbose,{default:!1}).option("--access-key-id","The AWS access key").option("--secret-access-key","The AWS secret access key").option("--region","The AWS region").option("--output","The AWS output format").option("--quiet","Suppress output").action(async(options)=>{log.debug("Running `buddy configure:aws` ...",options);await configureAws(options);process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"configure")}async function configureAws(options){const startTime=performance.now(),awsAccessKeyId=options?.accessKeyId??process.env.AWS_ACCESS_KEY_ID,awsSecretAccessKey=options?.secretAccessKey??process.env.AWS_SECRET_ACCESS_KEY,defaultRegion="us-east-1",defaultOutputFormat=options?.output??"json",profile=process.env.AWS_PROFILE??options?.profile,command=profile?`aws configure --profile ${profile}`:"aws configure",input=`${awsAccessKeyId}
2
2
  ${awsSecretAccessKey}
3
3
  ${defaultRegion}
4
4
  ${defaultOutputFormat}
5
- `,result=await runCommand(command,{cwd:p.projectPath(),stdin:"pipe",input});if(result.isErr){await outro("While running the cloud command, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}if(options?.quiet)return;await outro("Exited",{startTime,useSeconds:!0})}
5
+ `,result=await runCommand(command,{cwd:p.projectPath(),stdin:"pipe",input});if(resultFailed(result)){await outro("While running the cloud command, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}if(options?.quiet)return;await outro("Exited",{startTime,useSeconds:!0})}
@@ -1 +1 @@
1
- import{chmodSync,existsSync,readdirSync}from"node:fs";import process from"node:process";import{runAction}from"@stacksjs/actions";import{bold,cyan,dim,intro,log,onUnknownSubcommand,runCommand}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{resolve}from"@stacksjs/path";import{isFolder}from"@stacksjs/storage";import{ExitCode}from"@stacksjs/types";import{uninstallAllFeatures}from"./features";import{ensurePantryDependencies,ensurePantryInstalled}from"./setup";export function create(buddy){const descriptions={name:"The name of the project",command:"Create a new Stacks project",ui:"Are you building a UI?",components:"Are you building UI components?",webComponents:"Automagically built optimized custom elements/web components?",views:"How about views?",functions:"Are you developing functions/composables?",api:"Are you building an API?",database:"Do you need a database?",notifications:"Do you need notifications? e.g. email, SMS, push or chat notifications",cache:"Do you need caching?",email:"Do you need email?",project:"Target a specific project",minimal:"Skip optional feature bundles (cms, commerce, dashboard, marketing, monitoring, realtime, queue) \u2014 bare-bones API/SPA starter that can re-add them later via `./buddy <feature>:install`.",withCore:"Keep the framework vendored in `storage/framework/core` as a Bun workspace, for working ON Stacks. Apps that only work WITH Stacks want the default, which resolves every @stacksjs/* package from npm.",verbose:"Enable verbose output"};buddy.command("new [name]",descriptions.command).alias("create [name]").option("-n, --name [name]",descriptions.name,{default:!1}).option("-u, --ui",descriptions.ui,{default:!0}).option("-c, --components",descriptions.components,{default:!0}).option("-w, --web-components",descriptions.webComponents,{default:!0}).option("-p, --views",descriptions.views,{default:!0}).option("-f, --functions",descriptions.functions,{default:!0}).option("-a, --api",descriptions.api,{default:!0}).option("-d, --database",descriptions.database,{default:!0}).option("-ca, --cache",descriptions.cache,{default:!1}).option("-e, --email",descriptions.email,{default:!1}).option("-P, --project [project]",descriptions.project,{default:!1}).option("-m, --minimal",descriptions.minimal,{default:!1}).option("--with-core",descriptions.withCore,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{log.debug("Running `buddy new <name>` ...",options);const startTime=await intro("buddy new");name=name??options.name;const path=resolve(process.cwd(),name);isFolderCheck(path);await onlineCheck();const result=await download(name,path,options);if(result.isErr){log.error(result.error);process.exit(ExitCode.FatalError)}ensureExecutableScripts(path);await ensureEnv(path,options);await install(path,options);if(!options.withCore)await unvendorCore(path,options);if(options.minimal)await stripFeatures(path);if(startTime){const time=performance.now()-startTime;log.success(dim(`[${time.toFixed(2)}ms] Completed`))}log.info(bold("Welcome to the Stacks Framework! \u269B\uFE0F"));log.info(`Get started: ${cyan(`cd ${name}`)} and then ${cyan("./buddy dev")}`);log.info(`Run ${cyan("./buddy doctor")} anytime to check your setup`);log.info("To learn more, visit https://stacksjs.com");process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"new")}function isFolderCheck(path){if(!isFolder(path))return;if(readdirSync(path).filter((entry)=>entry!==".git").length===0)return;log.error(`Path ${path} already exists`);process.exit(ExitCode.FatalError)}async function onlineCheck(){if(await isOnline())return;log.info("It appears you are disconnected from the internet.");log.info("Creating a new project requires a brief internet connection to download the template and install dependencies.");process.exit(ExitCode.FatalError)}async function isOnline(){try{return(await fetch("https://github.com",{method:"HEAD",signal:AbortSignal.timeout(3000)})).ok}catch{return!1}}async function download(name,path,_options){log.info("Setting up your stack.");try{const{downloadTemplate}=await import("@stacksjs/gitit");await downloadTemplate("gh:stacksjs/stacks",{dir:name,force:!0});log.success(`Successfully scaffolded your project at ${cyan(path)}`);return{isErr:!1}}catch(error){return{isErr:!0,error:error instanceof Error?error.message:String(error)}}}function ensureExecutableScripts(path){for(const script of["buddy","bootstrap"])try{chmodSync(resolve(path,script),493)}catch{}}async function ensureEnv(path,_options){log.info("Ensuring your environment is ready...");await ensurePantryInstalled();await ensurePantryDependencies(path);log.success("Environment is ready")}async function install(path,options){log.info("Installing & setting up Stacks");log.info("Copying .env.example \u2192 .env");let result=await runCommand("cp .env.example .env",{...options,cwd:path});if(result?.isErr){log.error(result.error);process.exit(ExitCode.FatalError)}log.info("Removing template-encrypted env files...");const{rm}=await import("node:fs/promises");for(const stale of[".env.development",".env.staging",".env.production",".env.keys"])await rm(`${path}/${stale}`,{force:!0});log.info("Generating application key...");const keyResult=await runAction(Action.KeyGenerate,{...options,cwd:path});if(keyResult.isErr){log.error(keyResult.error);process.exit(ExitCode.FatalError)}if(existsSync(resolve(path,".git")))log.info("Existing git repository detected, skipping git init");else{log.info("Initializing git repository...");result=await runCommand("git init",{...options,cwd:path});if(result.isErr){log.error(result.error);process.exit(ExitCode.FatalError)}}log.success("Installed & set-up \uD83D\uDE80")}async function unvendorCore(path,options){log.info("Resolving the framework from npm (pass --with-core to keep it vendored)...");const result=await runCommand("./buddy unpublish:core --all --force",{...options,cwd:path});if(result.isErr){log.error(result.error);process.exit(ExitCode.FatalError)}log.success("Framework resolved from npm")}async function stripFeatures(path){log.info("Stripping optional feature bundles (--minimal)...");const results=await uninstallAllFeatures({root:path});let strippedAny=!1;for(const{feature,configOutcome,filesRemoved}of results)if(configOutcome==="flipped"||filesRemoved.length>0){strippedAny=!0;const fileSummary=filesRemoved.length>0?` (${filesRemoved.length} path${filesRemoved.length===1?"":"s"})`:"";log.info(` - ${feature}${fileSummary}`)}if(!strippedAny)log.info(" \u2192 no feature scaffolding present; nothing to strip.");else log.success("Minimal skeleton ready \u2014 run `./buddy <feature>:install` to add features back.")}
1
+ import{chmodSync,existsSync,readdirSync}from"node:fs";import process from"node:process";import{runAction}from"@stacksjs/actions";import{bold,cyan,dim,intro,log,onUnknownSubcommand,runCommand}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{resolve}from"@stacksjs/path";import{isFolder}from"@stacksjs/storage";import{ExitCode}from"@stacksjs/types";import{uninstallAllFeatures}from"./features";import{ensurePantryDependencies,ensurePantryInstalled}from"./setup";import{resultFailed}from"../result";export function create(buddy){const descriptions={name:"The name of the project",command:"Create a new Stacks project",ui:"Are you building a UI?",components:"Are you building UI components?",webComponents:"Automagically built optimized custom elements/web components?",views:"How about views?",functions:"Are you developing functions/composables?",api:"Are you building an API?",database:"Do you need a database?",notifications:"Do you need notifications? e.g. email, SMS, push or chat notifications",cache:"Do you need caching?",email:"Do you need email?",project:"Target a specific project",minimal:"Skip optional feature bundles (cms, commerce, dashboard, marketing, monitoring, realtime, queue) \u2014 bare-bones API/SPA starter that can re-add them later via `./buddy <feature>:install`.",withCore:"Keep the framework vendored in `storage/framework/core` as a Bun workspace, for working ON Stacks. Apps that only work WITH Stacks want the default, which resolves every @stacksjs/* package from npm.",verbose:"Enable verbose output"};buddy.command("new [name]",descriptions.command).alias("create [name]").option("-n, --name [name]",descriptions.name,{default:!1}).option("-u, --ui",descriptions.ui,{default:!0}).option("-c, --components",descriptions.components,{default:!0}).option("-w, --web-components",descriptions.webComponents,{default:!0}).option("-p, --views",descriptions.views,{default:!0}).option("-f, --functions",descriptions.functions,{default:!0}).option("-a, --api",descriptions.api,{default:!0}).option("-d, --database",descriptions.database,{default:!0}).option("-ca, --cache",descriptions.cache,{default:!1}).option("-e, --email",descriptions.email,{default:!1}).option("-P, --project [project]",descriptions.project,{default:!1}).option("-m, --minimal",descriptions.minimal,{default:!1}).option("--with-core",descriptions.withCore,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{log.debug("Running `buddy new <name>` ...",options);const startTime=await intro("buddy new");name=name??options.name;const path=resolve(process.cwd(),name);isFolderCheck(path);await onlineCheck();const result=await download(name,path,options);if(resultFailed(result)){log.error(result.error);process.exit(ExitCode.FatalError)}ensureExecutableScripts(path);await ensureEnv(path,options);await install(path,options);if(!options.withCore)await unvendorCore(path,options);if(options.minimal)await stripFeatures(path);if(startTime){const time=performance.now()-startTime;log.success(dim(`[${time.toFixed(2)}ms] Completed`))}log.info(bold("Welcome to the Stacks Framework! \u269B\uFE0F"));log.info(`Get started: ${cyan(`cd ${name}`)} and then ${cyan("./buddy dev")}`);log.info(`Run ${cyan("./buddy doctor")} anytime to check your setup`);log.info("To learn more, visit https://stacksjs.com");process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"new")}function isFolderCheck(path){if(!isFolder(path))return;if(readdirSync(path).filter((entry)=>entry!==".git").length===0)return;log.error(`Path ${path} already exists`);process.exit(ExitCode.FatalError)}async function onlineCheck(){if(await isOnline())return;log.info("It appears you are disconnected from the internet.");log.info("Creating a new project requires a brief internet connection to download the template and install dependencies.");process.exit(ExitCode.FatalError)}async function isOnline(){try{return(await fetch("https://github.com",{method:"HEAD",signal:AbortSignal.timeout(3000)})).ok}catch{return!1}}async function download(name,path,_options){log.info("Setting up your stack.");try{const{downloadTemplate}=await import("@stacksjs/gitit");await downloadTemplate("gh:stacksjs/stacks",{dir:name,force:!0});log.success(`Successfully scaffolded your project at ${cyan(path)}`);return{isErr:!1}}catch(error){return{isErr:!0,error:error instanceof Error?error.message:String(error)}}}function ensureExecutableScripts(path){for(const script of["buddy","bootstrap"])try{chmodSync(resolve(path,script),493)}catch{}}async function ensureEnv(path,_options){log.info("Ensuring your environment is ready...");await ensurePantryInstalled();await ensurePantryDependencies(path);log.success("Environment is ready")}async function install(path,options){log.info("Installing & setting up Stacks");log.info("Copying .env.example \u2192 .env");let result=await runCommand("cp .env.example .env",{...options,cwd:path});if(resultFailed(result)){log.error(result.error);process.exit(ExitCode.FatalError)}log.info("Removing template-encrypted env files...");const{rm}=await import("node:fs/promises");for(const stale of[".env.development",".env.staging",".env.production",".env.keys"])await rm(`${path}/${stale}`,{force:!0});log.info("Generating application key...");const keyResult=await runAction(Action.KeyGenerate,{...options,cwd:path});if(resultFailed(keyResult)){log.error(keyResult.error);process.exit(ExitCode.FatalError)}if(existsSync(resolve(path,".git")))log.info("Existing git repository detected, skipping git init");else{log.info("Initializing git repository...");result=await runCommand("git init",{...options,cwd:path});if(resultFailed(result)){log.error(result.error);process.exit(ExitCode.FatalError)}}log.success("Installed & set-up \uD83D\uDE80")}async function unvendorCore(path,options){log.info("Resolving the framework from npm (pass --with-core to keep it vendored)...");const result=await runCommand("./buddy unpublish:core --all --force",{...options,cwd:path});if(resultFailed(result)){log.error(result.error);process.exit(ExitCode.FatalError)}log.success("Framework resolved from npm")}async function stripFeatures(path){log.info("Stripping optional feature bundles (--minimal)...");const results=await uninstallAllFeatures({root:path});let strippedAny=!1;for(const{feature,configOutcome,filesRemoved}of results)if(configOutcome==="flipped"||filesRemoved.length>0){strippedAny=!0;const fileSummary=filesRemoved.length>0?` (${filesRemoved.length} path${filesRemoved.length===1?"":"s"})`:"";log.info(` - ${feature}${fileSummary}`)}if(!strippedAny)log.info(" \u2192 no feature scaffolding present; nothing to strip.");else log.success("Minimal skeleton ready \u2014 run `./buddy <feature>:install` to add features back.")}
@@ -1,4 +1,4 @@
1
- import{existsSync,mkdirSync,readFileSync,readdirSync,statSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,isAbsolute,join,relative,resolve}from"node:path";import process from"node:process";import{pathToFileURL}from"node:url";import{runAction}from"@stacksjs/actions";import{italic,onUnknownSubcommand,outro,prompts,runCommand}from"@stacksjs/cli";import{app,dns as dnsConfig,email as emailConfig,cloud as cloudConfig}from"@stacksjs/config";import{addDomain,hasUserDomainBeenAddedToCloud,syncDnsConfig}from"@stacksjs/dns";import{loadProjectDnsConfig}from"../config";import{encryptEnv,env}from"@stacksjs/env";import{Action}from"@stacksjs/enums";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{getErrorCode,getErrorMessage}from"@stacksjs/utils";import{ensureAppKey,ensureEnvIsSet}from"./setup";const log={info:(...args)=>console.log("\u2139",...args),success:(...args)=>console.log("\u2713",...args),warn:(...args)=>console.log("\u26A0",...args),error:(...args)=>console.error("\u2717",...args),debug:(...args)=>{if(process.argv.includes("--verbose")||process.argv.includes("-v"))console.log("\uD83D\uDD0D",...args)}},MAIL_PACKAGE_DOMAIN="github.com/mail-os/mail",MAIL_PACKAGE_SPEC=`${MAIL_PACKAGE_DOMAIN}@0.1.0`,MAIL_TARGET_PLATFORM="linux-x86_64",MAIL_BINARY_NAMES=["mail","mail-x86_64-linux","mail-x86_64-linux-gnu"];function collectMatchingFiles(root,names,maxDepth=8){const nameSet=new Set(names),matches=[];if(!existsSync(root))return matches;function walk(dir,depth){if(depth>maxDepth)return;for(const entry of readdirSync(dir)){if(entry===".git"||entry==="node_modules")continue;const fullPath=join(dir,entry),stat=statSync(fullPath);if(stat.isDirectory())walk(fullPath,depth+1);else if(stat.isFile()&&nameSet.has(entry))matches.push(fullPath)}}walk(root,0);return matches}function isElfBinary(filePath){const header=readFileSync(filePath).slice(0,4);return header[0]===127&&header[1]===69&&header[2]===76&&header[3]===70}function resolvePantryInstallCommand(){const localPantryCli=join(homedir(),"Code","Tools","pantry","packages","ts-pantry","bin","cli.ts");if(existsSync(localPantryCli))return{command:"bun",args:[localPantryCli]};const projectPantry=p.projectPath("pantry/.bin/pantry");if(existsSync(projectPantry))return{command:projectPantry,args:[]};const globalPantry=join(homedir(),".local","share","pantry","global","bin","pantry");if(existsSync(globalPantry))return{command:globalPantry,args:[]};return{command:"pantry",args:[]}}async function installMailBinaryWithPantry(){const{execFileSync}=await import("node:child_process"),pantry=resolvePantryInstallCommand();execFileSync(pantry.command,[...pantry.args,"install",MAIL_PACKAGE_SPEC,"--install-dir",p.projectPath("pantry"),"--platform",MAIL_TARGET_PLATFORM,"--quiet"],{cwd:p.projectPath(),stdio:process.argv.includes("--verbose")||process.argv.includes("-v")?"inherit":"pipe",env:process.env})}async function findPantryMailBinary(){const directCandidates=[...MAIL_BINARY_NAMES.map((name)=>p.projectPath(`pantry/.bin/${name}`)),...MAIL_BINARY_NAMES.map((name)=>join(homedir(),".local","share","pantry","global","bin",name))];for(const candidate of directCandidates)if(existsSync(candidate)&&isElfBinary(candidate))return candidate;for(const root of[p.projectPath("pantry"),join(homedir(),".local","share","pantry")])for(const candidate of collectMatchingFiles(root,MAIL_BINARY_NAMES))if(isElfBinary(candidate))return candidate;return null}async function ensureDeployPrerequisites(verbose=!1){const cwd=p.projectPath();await ensureEnvIsSet({cwd,verbose});await ensureAppKey(cwd)}function loadAwsCredentialsFromFile(){const credentialsPath=join(homedir(),".aws","credentials"),configPath=join(homedir(),".aws","config");if(!existsSync(credentialsPath))return{};try{const lines=readFileSync(credentialsPath,"utf-8").split(`
1
+ import{existsSync,mkdirSync,readFileSync,readdirSync,statSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,isAbsolute,join,relative,resolve}from"node:path";import process from"node:process";import{pathToFileURL}from"node:url";import{runAction}from"@stacksjs/actions";import{italic,onUnknownSubcommand,outro,prompts,runCommand}from"@stacksjs/cli";import{app,dns as dnsConfig,email as emailConfig,cloud as cloudConfig}from"@stacksjs/config";import{addDomain,hasUserDomainBeenAddedToCloud,syncDnsConfig}from"@stacksjs/dns";import{loadProjectDnsConfig}from"../config";import{encryptEnv,env}from"@stacksjs/env";import{Action}from"@stacksjs/enums";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{getErrorCode,getErrorMessage}from"@stacksjs/utils";import{ensureAppKey,ensureEnvIsSet}from"./setup";import{resultFailed}from"../result";const log={info:(...args)=>console.log("\u2139",...args),success:(...args)=>console.log("\u2713",...args),warn:(...args)=>console.log("\u26A0",...args),error:(...args)=>console.error("\u2717",...args),debug:(...args)=>{if(process.argv.includes("--verbose")||process.argv.includes("-v"))console.log("\uD83D\uDD0D",...args)}},MAIL_PACKAGE_DOMAIN="github.com/mail-os/mail",MAIL_PACKAGE_SPEC=`${MAIL_PACKAGE_DOMAIN}@0.1.0`,MAIL_TARGET_PLATFORM="linux-x86_64",MAIL_BINARY_NAMES=["mail","mail-x86_64-linux","mail-x86_64-linux-gnu"];function collectMatchingFiles(root,names,maxDepth=8){const nameSet=new Set(names),matches=[];if(!existsSync(root))return matches;function walk(dir,depth){if(depth>maxDepth)return;for(const entry of readdirSync(dir)){if(entry===".git"||entry==="node_modules")continue;const fullPath=join(dir,entry),stat=statSync(fullPath);if(stat.isDirectory())walk(fullPath,depth+1);else if(stat.isFile()&&nameSet.has(entry))matches.push(fullPath)}}walk(root,0);return matches}function isElfBinary(filePath){const header=readFileSync(filePath).slice(0,4);return header[0]===127&&header[1]===69&&header[2]===76&&header[3]===70}function resolvePantryInstallCommand(){const localPantryCli=join(homedir(),"Code","Tools","pantry","packages","ts-pantry","bin","cli.ts");if(existsSync(localPantryCli))return{command:"bun",args:[localPantryCli]};const projectPantry=p.projectPath("pantry/.bin/pantry");if(existsSync(projectPantry))return{command:projectPantry,args:[]};const globalPantry=join(homedir(),".local","share","pantry","global","bin","pantry");if(existsSync(globalPantry))return{command:globalPantry,args:[]};return{command:"pantry",args:[]}}async function installMailBinaryWithPantry(){const{execFileSync}=await import("node:child_process"),pantry=resolvePantryInstallCommand();execFileSync(pantry.command,[...pantry.args,"install",MAIL_PACKAGE_SPEC,"--install-dir",p.projectPath("pantry"),"--platform",MAIL_TARGET_PLATFORM,"--quiet"],{cwd:p.projectPath(),stdio:process.argv.includes("--verbose")||process.argv.includes("-v")?"inherit":"pipe",env:process.env})}async function findPantryMailBinary(){const directCandidates=[...MAIL_BINARY_NAMES.map((name)=>p.projectPath(`pantry/.bin/${name}`)),...MAIL_BINARY_NAMES.map((name)=>join(homedir(),".local","share","pantry","global","bin",name))];for(const candidate of directCandidates)if(existsSync(candidate)&&isElfBinary(candidate))return candidate;for(const root of[p.projectPath("pantry"),join(homedir(),".local","share","pantry")])for(const candidate of collectMatchingFiles(root,MAIL_BINARY_NAMES))if(isElfBinary(candidate))return candidate;return null}async function ensureDeployPrerequisites(verbose=!1){const cwd=p.projectPath();await ensureEnvIsSet({cwd,verbose});await ensureAppKey(cwd)}function loadAwsCredentialsFromFile(){const credentialsPath=join(homedir(),".aws","credentials"),configPath=join(homedir(),".aws","config");if(!existsSync(credentialsPath))return{};try{const lines=readFileSync(credentialsPath,"utf-8").split(`
2
2
  `),profiles=["default","stacks"];let currentProfile="",credentials={};const profileCredentials={};for(const line of lines){const trimmed=line.trim(),profileMatch=trimmed.match(/^\[(.+)\]$/);if(profileMatch?.[1]){currentProfile=profileMatch[1];profileCredentials[currentProfile]={};continue}const keyValue=trimmed.match(/^(\w+)\s*=\s*(.+)$/);if(keyValue&&currentProfile){const[,key,value]=keyValue,target=profileCredentials[currentProfile];if(!target||value===void 0)continue;if(key==="aws_access_key_id")target.accessKeyId=value;else if(key==="aws_secret_access_key")target.secretAccessKey=value}}for(const profile of profiles)if(profileCredentials[profile]?.accessKeyId&&profileCredentials[profile]?.secretAccessKey){credentials=profileCredentials[profile];log.debug(`Using AWS credentials from ~/.aws/credentials [${profile}] profile`);break}if(!credentials.accessKeyId){for(const[profile,creds]of Object.entries(profileCredentials))if(creds.accessKeyId&&creds.secretAccessKey){credentials=creds;log.debug(`Using AWS credentials from ~/.aws/credentials [${profile}] profile`);break}}let region;if(existsSync(configPath)){const regionMatch=readFileSync(configPath,"utf-8").match(/region\s*=\s*(.+)/);if(regionMatch?.[1])region=regionMatch[1].trim()}return{...credentials,region}}catch(error){log.debug("Failed to read AWS credentials file:",error);return{}}}async function setupEmailDnsRecords(emailDomain,region,logger,options){logger.info("Setting up email DNS records...");try{const{SESClient}=await import("@stacksjs/ts-cloud"),{Route53Client}=await import("@stacksjs/ts-cloud"),ses=new SESClient(region),route53=new Route53Client(region);logger.info(`Getting DKIM tokens for ${emailDomain}...`);const tokens=(await ses.getEmailIdentity(emailDomain)).DkimAttributes?.Tokens||[];if(tokens.length===0){logger.warn("No DKIM tokens found - domain may not be set up in SES yet");return}logger.info(`Found ${tokens.length} DKIM tokens`);const zone=(await route53.listHostedZones()).HostedZones?.find((z)=>z.Name===`${emailDomain}.`);if(!zone){logger.warn(`Hosted zone not found for ${emailDomain} - DNS records must be added manually`);logger.info("DKIM records needed:");for(const token of tokens)logger.info(` CNAME: ${token}._domainkey.${emailDomain} -> ${token}.dkim.amazonses.com`);logger.info(` MX: ${emailDomain} -> 10 inbound-smtp.${region}.amazonaws.com`);return}const hostedZoneId=zone.Id?.replace("/hostedzone/","");logger.info(`Found hosted zone: ${hostedZoneId}`);for(const token of tokens){const recordName=`${token}._domainkey.${emailDomain}`,recordValue=`${token}.dkim.amazonses.com`;try{await route53.changeResourceRecordSets({HostedZoneId:hostedZoneId,ChangeBatch:{Changes:[{Action:"UPSERT",ResourceRecordSet:{Name:recordName,Type:"CNAME",TTL:300,ResourceRecords:[{Value:recordValue}]}}]}});logger.success(`Added DKIM record: ${token}._domainkey`)}catch(e){logger.warn(`Failed to add DKIM record: ${getErrorMessage(e)}`)}}const mailSubdomain=options?.mailSubdomain||"mail",mxTarget=options?.mode==="server"?`10 ${mailSubdomain}.${emailDomain}`:`10 inbound-smtp.${region}.amazonaws.com`;try{await route53.changeResourceRecordSets({HostedZoneId:hostedZoneId,ChangeBatch:{Changes:[{Action:"UPSERT",ResourceRecordSet:{Name:emailDomain,Type:"MX",TTL:300,ResourceRecords:[{Value:mxTarget}]}}]}});logger.success(`Added MX record: ${mxTarget}`)}catch(e){logger.warn(`Failed to add MX record: ${getErrorMessage(e)}`)}try{await route53.changeResourceRecordSets({HostedZoneId:hostedZoneId,ChangeBatch:{Changes:[{Action:"UPSERT",ResourceRecordSet:{Name:emailDomain,Type:"TXT",TTL:300,ResourceRecords:[{Value:'"v=spf1 include:amazonses.com ~all"'}]}}]}});logger.success("Added SPF record")}catch(e){logger.warn(`Failed to add SPF record: ${getErrorMessage(e)}`)}try{await route53.changeResourceRecordSets({HostedZoneId:hostedZoneId,ChangeBatch:{Changes:[{Action:"UPSERT",ResourceRecordSet:{Name:`_dmarc.${emailDomain}`,Type:"TXT",TTL:300,ResourceRecords:[{Value:`"v=DMARC1;p=quarantine;pct=25;rua=mailto:dmarcreports@${emailDomain}"`}]}}]}});logger.success("Added DMARC record")}catch(e){logger.warn(`Failed to add DMARC record: ${getErrorMessage(e)}`)}try{const ruleSetName=`${process.env.APP_NAME?.toLowerCase().replace(/[^a-z0-9]/g,"-")||"stacks"}-email-rules`;await ses.setActiveReceiptRuleSet(ruleSetName);logger.success(`Activated email receipt rule set: ${ruleSetName}`)}catch(e){logger.warn(`Failed to activate receipt rule set: ${getErrorMessage(e)}`)}logger.success("Email DNS records configured!");logger.info("Note: DKIM verification may take 5-15 minutes to complete")}catch(error){logger.warn(`Failed to set up email DNS records: ${getErrorMessage(error)}`);logger.info("You can manually set up DNS records using: buddy email:verify")}}async function createDefaultMailUser(appName,emailDomain,region,logger){try{const{DynamoDBClient}=await import("@stacksjs/ts-cloud"),crypto=await import("crypto"),dynamodb=new DynamoDBClient(region),tableName=`${appName}-mail-users`,mailboxes=emailConfig?.mailboxes||[];if(mailboxes.length===0){const defaultEmail=`admin@${emailDomain}`,defaultPassword=crypto.randomBytes(16).toString("hex"),passwordHash=crypto.createHash("sha256").update(defaultPassword).digest("hex");try{await dynamodb.putItem({TableName:tableName,Item:{email:{S:defaultEmail},passwordHash:{S:passwordHash},createdAt:{S:new Date().toISOString()},displayName:{S:"Admin"}}});logger.success(`Created default mail user: ${defaultEmail}`);logger.info(`Password: ${defaultPassword}`);logger.info("Save this password - it will not be shown again!")}catch(e){const msg=getErrorMessage(e);if(msg.includes("ConditionalCheckFailedException")||msg.includes("already exists"))logger.debug("Default mail user already exists");else throw e}}else for(const mailbox of mailboxes){const mb=mailbox,email=typeof mailbox==="string"?`${mailbox}@${emailDomain}`:`${mb.name||mb.address?.split("@")[0]}@${emailDomain}`,password=typeof mailbox==="object"&&mb.password?mb.password:crypto.randomBytes(16).toString("hex"),passwordHash=crypto.createHash("sha256").update(password).digest("hex");try{await dynamodb.putItem({TableName:tableName,Item:{email:{S:email},passwordHash:{S:passwordHash},createdAt:{S:new Date().toISOString()},displayName:{S:typeof mailbox==="object"?mb.displayName||mb.name||email:mailbox}}});logger.success(`Created mail user: ${email}`);if(typeof mailbox!=="object"||!mb.password)logger.info(` Password: ${password}`)}catch(e){const msg=getErrorMessage(e);if(msg.includes("ConditionalCheckFailedException"))logger.debug(`Mail user ${email} already exists`);else logger.warn(`Failed to create mail user ${email}: ${msg}`)}}}catch(error){logger.warn(`Failed to create mail users: ${getErrorMessage(error)}`)}}async function uploadMailServerToS3(bucketName,region,mode){try{const{S3Client:S3}=await import("@stacksjs/ts-cloud"),s3Client=new S3(region);if(mode==="serverless"){const serverTsPath=p.frameworkPath("core/mail-server/server.ts");if(existsSync(serverTsPath)){const serverCode=readFileSync(serverTsPath,"utf-8");await s3Client.putObject({bucket:bucketName,key:"mail-server/server.ts",body:serverCode,contentType:"text/typescript"});log.success("Uploaded serverless mail server code to S3")}const pkgPath=p.frameworkPath("core/mail-server/package.json");if(existsSync(pkgPath)){const pkgJson=readFileSync(pkgPath,"utf-8");await s3Client.putObject({bucket:bucketName,key:"mail-server/package.json",body:pkgJson,contentType:"application/json"})}return}let binaryUploaded=!1;try{await installMailBinaryWithPantry();const linuxBinaryPath=await findPantryMailBinary();if(linuxBinaryPath&&existsSync(linuxBinaryPath)&&isElfBinary(linuxBinaryPath)){log.info(`Uploading Pantry mail binary: ${linuxBinaryPath}`);const binaryContent=readFileSync(linuxBinaryPath);await s3Client.putObject({bucket:bucketName,key:"mail-server/smtp-server",body:binaryContent,contentType:"application/octet-stream"});log.success("Uploaded Linux x86_64 mail server binary to S3");binaryUploaded=!0}}catch(error){log.debug(`Pantry mail install failed: ${getErrorMessage(error)}`)}if(!binaryUploaded)log.warn(`No Pantry-provided ${MAIL_TARGET_PLATFORM} mail binary found. Release ${MAIL_PACKAGE_DOMAIN}, then run the Pantry binary sync for that package.`)}catch(uploadErr){log.debug(`Could not upload mail server to S3 (bucket may not exist yet): ${uploadErr.message}`)}}async function loadTsCloudConfig(envName){try{const base=p.projectPath("config/cloud.ts");return(await import(envName?`${base}?env=${envName}`:base)).tsCloud}catch(err){log.debug("Could not load config/cloud.ts tsCloud export:",err);return}}function resolveProvider(tsCloudConfig){return tsCloudConfig?.cloud?.provider||process.env.CLOUD_PROVIDER||"aws"}function readWaitSecs(name,defaultSecs){const secs=process.env[name]?Number.parseInt(process.env[name],10):Number.NaN;return Number.isFinite(secs)&&secs>0?secs:defaultSecs}function fmtDuration(secs){const m=Math.floor(secs/60),s=secs%60;return m?s?`${m}m${s}s`:`${m}m`:`${s}s`}async function pollUntil(opts){log.info(`${opts.label} (up to ${fmtDuration(opts.timeoutSecs)})...`);const started=Date.now(),deadline=started+opts.timeoutSecs*1000;let lastHeartbeat=0;for(;;)try{await opts.check();return}catch{const elapsedSecs=Math.floor((Date.now()-started)/1000);if(Date.now()>deadline)throw Error(opts.timeoutMessage(elapsedSecs));if(elapsedSecs-lastHeartbeat>=30){log.info(` \u2026 still waiting (${elapsedSecs}s elapsed)`);lastHeartbeat=elapsedSecs}await new Promise((r)=>setTimeout(r,opts.intervalMs??5000))}}async function waitForRemoteReady(ip){const{sshExecOrThrow}=await import("@stacksjs/ts-cloud"),run=(remote)=>sshExecOrThrow(ip,remote,{user:"root",connectTimeoutSec:10}),sshWaitSecs=readWaitSecs("TS_CLOUD_SSH_WAIT_SECS",480);await pollUntil({label:"Waiting for SSH to come up",timeoutSecs:sshWaitSecs,check:()=>run("true"),timeoutMessage:(elapsed)=>`SSH did not become reachable on ${ip} within ${fmtDuration(sshWaitSecs)} (waited ${elapsed}s). `+"The box may still be booting \u2014 raise TS_CLOUD_SSH_WAIT_SECS and retry."});log.success("SSH is up");log.info("Waiting for cloud-init (installing bun + caddy)...");try{await run("cloud-init status --wait || true")}catch(err){log.debug("cloud-init status --wait returned non-zero (continuing):",err)}const bootWaitSecs=readWaitSecs("TS_CLOUD_BOOT_WAIT_SECS",720);await pollUntil({label:"Waiting for the bun runtime",timeoutSecs:bootWaitSecs,check:()=>run("test -x /usr/local/bin/bun"),timeoutMessage:(elapsed)=>`bun runtime did not appear at /usr/local/bin/bun within ${fmtDuration(bootWaitSecs)} (waited ${elapsed}s). `+"cloud-init may have failed \u2014 SSH in and check /var/log/cloud-init-output.log; "+"raise TS_CLOUD_BOOT_WAIT_SECS for slow regions."});log.success("Server is ready (bun installed)")}async function resolveDeclaredTenants(){try{const{config}=await import("@stacksjs/config"),tenants=config.cloud?.tenants;return Array.isArray(tenants)?tenants.filter((slug)=>typeof slug==="string"):[]}catch{return[]}}export async function resolveDeployEnvValues(environment,tsCloudConfig){const fileName=environment==="production"?".env.production":environment==="staging"?".env.staging":existsSync(p.projectPath(".env.development"))?".env.development":".env",filePath=p.projectPath(fileName);if(!existsSync(filePath))return{};try{const{getEnv}=await import("@stacksjs/env"),result=getEnv(void 0,{file:fileName,format:"json"});if(!result.success||!result.output){log.debug(`[deploy] Could not read ${fileName} for site env merging: ${result.error??"unknown error"}`);return{}}const parsed=JSON.parse(result.output),values={};for(const[key,value]of Object.entries(parsed)){if(/^DOTENV_(PUBLIC|PRIVATE)_KEY/.test(key))continue;values[key]=String(value)}const{foreignTenantKeys,partitionTenantEnv}=await import("@stacksjs/env"),partition=partitionTenantEnv(values,{self:tsCloudConfig?.project?.slug,tenants:await resolveDeclaredTenants()});for(const{tenant,keys}of foreignTenantKeys(partition))log.warn(`[deploy] Skipping ${keys.length} '${tenant}' key(s) in ${fileName} \u2014 they belong to that tenant's own `+`repository, and shipping them writes its secrets into this project's site .env files. Remove them with: buddy env:check --file ${fileName}. Keys: ${keys.join(", ")}`);return partition.own}catch(error){log.debug(`[deploy] Failed to resolve ${fileName} for site env merging:`,error);return{}}}export function mergeSiteDeployEnv(sites,resolvedDeployEnv){return Object.fromEntries(Object.entries(sites).map(([siteName,site])=>{if(!site)return[siteName,site];const base={...resolvedDeployEnv};if(site.port!==void 0)delete base.PORT;return[siteName,{...site,env:{...base,...site.env||{}}}]}))}function looksLikeSqliteFile(value){return typeof value==="string"&&/\.(?:sqlite3?|db)$/i.test(value.trim())}export function siteSqlitePath(siteEnv={}){if(String(siteEnv.DB_CONNECTION??"sqlite").trim().toLowerCase()!=="sqlite")return null;const configured=typeof siteEnv.DB_DATABASE_PATH==="string"&&siteEnv.DB_DATABASE_PATH.trim()?siteEnv.DB_DATABASE_PATH:looksLikeSqliteFile(siteEnv.DB_DATABASE)?siteEnv.DB_DATABASE:"database/stacks.sqlite",path=String(configured).trim().replace(/^\.\//,"");if(!path||path.startsWith("/")||path.startsWith("~")||path.split("/").includes(".."))return null;return path}export function tsCloudPersistentStateSupport(buildSiteDeployScript){const missing=[];if(typeof buildSiteDeployScript!=="function")return{ok:!1,missing:["buildSiteDeployScript"]};const target="/var/www/probe-shared/database/probe.sqlite";let script="";try{script=buildSiteDeployScript({siteName:"probe",slug:"probe",appDir:"/var/www/probe-probe",artifactFetch:[],releaseId:"probe",execStart:"/bin/true",envEntries:{},port:3000,sharedPaths:[{path:"database/probe.sqlite",target,seed:!0}]}).join(`
3
3
  `)}catch{return{ok:!1,missing:["shared paths with an explicit target"]}}if(!(script.includes("cp -a")&&script.includes("/current/")))missing.push("adoption of existing state into shared/");if(!script.includes(`ln -sfn ${target} `))missing.push("shared paths with an explicit target");return{ok:missing.length===0,missing}}export function projectDatabaseTarget(slug,relativePath){return`/var/www/${slug}-shared/${relativePath}`}function runsMigrations(site){return Array.isArray(site?.preStart)&&site.preStart.some((cmd)=>typeof cmd==="string"&&/\bmigrate\b/.test(cmd))}export function applyPersistentStatePaths(sites,slug){const isServerApp=(site)=>!!site&&typeof site.start==="string",appSites=Object.entries(sites).filter(([,site])=>isServerApp(site)),owner=(appSites.find(([,site])=>runsMigrations(site))??appSites[0])?.[0],out={};for(const[name,site]of Object.entries(sites)){if(!isServerApp(site)){out[name]=site;continue}const sqlite=siteSqlitePath(site.env||{}),framework=["storage/logs"];if(sqlite)framework.unshift({path:sqlite,target:projectDatabaseTarget(slug,sqlite),seed:name===owner});const declared=Array.isArray(site.sharedPaths)?site.sharedPaths.filter(Boolean):[],byPath=new Map;for(const entry of[...declared,...framework])byPath.set(typeof entry==="string"?entry:entry.path,entry);out[name]={...site,sharedPaths:[...byPath.values()]}}return out}function prefixHostForEnv(host,prefix){if(host.startsWith(`${prefix}.`)||host.startsWith(`www.${prefix}.`))return host;if(host.startsWith("www."))return`www.${prefix}.${host.slice(4)}`;return`${prefix}.${host}`}export function applyEnvironmentToSites(sites,environment,config){const prefix=config?.environments?.[environment]?.domainPrefix;if(!prefix||environment==="production")return sites;const allHosts=[];for(const site of Object.values(sites)){const d=site?.domain;if(typeof d==="string")allHosts.push(d);else if(Array.isArray(d)){for(const x of d)if(typeof x==="string")allHosts.push(x)}}allHosts.sort((a,b)=>b.length-a.length);const rewrite=(val)=>{let r=val;for(const h of allHosts){const esc=h.replace(/[.]/g,"\\.");r=r.replace(new RegExp(`//${esc}(?=[/:?#]|$)`,"g"),`//${prefixHostForEnv(h,prefix)}`)}return r},out={};for(const[name,site]of Object.entries(sites)){if(!site){out[name]=site;continue}const s={...site};if(typeof s.domain==="string")s.domain=prefixHostForEnv(s.domain,prefix);else if(Array.isArray(s.domain))s.domain=s.domain.map((d)=>typeof d==="string"?prefixHostForEnv(d,prefix):d);if(typeof s.redirect==="string")s.redirect=rewrite(s.redirect);if(s.env&&typeof s.env==="object"){const e={...s.env};for(const k of Object.keys(e))if(typeof e[k]==="string")e[k]=rewrite(e[k]);s.env=e}out[name]=s}return out}async function deployToHetzner(tsCloudConfig,deployEnv,options){const verbose=options.verbose===!0,environment=deployEnv==="prod"?"production":deployEnv,apiToken=tsCloudConfig.hetzner?.apiToken||process.env.HCLOUD_TOKEN||process.env.HETZNER_API_TOKEN,persistedAttachBox=resolvePersistedAttachTargetBox(tsCloudConfig,environment);if(!apiToken&&!persistedAttachBox){log.error("No Hetzner API token found. Set HCLOUD_TOKEN in your .env (or hetzner.apiToken in config/cloud.ts).");process.exit(ExitCode.FatalError)}const sshPubKey=join(homedir(),".ssh","id_ed25519.pub");if(!existsSync(sshPubKey)){log.error(`SSH public key not found at ${sshPubKey}.`);log.info("ts-cloud deploys to Hetzner over SSH and registers this key on the server.");log.info("Generate one with: ssh-keygen -t ed25519");process.exit(ExitCode.FatalError)}const{createCloudDriver,deployAllComputeSites,ensureManagementDashboard,resolveSiteKind,buildSiteDeployScript}=await loadTsCloudDeployApi(),support=tsCloudPersistentStateSupport(buildSiteDeployScript);if(!support.ok){log.error("This ts-cloud cannot keep your database across deploys, and deploying anyway would destroy it.");log.error(`Missing: ${support.missing.join(", ")}.`);log.error("Upgrade @stacksjs/ts-cloud (bun update @stacksjs/ts-cloud), or point TS_CLOUD_MODULE at a build that has it.");process.exit(ExitCode.FatalError)}try{await runHetznerDeploy({tsCloudConfig,environment,verbose,docker:options.docker===!0,createCloudDriver,deployAllComputeSites,ensureManagementDashboard,resolveSiteKind,onlySite:options.site||void 0,persistedAttachBox})}catch(err){log.error("Hetzner deploy failed:");console.error(err instanceof Error?err.stack||err.message:err);process.exit(ExitCode.FatalError)}}export function resolveProjectTsCloudModule(projectRoot=process.cwd()){const manifestPath=join(projectRoot,"node_modules","@stacksjs","ts-cloud","package.json");if(!existsSync(manifestPath))return;const manifest=JSON.parse(readFileSync(manifestPath,"utf8")),rootExport=manifest.exports?.["."],entry=manifest.module??(typeof rootExport==="string"?rootExport:rootExport?.import);if(!entry)return;const modulePath=resolve(dirname(manifestPath),entry);return existsSync(modulePath)?modulePath:void 0}export async function loadTsCloudDeployApi(){const requested=process.env.TS_CLOUD_MODULE?.trim();if(!requested){const projectModule=resolveProjectTsCloudModule();return projectModule?import(pathToFileURL(projectModule).href):import("@stacksjs/ts-cloud")}const modulePath=isAbsolute(requested)?requested:resolve(process.cwd(),requested);if(!existsSync(modulePath))throw Error(`TS_CLOUD_MODULE points to a missing module: ${modulePath}`);return import(pathToFileURL(modulePath).href)}export function shouldInjectManagementDashboard(tsCloudConfig){return!tsCloudConfig.cloud?.attachTo}export function reconcilePartialDeployManagementDashboards(tsCloudConfig,livePorts){const sites=tsCloudConfig.sites,preserved=[],removed=[];if(!sites)return{preserved,removed};for(const[siteName,site]of Object.entries(sites)){if(siteName!=="dashboard"&&!siteName.startsWith("dashboard-"))continue;const livePort=livePorts[siteName];if(typeof livePort!=="number"||!Number.isInteger(livePort)||livePort<1||livePort>65535){delete sites[siteName];removed.push(siteName);continue}const next={...site,port:livePort};if(typeof next.start==="string")next.start=next.start.replace(/(--port(?:=|\s+))\d+/,`$1${livePort}`);sites[siteName]=next;preserved.push(siteName)}return{preserved,removed}}async function reconcilePartialDeployManagementDashboardsWithLiveBox(tsCloudConfig,ip){const slug=String(tsCloudConfig.project?.slug||"app").replace(/[^a-z0-9._-]+/gi,"-"),siteNames=Object.keys(tsCloudConfig.sites??{}).filter((name)=>name==="dashboard"||name.startsWith("dashboard-"));if(siteNames.length===0)return;const units=siteNames.map((siteName)=>({siteName,unit:`${slug}-${siteName}.service`})),probe=`
4
4
  const units = ${JSON.stringify(units)}
@@ -262,5 +262,5 @@ EOF
262
262
  systemctl daemon-reload
263
263
  systemctl enable --now mail-health.timer >/dev/null 2>&1
264
264
  # 6) Restart only when the startup-read env actually changed (domain or DKIM key).
265
- if [ "$ENV_CHANGED" = 1 ]; then systemctl restart mail 2>/dev/null || true; echo "MAILTENANT:env-changed+restarted,forwards=$FWD_STATE"; else echo "MAILTENANT:current,forwards=$FWD_STATE"; fi`;try{const out=execSync(`ssh ${sshArgs.map((a)=>`'${a}'`).join(" ")} bash -s`,{input:script,encoding:"utf8",stdio:["pipe","pipe","pipe"]}),line=(out.match(/MAILTENANT:[^\n]*/)||[])[0]||"MAILTENANT:done",mailHostFromOut=(out.match(/MAILHOST:([^\n]*)/)||[])[1]?.trim(),mailHost=mailHostFromOut||`mail.${domain}`,dkimPubB64=(out.match(/DKIMPUB:([^\n]*)/)||[])[1]?.trim()||void 0,madeAddrs=new Set([...out.matchAll(/MADE:([^\n]+)/g)].flatMap((m)=>m[1]?[m[1].trim()]:[])),migratedAddrs=new Set([...out.matchAll(/MIGRATED:([^\n]+)/g)].flatMap((m)=>m[1]?[m[1].trim()]:[])),created=boxes.filter((b)=>madeAddrs.has(b.address)).map((b)=>({address:b.address,password:b.password}));logger.success(`Mail routing reconciled (${line.replace("MAILTENANT:","")})`);const failed=[...out.matchAll(/FAIL:([^\n]+)/g)].flatMap((m)=>m[1]?[m[1].trim()]:[]);if(failed.length)logger.warn(`Mail: the server refused ${failed.length} mailbox(es): ${failed.join(", ")}`);const forwardError=(out.match(/FWDERR:([^\n]*)/)||[])[1]?.trim();if(forwardError)logger.warn(`Mail: the forward rules were not merged: ${forwardError}`);const certHost=(out.match(/CERTHOST:([^\n]*)/)||[])[1]?.trim();if(certHost)logger.success(`Mail: ${certHost} added to the mail certificate \u2014 clients can use it as the server name`);const certError=(out.match(/CERTFAIL:([^\n]*)/)||[])[1]?.trim();if(certError)logger.warn(`Mail: could not issue a certificate for mail.${domain} (clients should use ${mailHostFromOut||"the shared mail host"}): ${certError}`);const seen=new Set([...madeAddrs,...migratedAddrs,...[...out.matchAll(/EXISTS:([^\n]+)/g)].flatMap((m)=>m[1]?[m[1].trim()]:[])]),unaccounted=boxes.filter((b)=>!seen.has(b.address)).map((b)=>b.address);if(unaccounted.length)logger.warn(`Mail: ${unaccounted.length} declared mailbox(es) were not reconciled: ${unaccounted.join(", ")}`);if(created.length){logger.info(`Mail: created ${created.length} mailbox(es) \u2014 credentials below (save them; shown once):`);for(const b of created)logger.info(` ${b.address} ${b.password}`)}if(migratedAddrs.size)logger.success(`Mail: migrated ${migratedAddrs.size} legacy mailbox username(s) to isolated full addresses`);return domain?{domain,mailHost,dkimPubB64,created}:null}catch(err){logger.warn(`Mail routing reconcile skipped: ${getErrorMessage(err)}`);return null}}export async function reconcileMailDns(res,ip,logger){const{domain,dkimPubB64}=res;let{mailHost}=res;try{const dns=await import("node:dns"),own=`mail.${domain}`;if(own!==mailHost&&(await dns.promises.resolve4(own)).includes(ip))mailHost=own}catch{}const spf=`v=spf1 ip4:${ip} ~all`,cfg=emailConfig||{},firstBox=resolveMailboxes(cfg.mailboxes,domain)[0]?.address,dmarc=`v=DMARC1; p=quarantine; rua=mailto:${(typeof cfg.from?.address==="string"&&cfg.from.address.includes("@")?cfg.from.address:void 0)||firstBox||`chris@${domain}`}`,dkim=dkimPubB64?`v=DKIM1; k=rsa; p=${dkimPubB64}`:void 0,apiKey=process.env.PORKBUN_API_KEY,secretKey=process.env.PORKBUN_SECRET_KEY;if(!apiKey||!secretKey){logger.warn(`Mail DNS: no Porkbun credentials \u2014 add these records for ${domain} by hand:`);logger.info(` MX @ 10 ${mailHost}`);logger.info(` TXT @ ${spf}`);if(dkim)logger.info(` TXT mail._domainkey ${dkim}`);logger.info(` TXT _dmarc ${dmarc}`);return}const auth={apikey:apiKey,secretapikey:secretKey},call=async(path,body)=>{return(await fetch(`https://api.porkbun.com/api/json/v3/${path}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...auth,...body})})).json().catch(()=>({}))},ownership=await call(`dns/retrieve/${domain}`,{});if(ownership?.status!=="SUCCESS"){logger.warn(`Mail DNS skipped: ${domain} is not administered by these Porkbun credentials (${ownership?.message||"domain not in this account"}).`);logger.info(`Set \`domain\` in config/email.ts to a zone you own, or add these records for ${domain} by hand:`);logger.info(` MX @ 10 ${mailHost}`);logger.info(` TXT @ ${spf}`);if(dkim)logger.info(` TXT mail._domainkey ${dkim}`);logger.info(` TXT _dmarc ${dmarc}`);return}const upsert=async(type,name,content,extra={})=>{const sub=name==="@"?"":`/${name}`;await call(`dns/deleteByNameType/${domain}/${type}${sub}`,{});const created=await call(`dns/create/${domain}`,{name:name==="@"?"":name,type,content,ttl:"600",...extra});if(created?.status!=="SUCCESS")throw Error(`${type} ${name}: ${created?.message||"unknown Porkbun error"}`)};try{await upsert("MX","@",mailHost,{prio:"10"});await upsert("TXT","@",spf);if(dkim)await upsert("TXT","mail._domainkey",dkim);await upsert("TXT","_dmarc",dmarc);await upsert("A","mail",ip);logger.success(`Mail DNS published for ${domain} (MX\u2192${mailHost}, SPF, DKIM, DMARC, mail.${domain}\u2192${ip})`)}catch(err){logger.warn(`Mail DNS reconcile skipped for ${domain}: ${getErrorMessage(err)}`)}}export function configDnsDomains(sites){const domains=new Set;for(const site of Object.values(sites))if(!site?.redirect&&site?.domain&&typeof site.domain==="string")domains.add(site.domain.replace(/^www\./,""));return[...domains]}export function dnsProviderNameFromNameservers(nameservers){const normalized=nameservers.map((name)=>name.toLowerCase().replace(/\.$/,""));if(normalized.some((name)=>name.endsWith(".porkbun.com")))return"porkbun";if(normalized.some((name)=>name.endsWith(".ns.cloudflare.com")))return"cloudflare";if(normalized.some((name)=>/(^|\.)awsdns-\d+\.(?:com|net|org|co\.uk)$/.test(name)))return"route53";if(normalized.some((name)=>name.endsWith(".domaincontrol.com")))return"godaddy";return null}async function reconcileConfigDns(sites,logger){const projectDnsConfig=await loadProjectDnsConfig(dnsConfig);if(["a","aaaa","cname","mx","txt"].reduce((total,key)=>total+(Array.isArray(projectDnsConfig?.[key])?projectDnsConfig[key].length:0),0)===0)return;for(const domain of configDnsDomains(sites))try{const result=await syncDnsConfig(domain,projectDnsConfig);if(!result.provider)continue;if(result.created||result.failed)logger.info(`DNS (config/dns.ts) ${domain}: ${result.created} created, ${result.kept} kept${result.failed?`, ${result.failed} failed`:""}`)}catch(err){logger.warn(`DNS (config/dns.ts) reconcile for ${domain} failed: ${err instanceof Error?err.message:String(err)}`)}}async function reconcileHetznerDns(sites,ip,logger,ipv6){const published=[],domains=new Set;for(const site of Object.values(sites))if(site?.domain&&typeof site.domain==="string")domains.add(site.domain.replace(/^www\./,""));if(domains.size===0)return published;const providerConfigs=[];if(process.env.PORKBUN_API_KEY&&process.env.PORKBUN_SECRET_KEY)providerConfigs.push({provider:"porkbun",apiKey:process.env.PORKBUN_API_KEY,secretKey:process.env.PORKBUN_SECRET_KEY});if(process.env.CLOUDFLARE_API_TOKEN)providerConfigs.push({provider:"cloudflare",apiToken:process.env.CLOUDFLARE_API_TOKEN});if(process.env.GODADDY_API_KEY&&process.env.GODADDY_API_SECRET)providerConfigs.push({provider:"godaddy",apiKey:process.env.GODADDY_API_KEY,apiSecret:process.env.GODADDY_API_SECRET,environment:process.env.GODADDY_ENVIRONMENT});if(process.env.AWS_ACCESS_KEY_ID||process.env.AWS_PROFILE)providerConfigs.push({provider:"route53"});if(providerConfigs.length===0){logger.warn("DNS: no DNS provider credentials found (PORKBUN_API_KEY/\u2026); skipping DNS reconciliation.");for(const d of domains)logger.info(` Point manually: A ${d} \u2192 ${ip} and A www.${d} \u2192 ${ip}`);return published}const{createDnsProvider,detectDnsProvider,reconcileAddressRecords}=await import("@stacksjs/ts-cloud");logger.info("Reconciling DNS records...");const resolveA=async(fqdn)=>{try{const{resolve4}=await import("node:dns/promises");return await resolve4(fqdn)}catch{return[]}},resolveAuthoritativeNameservers=async(domain)=>{try{const{resolveNs}=await import("node:dns/promises");return await resolveNs(domain)}catch{return[]}};for(const domain of domains)try{let provider=await detectDnsProvider(domain,providerConfigs).catch((err)=>{logger.warn(` DNS: ignoring a configured provider for ${domain} - its credentials were rejected (${err?.message||err})`);return});if(!provider){const providerName=dnsProviderNameFromNameservers(await resolveAuthoritativeNameservers(domain)),providerConfig=providerConfigs.find((config)=>config.provider===providerName);if(providerConfig)provider=createDnsProvider(providerConfig)}if(!provider){for(const sub of["","www"]){const fqdn=sub?`${sub}.${domain}`:domain,current=await resolveA(fqdn);if(current.includes(ip))logger.success(` DNS: ${fqdn} \u2192 ${ip} (externally managed, already correct)`);else if(current.length===0)logger.warn(` DNS: ${fqdn} does not resolve and no configured provider manages ${domain} \u2014 create it manually: A ${fqdn} \u2192 ${ip}`);else logger.warn(` DNS: ${fqdn} resolves to ${current.join(", ")} but this deploy targets ${ip}, and no configured provider manages ${domain} \u2014 update it manually: A ${fqdn} \u2192 ${ip}`)}continue}for(const sub of["","www"]){const fqdn=sub?`${sub}.${domain}`:domain,report=await reconcileAddressRecords({provider,zone:domain,fqdn,ipv4:ip,ipv6});for(const record of report.published){logger.success(` DNS: ${record.fqdn} \u2192 ${record.content} (${provider.name})`);published.push(record.fqdn)}for(const warning of report.warnings)logger.warn(` DNS: ${warning}`)}}catch(err){logger.warn(` DNS: ${domain} reconciliation failed: ${err?.message||err}`)}return published}async function buildContainerImageWithPantry(args){const{slug,sites,verbose}=args,{execSync}=await import("node:child_process");let cli;for(const candidate of["pantry","ts-pantry"])try{execSync(`command -v ${candidate}`,{stdio:"pipe"});cli=candidate;break}catch{}if(!cli){log.warn("pantry CLI not found on PATH \u2014 skipping container image build. Install pantry to enable `--docker`.");return}const dockerfile="storage/framework/Dockerfile",canPush=Boolean(process.env.PANTRY_REGISTRY_TOKEN||process.env.PANTRY_TOKEN);for(const[siteName,site]of Object.entries(sites)){if(!site?.start)continue;const tag=`${slug}-${siteName}:latest`;log.info(`[${siteName}] building image ${tag} with pantry (native, no Docker daemon)...`);const flags=["build",".","-t",tag,"-f",dockerfile,"--run-mode","skip"];if(canPush)flags.push("--push");execSync(`${cli} ${flags.map((f)=>f.includes(" ")?`'${f}'`:f).join(" ")}`,{stdio:verbose?"inherit":"pipe",maxBuffer:536870912});log.success(`[${siteName}] image built${canPush?" + pushed to the pantry registry":""}`)}}export function deploy(buddy){const descriptions={deploy:"Deploy your project",project:"Target a specific project",production:"Deploy to production",development:"Deploy to development",staging:"Deploy to staging",yes:"Confirm all prompts by default",domain:"Specify a domain to deploy to",verbose:"Enable verbose output"};buddy.command("deploy [env]",descriptions.deploy).option("--domain",descriptions.domain,{default:void 0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--prod",descriptions.production,{default:!0}).option("--dev",descriptions.development,{default:!1}).option("--yes",descriptions.yes,{default:!1}).option("--site <name>","Deploy only this one site to the existing server (multi-tenant surgical add)",{default:void 0}).option("--staging",descriptions.staging,{default:!1}).option("--docker","Also build an OCI image with pantry (native, no Docker daemon) and push it to the pantry registry",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(envArg,options)=>{log.debug("Running `buddy deploy` ...",options);if(process.argv.includes("--dry-run")||options.dryRun===!0){log.error("`buddy deploy --dry-run` is not supported.");log.info("Deploy has no preview mode: provisioning, release shipping, and DNS all mutate real infrastructure.");log.info("To see what would ship without touching the server, inspect `config/cloud.ts` sites, or run `buddy deploy --site <name>` to narrow a real deploy to one site.");process.exit(ExitCode.FatalError)}await ensureDeployPrerequisites(options.verbose===!0);const deployEnv=envArg||(options.staging?"staging":options.dev?"development":"production"),deployEnvName=deployEnv==="prod"?"production":deployEnv==="dev"?"development":deployEnv;process.env.APP_ENV=deployEnvName;{const envFile=deployEnvName==="production"?".env.production":`.env.${deployEnvName}`;if(existsSync(p.projectPath(envFile)))try{const{loadEnv}=await import("@stacksjs/env");loadEnv({path:envFile,env:deployEnvName,keysFile:".env.keys",overload:!0,quiet:!0})}catch(err){log.warn(`Could not load ${envFile}: ${getErrorMessage(err)}`)}}const tsCloudConfig=await loadTsCloudConfig(deployEnvName);if(tsCloudConfig&&resolveProvider(tsCloudConfig)==="hetzner"){await deployToHetzner(tsCloudConfig,deployEnv,options);return}if(process.env.AWS_ACCESS_KEY_ID&&process.env.AWS_SECRET_ACCESS_KEY)delete process.env.AWS_PROFILE;const startTime=performance.now();console.log("");console.log("\uD83D\uDE80 Deploy");console.log("");let productionUrl;if(deployEnv==="production"||deployEnv==="prod"){const prodEnvPath=p.projectPath(".env.production");if(existsSync(prodEnvPath)){const urlMatch=readFileSync(prodEnvPath,"utf-8").match(/^APP_URL=(.+)$/m);if(urlMatch?.[1]){productionUrl=urlMatch[1].trim();log.debug("Using APP_URL from .env.production:",productionUrl)}}}const envUrl=env.APP_URL,domain=options.domain||productionUrl||envUrl||app.url;if((options.prod||deployEnv==="production"||deployEnv==="prod")&&!options.yes)await confirmProductionDeployment();if(!domain){log.info("No domain found in your .env.production or ./config/app.ts");log.info("Please ensure your domain is properly configured.");log.info("For more info, check out the docs or join our Discord.");process.exit(ExitCode.FatalError)}log.info(`Deploying to ${italic(domain)} (${deployEnv})`);await checkIfAwsIsBootstrapped(options);options.domain=await configureDomain(domain,options,startTime);const result=await runAction(Action.Deploy,options);if(result.isErr){await outro("While running the `buddy deploy`, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Project deployed.",{startTime,useSeconds:!0})});onUnknownSubcommand(buddy,"deploy")}async function confirmProductionDeployment(){if(!process.stdin.isTTY){log.error("Refusing to deploy to production from a non-interactive shell without confirmation.");log.info(" \u27A1\uFE0F Re-run with `--yes` to confirm (e.g. in CI): `buddy deploy --prod --yes`");process.exit(ExitCode.InvalidArgument)}if(!await prompts.confirm({message:"Are you sure you want to deploy to production?",initial:!0})){log.info("Aborting deployment...");process.exit(ExitCode.InvalidArgument)}}async function configureDomain(domain,options,startTime){log.debug("Configuring domain...",domain);if(!domain){log.info("We could not identify a domain to deploy to.");log.warn("Please set your .env or ./config/app.ts properly.");log.info("Alternatively, specify a domain to deploy via the `--domain` flag.");console.log("");log.info(" \u27A1\uFE0F Example: `buddy deploy --domain example.com`");console.log("");process.exit(ExitCode.FatalError)}if(domain.includes("localhost")){log.info("You are deploying to a local environment.");log.warn("Please set your .env or ./config/app.ts properly. The domain we are deploying cannot be a `localhost` domain.");log.info("Alternatively, specify a domain to deploy via the `--domain` flag.");console.log("");log.info(" \u27A1\uFE0F Example: `buddy deploy --domain example.com`");console.log("");process.exit(ExitCode.FatalError)}if(await hasUserDomainBeenAddedToCloud(domain)){log.info("Domain is properly configured");log.info("Your cloud is deploying...");log.info(`${italic("This may take a while...")}`);return domain}console.log("");log.info(` \uD83D\uDC4B It appears to be your first ${italic(domain)} deployment.`);console.log("");log.info(italic("Let\u2019s ensure it is all connected properly."));log.info(italic("One moment..."));console.log("");const result=await addDomain({...options,deploy:!0,startTime});if(result.isErr){await outro("While running the `buddy deploy`, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Added your domain.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}async function promptAndSaveCredentials(){const accessKeyId=await prompts.text({message:"AWS Access Key ID:",validate:(value)=>value.length>0?!0:"Access Key ID is required"});if(!accessKeyId){log.info("Deployment cancelled");process.exit(ExitCode.Success)}const secretAccessKey=await prompts.password({message:"AWS Secret Access Key:",validate:(value)=>value.length>0?!0:"Secret Access Key is required"});if(!secretAccessKey){log.info("Deployment cancelled");process.exit(ExitCode.Success)}const region=await prompts.text({message:"AWS Region:",initial:"us-east-1"});if(!region){log.info("Deployment cancelled");process.exit(ExitCode.Success)}const{setEnv}=await import("@stacksjs/env");await setEnv("AWS_ACCESS_KEY_ID",accessKeyId,{file:".env.production",encrypt:!0});await setEnv("AWS_SECRET_ACCESS_KEY",secretAccessKey,{file:".env.production",encrypt:!0});await setEnv("AWS_REGION",region||"us-east-1",{file:".env.production"});process.env.AWS_ACCESS_KEY_ID=accessKeyId;process.env.AWS_SECRET_ACCESS_KEY=secretAccessKey;process.env.AWS_REGION=region||"us-east-1";log.success("AWS credentials saved securely to .env.production");console.log("")}function loadAwsCredentialsFromEnv(){const environment=process.env.APP_ENV||process.env.NODE_ENV||"production",envFiles=[p.projectPath(`.env.${environment}`),p.projectPath(".env")];for(const envPath of envFiles){if(!existsSync(envPath))continue;try{const lines=readFileSync(envPath,"utf-8").split(`
265
+ if [ "$ENV_CHANGED" = 1 ]; then systemctl restart mail 2>/dev/null || true; echo "MAILTENANT:env-changed+restarted,forwards=$FWD_STATE"; else echo "MAILTENANT:current,forwards=$FWD_STATE"; fi`;try{const out=execSync(`ssh ${sshArgs.map((a)=>`'${a}'`).join(" ")} bash -s`,{input:script,encoding:"utf8",stdio:["pipe","pipe","pipe"]}),line=(out.match(/MAILTENANT:[^\n]*/)||[])[0]||"MAILTENANT:done",mailHostFromOut=(out.match(/MAILHOST:([^\n]*)/)||[])[1]?.trim(),mailHost=mailHostFromOut||`mail.${domain}`,dkimPubB64=(out.match(/DKIMPUB:([^\n]*)/)||[])[1]?.trim()||void 0,madeAddrs=new Set([...out.matchAll(/MADE:([^\n]+)/g)].flatMap((m)=>m[1]?[m[1].trim()]:[])),migratedAddrs=new Set([...out.matchAll(/MIGRATED:([^\n]+)/g)].flatMap((m)=>m[1]?[m[1].trim()]:[])),created=boxes.filter((b)=>madeAddrs.has(b.address)).map((b)=>({address:b.address,password:b.password}));logger.success(`Mail routing reconciled (${line.replace("MAILTENANT:","")})`);const failed=[...out.matchAll(/FAIL:([^\n]+)/g)].flatMap((m)=>m[1]?[m[1].trim()]:[]);if(failed.length)logger.warn(`Mail: the server refused ${failed.length} mailbox(es): ${failed.join(", ")}`);const forwardError=(out.match(/FWDERR:([^\n]*)/)||[])[1]?.trim();if(forwardError)logger.warn(`Mail: the forward rules were not merged: ${forwardError}`);const certHost=(out.match(/CERTHOST:([^\n]*)/)||[])[1]?.trim();if(certHost)logger.success(`Mail: ${certHost} added to the mail certificate \u2014 clients can use it as the server name`);const certError=(out.match(/CERTFAIL:([^\n]*)/)||[])[1]?.trim();if(certError)logger.warn(`Mail: could not issue a certificate for mail.${domain} (clients should use ${mailHostFromOut||"the shared mail host"}): ${certError}`);const seen=new Set([...madeAddrs,...migratedAddrs,...[...out.matchAll(/EXISTS:([^\n]+)/g)].flatMap((m)=>m[1]?[m[1].trim()]:[])]),unaccounted=boxes.filter((b)=>!seen.has(b.address)).map((b)=>b.address);if(unaccounted.length)logger.warn(`Mail: ${unaccounted.length} declared mailbox(es) were not reconciled: ${unaccounted.join(", ")}`);if(created.length){logger.info(`Mail: created ${created.length} mailbox(es) \u2014 credentials below (save them; shown once):`);for(const b of created)logger.info(` ${b.address} ${b.password}`)}if(migratedAddrs.size)logger.success(`Mail: migrated ${migratedAddrs.size} legacy mailbox username(s) to isolated full addresses`);return domain?{domain,mailHost,dkimPubB64,created}:null}catch(err){logger.warn(`Mail routing reconcile skipped: ${getErrorMessage(err)}`);return null}}export async function reconcileMailDns(res,ip,logger){const{domain,dkimPubB64}=res;let{mailHost}=res;try{const dns=await import("node:dns"),own=`mail.${domain}`;if(own!==mailHost&&(await dns.promises.resolve4(own)).includes(ip))mailHost=own}catch{}const spf=`v=spf1 ip4:${ip} ~all`,cfg=emailConfig||{},firstBox=resolveMailboxes(cfg.mailboxes,domain)[0]?.address,dmarc=`v=DMARC1; p=quarantine; rua=mailto:${(typeof cfg.from?.address==="string"&&cfg.from.address.includes("@")?cfg.from.address:void 0)||firstBox||`chris@${domain}`}`,dkim=dkimPubB64?`v=DKIM1; k=rsa; p=${dkimPubB64}`:void 0,apiKey=process.env.PORKBUN_API_KEY,secretKey=process.env.PORKBUN_SECRET_KEY;if(!apiKey||!secretKey){logger.warn(`Mail DNS: no Porkbun credentials \u2014 add these records for ${domain} by hand:`);logger.info(` MX @ 10 ${mailHost}`);logger.info(` TXT @ ${spf}`);if(dkim)logger.info(` TXT mail._domainkey ${dkim}`);logger.info(` TXT _dmarc ${dmarc}`);return}const auth={apikey:apiKey,secretapikey:secretKey},call=async(path,body)=>{return(await fetch(`https://api.porkbun.com/api/json/v3/${path}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...auth,...body})})).json().catch(()=>({}))},ownership=await call(`dns/retrieve/${domain}`,{});if(ownership?.status!=="SUCCESS"){logger.warn(`Mail DNS skipped: ${domain} is not administered by these Porkbun credentials (${ownership?.message||"domain not in this account"}).`);logger.info(`Set \`domain\` in config/email.ts to a zone you own, or add these records for ${domain} by hand:`);logger.info(` MX @ 10 ${mailHost}`);logger.info(` TXT @ ${spf}`);if(dkim)logger.info(` TXT mail._domainkey ${dkim}`);logger.info(` TXT _dmarc ${dmarc}`);return}const upsert=async(type,name,content,extra={})=>{const sub=name==="@"?"":`/${name}`;await call(`dns/deleteByNameType/${domain}/${type}${sub}`,{});const created=await call(`dns/create/${domain}`,{name:name==="@"?"":name,type,content,ttl:"600",...extra});if(created?.status!=="SUCCESS")throw Error(`${type} ${name}: ${created?.message||"unknown Porkbun error"}`)};try{await upsert("MX","@",mailHost,{prio:"10"});await upsert("TXT","@",spf);if(dkim)await upsert("TXT","mail._domainkey",dkim);await upsert("TXT","_dmarc",dmarc);await upsert("A","mail",ip);logger.success(`Mail DNS published for ${domain} (MX\u2192${mailHost}, SPF, DKIM, DMARC, mail.${domain}\u2192${ip})`)}catch(err){logger.warn(`Mail DNS reconcile skipped for ${domain}: ${getErrorMessage(err)}`)}}export function configDnsDomains(sites){const domains=new Set;for(const site of Object.values(sites))if(!site?.redirect&&site?.domain&&typeof site.domain==="string")domains.add(site.domain.replace(/^www\./,""));return[...domains]}export function dnsProviderNameFromNameservers(nameservers){const normalized=nameservers.map((name)=>name.toLowerCase().replace(/\.$/,""));if(normalized.some((name)=>name.endsWith(".porkbun.com")))return"porkbun";if(normalized.some((name)=>name.endsWith(".ns.cloudflare.com")))return"cloudflare";if(normalized.some((name)=>/(^|\.)awsdns-\d+\.(?:com|net|org|co\.uk)$/.test(name)))return"route53";if(normalized.some((name)=>name.endsWith(".domaincontrol.com")))return"godaddy";return null}async function reconcileConfigDns(sites,logger){const projectDnsConfig=await loadProjectDnsConfig(dnsConfig);if(["a","aaaa","cname","mx","txt"].reduce((total,key)=>total+(Array.isArray(projectDnsConfig?.[key])?projectDnsConfig[key].length:0),0)===0)return;for(const domain of configDnsDomains(sites))try{const result=await syncDnsConfig(domain,projectDnsConfig);if(!result.provider)continue;if(result.created||result.failed)logger.info(`DNS (config/dns.ts) ${domain}: ${result.created} created, ${result.kept} kept${result.failed?`, ${result.failed} failed`:""}`)}catch(err){logger.warn(`DNS (config/dns.ts) reconcile for ${domain} failed: ${err instanceof Error?err.message:String(err)}`)}}async function reconcileHetznerDns(sites,ip,logger,ipv6){const published=[],domains=new Set;for(const site of Object.values(sites))if(site?.domain&&typeof site.domain==="string")domains.add(site.domain.replace(/^www\./,""));if(domains.size===0)return published;const providerConfigs=[];if(process.env.PORKBUN_API_KEY&&process.env.PORKBUN_SECRET_KEY)providerConfigs.push({provider:"porkbun",apiKey:process.env.PORKBUN_API_KEY,secretKey:process.env.PORKBUN_SECRET_KEY});if(process.env.CLOUDFLARE_API_TOKEN)providerConfigs.push({provider:"cloudflare",apiToken:process.env.CLOUDFLARE_API_TOKEN});if(process.env.GODADDY_API_KEY&&process.env.GODADDY_API_SECRET)providerConfigs.push({provider:"godaddy",apiKey:process.env.GODADDY_API_KEY,apiSecret:process.env.GODADDY_API_SECRET,environment:process.env.GODADDY_ENVIRONMENT});if(process.env.AWS_ACCESS_KEY_ID||process.env.AWS_PROFILE)providerConfigs.push({provider:"route53"});if(providerConfigs.length===0){logger.warn("DNS: no DNS provider credentials found (PORKBUN_API_KEY/\u2026); skipping DNS reconciliation.");for(const d of domains)logger.info(` Point manually: A ${d} \u2192 ${ip} and A www.${d} \u2192 ${ip}`);return published}const{createDnsProvider,detectDnsProvider,reconcileAddressRecords}=await import("@stacksjs/ts-cloud");logger.info("Reconciling DNS records...");const resolveA=async(fqdn)=>{try{const{resolve4}=await import("node:dns/promises");return await resolve4(fqdn)}catch{return[]}},resolveAuthoritativeNameservers=async(domain)=>{try{const{resolveNs}=await import("node:dns/promises");return await resolveNs(domain)}catch{return[]}};for(const domain of domains)try{let provider=await detectDnsProvider(domain,providerConfigs).catch((err)=>{logger.warn(` DNS: ignoring a configured provider for ${domain} - its credentials were rejected (${err?.message||err})`);return});if(!provider){const providerName=dnsProviderNameFromNameservers(await resolveAuthoritativeNameservers(domain)),providerConfig=providerConfigs.find((config)=>config.provider===providerName);if(providerConfig)provider=createDnsProvider(providerConfig)}if(!provider){for(const sub of["","www"]){const fqdn=sub?`${sub}.${domain}`:domain,current=await resolveA(fqdn);if(current.includes(ip))logger.success(` DNS: ${fqdn} \u2192 ${ip} (externally managed, already correct)`);else if(current.length===0)logger.warn(` DNS: ${fqdn} does not resolve and no configured provider manages ${domain} \u2014 create it manually: A ${fqdn} \u2192 ${ip}`);else logger.warn(` DNS: ${fqdn} resolves to ${current.join(", ")} but this deploy targets ${ip}, and no configured provider manages ${domain} \u2014 update it manually: A ${fqdn} \u2192 ${ip}`)}continue}for(const sub of["","www"]){const fqdn=sub?`${sub}.${domain}`:domain,report=await reconcileAddressRecords({provider,zone:domain,fqdn,ipv4:ip,ipv6});for(const record of report.published){logger.success(` DNS: ${record.fqdn} \u2192 ${record.content} (${provider.name})`);published.push(record.fqdn)}for(const warning of report.warnings)logger.warn(` DNS: ${warning}`)}}catch(err){logger.warn(` DNS: ${domain} reconciliation failed: ${err?.message||err}`)}return published}async function buildContainerImageWithPantry(args){const{slug,sites,verbose}=args,{execSync}=await import("node:child_process");let cli;for(const candidate of["pantry","ts-pantry"])try{execSync(`command -v ${candidate}`,{stdio:"pipe"});cli=candidate;break}catch{}if(!cli){log.warn("pantry CLI not found on PATH \u2014 skipping container image build. Install pantry to enable `--docker`.");return}const dockerfile="storage/framework/Dockerfile",canPush=Boolean(process.env.PANTRY_REGISTRY_TOKEN||process.env.PANTRY_TOKEN);for(const[siteName,site]of Object.entries(sites)){if(!site?.start)continue;const tag=`${slug}-${siteName}:latest`;log.info(`[${siteName}] building image ${tag} with pantry (native, no Docker daemon)...`);const flags=["build",".","-t",tag,"-f",dockerfile,"--run-mode","skip"];if(canPush)flags.push("--push");execSync(`${cli} ${flags.map((f)=>f.includes(" ")?`'${f}'`:f).join(" ")}`,{stdio:verbose?"inherit":"pipe",maxBuffer:536870912});log.success(`[${siteName}] image built${canPush?" + pushed to the pantry registry":""}`)}}export function deploy(buddy){const descriptions={deploy:"Deploy your project",project:"Target a specific project",production:"Deploy to production",development:"Deploy to development",staging:"Deploy to staging",yes:"Confirm all prompts by default",domain:"Specify a domain to deploy to",verbose:"Enable verbose output"};buddy.command("deploy [env]",descriptions.deploy).option("--domain",descriptions.domain,{default:void 0}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--prod",descriptions.production,{default:!0}).option("--dev",descriptions.development,{default:!1}).option("--yes",descriptions.yes,{default:!1}).option("--site <name>","Deploy only this one site to the existing server (multi-tenant surgical add)",{default:void 0}).option("--staging",descriptions.staging,{default:!1}).option("--docker","Also build an OCI image with pantry (native, no Docker daemon) and push it to the pantry registry",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(envArg,options)=>{log.debug("Running `buddy deploy` ...",options);if(process.argv.includes("--dry-run")||options.dryRun===!0){log.error("`buddy deploy --dry-run` is not supported.");log.info("Deploy has no preview mode: provisioning, release shipping, and DNS all mutate real infrastructure.");log.info("To see what would ship without touching the server, inspect `config/cloud.ts` sites, or run `buddy deploy --site <name>` to narrow a real deploy to one site.");process.exit(ExitCode.FatalError)}await ensureDeployPrerequisites(options.verbose===!0);const deployEnv=envArg||(options.staging?"staging":options.dev?"development":"production"),deployEnvName=deployEnv==="prod"?"production":deployEnv==="dev"?"development":deployEnv;process.env.APP_ENV=deployEnvName;{const envFile=deployEnvName==="production"?".env.production":`.env.${deployEnvName}`;if(existsSync(p.projectPath(envFile)))try{const{loadEnv}=await import("@stacksjs/env");loadEnv({path:envFile,env:deployEnvName,keysFile:".env.keys",overload:!0,quiet:!0})}catch(err){log.warn(`Could not load ${envFile}: ${getErrorMessage(err)}`)}}const tsCloudConfig=await loadTsCloudConfig(deployEnvName);if(tsCloudConfig&&resolveProvider(tsCloudConfig)==="hetzner"){await deployToHetzner(tsCloudConfig,deployEnv,options);return}if(process.env.AWS_ACCESS_KEY_ID&&process.env.AWS_SECRET_ACCESS_KEY)delete process.env.AWS_PROFILE;const startTime=performance.now();console.log("");console.log("\uD83D\uDE80 Deploy");console.log("");let productionUrl;if(deployEnv==="production"||deployEnv==="prod"){const prodEnvPath=p.projectPath(".env.production");if(existsSync(prodEnvPath)){const urlMatch=readFileSync(prodEnvPath,"utf-8").match(/^APP_URL=(.+)$/m);if(urlMatch?.[1]){productionUrl=urlMatch[1].trim();log.debug("Using APP_URL from .env.production:",productionUrl)}}}const envUrl=env.APP_URL,domain=options.domain||productionUrl||envUrl||app.url;if((options.prod||deployEnv==="production"||deployEnv==="prod")&&!options.yes)await confirmProductionDeployment();if(!domain){log.info("No domain found in your .env.production or ./config/app.ts");log.info("Please ensure your domain is properly configured.");log.info("For more info, check out the docs or join our Discord.");process.exit(ExitCode.FatalError)}log.info(`Deploying to ${italic(domain)} (${deployEnv})`);await checkIfAwsIsBootstrapped(options);options.domain=await configureDomain(domain,options,startTime);const result=await runAction(Action.Deploy,options);if(resultFailed(result)){await outro("While running the `buddy deploy`, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Project deployed.",{startTime,useSeconds:!0})});onUnknownSubcommand(buddy,"deploy")}async function confirmProductionDeployment(){if(!process.stdin.isTTY){log.error("Refusing to deploy to production from a non-interactive shell without confirmation.");log.info(" \u27A1\uFE0F Re-run with `--yes` to confirm (e.g. in CI): `buddy deploy --prod --yes`");process.exit(ExitCode.InvalidArgument)}if(!await prompts.confirm({message:"Are you sure you want to deploy to production?",initial:!0})){log.info("Aborting deployment...");process.exit(ExitCode.InvalidArgument)}}async function configureDomain(domain,options,startTime){log.debug("Configuring domain...",domain);if(!domain){log.info("We could not identify a domain to deploy to.");log.warn("Please set your .env or ./config/app.ts properly.");log.info("Alternatively, specify a domain to deploy via the `--domain` flag.");console.log("");log.info(" \u27A1\uFE0F Example: `buddy deploy --domain example.com`");console.log("");process.exit(ExitCode.FatalError)}if(domain.includes("localhost")){log.info("You are deploying to a local environment.");log.warn("Please set your .env or ./config/app.ts properly. The domain we are deploying cannot be a `localhost` domain.");log.info("Alternatively, specify a domain to deploy via the `--domain` flag.");console.log("");log.info(" \u27A1\uFE0F Example: `buddy deploy --domain example.com`");console.log("");process.exit(ExitCode.FatalError)}if(await hasUserDomainBeenAddedToCloud(domain)){log.info("Domain is properly configured");log.info("Your cloud is deploying...");log.info(`${italic("This may take a while...")}`);return domain}console.log("");log.info(` \uD83D\uDC4B It appears to be your first ${italic(domain)} deployment.`);console.log("");log.info(italic("Let\u2019s ensure it is all connected properly."));log.info(italic("One moment..."));console.log("");const result=await addDomain({...options,deploy:!0,startTime});if(resultFailed(result)){await outro("While running the `buddy deploy`, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Added your domain.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)}async function promptAndSaveCredentials(){const accessKeyId=await prompts.text({message:"AWS Access Key ID:",validate:(value)=>value.length>0?!0:"Access Key ID is required"});if(!accessKeyId){log.info("Deployment cancelled");process.exit(ExitCode.Success)}const secretAccessKey=await prompts.password({message:"AWS Secret Access Key:",validate:(value)=>value.length>0?!0:"Secret Access Key is required"});if(!secretAccessKey){log.info("Deployment cancelled");process.exit(ExitCode.Success)}const region=await prompts.text({message:"AWS Region:",initial:"us-east-1"});if(!region){log.info("Deployment cancelled");process.exit(ExitCode.Success)}const{setEnv}=await import("@stacksjs/env");await setEnv("AWS_ACCESS_KEY_ID",accessKeyId,{file:".env.production",encrypt:!0});await setEnv("AWS_SECRET_ACCESS_KEY",secretAccessKey,{file:".env.production",encrypt:!0});await setEnv("AWS_REGION",region||"us-east-1",{file:".env.production"});process.env.AWS_ACCESS_KEY_ID=accessKeyId;process.env.AWS_SECRET_ACCESS_KEY=secretAccessKey;process.env.AWS_REGION=region||"us-east-1";log.success("AWS credentials saved securely to .env.production");console.log("")}function loadAwsCredentialsFromEnv(){const environment=process.env.APP_ENV||process.env.NODE_ENV||"production",envFiles=[p.projectPath(`.env.${environment}`),p.projectPath(".env")];for(const envPath of envFiles){if(!existsSync(envPath))continue;try{const lines=readFileSync(envPath,"utf-8").split(`
266
266
  `);let accessKeyId,secretAccessKey,region,accountId;for(const line of lines){const trimmed=line.trim();if(trimmed.startsWith("#")||!trimmed.includes("="))continue;const[key,...valueParts]=trimmed.split("="),value=valueParts.join("=").trim();if(key==="AWS_ACCESS_KEY_ID"&&value)accessKeyId=value;else if(key==="AWS_SECRET_ACCESS_KEY"&&value)secretAccessKey=value;else if(key==="AWS_REGION"&&value)region=value;else if(key==="AWS_ACCOUNT_ID"&&value)accountId=value}if(accessKeyId&&secretAccessKey){log.debug(`Found AWS credentials in ${envPath}`);return{accessKeyId,secretAccessKey,region,accountId}}}catch(error){log.debug(`Failed to read ${envPath} file:`,error)}}return{}}async function checkIfAwsIsBootstrapped(options){let handlingAlreadyExists=!1;try{log.info("Ensuring AWS cloud stack exists...");let hasCredentials=process.env.AWS_ACCESS_KEY_ID&&process.env.AWS_SECRET_ACCESS_KEY;if(!hasCredentials){const envCredentials=loadAwsCredentialsFromEnv();if(envCredentials.accessKeyId&&envCredentials.secretAccessKey){process.env.AWS_ACCESS_KEY_ID=envCredentials.accessKeyId;process.env.AWS_SECRET_ACCESS_KEY=envCredentials.secretAccessKey;if(envCredentials.region&&!process.env.AWS_REGION)process.env.AWS_REGION=envCredentials.region;if(envCredentials.accountId&&!process.env.AWS_ACCOUNT_ID)process.env.AWS_ACCOUNT_ID=envCredentials.accountId;hasCredentials=!0;const environment=process.env.APP_ENV||process.env.NODE_ENV||"production";log.success(`Using AWS credentials from .env.${environment}`)}}if(!hasCredentials){const fileCredentials=loadAwsCredentialsFromFile();if(fileCredentials.accessKeyId&&fileCredentials.secretAccessKey){process.env.AWS_ACCESS_KEY_ID=fileCredentials.accessKeyId;process.env.AWS_SECRET_ACCESS_KEY=fileCredentials.secretAccessKey;if(fileCredentials.region&&!process.env.AWS_REGION)process.env.AWS_REGION=fileCredentials.region;hasCredentials=!0;log.success("Using AWS credentials from ~/.aws/credentials")}}if(!hasCredentials){log.info("AWS credentials not found in .env or ~/.aws/credentials.");log.info("You can either:");log.info(" 1. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env.production");log.info(" 2. Add credentials to ~/.aws/credentials");log.info(" 3. Configure them interactively below");console.log("");if(options?.yes){log.info("Skipping credential setup (--yes flag provided)");process.exit(ExitCode.FatalError)}const setupCredentials=await prompts.confirm({message:"Would you like to configure AWS credentials now?",initial:!0});log.debug("setupCredentials response:",setupCredentials,typeof setupCredentials);if(setupCredentials===void 0||setupCredentials===!1){if(setupCredentials===void 0){console.log("");log.info("Deployment cancelled");process.exit(ExitCode.Success)}console.log("");log.info("Skipping cloud infrastructure check");log.info("You can configure AWS credentials later by running: buddy configure:aws");return!0}await promptAndSaveCredentials()}else log.success("AWS credentials found");const appName=(process.env.APP_NAME||app.name||"stacks").toLowerCase().replace(/[^a-z0-9-]/g,"-"),stackName=`${appName}-cloud`,{AWSCloudFormationClient}=await import("@stacksjs/ts-cloud"),cfnClient=new AWSCloudFormationClient(process.env.AWS_REGION||"us-east-1");let stackExists=!1,needsEmailUpdate=!1;try{const stack=(await cfnClient.describeStacks({stackName})).Stacks?.[0];if(stack){stackExists=!0;log.success("Cloud stack exists");const{AWSCloudFormationClient}=await import("@stacksjs/ts-cloud"),resources=await new AWSCloudFormationClient(process.env.AWS_REGION||"us-east-1").listStackResources(stackName),hasEmailBucket=resources.StackResourceSummaries?.some((r)=>r.LogicalResourceId==="EmailBucket"),hasOutboundLambda=resources.StackResourceSummaries?.some((r)=>r.LogicalResourceId==="OutboundEmailLambda"),hasConversionLambda=resources.StackResourceSummaries?.some((r)=>r.LogicalResourceId==="EmailConversionLambda"),hasNotificationTopic=resources.StackResourceSummaries?.some((r)=>r.LogicalResourceId==="EmailNotificationTopic"),hasMailApiLambda=resources.StackResourceSummaries?.some((r)=>r.LogicalResourceId==="MailApiLambda"),hasMailUsersTable=resources.StackResourceSummaries?.some((r)=>r.LogicalResourceId==="MailUsersTable"),hasMailServerInstance=resources.StackResourceSummaries?.some((r)=>r.LogicalResourceId==="MailServerInstance"),currentEmailDomain=stack.Outputs?.find((o)=>o.OutputKey==="EmailDomain")?.OutputValue,configuredDomain=(emailConfig?.from?.address?.includes("@")?emailConfig.from.address.split("@")[1]:void 0)||"stacksjs.com";if(!hasEmailBucket&&emailConfig?.server?.scan!==void 0){log.info("Email infrastructure not found in stack, will update...");needsEmailUpdate=!0}else if(currentEmailDomain&&currentEmailDomain!==configuredDomain){log.info(`Email domain changed: ${currentEmailDomain} -> ${configuredDomain}, will update...`);needsEmailUpdate=!0}else if(hasEmailBucket&&(!hasOutboundLambda||!hasConversionLambda||!hasNotificationTopic)){log.info("Email infrastructure incomplete, will update...");needsEmailUpdate=!0}else if(hasEmailBucket&&(!hasMailApiLambda||!hasMailUsersTable)){log.info("Mail API infrastructure missing, will update...");needsEmailUpdate=!0}else if(hasEmailBucket&&!hasMailServerInstance&&emailConfig?.server?.enabled){log.info("Mail server EC2 instance missing, will update...");needsEmailUpdate=!0}const currentMode=(stack.Outputs||[]).find((o)=>o.OutputKey==="MailServerMode")?.OutputValue,configuredMode=emailConfig?.server?.mode||"serverless";if(currentMode&&currentMode!==configuredMode){log.info(`Mail server mode changed: ${currentMode} -> ${configuredMode}, will update...`);needsEmailUpdate=!0}if(hasMailServerInstance&&emailConfig?.server?.enabled){if(process.env.FORCE_MAIL_UPDATE==="true"){log.info("Forcing mail server update...");needsEmailUpdate=!0}}if(!needsEmailUpdate)return!0}}catch(error){const caught=error&&typeof error==="object"?error:{message:String(error)};log.debug(`Stack not found: ${getErrorMessage(error)}`)}if(!stackExists)log.info("Cloud stack not found, will be created by deploy action");return!0}catch(err){if(!handlingAlreadyExists){log.error("Error checking cloud infrastructure");log.error(`Error: ${getErrorMessage(err)}`);if(options?.verbose)console.error(err)}process.exit(ExitCode.FatalError)}}
@@ -1,4 +1,4 @@
1
- import{createHash}from"node:crypto";import{chmodSync,copyFileSync,existsSync,mkdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{basename,join,resolve}from"node:path";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";const REQUIRED_TOOLS=["codesign","security","productbuild","pkgutil","xcrun"];function env(name){return process.env[name]?.trim()||""}function xml(value){return value.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}function packageMetadata(){const packageJson=JSON.parse(readFileSync(projectPath("package.json"),"utf8"));return{name:packageJson.name||"Stacks",version:packageJson.version||"0.1.0"}}export function resolveAppleDesktopConfig(options={}){const metadata=packageMetadata(),provisioningProfile=options.provisioningProfile||env("APPLE_PROVISIONING_PROFILE"),apiKeyPath=options.apiKeyPath||env("APP_STORE_CONNECT_API_KEY_PATH");return{appName:options.appName||env("APPLE_APP_NAME")||env("APP_NAME")||metadata.name,bundleId:options.bundleId||env("APPLE_BUNDLE_ID"),teamId:options.teamId||env("APPLE_TEAM_ID"),version:options.appVersion||env("APPLE_APP_VERSION")||metadata.version,buildNumber:options.buildNumber||env("APPLE_BUILD_NUMBER")||env("GITHUB_RUN_NUMBER")||String(Math.floor(Date.now()/1000)),minimumMacos:options.minimumMacos||env("APPLE_MINIMUM_MACOS")||"13.0",category:options.category||env("APPLE_APP_CATEGORY")||"public.app-category.productivity",appSigningIdentity:options.appSigningIdentity||env("APPLE_APP_SIGNING_IDENTITY"),installerSigningIdentity:options.installerSigningIdentity||env("APPLE_INSTALLER_SIGNING_IDENTITY"),provisioningProfile:provisioningProfile?resolve(provisioningProfile):"",apiKeyId:options.apiKeyId||env("APP_STORE_CONNECT_API_KEY_ID"),apiIssuerId:options.apiIssuerId||env("APP_STORE_CONNECT_API_ISSUER_ID"),apiKeyPath:apiKeyPath?resolve(apiKeyPath):"",icon:options.icon||env("APPLE_APP_ICON")||void 0}}export function validateAppleDesktopConfig(config,includeApi=!0){const errors=[];if(process.platform!=="darwin")errors.push("Mac App Store packaging must run on macOS");if(!config.appName)errors.push("APPLE_APP_NAME or --app-name is required");if(!/^[A-Za-z0-9][A-Za-z0-9.-]+$/.test(config.bundleId))errors.push("APPLE_BUNDLE_ID must be a reverse-DNS bundle identifier");if(!/^[A-Z0-9]{10}$/.test(config.teamId))errors.push("APPLE_TEAM_ID must be the 10-character Apple Developer team ID");if(!/^\d+(?:\.\d+){0,2}$/.test(config.version))errors.push("The marketing version must contain one to three numeric components");if(!/^[A-Za-z0-9.-]+$/.test(config.buildNumber))errors.push("The build number must contain only letters, numbers, periods, and hyphens");if(!config.appSigningIdentity)errors.push("APPLE_APP_SIGNING_IDENTITY is required");if(!config.installerSigningIdentity)errors.push("APPLE_INSTALLER_SIGNING_IDENTITY is required");if(!config.provisioningProfile||!existsSync(config.provisioningProfile))errors.push("APPLE_PROVISIONING_PROFILE must point to an existing .provisionprofile file");if(config.icon&&!existsSync(resolve(config.icon)))errors.push(`Apple app icon does not exist: ${config.icon}`);if(includeApi){if(!config.apiKeyId)errors.push("APP_STORE_CONNECT_API_KEY_ID is required");if(!config.apiIssuerId)errors.push("APP_STORE_CONNECT_API_ISSUER_ID is required");if(!config.apiKeyPath||!existsSync(config.apiKeyPath))errors.push("APP_STORE_CONNECT_API_KEY_PATH must point to an existing .p8 file")}for(const tool of REQUIRED_TOOLS)if(!Bun.which(tool))errors.push(`Required Apple command is unavailable: ${tool}`);return errors}export function renderInfoPlist(config,executable="stacks-desktop"){const iconEntry=config.icon?`
1
+ import{createHash}from"node:crypto";import{chmodSync,copyFileSync,existsSync,mkdirSync,readFileSync,rmSync,writeFileSync}from"node:fs";import{basename,join,resolve}from"node:path";import process from"node:process";import{log}from"@stacksjs/cli";import{projectPath,storagePath}from"@stacksjs/path";import{resultFailed}from"../result";const REQUIRED_TOOLS=["codesign","security","productbuild","pkgutil","xcrun"];function env(name){return process.env[name]?.trim()||""}function xml(value){return value.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}function packageMetadata(){const packageJson=JSON.parse(readFileSync(projectPath("package.json"),"utf8"));return{name:packageJson.name||"Stacks",version:packageJson.version||"0.1.0"}}export function resolveAppleDesktopConfig(options={}){const metadata=packageMetadata(),provisioningProfile=options.provisioningProfile||env("APPLE_PROVISIONING_PROFILE"),apiKeyPath=options.apiKeyPath||env("APP_STORE_CONNECT_API_KEY_PATH");return{appName:options.appName||env("APPLE_APP_NAME")||env("APP_NAME")||metadata.name,bundleId:options.bundleId||env("APPLE_BUNDLE_ID"),teamId:options.teamId||env("APPLE_TEAM_ID"),version:options.appVersion||env("APPLE_APP_VERSION")||metadata.version,buildNumber:options.buildNumber||env("APPLE_BUILD_NUMBER")||env("GITHUB_RUN_NUMBER")||String(Math.floor(Date.now()/1000)),minimumMacos:options.minimumMacos||env("APPLE_MINIMUM_MACOS")||"13.0",category:options.category||env("APPLE_APP_CATEGORY")||"public.app-category.productivity",appSigningIdentity:options.appSigningIdentity||env("APPLE_APP_SIGNING_IDENTITY"),installerSigningIdentity:options.installerSigningIdentity||env("APPLE_INSTALLER_SIGNING_IDENTITY"),provisioningProfile:provisioningProfile?resolve(provisioningProfile):"",apiKeyId:options.apiKeyId||env("APP_STORE_CONNECT_API_KEY_ID"),apiIssuerId:options.apiIssuerId||env("APP_STORE_CONNECT_API_ISSUER_ID"),apiKeyPath:apiKeyPath?resolve(apiKeyPath):"",icon:options.icon||env("APPLE_APP_ICON")||void 0}}export function validateAppleDesktopConfig(config,includeApi=!0){const errors=[];if(process.platform!=="darwin")errors.push("Mac App Store packaging must run on macOS");if(!config.appName)errors.push("APPLE_APP_NAME or --app-name is required");if(!/^[A-Za-z0-9][A-Za-z0-9.-]+$/.test(config.bundleId))errors.push("APPLE_BUNDLE_ID must be a reverse-DNS bundle identifier");if(!/^[A-Z0-9]{10}$/.test(config.teamId))errors.push("APPLE_TEAM_ID must be the 10-character Apple Developer team ID");if(!/^\d+(?:\.\d+){0,2}$/.test(config.version))errors.push("The marketing version must contain one to three numeric components");if(!/^[A-Za-z0-9.-]+$/.test(config.buildNumber))errors.push("The build number must contain only letters, numbers, periods, and hyphens");if(!config.appSigningIdentity)errors.push("APPLE_APP_SIGNING_IDENTITY is required");if(!config.installerSigningIdentity)errors.push("APPLE_INSTALLER_SIGNING_IDENTITY is required");if(!config.provisioningProfile||!existsSync(config.provisioningProfile))errors.push("APPLE_PROVISIONING_PROFILE must point to an existing .provisionprofile file");if(config.icon&&!existsSync(resolve(config.icon)))errors.push(`Apple app icon does not exist: ${config.icon}`);if(includeApi){if(!config.apiKeyId)errors.push("APP_STORE_CONNECT_API_KEY_ID is required");if(!config.apiIssuerId)errors.push("APP_STORE_CONNECT_API_ISSUER_ID is required");if(!config.apiKeyPath||!existsSync(config.apiKeyPath))errors.push("APP_STORE_CONNECT_API_KEY_PATH must point to an existing .p8 file")}for(const tool of REQUIRED_TOOLS)if(!Bun.which(tool))errors.push(`Required Apple command is unavailable: ${tool}`);return errors}export function renderInfoPlist(config,executable="stacks-desktop"){const iconEntry=config.icon?`
2
2
  <key>CFBundleIconFile</key>
3
3
  <string>AppIcon</string>`:"";return`<?xml version="1.0" encoding="UTF-8"?>
4
4
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -93,7 +93,7 @@ jobs:
93
93
  s3-prefix: \${{ vars.RELEASE_S3_PREFIX }}
94
94
  s3-public-url: \${{ vars.RELEASE_S3_PUBLIC_URL }}
95
95
  secrets: inherit
96
- `}function command(args,cwd=projectPath()){const result=Bun.spawnSync(args,{cwd,stdin:"inherit",stdout:"inherit",stderr:"inherit"});if(result.exitCode!==0)throw Error(`${args[0]} ${args.slice(1).join(" ")} exited with code ${result.exitCode}`)}function signingIdentityExists(identity){if(!identity)return!1;const result=Bun.spawnSync(["security","find-identity","-v"],{stdout:"pipe",stderr:"pipe"});return result.exitCode===0&&result.stdout.toString().includes(identity)}function provisioningProfileMatches(config){if(!config.provisioningProfile||!existsSync(config.provisioningProfile))return!1;const result=Bun.spawnSync(["security","cms","-D","-i",config.provisioningProfile],{stdout:"pipe",stderr:"pipe"});if(result.exitCode!==0)return!1;const profile=result.stdout.toString();return profile.includes(config.bundleId)&&profile.includes(config.teamId)}async function buildDesktop(){const{runAction}=await import("@stacksjs/actions"),{Action}=await import("@stacksjs/enums"),result=await runAction(Action.BuildDesktop);if(result.isErr)throw result.error}function sha256(path){return createHash("sha256").update(readFileSync(path)).digest("hex")}async function packageAppleDesktop(config,skipBuild=!1){const errors=validateAppleDesktopConfig(config,!1);if(!signingIdentityExists(config.appSigningIdentity))errors.push(`App signing identity is not installed: ${config.appSigningIdentity}`);if(!signingIdentityExists(config.installerSigningIdentity))errors.push(`Installer signing identity is not installed: ${config.installerSigningIdentity}`);if(!provisioningProfileMatches(config))errors.push(`Provisioning profile does not match ${config.teamId}.${config.bundleId}`);if(errors.length)throw Error(errors.join(`
96
+ `}function command(args,cwd=projectPath()){const result=Bun.spawnSync(args,{cwd,stdin:"inherit",stdout:"inherit",stderr:"inherit"});if(result.exitCode!==0)throw Error(`${args[0]} ${args.slice(1).join(" ")} exited with code ${result.exitCode}`)}function signingIdentityExists(identity){if(!identity)return!1;const result=Bun.spawnSync(["security","find-identity","-v"],{stdout:"pipe",stderr:"pipe"});return result.exitCode===0&&result.stdout.toString().includes(identity)}function provisioningProfileMatches(config){if(!config.provisioningProfile||!existsSync(config.provisioningProfile))return!1;const result=Bun.spawnSync(["security","cms","-D","-i",config.provisioningProfile],{stdout:"pipe",stderr:"pipe"});if(result.exitCode!==0)return!1;const profile=result.stdout.toString();return profile.includes(config.bundleId)&&profile.includes(config.teamId)}async function buildDesktop(){const{runAction}=await import("@stacksjs/actions"),{Action}=await import("@stacksjs/enums"),result=await runAction(Action.BuildDesktop);if(resultFailed(result))throw result.error}function sha256(path){return createHash("sha256").update(readFileSync(path)).digest("hex")}async function packageAppleDesktop(config,skipBuild=!1){const errors=validateAppleDesktopConfig(config,!1);if(!signingIdentityExists(config.appSigningIdentity))errors.push(`App signing identity is not installed: ${config.appSigningIdentity}`);if(!signingIdentityExists(config.installerSigningIdentity))errors.push(`Installer signing identity is not installed: ${config.installerSigningIdentity}`);if(!provisioningProfileMatches(config))errors.push(`Provisioning profile does not match ${config.teamId}.${config.bundleId}`);if(errors.length)throw Error(errors.join(`
97
97
  `));if(!skipBuild)await buildDesktop();const desktopDist=storagePath("framework/desktop-dist"),launcher=join(desktopDist,"stacks-desktop"),runtime=join(desktopDist,"craft-runtime"),manifest=join(desktopDist,"desktop.json");for(const path of[launcher,runtime,manifest])if(!existsSync(path))throw Error(`Desktop build artifact is missing: ${path}`);const appleDir=join(desktopDist,"apple"),appPath=join(appleDir,`${config.appName}.app`),contents=join(appPath,"Contents"),macosDir=join(contents,"MacOS"),resourcesDir=join(contents,"Resources");if(existsSync(appleDir))rmSync(appleDir,{recursive:!0});mkdirSync(macosDir,{recursive:!0});mkdirSync(resourcesDir,{recursive:!0});copyFileSync(launcher,join(macosDir,"stacks-desktop"));copyFileSync(runtime,join(macosDir,"craft-runtime"));copyFileSync(manifest,join(macosDir,"desktop.json"));copyFileSync(config.provisioningProfile,join(contents,"embedded.provisionprofile"));chmodSync(join(macosDir,"stacks-desktop"),493);chmodSync(join(macosDir,"craft-runtime"),493);if(config.icon)copyFileSync(resolve(config.icon),join(resourcesDir,"AppIcon.icns"));const infoPlist=join(contents,"Info.plist"),appEntitlements=join(appleDir,"app.entitlements"),helperEntitlements=join(appleDir,"helper.entitlements");writeFileSync(infoPlist,renderInfoPlist(config));writeFileSync(appEntitlements,renderAppEntitlements(config));writeFileSync(helperEntitlements,renderHelperEntitlements());command(["plutil","-lint",infoPlist,appEntitlements,helperEntitlements]);command(["codesign","--force","--timestamp","--options","runtime","--entitlements",helperEntitlements,"--sign",config.appSigningIdentity,join(macosDir,"craft-runtime")]);command(["codesign","--force","--timestamp","--options","runtime","--entitlements",appEntitlements,"--sign",config.appSigningIdentity,appPath]);command(["codesign","--verify","--deep","--strict","--verbose=2",appPath]);command(["codesign","-d","--entitlements",":-",appPath]);const packagePath=join(appleDir,`${config.appName}-${config.version}-${config.buildNumber}.pkg`);command(["productbuild","--component",appPath,"/Applications","--sign",config.installerSigningIdentity,packagePath]);command(["pkgutil","--check-signature",packagePath]);const packageHash=sha256(packagePath);writeFileSync(join(appleDir,"checksums.sha256"),`${packageHash} ${basename(packagePath)}
98
98
  `);writeFileSync(join(appleDir,"apple-provenance.json"),`${JSON.stringify({schemaVersion:"1.0.0",appName:config.appName,bundleId:config.bundleId,teamId:config.teamId,version:config.version,buildNumber:config.buildNumber,minimumMacos:config.minimumMacos,package:{name:basename(packagePath),sha256:packageHash},sourceRevision:Bun.spawnSync(["git","rev-parse","HEAD"],{cwd:projectPath()}).stdout.toString().trim(),craft:{sha256:sha256(join(macosDir,"craft-runtime"))}},null,2)}
99
99
  `);return packagePath}function validateOrUpload(packagePath,config,validateOnly){const errors=validateAppleDesktopConfig(config,!0);if(errors.length)throw Error(errors.join(`