@stacksjs/buddy 0.74.63 → 0.74.65

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.
@@ -1 +1 @@
1
- import{chmodSync,cpSync,existsSync,readFileSync,readdirSync,rmSync,writeFileSync}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";import{fetchPublishedVersions}from"../registry";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) - 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)){await log.error(result.error);process.exit(ExitCode.FatalError)}ensureExecutableScripts(path);applyAppVcsTemplate(path);applyAppConfigTemplate(path);removeFrameworkTests(path);await ensureEnv(path,options);if(options.minimal)await stripFeatures(path);await install(path,options);if(!options.withCore)await unvendorCore(path,options);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");await log.flush();process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"new")}function isFolderCheck(path){if(!isFolder(path))return;if(readdirSync(path).filter((entry)=>entry!==".git").length===0)return;console.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.");await log.flush();process.exit(ExitCode.FatalError)}async function isOnline(){try{return(await fetch("https://github.com",{method:"HEAD",signal:AbortSignal.timeout(3000)})).ok}catch{return!1}}export async function templateRef(fetchVersions=fetchPublishedVersions){try{const{latest}=await fetchVersions("stacks");return latest?`v${latest}`:null}catch{return null}}export function templateSpec(ref){return ref?`gh:stacksjs/stacks#${ref}`:"gh:stacksjs/stacks"}async function download(name,path,_options){log.info("Setting up your stack.");const ref=await templateRef();try{const{downloadTemplate}=await import("@stacksjs/gitit");try{await downloadTemplate(templateSpec(ref),{dir:name,force:!0})}catch(error){if(!ref)throw error;log.warn(`No ${ref} tag to scaffold from (${error instanceof Error?error.message:String(error)}).`);log.info("Falling back to the default branch. If this app does not typecheck, that gap is why.");await downloadTemplate(templateSpec(null),{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 applyAppVcsTemplate(path){const source=resolve(path,"storage/framework/defaults/vcs/github"),destination=resolve(path,".github");if(!existsSync(source)){log.warn("No app CI template found at storage/framework/defaults/vcs/github - leaving .github as downloaded.");return}log.info("Installing app-shaped GitHub workflows...");try{rmSync(destination,{recursive:!0,force:!0});cpSync(source,destination,{recursive:!0});log.success("App CI installed")}catch(error){log.warn(`Could not install the app CI template: ${error instanceof Error?error.message:String(error)}`)}}function applyAppConfigTemplate(path){const source=resolve(path,"storage/framework/defaults/scaffold/config"),destination=resolve(path,"config");if(!existsSync(source)){log.warn("No app config template found at storage/framework/defaults/scaffold/config - leaving config as downloaded.");return}const slug=path.replace(/\/+$/,"").split("/").pop()||"stacks-app",displayName=slug.split(/[-_\s]+/).filter(Boolean).map((part)=>`${part.charAt(0).toUpperCase()}${part.slice(1)}`).join(" ");log.info("Installing app-safe infrastructure configuration...");try{for(const file of readdirSync(source)){if(!file.endsWith(".ts"))continue;const rendered=readFileSync(resolve(source,file),"utf8").replaceAll("__APP_NAME__",displayName).replaceAll("__APP_SLUG__",slug);writeFileSync(resolve(destination,file),rendered)}log.success("App-safe infrastructure configuration installed")}catch(error){log.warn(`Could not install the app config template: ${error instanceof Error?error.message:String(error)}`)}}function removeFrameworkTests(path){const tests=resolve(path,"tests");if(!existsSync(tests))return;log.info("Removing the framework's own test suite...");for(const entry of readdirSync(tests)){if(entry==="setup.ts")continue;rmSync(resolve(tests,entry),{recursive:!0,force:!0})}log.success("App starts with a clean test suite")}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)){await 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)){await 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)){await 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)){const reason=result.error instanceof Error?result.error.stack??result.error.message:String(result.error??"").trim();log.error("Could not resolve the framework from npm.");log.error(reason.length>0?reason:"The step failed without reporting a reason.");log.error("");log.error(`The project at ${path} is half converted: the vendored framework has been`);log.error("removed and the published packages are not in place yet, so ./buddy cannot");log.error("boot there. Finish it by hand with:");log.error("");await log.error(` cd ${path} && rm -f node_modules/stacks && bun install`);await log.error("");await log.error("Or start over with `--with-core` to keep the framework vendored.");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 - run `./buddy <feature>:install` to add features back.")}
1
+ import{chmodSync,cpSync,existsSync,readFileSync,readdirSync,rmSync,writeFileSync}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";import{appIdentity,applyAppEnvTemplate,renderTemplate}from"../scaffold-app";import{applyAppSiteTemplate}from"../scaffold-site";import{fetchPublishedVersions}from"../registry";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) - 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)){await log.error(result.error);process.exit(ExitCode.FatalError)}ensureExecutableScripts(path);applyAppVcsTemplate(path);applyAppConfigTemplate(path);applyAppEnv(path);replaceFrameworkSite(path);removeFrameworkTests(path);await ensureEnv(path,options);if(options.minimal)await stripFeatures(path);await install(path,options);if(!options.withCore)await unvendorCore(path,options);if(options.minimal)await regenerateAutoImports(path,options);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");await log.flush();process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"new")}function isFolderCheck(path){if(!isFolder(path))return;if(readdirSync(path).filter((entry)=>entry!==".git").length===0)return;console.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.");await log.flush();process.exit(ExitCode.FatalError)}async function isOnline(){try{return(await fetch("https://github.com",{method:"HEAD",signal:AbortSignal.timeout(3000)})).ok}catch{return!1}}export async function templateRef(fetchVersions=fetchPublishedVersions){try{const{latest}=await fetchVersions("stacks");return latest?`v${latest}`:null}catch{return null}}export function templateSpec(ref){return ref?`gh:stacksjs/stacks#${ref}`:"gh:stacksjs/stacks"}async function download(name,path,_options){log.info("Setting up your stack.");const ref=await templateRef();try{const{downloadTemplate}=await import("@stacksjs/gitit");try{await downloadTemplate(templateSpec(ref),{dir:name,force:!0})}catch(error){if(!ref)throw error;log.warn(`No ${ref} tag to scaffold from (${error instanceof Error?error.message:String(error)}).`);log.info("Falling back to the default branch. If this app does not typecheck, that gap is why.");await downloadTemplate(templateSpec(null),{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 applyAppVcsTemplate(path){const source=resolve(path,"storage/framework/defaults/vcs/github"),destination=resolve(path,".github");if(!existsSync(source)){log.warn("No app CI template found at storage/framework/defaults/vcs/github - leaving .github as downloaded.");return}log.info("Installing app-shaped GitHub workflows...");try{rmSync(destination,{recursive:!0,force:!0});cpSync(source,destination,{recursive:!0});log.success("App CI installed")}catch(error){log.warn(`Could not install the app CI template: ${error instanceof Error?error.message:String(error)}`)}}function applyAppConfigTemplate(path){const source=resolve(path,"storage/framework/defaults/scaffold/config"),destination=resolve(path,"config");if(!existsSync(source)){log.warn("No app config template found at storage/framework/defaults/scaffold/config - leaving config as downloaded.");return}const identity=appIdentity(path);log.info("Installing app-safe infrastructure configuration...");try{for(const file of readdirSync(source)){if(!file.endsWith(".ts"))continue;const template=readFileSync(resolve(source,file),"utf8");writeFileSync(resolve(destination,file),renderTemplate(template,identity))}log.success("App-safe infrastructure configuration installed")}catch(error){log.warn(`Could not install the app config template: ${error instanceof Error?error.message:String(error)}`)}}function applyAppEnv(path){try{const changed=applyAppEnvTemplate(path,appIdentity(path));if(changed.length>0)log.success(`Named the app in .env.example (${changed.join(", ")})`)}catch(error){log.warn(`Could not name the app in .env.example: ${error instanceof Error?error.message:String(error)}`)}}function replaceFrameworkSite(path){log.info("Replacing the stacksjs.com site with a starter page...");try{const removed=applyAppSiteTemplate(path);log.success(`Starter page installed (${removed.length} stacksjs.com path${removed.length===1?"":"s"} removed)`)}catch(error){log.warn(`Could not install the starter page: ${error instanceof Error?error.message:String(error)}`)}}function removeFrameworkTests(path){const tests=resolve(path,"tests");if(!existsSync(tests))return;log.info("Removing the framework's own test suite...");for(const entry of readdirSync(tests)){if(entry==="setup.ts")continue;rmSync(resolve(tests,entry),{recursive:!0,force:!0})}log.success("App starts with a clean test suite")}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)){await 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)){await 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)){await 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)){const reason=result.error instanceof Error?result.error.stack??result.error.message:String(result.error??"").trim();log.error("Could not resolve the framework from npm.");log.error(reason.length>0?reason:"The step failed without reporting a reason.");log.error("");log.error(`The project at ${path} is half converted: the vendored framework has been`);log.error("removed and the published packages are not in place yet, so ./buddy cannot");log.error("boot there. Finish it by hand with:");log.error("");await log.error(` cd ${path} && rm -f node_modules/stacks && bun install`);await log.error("");await log.error("Or start over with `--with-core` to keep the framework vendored.");process.exit(ExitCode.FatalError)}log.success("Framework resolved from npm")}async function regenerateAutoImports(path,options){const result=await runCommand("./buddy generate --types",{...options,cwd:path});if(resultFailed(result))log.warn("Could not regenerate the auto-import barrels. Run `./buddy generate --types` in the project before `./buddy dev`.")}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 - run `./buddy <feature>:install` to add features back.")}
@@ -1 +1 @@
1
- import process from"node:process";import{log,onUnknownSubcommand}from"@stacksjs/cli";import{decryptEnv,encryptEnv,getEnv,getKeypair,resolveEnvFile,rotateKeypair,setEnv}from"@stacksjs/env";import{ExitCode}from"@stacksjs/types";export function env(buddy){const descriptions={env:"Interact with your environment variables",get:"Get an environment variable",set:"Set an environment variable",encrypt:"Encrypt a value",decrypt:"Decrypt a value",check:"Check environment configuration and validate setup",pretty:"Pretty print the result",stdout:"Output the result to stdout",keypair:"Generate a keypair",fileKeys:"The path to the file containing the keys",excludeKey:"The key to exclude from encryption",rotate:"Rotate a keypair",rotateStdout:"Print the rotated file and its new keypair without writing either",rotateDryRun:"Report what would change without writing anything",format:"The format to output the result (json, shell, eval)",all:"Get all environment variables",file:"The environment file to use",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("env:get [key]",descriptions.get).option("-f, --file [file]",descriptions.file,{default:""}).option("--format [format]",descriptions.format,{default:"json"}).option("-a, --all",descriptions.all,{default:!1}).option("-p, --pretty",descriptions.pretty,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy env:get SECRET").example("buddy env:get SECRET --file .env.production").example("buddy env:get --all --pretty").example("buddy env:get --format shell").example("buddy env:get --format eval").example("buddy env:get --format json").action(async(key,options)=>{log.debug("Running `buddy env:get` ...",options);const result=getEnv(key,{file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,all:options.all,format:options.format,prettyPrint:options.pretty});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:set [key] [value]",descriptions.set).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("--plain","Don't encrypt the value",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy env:set SECRET=value").example("buddy env:set SECRET value").example("buddy env:set SECRET value --file .env.production").example("buddy env:set SECRET value --plain").action(async(key,value,options)=>{log.debug("Running `buddy env:set` ...",options);if(!key){console.error("A key is required. Pass an empty value to clear it: `buddy env:set KEY ''`");process.exit(ExitCode.FatalError)}if(value===void 0){console.error(`No value given for ${key}. Pass one, or an empty string to clear it: \`buddy env:set ${key} ''\``);process.exit(ExitCode.FatalError)}const result=setEnv(key,value,{file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,plain:options.plain});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:encrypt [key]",descriptions.encrypt).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("-o, --stdout",descriptions.stdout,{default:!1}).option("-ek, --exclude-key [excludeKey]",descriptions.excludeKey,{default:""}).example("buddy env:encrypt").example("buddy env:encrypt --file .env.production").example('buddy env:encrypt -k "SECRET_*"').action(async(key,options)=>{log.debug("Running `buddy env:encrypt` ...",options);const result=encryptEnv({file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,key,excludeKey:options.excludeKey,stdout:options.stdout});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:decrypt [key]",descriptions.decrypt).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("-o, --stdout",descriptions.stdout,{default:!1}).example("buddy env:decrypt").example("buddy env:decrypt --file .env.production").example('buddy env:decrypt -k "SECRET_*"').action(async(key,options)=>{log.debug("Running `buddy env:decrypt` ...",options);const result=decryptEnv({file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,key,stdout:options.stdout});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:keypair [key]",descriptions.keypair).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("--format [format]",descriptions.format,{default:"json"}).example("buddy env:keypair").example("buddy env:keypair --file .env.production").example("buddy env:keypair DOTENV_PRIVATE_KEY").action(async(key,options)=>{log.debug("Running `buddy env:keypair` ...",options);const result=getKeypair(key,{file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,format:options.format});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:rotate [key]",descriptions.rotate).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("-o, --stdout",descriptions.rotateStdout,{default:!1}).option("--dry-run",descriptions.rotateDryRun,{default:!1}).option("-ek, --exclude-key [excludeKey]",descriptions.excludeKey,{default:""}).example("buddy env:rotate").example("buddy env:rotate --file .env.production").example("buddy env:rotate --file .env.production --dry-run").action(async(key,options)=>{log.debug("Running `buddy env:rotate` ...",options);const result=rotateKeypair({file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,key,excludeKey:options.excludeKey,stdout:options.stdout,dryRun:options.dryRun});if(result.success){console.log(result.output);if(result.notice)console.error(result.notice);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:check",descriptions.check).option("-f, --file [file]",descriptions.file,{default:""}).option("--strict","Require every value in a committed env file to be encrypted, not just secret-shaped ones",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy env:check").example("buddy env:check --file .env.production").example("buddy env:check --file .env.production --strict").action(async(options)=>{log.debug("Running `buddy env:check` ...",options);const{bold,dim,green,red,yellow,intro}=await import("@stacksjs/cli"),{storage}=await import("@stacksjs/storage"),{existsSync}=await import("node:fs"),{resolve}=await import("node:path");await intro("buddy env:check");const checks=[],envFile=options.file||".env",envPath=resolve(process.cwd(),envFile);if(existsSync(envPath)){checks.push({name:`${envFile} file`,status:"pass",message:"Found"});try{const envContent=await storage.readTextFile(envPath),contentStr=typeof envContent==="string"?envContent:envContent.data,values=parseEnvAssignments(contentStr),keys=Object.keys(values),varCount=keys.length;checks.push({name:"Environment variables",status:"pass",message:`${varCount} variables defined`});if("APP_KEY"in values)if((values.APP_KEY??"").length>0)checks.push({name:"APP_KEY",status:"pass",message:"Set"});else checks.push({name:"APP_KEY",status:"warn",message:"Empty (run: buddy key:generate)"});else checks.push({name:"APP_KEY",status:"warn",message:"Not found (run: buddy key:generate)"});const hasPublicKey=keys.some((key)=>key.startsWith("DOTENV_PUBLIC_KEY")),hasPrivateKey=keys.some((key)=>key.startsWith("DOTENV_PRIVATE_KEY"))||existsSync(resolve(process.cwd(),".env.keys"));if(hasPublicKey&&hasPrivateKey)checks.push({name:"Encryption keys",status:"pass",message:"Public and private keys configured"});else if(hasPublicKey||hasPrivateKey)checks.push({name:"Encryption keys",status:"warn",message:"Incomplete keypair (run: buddy env:keypair)"});else checks.push({name:"Encryption keys",status:"warn",message:"Not configured (optional)"});const declaredTenants=await resolveDeclaredTenants();if(declaredTenants.tenants.length===0)checks.push({name:"Tenant isolation",status:"pass",message:"No tenants declared (cloud.tenants)"});else{const{foreignTenantKeys,partitionTenantEnv}=await import("@stacksjs/env"),foreign=foreignTenantKeys(partitionTenantEnv(values,declaredTenants)),isShipped=/^\.env\.[a-z]+$/.test(envFile),total=foreign.reduce((sum,entry)=>sum+entry.keys.length,0);if(foreign.length===0)checks.push({name:"Tenant isolation",status:"pass",message:`No foreign keys (checked ${declaredTenants.tenants.join(", ")})`});else if(!isShipped)checks.push({name:"Tenant isolation",status:"pass",message:`${total} archived key(s) - ${envFile} is local-only and never deployed`});else{checks.push({name:"Tenant isolation",status:"warn",message:`${total} key(s) belong to another tenant - move them to .env`});for(const{tenant,keys}of foreign)checks.push({name:` ${tenant}`,status:"warn",message:keys.join(", ")})}}const{plaintextSecrets,trackedEnvFiles}=await import("@stacksjs/env");if(!trackedEnvFiles(gitLsFiles()).includes(envFile))checks.push({name:"Committed secrets",status:"pass",message:`${envFile} is not committed; plaintext here stays local`});else{let placeholders={};try{const examplePath=resolve(process.cwd(),".env.example");if(existsSync(examplePath))placeholders=parseEnvAssignments(await storage.readTextFile(examplePath).then((f)=>f.data))}catch{}const leaked=plaintextSecrets(values,{placeholders,strict:options.strict});if(leaked.length===0)checks.push({name:"Committed secrets",status:"pass",message:`No unencrypted secrets in ${envFile}`});else checks.push({name:"Committed secrets",status:"fail",message:`${leaked.length} unencrypted secret${leaked.length===1?"":"s"} in committed ${envFile}: ${leaked.map((f)=>f.key).join(", ")}. Run \`buddy env:encrypt\`, then rotate them - they are in git history.`})}}catch(error){checks.push({name:`${envFile} content`,status:"fail",message:`Cannot read file: ${error}`})}}else checks.push({name:`${envFile} file`,status:"fail",message:"Not found"});const keysPath=resolve(process.cwd(),".env.keys");if(existsSync(keysPath))checks.push({name:".env.keys file",status:"pass",message:"Found"});else checks.push({name:".env.keys file",status:"warn",message:"Not found (optional for encryption)"});console.log("");console.log(bold("Environment Configuration Check:"));console.log(dim("\u2500".repeat(60)));console.log("");let hasFailures=!1,hasWarnings=!1;for(const check of checks){let statusIcon="",statusColor=(text)=>text;if(check.status==="pass"){statusIcon="\u2713";statusColor=green}else if(check.status==="warn"){statusIcon="\u26A0";statusColor=yellow;hasWarnings=!0}else{statusIcon="\u2717";statusColor=red;hasFailures=!0}console.log(`${statusColor(statusIcon)} ${bold(check.name.padEnd(25))} ${dim(check.message)}`)}console.log("");console.log(dim("\u2500".repeat(60)));console.log("");if(hasFailures){console.log(red("\u2717 Some critical checks failed. Please address the issues above."));process.exit(ExitCode.FatalError)}else if(hasWarnings){console.log(yellow("\u26A0 Some checks have warnings. Your environment should work but may have issues."));process.exit(ExitCode.Success)}else{console.log(green("\u2713 All checks passed! Your environment configuration looks healthy."));process.exit(ExitCode.Success)}});onUnknownSubcommand(buddy,"env")}async function resolveDeclaredTenants(){try{const{config}=await import("@stacksjs/config"),{cloud,app}=config,tenants=Array.isArray(cloud?.tenants)?cloud.tenants.filter((slug)=>typeof slug==="string"):[];return{self:app?.name,tenants}}catch{return{tenants:[]}}}function gitLsFiles(){try{const result=Bun.spawnSync(["git","ls-files"],{cwd:process.cwd(),stdout:"pipe",stderr:"ignore"});return result.success?result.stdout.toString():""}catch{return""}}export function parseEnvAssignments(content){const values={},assignment=/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/i,lines=content.split(/\r?\n/);for(let i=0;i<lines.length;i++){const line=lines[i];if(!line.trim()||line.trimStart().startsWith("#"))continue;const match=line.match(assignment);if(!match)continue;const[,key,first]=match;let raw=first.trim();const quote=raw[0]==='"'||raw[0]==="'"?raw[0]:void 0;if(quote){raw=raw.slice(1);while(!raw.endsWith(quote)&&i+1<lines.length){i++;raw+=lines[i]}raw=raw.endsWith(quote)?raw.slice(0,-1):raw}else raw=raw.replace(/\s+#.*$/,"").trim();values[key]=raw}return values}
1
+ import process from"node:process";import{log,onUnknownSubcommand}from"@stacksjs/cli";import{decryptEnv,encryptEnv,getEnv,getKeypair,resolveEnvFile,rotateKeypair,setEnv}from"@stacksjs/env";import{ExitCode}from"@stacksjs/types";export function env(buddy){const descriptions={env:"Interact with your environment variables",get:"Get an environment variable",set:"Set an environment variable",encrypt:"Encrypt a value",decrypt:"Decrypt a value",check:"Check environment configuration and validate setup",pretty:"Pretty print the result",stdout:"Output the result to stdout",keypair:"Generate a keypair",fileKeys:"The path to the file containing the keys",excludeKey:"The key to exclude from encryption",rotate:"Rotate a keypair",rotateStdout:"Print the rotated file and its new keypair without writing either",rotateDryRun:"Report what would change without writing anything",format:"The format to output the result (json, shell, eval)",all:"Get all environment variables",file:"The environment file to use",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("env:get [key]",descriptions.get).option("-f, --file [file]",descriptions.file,{default:""}).option("--format [format]",descriptions.format,{default:"json"}).option("-a, --all",descriptions.all,{default:!1}).option("-t, --pretty",descriptions.pretty,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy env:get SECRET").example("buddy env:get SECRET --file .env.production").example("buddy env:get --all --pretty").example("buddy env:get --format shell").example("buddy env:get --format eval").example("buddy env:get --format json").action(async(key,options)=>{log.debug("Running `buddy env:get` ...",options);const result=getEnv(key,{file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,all:options.all,format:options.format,prettyPrint:options.pretty});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:set [key] [value]",descriptions.set).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("--plain","Don't encrypt the value",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy env:set SECRET=value").example("buddy env:set SECRET value").example("buddy env:set SECRET value --file .env.production").example("buddy env:set SECRET value --plain").action(async(key,value,options)=>{log.debug("Running `buddy env:set` ...",options);if(!key){console.error("A key is required. Pass an empty value to clear it: `buddy env:set KEY ''`");process.exit(ExitCode.FatalError)}if(value===void 0){console.error(`No value given for ${key}. Pass one, or an empty string to clear it: \`buddy env:set ${key} ''\``);process.exit(ExitCode.FatalError)}const result=setEnv(key,value,{file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,plain:options.plain});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:encrypt [key]",descriptions.encrypt).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("-o, --stdout",descriptions.stdout,{default:!1}).option("-ek, --exclude-key [excludeKey]",descriptions.excludeKey,{default:""}).example("buddy env:encrypt").example("buddy env:encrypt --file .env.production").example('buddy env:encrypt -k "SECRET_*"').action(async(key,options)=>{log.debug("Running `buddy env:encrypt` ...",options);const result=encryptEnv({file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,key,excludeKey:options.excludeKey,stdout:options.stdout});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:decrypt [key]",descriptions.decrypt).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("-o, --stdout",descriptions.stdout,{default:!1}).example("buddy env:decrypt").example("buddy env:decrypt --file .env.production").example('buddy env:decrypt -k "SECRET_*"').action(async(key,options)=>{log.debug("Running `buddy env:decrypt` ...",options);const result=decryptEnv({file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,key,stdout:options.stdout});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:keypair [key]",descriptions.keypair).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("--format [format]",descriptions.format,{default:"json"}).example("buddy env:keypair").example("buddy env:keypair --file .env.production").example("buddy env:keypair DOTENV_PRIVATE_KEY").action(async(key,options)=>{log.debug("Running `buddy env:keypair` ...",options);const result=getKeypair(key,{file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,format:options.format});if(result.success){console.log(result.output);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:rotate [key]",descriptions.rotate).option("-f, --file [file]",descriptions.file,{default:""}).option("-fk, --file-keys [fileKeys]",descriptions.fileKeys,{default:""}).option("-o, --stdout",descriptions.rotateStdout,{default:!1}).option("--dry-run",descriptions.rotateDryRun,{default:!1}).option("-ek, --exclude-key [excludeKey]",descriptions.excludeKey,{default:""}).example("buddy env:rotate").example("buddy env:rotate --file .env.production").example("buddy env:rotate --file .env.production --dry-run").action(async(key,options)=>{log.debug("Running `buddy env:rotate` ...",options);const result=rotateKeypair({file:resolveEnvFile(options.file,options.env),keysFile:options.fileKeys,key,excludeKey:options.excludeKey,stdout:options.stdout,dryRun:options.dryRun});if(result.success){console.log(result.output);if(result.notice)console.error(result.notice);process.exit(ExitCode.Success)}else{console.error(result.error);process.exit(ExitCode.FatalError)}});buddy.command("env:check",descriptions.check).option("-f, --file [file]",descriptions.file,{default:""}).option("--strict","Require every value in a committed env file to be encrypted, not just secret-shaped ones",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy env:check").example("buddy env:check --file .env.production").example("buddy env:check --file .env.production --strict").action(async(options)=>{log.debug("Running `buddy env:check` ...",options);const{bold,dim,green,red,yellow,intro}=await import("@stacksjs/cli"),{storage}=await import("@stacksjs/storage"),{existsSync}=await import("node:fs"),{resolve}=await import("node:path");await intro("buddy env:check");const checks=[],envFile=options.file||".env",envPath=resolve(process.cwd(),envFile);if(existsSync(envPath)){checks.push({name:`${envFile} file`,status:"pass",message:"Found"});try{const envContent=await storage.readTextFile(envPath),contentStr=typeof envContent==="string"?envContent:envContent.data,values=parseEnvAssignments(contentStr),keys=Object.keys(values),varCount=keys.length;checks.push({name:"Environment variables",status:"pass",message:`${varCount} variables defined`});if("APP_KEY"in values)if((values.APP_KEY??"").length>0)checks.push({name:"APP_KEY",status:"pass",message:"Set"});else checks.push({name:"APP_KEY",status:"warn",message:"Empty (run: buddy key:generate)"});else checks.push({name:"APP_KEY",status:"warn",message:"Not found (run: buddy key:generate)"});const hasPublicKey=keys.some((key)=>key.startsWith("DOTENV_PUBLIC_KEY")),hasPrivateKey=keys.some((key)=>key.startsWith("DOTENV_PRIVATE_KEY"))||existsSync(resolve(process.cwd(),".env.keys"));if(hasPublicKey&&hasPrivateKey)checks.push({name:"Encryption keys",status:"pass",message:"Public and private keys configured"});else if(hasPublicKey||hasPrivateKey)checks.push({name:"Encryption keys",status:"warn",message:"Incomplete keypair (run: buddy env:keypair)"});else checks.push({name:"Encryption keys",status:"warn",message:"Not configured (optional)"});const declaredTenants=await resolveDeclaredTenants();if(declaredTenants.tenants.length===0)checks.push({name:"Tenant isolation",status:"pass",message:"No tenants declared (cloud.tenants)"});else{const{foreignTenantKeys,partitionTenantEnv}=await import("@stacksjs/env"),foreign=foreignTenantKeys(partitionTenantEnv(values,declaredTenants)),isShipped=/^\.env\.[a-z]+$/.test(envFile),total=foreign.reduce((sum,entry)=>sum+entry.keys.length,0);if(foreign.length===0)checks.push({name:"Tenant isolation",status:"pass",message:`No foreign keys (checked ${declaredTenants.tenants.join(", ")})`});else if(!isShipped)checks.push({name:"Tenant isolation",status:"pass",message:`${total} archived key(s) - ${envFile} is local-only and never deployed`});else{checks.push({name:"Tenant isolation",status:"warn",message:`${total} key(s) belong to another tenant - move them to .env`});for(const{tenant,keys}of foreign)checks.push({name:` ${tenant}`,status:"warn",message:keys.join(", ")})}}const{plaintextSecrets,trackedEnvFiles}=await import("@stacksjs/env");if(!trackedEnvFiles(gitLsFiles()).includes(envFile))checks.push({name:"Committed secrets",status:"pass",message:`${envFile} is not committed; plaintext here stays local`});else{let placeholders={};try{const examplePath=resolve(process.cwd(),".env.example");if(existsSync(examplePath))placeholders=parseEnvAssignments(await storage.readTextFile(examplePath).then((f)=>f.data))}catch{}const leaked=plaintextSecrets(values,{placeholders,strict:options.strict});if(leaked.length===0)checks.push({name:"Committed secrets",status:"pass",message:`No unencrypted secrets in ${envFile}`});else checks.push({name:"Committed secrets",status:"fail",message:`${leaked.length} unencrypted secret${leaked.length===1?"":"s"} in committed ${envFile}: ${leaked.map((f)=>f.key).join(", ")}. Run \`buddy env:encrypt\`, then rotate them - they are in git history.`})}}catch(error){checks.push({name:`${envFile} content`,status:"fail",message:`Cannot read file: ${error}`})}}else checks.push({name:`${envFile} file`,status:"fail",message:"Not found"});const keysPath=resolve(process.cwd(),".env.keys");if(existsSync(keysPath))checks.push({name:".env.keys file",status:"pass",message:"Found"});else checks.push({name:".env.keys file",status:"warn",message:"Not found (optional for encryption)"});console.log("");console.log(bold("Environment Configuration Check:"));console.log(dim("\u2500".repeat(60)));console.log("");let hasFailures=!1,hasWarnings=!1;for(const check of checks){let statusIcon="",statusColor=(text)=>text;if(check.status==="pass"){statusIcon="\u2713";statusColor=green}else if(check.status==="warn"){statusIcon="\u26A0";statusColor=yellow;hasWarnings=!0}else{statusIcon="\u2717";statusColor=red;hasFailures=!0}console.log(`${statusColor(statusIcon)} ${bold(check.name.padEnd(25))} ${dim(check.message)}`)}console.log("");console.log(dim("\u2500".repeat(60)));console.log("");if(hasFailures){console.log(red("\u2717 Some critical checks failed. Please address the issues above."));process.exit(ExitCode.FatalError)}else if(hasWarnings){console.log(yellow("\u26A0 Some checks have warnings. Your environment should work but may have issues."));process.exit(ExitCode.Success)}else{console.log(green("\u2713 All checks passed! Your environment configuration looks healthy."));process.exit(ExitCode.Success)}});onUnknownSubcommand(buddy,"env")}async function resolveDeclaredTenants(){try{const{config}=await import("@stacksjs/config"),{cloud,app}=config,tenants=Array.isArray(cloud?.tenants)?cloud.tenants.filter((slug)=>typeof slug==="string"):[];return{self:app?.name,tenants}}catch{return{tenants:[]}}}function gitLsFiles(){try{const result=Bun.spawnSync(["git","ls-files"],{cwd:process.cwd(),stdout:"pipe",stderr:"ignore"});return result.success?result.stdout.toString():""}catch{return""}}export function parseEnvAssignments(content){const values={},assignment=/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/i,lines=content.split(/\r?\n/);for(let i=0;i<lines.length;i++){const line=lines[i];if(!line.trim()||line.trimStart().startsWith("#"))continue;const match=line.match(assignment);if(!match)continue;const[,key,first]=match;let raw=first.trim();const quote=raw[0]==='"'||raw[0]==="'"?raw[0]:void 0;if(quote){raw=raw.slice(1);while(!raw.endsWith(quote)&&i+1<lines.length){i++;raw+=lines[i]}raw=raw.endsWith(quote)?raw.slice(0,-1):raw}else raw=raw.replace(/\s+#.*$/,"").trim();values[key]=raw}return values}
@@ -1,4 +1,4 @@
1
- import process from"node:process";import{generateComponentMeta,generateCoreSymlink,generateIdeHelpers,generateLibEntries,generateOpenApiSpec,generatePantryConfig,generateProjectImages,generateTypes,generateVsCodeCustomData,generateWebTypes,invoke as startGenerationProcess,watchTypes}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{frameworkPath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{reportFailure,resultFailed}from"../result";export async function runTypeGeneration(options){await generateTypes(options);try{const{buildDatabaseSchema}=await import("@stacksjs/orm");await buildDatabaseSchema()}catch(err){log.warn(`[generate:db-types] skipped: ${err.message}`)}if(options.watch)await watchTypes(options)}export function generate(buddy){const descriptions={command:"Automagically build any of your libraries/packages for production use. Select any of the following packages",types:"Generate your TypeScript types",entries:"Generate your function & Component Library Entry Points",webTypes:"Generate web-types.json for IDEs",customData:"Generate VS Code custom data (custom-elements.json) for IDEs",ideHelpers:"Generate IDE helpers",componentMeta:"Generate component meta information",coreSymlink:"Generate symlink of the core framework to the project root",pantry:"Generate the pantry configuration file",openApi:"Generate the OpenAPI specification",images:"Generate every image declared in config/images.ts",og:"Generate the social cards used by link previews",appStore:"Generate the App Store screenshot set",appIcons:"Generate the app icon and favicon sets",select:"What are you trying to generate?",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("generate",descriptions.command).option("-t, --types",descriptions.types).option("-e, --entries",descriptions.entries).option("-w, --web-types",descriptions.webTypes).option("-c, --custom-data",descriptions.customData).option("-i, --ide-helpers",descriptions.ideHelpers).option("-c, --component-meta",descriptions.componentMeta).option("-p, --pantry",descriptions.pantry).option("-o, --openapi",descriptions.openApi).option("--images",descriptions.images).option("-p, --project [project]",descriptions.project,{default:!1}).option("--core-symlink",descriptions.coreSymlink).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate` ...",options);await startGenerationProcess(options);process.exit(ExitCode.Success)});buddy.command("generate:types",descriptions.types).option("-p, --project [project]",descriptions.project,{default:!1}).option("-w, --watch","Re-run on changes to models/ and config/",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:types` ...",options);await runTypeGeneration(options)});buddy.command("generate:db-types","Refresh database/types.d.ts for db.selectFrom autocomplete (stacksjs/stacks#1923)").option("--dry-run","Print the would-be file content without writing",{default:!1}).option("--framework","Write the framework's own FrameworkSchema instead of the app's DatabaseSchema",{default:!1}).action(async(options)=>{const{buildDatabaseSchema}=await import("@stacksjs/orm"),result=await buildDatabaseSchema(options.framework?{dryRun:options.dryRun,target:"framework",outFile:frameworkPath("core/database/src/framework-schema.ts"),migrationsDir:projectPath("database/migrations")}:{dryRun:options.dryRun});if(options.dryRun)console.log(result.content);for(const e of result.errors)log.warn(`[generate:db-types] ${e.file}: ${e.error}`);log.info(`[generate:db-types] resolved ${result.tables.length} table(s)`)});buddy.command("generate:vschema","Derive a Vitess VSchema from your models (writes database/vschema.json)").option("--dry-run","Print the VSchema without writing it",{default:!1}).option("--out [path]","Where to write the VSchema",{default:"database/vschema.json"}).action(async(options)=>{const{generateVSchema}=await import("@stacksjs/actions"),result=await generateVSchema({dryRun:options.dryRun,out:options.out});if(!result.ok){console.error(`
1
+ import process from"node:process";import{generateComponentMeta,generateCoreSymlink,generateIdeHelpers,generateLibEntries,generateOpenApiSpec,generatePantryConfig,generateProjectImages,generateTypes,generateVsCodeCustomData,generateWebTypes,invoke as startGenerationProcess,watchTypes}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{frameworkPath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{reportFailure,resultFailed}from"../result";export async function runTypeGeneration(options){await generateTypes(options);try{const{buildDatabaseSchema}=await import("@stacksjs/orm");await buildDatabaseSchema()}catch(err){log.warn(`[generate:db-types] skipped: ${err.message}`)}if(options.watch)await watchTypes(options)}export function generate(buddy){const descriptions={command:"Automagically build any of your libraries/packages for production use. Select any of the following packages",types:"Generate your TypeScript types",entries:"Generate your function & Component Library Entry Points",webTypes:"Generate web-types.json for IDEs",customData:"Generate VS Code custom data (custom-elements.json) for IDEs",ideHelpers:"Generate IDE helpers",componentMeta:"Generate component meta information",coreSymlink:"Generate symlink of the core framework to the project root",pantry:"Generate the pantry configuration file",openApi:"Generate the OpenAPI specification",images:"Generate every image declared in config/images.ts",og:"Generate the social cards used by link previews",appStore:"Generate the App Store screenshot set",appIcons:"Generate the app icon and favicon sets",select:"What are you trying to generate?",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("generate",descriptions.command).option("-t, --types",descriptions.types).option("-e, --entries",descriptions.entries).option("-w, --web-types",descriptions.webTypes).option("-d, --custom-data",descriptions.customData).option("-i, --ide-helpers",descriptions.ideHelpers).option("-c, --component-meta",descriptions.componentMeta).option("-r, --pantry",descriptions.pantry).option("-o, --openapi",descriptions.openApi).option("--images",descriptions.images).option("-p, --project [project]",descriptions.project,{default:!1}).option("--core-symlink",descriptions.coreSymlink).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate` ...",options);await startGenerationProcess(options);process.exit(ExitCode.Success)});buddy.command("generate:types",descriptions.types).option("-p, --project [project]",descriptions.project,{default:!1}).option("-w, --watch","Re-run on changes to models/ and config/",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:types` ...",options);await runTypeGeneration(options)});buddy.command("generate:db-types","Refresh database/types.d.ts for db.selectFrom autocomplete (stacksjs/stacks#1923)").option("--dry-run","Print the would-be file content without writing",{default:!1}).option("--framework","Write the framework's own FrameworkSchema instead of the app's DatabaseSchema",{default:!1}).action(async(options)=>{const{buildDatabaseSchema}=await import("@stacksjs/orm"),result=await buildDatabaseSchema(options.framework?{dryRun:options.dryRun,target:"framework",outFile:frameworkPath("core/database/src/framework-schema.ts"),migrationsDir:projectPath("database/migrations")}:{dryRun:options.dryRun});if(options.dryRun)console.log(result.content);for(const e of result.errors)log.warn(`[generate:db-types] ${e.file}: ${e.error}`);log.info(`[generate:db-types] resolved ${result.tables.length} table(s)`)});buddy.command("generate:vschema","Derive a Vitess VSchema from your models (writes database/vschema.json)").option("--dry-run","Print the VSchema without writing it",{default:!1}).option("--out [path]","Where to write the VSchema",{default:"database/vschema.json"}).action(async(options)=>{const{generateVSchema}=await import("@stacksjs/actions"),result=await generateVSchema({dryRun:options.dryRun,out:options.out});if(!result.ok){console.error(`
2
2
  \u274C ${result.error}
3
3
  `);process.exit(ExitCode.FatalError)}console.log(result.report);if(options.dryRun)console.log(JSON.stringify(result.vschema,null,2));else log.success(`Wrote ${result.path} (${result.tableCount} tables)`)});buddy.command("generate:erd-diagram","Derive an entity-relationship diagram from your models (writes docs/erd.md)").alias("generate:erd").option("--dry-run","Print the diagram without writing it",{default:!1}).option("--out [path]","Where to write the diagram; a .md target gets a fenced mermaid block",{default:"docs/erd.md"}).option("--no-columns","Relationships only, without the column lists",{default:!0}).option("--only [tables]","Comma-separated tables or model names to include",{default:""}).action(async(options)=>{const{generateErd}=await import("@stacksjs/actions"),only=(options.only??"").split(",").map((entry)=>entry.trim()).filter(Boolean),result=await generateErd({dryRun:options.dryRun,out:options.out,noColumns:options.columns===!1,only});if(!result.ok){console.error(`
4
4
  \u274C ${result.error}
@@ -0,0 +1,29 @@
1
+ export declare function appIdentity(path: string): AppIdentity;
2
+ /*.ts` may use, and what each becomes. */
3
+ export declare function templatePlaceholders(identity: AppIdentity): Record<string, string>;
4
+ export declare function renderTemplate(source: string, identity: AppIdentity): string;
5
+ /**
6
+ * The `.env.example` values that name stacksjs.com rather than the app, and
7
+ * what a new app gets instead. `null` drops the line: those variables only
8
+ * mean something on the stacks box.
9
+ *
10
+ * `.env` is copied from `.env.example` a step later, so rewriting the example
11
+ * fixes both, and the example the app commits describes the app.
12
+ */
13
+ export declare function appEnvValues(identity: AppIdentity): Record<string, string | null>;
14
+ /**
15
+ * Rewrite `.env.example` for the new app. Only the keys `appEnvValues` names
16
+ * are touched, and only where they are set (`KEY=...` at the start of a line),
17
+ * so comments and every other default survive as written.
18
+ *
19
+ * Returns the keys it changed.
20
+ */
21
+ export declare function applyAppEnvTemplate(root: string, identity: AppIdentity): string[];
22
+ /** The names a new app goes by, all derived from the directory `buddy new` created. */
23
+ export declare interface AppIdentity {
24
+ slug: string
25
+ displayName: string
26
+ kebab: string
27
+ snake: string
28
+ bundleId: string
29
+ }
@@ -0,0 +1,3 @@
1
+ import{existsSync,readFileSync,writeFileSync}from"node:fs";import{join}from"node:path";export function appIdentity(path){const slug=path.replace(/\/+$/,"").split("/").pop()||"stacks-app",words=slug.split(/[^a-z0-9]+/i).filter(Boolean),displayName=words.map((word)=>`${word.charAt(0).toUpperCase()}${word.slice(1)}`).join(" ")||"My App",kebab=words.join("-").toLowerCase().replace(/^[^a-z]+/,"")||"app";return{slug,displayName,kebab,snake:kebab.replaceAll("-","_"),bundleId:`com.example.${kebab}`}}export function templatePlaceholders(identity){return{__APP_NAME__:identity.displayName,__APP_SLUG__:identity.slug,__APP_TAG_PREFIX__:identity.kebab,__APP_BUNDLE_ID__:identity.bundleId}}export function renderTemplate(source,identity){let rendered=source;for(const[placeholder,value]of Object.entries(templatePlaceholders(identity)))rendered=rendered.replaceAll(placeholder,value);return rendered}export function appEnvValues(identity){return{APP_NAME:`"${identity.displayName.replaceAll('"',"")}"`,APP_URL:`${identity.kebab}.localhost`,DB_DATABASE:identity.snake,MAIL_FROM_ADDRESS:`"no-reply@${identity.kebab}.localhost"`,QUEUE_PREFIX:`${identity.snake}:queue`,PREDICTHQ_DB_PASSWORD:null}}export function applyAppEnvTemplate(root,identity){const file=join(root,".env.example");if(!existsSync(file))return[];const values=appEnvValues(identity),changed=[],lines=readFileSync(file,"utf8").split(`
2
+ `).flatMap((line)=>{const key=/^([A-Z0-9_]+)=/.exec(line)?.[1];if(!key||!(key in values))return[line];changed.push(key);const value=values[key];return value===null?[]:[`${key}=${value}`]});writeFileSync(file,lines.join(`
3
+ `));return changed}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Replace the stacksjs.com site with a neutral starter: delete every
3
+ * site-only path, then lay `defaults/scaffold/resources` over `resources/`.
4
+ *
5
+ * Directories the removal empties are removed too, so the app does not start
6
+ * with an empty `resources/data/` it has no use for.
7
+ *
8
+ * Returns the paths it removed. Throws when the starter template is missing,
9
+ * because an app with no `index.stx` falls back to the framework default,
10
+ * which is the same desktop demo this is removing.
11
+ */
12
+ export declare function applyAppSiteTemplate(root: string): string[];
13
+ /**
14
+ * Everything in the template that belongs to stacksjs.com rather than to an
15
+ * app, relative to the project root.
16
+ *
17
+ * `buddy new` scaffolds by downloading this repository, and this repository IS
18
+ * stacksjs.com: its `resources/` and `public/` hold the marketing site, its
19
+ * brand fonts, logos and park illustrations, and a desktop-OS demo; `docs/`
20
+ * and `content/blog/` are its documentation and blog. Left in place, a new app
21
+ * deployed as-is publishes a canonical URL and an Organization graph claiming
22
+ * to be stacksjs.com, the framework's docs and launch posts as its own, and
23
+ * carries several MB of someone else's brand.
24
+ *
25
+ * An explicit list rather than moving the site elsewhere: the stacks repo keeps
26
+ * serving stacksjs.com from the paths every app uses, so its deploy does not
27
+ * change. `scaffold-site.test.ts` runs this against the repository's real tree
28
+ * and fails on anything the list misses, which is what keeps it complete when
29
+ * the site grows a page.
30
+ */
31
+ export declare const SITE_ONLY_PATHS: readonly string[];
32
+ /** Where the app's own starter files live in the template, relative to the project root. */
33
+ export declare const APP_SITE_TEMPLATE: 'storage/framework/defaults/scaffold/resources';
@@ -0,0 +1 @@
1
+ import{cpSync,existsSync,readdirSync,rmSync}from"node:fs";import{join}from"node:path";export const SITE_ONLY_PATHS=["resources/views/index.stx","resources/views/coming-soon.stx","resources/views/compare","resources/views/features","resources/views/use-cases","resources/views/components/FeatureCard.stx","resources/views/layouts/marketing.stx","resources/views/partials/theme.stx","resources/data/comparisons.ts","resources/data/features.ts","resources/data/highlight.ts","resources/data/nav.ts","resources/data/use-cases.ts","resources/partials/marketing-footer.stx","resources/partials/marketing-head.stx","resources/partials/marketing-nav.stx","resources/partials/marketing-theme-toggle.stx","resources/emails/subscription-confirmation.stx","resources/assets/scripts/site-mode.js","public/assets/styles/marketing.css","public/assets/styles/theme.css","resources/assets/fonts/nps","public/assets/fonts/nps","public/assets/images","resources/assets/images/bg-img.png","resources/assets/images/marketing-park-camp.svg","resources/assets/images/marketing-park-geyser.svg","resources/assets/images/marketing-park-lighthouse.svg","resources/assets/images/marketing-park-trail.svg","resources/assets/images/marketing-park-wing.svg","resources/assets/images/park-ridge.svg","resources/assets/images/river.svg","resources/assets/images/topography.svg","resources/assets/images/wood-sign.svg","docs","content/blog","content/BLOG_STRATEGY.md","public/images/atomic-fx-diagram.png","public/images/atomic-ui-diagram.png","public/images/diagram.png","public/images/og-image.png","public/images/social.png","public/images/background-auth.jpg","public/images/background-features.jpg","public/images/screenshots/expenses.webp","public/images/screenshots/reporting.webp","public/images/screenshots/vat-returns.webp","public/images/logos/amex.jpg","public/images/logos/jcb.svg","public/images/logos/laravel.svg","public/images/logos/logo-dark.svg","public/images/logos/logo-mini.svg","public/images/logos/logo-transparent.svg","public/images/logos/logo-white.png","public/images/logos/logo.png","public/images/logos/logo.svg","public/images/logos/mastercard.svg","public/images/logos/meilisearch.svg","public/images/logos/mirage.svg","public/images/logos/statamic.svg","public/images/logos/statickit.svg","public/images/logos/transistor.svg","public/images/logos/tuple.svg","public/images/logos/visa.png","public/images/logos/visa.svgz","resources/assets/scripts/main.ts","resources/assets/styles/main.css","resources/layouts/Desktop.stx","resources/partials/welcome-pdf-content.stx","resources/partials/welcome-pdf-toolbar.stx","resources/components/ContextMenu.stx","resources/components/ContextMenuItem.stx","resources/components/DesktopIcon.stx","resources/components/FeatureButton.stx","resources/components/InputGroup.stx","resources/components/LoginForm.stx","resources/components/LoginScreen.stx","resources/components/NotificationPopup.stx","resources/components/PackageItem.stx","resources/components/StartMenu.stx","resources/components/StartMenuItem.stx","resources/components/Taskbar.stx","resources/components/Window.stx"],APP_SITE_TEMPLATE="storage/framework/defaults/scaffold/resources";export function applyAppSiteTemplate(root){const template=join(root,APP_SITE_TEMPLATE);if(!existsSync(template))throw Error(`No app site template at ${APP_SITE_TEMPLATE}`);const removed=[];for(const relative of SITE_ONLY_PATHS){const target=join(root,relative);if(!existsSync(target))continue;rmSync(target,{recursive:!0,force:!0});removed.push(relative)}for(const relative of removed){let dir=parentOf(relative);while(dir&&!KEPT_ROOTS.has(dir)&&isEmptyDir(join(root,dir))){rmSync(join(root,dir),{recursive:!0,force:!0});dir=parentOf(dir)}}cpSync(template,join(root,"resources"),{recursive:!0});return removed}const KEPT_ROOTS=new Set(["resources","public"]);function parentOf(relative){return relative.includes("/")?relative.slice(0,relative.lastIndexOf("/")):""}function isEmptyDir(dir){return existsSync(dir)&&readdirSync(dir).length===0}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.74.63",
5
+ "version": "0.74.65",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,69 +95,69 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.74.63",
99
- "@stacksjs/ai": "^0.74.63",
100
- "@stacksjs/alias": "^0.74.63",
101
- "@stacksjs/analytics": "^0.74.63",
102
- "@stacksjs/api": "^0.74.63",
103
- "@stacksjs/arrays": "^0.74.63",
104
- "@stacksjs/auth": "^0.74.63",
105
- "@stacksjs/browser-extension": "^0.74.63",
106
- "@stacksjs/build": "^0.74.63",
107
- "@stacksjs/cache": "^0.74.63",
108
- "@stacksjs/chat": "^0.74.63",
98
+ "@stacksjs/actions": "^0.74.65",
99
+ "@stacksjs/ai": "^0.74.65",
100
+ "@stacksjs/alias": "^0.74.65",
101
+ "@stacksjs/analytics": "^0.74.65",
102
+ "@stacksjs/api": "^0.74.65",
103
+ "@stacksjs/arrays": "^0.74.65",
104
+ "@stacksjs/auth": "^0.74.65",
105
+ "@stacksjs/browser-extension": "^0.74.65",
106
+ "@stacksjs/build": "^0.74.65",
107
+ "@stacksjs/cache": "^0.74.65",
108
+ "@stacksjs/chat": "^0.74.65",
109
109
  "@stacksjs/clapp": "^0.2.12",
110
- "@stacksjs/cli": "^0.74.63",
111
- "@stacksjs/cloud": "^0.74.63",
112
- "@stacksjs/cms": "^0.74.63",
113
- "@stacksjs/collections": "^0.74.63",
114
- "@stacksjs/config": "^0.74.63",
115
- "@stacksjs/database": "^0.74.63",
116
- "@stacksjs/desktop-build": "^0.74.63",
117
- "@stacksjs/dns": "^0.74.63",
110
+ "@stacksjs/cli": "^0.74.65",
111
+ "@stacksjs/cloud": "^0.74.65",
112
+ "@stacksjs/cms": "^0.74.65",
113
+ "@stacksjs/collections": "^0.74.65",
114
+ "@stacksjs/config": "^0.74.65",
115
+ "@stacksjs/database": "^0.74.65",
116
+ "@stacksjs/desktop-build": "^0.74.65",
117
+ "@stacksjs/dns": "^0.74.65",
118
118
  "@stacksjs/dnsx": "^0.2.3",
119
- "@stacksjs/email": "^0.74.63",
120
- "@stacksjs/enums": "^0.74.63",
121
- "@stacksjs/env": "^0.74.63",
122
- "@stacksjs/error-handling": "^0.74.63",
123
- "@stacksjs/events": "^0.74.63",
124
- "@stacksjs/features": "^0.74.63",
125
- "@stacksjs/git": "^0.74.63",
119
+ "@stacksjs/email": "^0.74.65",
120
+ "@stacksjs/enums": "^0.74.65",
121
+ "@stacksjs/env": "^0.74.65",
122
+ "@stacksjs/error-handling": "^0.74.65",
123
+ "@stacksjs/events": "^0.74.65",
124
+ "@stacksjs/features": "^0.74.65",
125
+ "@stacksjs/git": "^0.74.65",
126
126
  "@stacksjs/gitit": "^0.2.5",
127
- "@stacksjs/health": "^0.74.63",
127
+ "@stacksjs/health": "^0.74.65",
128
128
  "@stacksjs/httx": "^0.1.10",
129
- "@stacksjs/image": "^0.74.63",
130
- "@stacksjs/lint": "^0.74.63",
131
- "@stacksjs/logging": "^0.74.63",
132
- "@stacksjs/notifications": "^0.74.63",
133
- "@stacksjs/objects": "^0.74.63",
134
- "@stacksjs/orm": "^0.74.63",
135
- "@stacksjs/path": "^0.74.63",
136
- "@stacksjs/payments": "^0.74.63",
137
- "@stacksjs/realtime": "^0.74.63",
138
- "@stacksjs/router": "^0.74.63",
129
+ "@stacksjs/image": "^0.74.65",
130
+ "@stacksjs/lint": "^0.74.65",
131
+ "@stacksjs/logging": "^0.74.65",
132
+ "@stacksjs/notifications": "^0.74.65",
133
+ "@stacksjs/objects": "^0.74.65",
134
+ "@stacksjs/orm": "^0.74.65",
135
+ "@stacksjs/path": "^0.74.65",
136
+ "@stacksjs/payments": "^0.74.65",
137
+ "@stacksjs/realtime": "^0.74.65",
138
+ "@stacksjs/router": "^0.74.65",
139
139
  "@stacksjs/rpx": "^0.11.54",
140
- "@stacksjs/scheduler": "^0.74.63",
141
- "@stacksjs/search-engine": "^0.74.63",
142
- "@stacksjs/security": "^0.74.63",
143
- "@stacksjs/server": "^0.74.63",
144
- "@stacksjs/sites": "^0.74.63",
145
- "@stacksjs/skills": "^0.74.63",
146
- "@stacksjs/storage": "^0.74.63",
147
- "@stacksjs/strings": "^0.74.63",
148
- "@stacksjs/stx": "^0.2.286",
149
- "@stacksjs/testing": "^0.74.63",
150
- "@stacksjs/tinker": "^0.74.63",
140
+ "@stacksjs/scheduler": "^0.74.65",
141
+ "@stacksjs/search-engine": "^0.74.65",
142
+ "@stacksjs/security": "^0.74.65",
143
+ "@stacksjs/server": "^0.74.65",
144
+ "@stacksjs/sites": "^0.74.65",
145
+ "@stacksjs/skills": "^0.74.65",
146
+ "@stacksjs/storage": "^0.74.65",
147
+ "@stacksjs/strings": "^0.74.65",
148
+ "@stacksjs/stx": "^0.2.306",
149
+ "@stacksjs/testing": "^0.74.65",
150
+ "@stacksjs/tinker": "^0.74.65",
151
151
  "@stacksjs/tlsx": "^0.13.19",
152
- "@stacksjs/ts-cloud": "^0.16.0",
153
- "@stacksjs/tunnel": "^0.74.63",
154
- "@stacksjs/types": "^0.74.63",
155
- "@stacksjs/ui": "^0.74.63",
156
- "@stacksjs/utils": "^0.74.63",
157
- "@stacksjs/validation": "^0.74.63",
152
+ "@stacksjs/ts-cloud": "^0.16.11",
153
+ "@stacksjs/tunnel": "^0.74.65",
154
+ "@stacksjs/types": "^0.74.65",
155
+ "@stacksjs/ui": "^0.74.65",
156
+ "@stacksjs/utils": "^0.74.65",
157
+ "@stacksjs/validation": "^0.74.65",
158
158
  "ajv": "^8.20.0",
159
159
  "ajv-formats": "^3.0.1",
160
- "bun-plugin-stx": "^0.2.286",
160
+ "bun-plugin-stx": "^0.2.306",
161
161
  "ts-pantry": "^0.11.35"
162
162
  },
163
163
  "devDependencies": {