@stacksjs/buddy 0.74.19 → 0.74.21
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/commands/env.js
CHANGED
|
@@ -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=config.cloud,app=config.app,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("-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}
|
|
@@ -20,3 +20,8 @@ export declare function publish(buddy: CLI): void;
|
|
|
20
20
|
* outside the project root.
|
|
21
21
|
*/
|
|
22
22
|
export declare function carryRelativeImports(sourcePath: string, targetPath: string, seen?: unknown): Promise<string[]>;
|
|
23
|
+
/*` export maps the specifier onto its build
|
|
24
|
+
* output. The names come back because a preloaded specifier has to be a DIRECT
|
|
25
|
+
* dependency of the app - see the caller (stacksjs/stacks#2433 neighbours).
|
|
26
|
+
*/
|
|
27
|
+
export declare function rewriteBunfigPreloads(bunfig: string): { next: string, packages: Set<string>, rewritten: number };
|
package/dist/commands/publish.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import{existsSync,mkdirSync,realpathSync}from"node:fs";import{cp,readdir,stat}from"node:fs/promises";import{homedir}from"node:os";import{dirname,join,resolve}from"node:path";import process from"node:process";import{italic,log,onUnknownSubcommand}from"@stacksjs/cli";import{path}from"@stacksjs/path";import{fs,globSync}from"@stacksjs/storage";import{pruneVendoredCoreFromWorkflows,splitFrameworkTypecheckScript}from"../workflow-prune";import{detectInstaller,findCoreReferences,isDanglingLink,rewriteCoreCommandPaths,rewriteCoreSourceImports,rewriteSurvivingFrameworkManifests}from"../unvendor-rewrite";import{fetchPublishedVersions}from"../registry";import{ExitCode}from"@stacksjs/types";export function publish(buddy){const descriptions={command:"Publish a Stacks default into your userland (app/) directory so you can customize it",model:"Publish a default model from storage/framework/defaults/app/Models/ to app/Models/",controller:"Publish a default controller from storage/framework/defaults/app/Controllers/ to app/Controllers/",middleware:"Publish a default middleware from storage/framework/defaults/app/Middleware/ to app/Middleware/",action:"Publish a default action from storage/framework/defaults/app/Actions/ to app/Actions/",core:"Publish a framework package source from node_modules/@stacksjs/<pkg>/ into storage/framework/core/<pkg>/ for editing",unpublishCore:"Drop a vendored storage/framework/core/<pkg>/ and go back to the installed @stacksjs/<pkg>",all:"Unvendor the whole framework: remove storage/framework/core and resolve every @stacksjs package from the `stacks` dependency in package.json",publishAll:"Vendor the whole framework: copy storage/framework/core out of a local Stacks checkout and wire it up as a Bun workspace",frameworkPath:"The Stacks checkout to vendor from (defaults to $STACKS_FRAMEWORK_PATH, ../stacks, then ~/Code/stacks)",coreStatus:"Report whether this project runs on a vendored storage/framework/core or on the published packages",name:"The name of the resource to publish (e.g. Cart, User)",pkg:"The name of the framework package (e.g. router, orm, faker - without @stacksjs/ prefix)",force:"Overwrite an existing userland file",forceUnpublish:"Delete the vendored source even when it has uncommitted changes",verbose:"Enable verbose output"};buddy.command("publish:model <name>",descriptions.model).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"model",name,defaultsDir:path.frameworkPath("defaults/app/Models"),userDir:path.userModelsPath(),force:!!options.force})});buddy.command("publish:controller <name>",descriptions.controller).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"controller",name,defaultsDir:path.frameworkPath("defaults/app/Controllers"),userDir:path.userControllersPath(),force:!!options.force})});buddy.command("publish:middleware <name>",descriptions.middleware).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"middleware",name,defaultsDir:path.frameworkPath("defaults/app/Middleware"),userDir:path.userMiddlewarePath(),force:!!options.force})});buddy.command("publish:action <name>",descriptions.action).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(name,options)=>{await publishResource({kind:"action",name,defaultsDir:path.frameworkPath("defaults/app/Actions"),userDir:path.userActionsPath(),force:!!options.force})});buddy.command("publish:core [pkg]",descriptions.core).option("--all",descriptions.publishAll,{default:!1}).option("--path <path>",descriptions.frameworkPath).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).example("buddy publish:core router").example("buddy publish:core --all").example("buddy publish:core --all --path ../stacks").action(async(pkg,options)=>{if(options.all){await vendorFramework(options.path,!!options.force);return}if(!pkg){await log.error("Usage: buddy publish:core <pkg> (or --all to vendor the whole framework as a workspace)");process.exit(ExitCode.FatalError)}await publishCorePackage(pkg,!!options.force)});buddy.command("core:status",descriptions.coreStatus).alias("publish:core:status").option("--verbose",descriptions.verbose,{default:!1}).example("buddy core:status").action(async()=>{await reportCoreStatus()});buddy.command("unpublish:core [pkg]",descriptions.unpublishCore).option("--all",descriptions.all,{default:!1}).option("--force",descriptions.forceUnpublish,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(pkg,options)=>{if(options.all){await unvendorFramework(!!options.force);return}if(!pkg){await log.error("Usage: buddy unpublish:core <pkg> (or --all to move the whole framework to node_modules)");process.exit(ExitCode.FatalError)}await unpublishCorePackage(pkg,!!options.force)});buddy.command("publish [resource] [name]",descriptions.command).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(resource,name,options)=>{if(!resource||!name){await log.error("Usage: buddy publish:<resource> <Name> (e.g. buddy publish:model Cart)");process.exit(ExitCode.FatalError)}const handler={model:()=>publishResource({kind:"model",name,defaultsDir:path.frameworkPath("defaults/app/Models"),userDir:path.userModelsPath(),force:!!options.force}),controller:()=>publishResource({kind:"controller",name,defaultsDir:path.frameworkPath("defaults/app/Controllers"),userDir:path.userControllersPath(),force:!!options.force}),middleware:()=>publishResource({kind:"middleware",name,defaultsDir:path.frameworkPath("defaults/app/Middleware"),userDir:path.userMiddlewarePath(),force:!!options.force}),action:()=>publishResource({kind:"action",name,defaultsDir:path.frameworkPath("defaults/app/Actions"),userDir:path.userActionsPath(),force:!!options.force}),core:()=>publishCorePackage(name,!!options.force)}[resource.toLowerCase()];if(!handler){await log.error(`Unknown publishable resource: ${italic(resource)}`);log.info("Available: model, controller, middleware, action, core");await log.flush();process.exit(ExitCode.FatalError)}await handler()});onUnknownSubcommand(buddy,"publish")}async function publishCorePackage(pkg,force){const shortName=pkg.replace(/^@stacksjs\//,"").replace(/^core\//,""),fail=(msg,hint)=>{process.stderr.write(`${msg}
|
|
2
2
|
`);if(hint)process.stderr.write(` ${hint}
|
|
3
3
|
`);process.exit(ExitCode.FatalError)};if(!shortName||shortName.includes("/")||shortName.includes(".."))fail(`Invalid package name: ${pkg}`,"Use a short name like `router` or the fully qualified `@stacksjs/router`.");const sourceDir=resolve(process.cwd(),"node_modules","@stacksjs",shortName),targetDir=path.frameworkPath(`core/${shortName}`);try{if(!(await stat(sourceDir)).isDirectory())fail(`${sourceDir} is not a directory.`)}catch{fail(`Could not find @stacksjs/${shortName} in node_modules.`,"Run `bun install` first, or check the package name.")}if(existsSync(targetDir)&&!force)fail(`Already published: ${targetDir.replace(`${process.cwd()}/`,"")}`,"Pass --force to overwrite.");mkdirSync(dirname(targetDir),{recursive:!0});const SKIP=new Set(["node_modules","dist",".bun",".cache","tsconfig.tsbuildinfo"]);let copied=0;await copyTreeFiltered(sourceDir,targetDir,SKIP,()=>copied++);log.success(`Published @stacksjs/${shortName} \u2192 ${italic(targetDir.replace(`${process.cwd()}/`,""))} (${copied} files)`);log.info("Edit freely - local changes win over the installed package.")}async function copyTreeFiltered(sourceDir,targetDir,skip,onFile){mkdirSync(targetDir,{recursive:!0});const entries=await readdir(sourceDir,{withFileTypes:!0});for(const entry of entries){if(skip.has(entry.name))continue;const src=`${sourceDir}/${entry.name}`,dst=`${targetDir}/${entry.name}`;if(entry.isDirectory()){await copyTreeFiltered(src,dst,skip,onFile);continue}await cp(src,dst,{force:!0,dereference:!0});onFile()}}async function publishResource(ctx){const{kind,name,defaultsDir,userDir,force}=ctx,fileName=name.endsWith(".ts")?name:`${name}.ts`,matches=globSync([`${defaultsDir}/**/${fileName}`],{absolute:!0});if(!matches.length){await log.error(`Could not find default ${kind}: ${italic(fileName)}`);log.info(`Looked under: ${italic(defaultsDir)}`);await log.flush();process.exit(ExitCode.FatalError)}if(matches.length>1){log.warn(`Multiple defaults match ${italic(fileName)}; using the first:`);for(const m of matches)log.info(` ${m}`)}const sourcePath=matches[0];if(!sourcePath)throw Error(`Could not resolve default ${kind}: ${fileName}`);const targetPath=`${userDir.replace(/\/$/,"")}/${fileName}`;if(existsSync(targetPath)&&!force){await log.error(`Already exists: ${italic(targetPath)}`);log.info("Pass --force to overwrite.");await log.flush();process.exit(ExitCode.FatalError)}mkdirSync(dirname(targetPath),{recursive:!0});await fs.promises.copyFile(sourcePath,targetPath);const carried=await carryRelativeImports(sourcePath,targetPath);log.success(`Published ${kind} ${italic(name)} \u2192 ${italic(targetPath.replace(`${process.cwd()}/`,""))}`);for(const file of carried)log.info(` + ${italic(file.replace(`${process.cwd()}/`,""))} (imported by it)`)}export async function carryRelativeImports(sourcePath,targetPath,seen=new Set){if(seen.has(sourcePath))return[];seen.add(sourcePath);const source=await fs.promises.readFile(sourcePath,"utf-8"),written=[],root=realpathSync(process.cwd()),targetDir=realpathSync(dirname(targetPath)),specifiers=[...source.matchAll(/from\s+['"](\.[^'"]+)['"]/g)].map((match)=>match[1]);for(const specifier of new Set(specifiers)){if(!specifier)continue;const candidates=specifier.endsWith(".ts")?[specifier]:[`${specifier}.ts`,`${specifier}/index.ts`];for(const candidate of candidates){const from=resolve(dirname(sourcePath),candidate),to=resolve(targetDir,candidate);if(!existsSync(from))continue;if(!to.startsWith(`${root}/`))break;if(!existsSync(to)){mkdirSync(dirname(to),{recursive:!0});await fs.promises.copyFile(from,to);written.push(to)}written.push(...await carryRelativeImports(from,to,seen));break}}return written}async function vendorFramework(explicitPath,force){const coreDir=path.frameworkPath("core"),rel=(p)=>p.replace(`${process.cwd()}/`,"");if(existsSync(coreDir)&&!force){log.info(`Already vendored: ${italic(rel(coreDir))}`);log.info("Pass --force to replace it with a fresh copy of the checkout.");return}const framework=resolveFrameworkCheckout(explicitPath);if(!framework){await log.error("No Stacks checkout found to vendor from.");log.info("Pass one with `--path <dir>`, or set STACKS_FRAMEWORK_PATH.");log.info("A checkout is required: the published packages ship `dist` only, so a copy of them would not be editable.");await log.flush();process.exit(ExitCode.FatalError)}const sourceCore=join(framework,"storage/framework/core"),corePkgPath=resolve(sourceCore,"package.json");if(!existsSync(corePkgPath)){await log.error(`${sourceCore} has no package.json - that does not look like a Stacks checkout.`);process.exit(ExitCode.FatalError)}const depName=JSON.parse(await fs.promises.readFile(corePkgPath,"utf-8")).name??"stacks";log.info(`Vendoring the framework from ${italic(framework)}...`);if(existsSync(coreDir))await fs.promises.rm(coreDir,{recursive:!0,force:!0});const SKIP=new Set(["node_modules",".bun",".cache","tsconfig.tsbuildinfo"]);let copied=0;await copyTreeFiltered(sourceCore,coreDir,SKIP,()=>copied++);const rootPkgPath=resolve(process.cwd(),"package.json"),rootPkg=JSON.parse(await fs.promises.readFile(rootPkgPath,"utf-8")),provided=new Set([depName]);for(const entry of await readdir(coreDir,{withFileTypes:!0})){if(!entry.isDirectory())continue;if(existsSync(resolve(coreDir,entry.name,"package.json")))provided.add(`@stacksjs/${entry.name}`)}let repointed=0;for(const field of["dependencies","devDependencies"]){const deps=rootPkg[field];if(!deps)continue;for(const name of Object.keys(deps)){if(!provided.has(name)||deps[name].startsWith("workspace:"))continue;deps[name]="workspace:*";repointed++}}if(!rootPkg.dependencies?.[depName]&&!rootPkg.devDependencies?.[depName])rootPkg.dependencies={...rootPkg.dependencies,[depName]:"workspace:*"};const workspaces=rootPkg.workspaces??[];let addedGlobs=0;for(const glob of["storage/framework/core","storage/framework/core/*"]){if(workspaces.some((existing)=>existing.replace(/^\.\//,"").replace(/\/$/,"")===glob))continue;workspaces.push(glob);addedGlobs++}rootPkg.workspaces=workspaces;await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
|
|
4
|
-
`);const bunfigPath=resolve(process.cwd(),"bunfig.toml");let rewrittenPreloads=0;if(existsSync(bunfigPath)){const bunfig=await fs.promises.readFile(bunfigPath,"utf-8"),next=bunfig.replace(/(["'])@stacksjs\/([\w-]+)\/([^"']+?)\.js\1/g,(match,quote,pkgName,subpath)=>{if(!provided.has(`@stacksjs/${pkgName}`))return match;rewrittenPreloads++;return`${quote}./storage/framework/core/${pkgName}/${subpath}.ts${quote}`});if(next!==bunfig)await fs.promises.writeFile(bunfigPath,next)}const tsconfigPath=resolve(process.cwd(),"tsconfig.json");let rewroteTsconfig=!1;if(existsSync(tsconfigPath)){const raw=await fs.promises.readFile(tsconfigPath,"utf-8"),next=raw.replace(/"extends"\s*:\s*"\.\/storage\/framework\/tsconfig\.app\.json"/,'"extends": "./storage/framework/core/tsconfig.json"');if(next!==raw){await fs.promises.writeFile(tsconfigPath,next);rewroteTsconfig=!0}}log.success(`Vendored ${copied} files into ${italic(rel(coreDir))}`);log.info(`package.json now depends on ${depName}@workspace:*`);if(repointed>0)log.info(`Repointed ${repointed} version range${repointed===1?"":"s"} to workspace:*`);if(addedGlobs>0)log.info(`Added ${addedGlobs} workspace glob${addedGlobs===1?"":"s"} for the vendored packages`);if(rewrittenPreloads>0)log.info(`Rewrote ${rewrittenPreloads} bunfig.toml preload path${rewrittenPreloads===1?"":"s"} to the vendored source`);if(rewroteTsconfig)log.info("tsconfig.json now extends storage/framework/core/tsconfig.json");log.info("Linking the workspace...");if(await Bun.spawn(["bun","install"],{cwd:process.cwd(),stdout:"inherit",stderr:"inherit"}).exited!==0){await log.error("`bun install` failed. The files are in place; re-run the install once the failure is resolved.");process.exit(ExitCode.FatalError)}log.success("This project now runs on the vendored framework - edits under storage/framework/core are live.");log.info("Go back to the published packages any time with `buddy unpublish:core --all`.")}function resolveFrameworkCheckout(explicit){const candidates=[explicit,process.env.STACKS_FRAMEWORK_PATH,resolve(process.cwd(),"../stacks"),join(homedir(),"Code/stacks")].filter(Boolean);for(const candidate of candidates){const full=resolve(candidate);if(full===resolve(process.cwd()))continue;if(existsSync(join(full,"storage/framework/core/package.json")))return full}return null}async function reportCoreStatus(){const coreDir=path.frameworkPath("core"),rel=(p)=>p.replace(`${process.cwd()}/`,""),vendored=existsSync(resolve(coreDir,"package.json")),rootPkgPath=resolve(process.cwd(),"package.json"),rootPkg=existsSync(rootPkgPath)?JSON.parse(await fs.promises.readFile(rootPkgPath,"utf-8")):{},declared=rootPkg.dependencies?.stacks??rootPkg.devDependencies?.stacks;if(vendored){const corePkg=JSON.parse(await fs.promises.readFile(resolve(coreDir,"package.json"),"utf-8")),packages=(await readdir(coreDir,{withFileTypes:!0})).filter((entry)=>entry.isDirectory()&&existsSync(resolve(coreDir,entry.name,"package.json")));log.info(`Layout: vendored - ${italic(rel(coreDir))} (${packages.length} packages, v${corePkg.version??"unknown"})`);log.info(`Declared: stacks@${declared??"(not declared)"}`);if(declared&&!declared.startsWith("workspace:"))log.warn(`The vendored copy is not linked: stacks is declared as ${declared}, not workspace:*. Run \`buddy publish:core --all --force\` to relink, or \`buddy unpublish:core --all\` to remove it.`);log.info("Move to the published packages with `buddy unpublish:core --all`.");return}const installed=resolve(process.cwd(),"node_modules/@stacksjs/buddy/package.json"),installedVersion=existsSync(installed)?JSON.parse(await fs.promises.readFile(installed,"utf-8")).version:void 0;log.info("Layout: published packages - no storage/framework/core in this project");log.info(`Declared: stacks@${declared??"(not declared)"}`);log.info(`Installed: ${installedVersion?`v${installedVersion}`:"(run bun install)"}`);log.info("Vendor the framework for local development with `buddy publish:core --all`.")}async function unpublishCorePackage(pkg,force){const shortName=normalizeCoreName(pkg),targetDir=path.frameworkPath(`core/${shortName}`),rel=(p)=>p.replace(`${process.cwd()}/`,"");if(!existsSync(targetDir)){log.info(`Not vendored: ${italic(rel(targetDir))} - nothing to do.`);return}const installed=resolve(process.cwd(),"node_modules","@stacksjs",shortName);if(!existsSync(installed)&&!force){await log.error(`@stacksjs/${shortName} is not installed, so removing the vendored copy would leave nothing to resolve.`);log.info(`Run \`bun add @stacksjs/${shortName}\` first, or pass --force to remove it anyway.`);await log.flush();process.exit(ExitCode.FatalError)}await assertNoUncommittedChanges(targetDir,force);await fs.promises.rm(targetDir,{recursive:!0,force:!0});log.success(`Unpublished ${italic(rel(targetDir))} - @stacksjs/${shortName} now resolves from node_modules.`)}async function unvendorFramework(force){const coreDir=path.frameworkPath("core"),rel=(p)=>p.replace(`${process.cwd()}/`,"");if(!existsSync(coreDir)){log.info("No storage/framework/core in this project - already on the installed packages.");return}const corePkgPath=resolve(coreDir,"package.json");if(!existsSync(corePkgPath)){await log.error(`${rel(coreDir)} has no package.json, so its version cannot be determined.`);log.info("Unvendor the packages individually with `buddy unpublish:core <pkg>` instead.");await log.flush();process.exit(ExitCode.FatalError)}const corePkg=JSON.parse(await fs.promises.readFile(corePkgPath,"utf-8")),version=corePkg.version;if(!version){await log.error(`${rel(corePkgPath)} has no version field.`);process.exit(ExitCode.FatalError)}await assertNoUncommittedChanges(coreDir,force);const depName=corePkg.name??"stacks",range=`^${await resolvePublishedVersion(depName,version)}`,rootPkgPath=resolve(process.cwd(),"package.json"),rootPkgRaw=await fs.promises.readFile(rootPkgPath,"utf-8"),rootPkg=JSON.parse(rootPkgRaw),provided=new Set([depName,...Object.keys(corePkg.dependencies??{}).filter((name)=>name.startsWith("@stacksjs/"))]);for(const entry of await readdir(coreDir,{withFileTypes:!0}))if(entry.isDirectory())provided.add(`@stacksjs/${entry.name}`);let repointed=0;const repointWorkspaceRanges=(pkg)=>{let touched=!1;for(const field of["dependencies","devDependencies","peerDependencies"]){const deps=pkg[field];if(!deps)continue;for(const[name,spec]of Object.entries(deps)){if(!spec.startsWith("workspace:")||!provided.has(name))continue;deps[name]=range;touched=!0;repointed++}}return touched};repointWorkspaceRanges(rootPkg);if(!rootPkg.dependencies?.[depName]&&!rootPkg.devDependencies?.[depName])rootPkg.dependencies={...rootPkg.dependencies,[depName]:range};let rewrittenScripts=0;for(const[name,script]of Object.entries(rootPkg.scripts??{})){if(!/storage\/framework\/core\/buddy\/src\/cli\.ts/.test(script))continue;rootPkg.scripts[name]=script.replace(/\bbunx?\s+(?:--bun\s+)?\.?\/?storage\/framework\/core\/buddy\/src\/cli\.ts/g,"./buddy");rewrittenScripts++}if(Array.isArray(rootPkg.workspaces)){rootPkg.workspaces=rootPkg.workspaces.filter((glob)=>!isCoreWorkspaceGlob(glob));if(rootPkg.workspaces.length===0)delete rootPkg.workspaces}await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
|
|
4
|
+
`);const bunfigPath=resolve(process.cwd(),"bunfig.toml");let rewrittenPreloads=0;if(existsSync(bunfigPath)){const bunfig=await fs.promises.readFile(bunfigPath,"utf-8"),next=bunfig.replace(/(["'])@stacksjs\/([\w-]+)\/([^"']+?)\.js\1/g,(match,quote,pkgName,subpath)=>{if(!provided.has(`@stacksjs/${pkgName}`))return match;rewrittenPreloads++;return`${quote}./storage/framework/core/${pkgName}/${subpath}.ts${quote}`});if(next!==bunfig)await fs.promises.writeFile(bunfigPath,next)}const tsconfigPath=resolve(process.cwd(),"tsconfig.json");let rewroteTsconfig=!1;if(existsSync(tsconfigPath)){const raw=await fs.promises.readFile(tsconfigPath,"utf-8"),next=raw.replace(/"extends"\s*:\s*"\.\/storage\/framework\/tsconfig\.app\.json"/,'"extends": "./storage/framework/core/tsconfig.json"');if(next!==raw){await fs.promises.writeFile(tsconfigPath,next);rewroteTsconfig=!0}}log.success(`Vendored ${copied} files into ${italic(rel(coreDir))}`);log.info(`package.json now depends on ${depName}@workspace:*`);if(repointed>0)log.info(`Repointed ${repointed} version range${repointed===1?"":"s"} to workspace:*`);if(addedGlobs>0)log.info(`Added ${addedGlobs} workspace glob${addedGlobs===1?"":"s"} for the vendored packages`);if(rewrittenPreloads>0)log.info(`Rewrote ${rewrittenPreloads} bunfig.toml preload path${rewrittenPreloads===1?"":"s"} to the vendored source`);if(rewroteTsconfig)log.info("tsconfig.json now extends storage/framework/core/tsconfig.json");log.info("Linking the workspace...");if(await Bun.spawn(["bun","install"],{cwd:process.cwd(),stdout:"inherit",stderr:"inherit"}).exited!==0){await log.error("`bun install` failed. The files are in place; re-run the install once the failure is resolved.");process.exit(ExitCode.FatalError)}log.success("This project now runs on the vendored framework - edits under storage/framework/core are live.");log.info("Go back to the published packages any time with `buddy unpublish:core --all`.")}function resolveFrameworkCheckout(explicit){const candidates=[explicit,process.env.STACKS_FRAMEWORK_PATH,resolve(process.cwd(),"../stacks"),join(homedir(),"Code/stacks")].filter(Boolean);for(const candidate of candidates){const full=resolve(candidate);if(full===resolve(process.cwd()))continue;if(existsSync(join(full,"storage/framework/core/package.json")))return full}return null}async function reportCoreStatus(){const coreDir=path.frameworkPath("core"),rel=(p)=>p.replace(`${process.cwd()}/`,""),vendored=existsSync(resolve(coreDir,"package.json")),rootPkgPath=resolve(process.cwd(),"package.json"),rootPkg=existsSync(rootPkgPath)?JSON.parse(await fs.promises.readFile(rootPkgPath,"utf-8")):{},declared=rootPkg.dependencies?.stacks??rootPkg.devDependencies?.stacks;if(vendored){const corePkg=JSON.parse(await fs.promises.readFile(resolve(coreDir,"package.json"),"utf-8")),packages=(await readdir(coreDir,{withFileTypes:!0})).filter((entry)=>entry.isDirectory()&&existsSync(resolve(coreDir,entry.name,"package.json")));log.info(`Layout: vendored - ${italic(rel(coreDir))} (${packages.length} packages, v${corePkg.version??"unknown"})`);log.info(`Declared: stacks@${declared??"(not declared)"}`);if(declared&&!declared.startsWith("workspace:"))log.warn(`The vendored copy is not linked: stacks is declared as ${declared}, not workspace:*. Run \`buddy publish:core --all --force\` to relink, or \`buddy unpublish:core --all\` to remove it.`);log.info("Move to the published packages with `buddy unpublish:core --all`.");return}const installed=resolve(process.cwd(),"node_modules/@stacksjs/buddy/package.json"),installedVersion=existsSync(installed)?JSON.parse(await fs.promises.readFile(installed,"utf-8")).version:void 0;log.info("Layout: published packages - no storage/framework/core in this project");log.info(`Declared: stacks@${declared??"(not declared)"}`);log.info(`Installed: ${installedVersion?`v${installedVersion}`:"(run bun install)"}`);log.info("Vendor the framework for local development with `buddy publish:core --all`.")}async function unpublishCorePackage(pkg,force){const shortName=normalizeCoreName(pkg),targetDir=path.frameworkPath(`core/${shortName}`),rel=(p)=>p.replace(`${process.cwd()}/`,"");if(!existsSync(targetDir)){log.info(`Not vendored: ${italic(rel(targetDir))} - nothing to do.`);return}const installed=resolve(process.cwd(),"node_modules","@stacksjs",shortName);if(!existsSync(installed)&&!force){await log.error(`@stacksjs/${shortName} is not installed, so removing the vendored copy would leave nothing to resolve.`);log.info(`Run \`bun add @stacksjs/${shortName}\` first, or pass --force to remove it anyway.`);await log.flush();process.exit(ExitCode.FatalError)}await assertNoUncommittedChanges(targetDir,force);await fs.promises.rm(targetDir,{recursive:!0,force:!0});log.success(`Unpublished ${italic(rel(targetDir))} - @stacksjs/${shortName} now resolves from node_modules.`)}export function rewriteBunfigPreloads(bunfig){const packages=new Set;let rewritten=0;return{next:bunfig.replace(/(["'])\.?\/?storage\/framework\/core\/([\w-]+)\/([^"']+?)\.ts\1/g,(_match,quote,pkgName,subpath)=>{rewritten++;packages.add(`@stacksjs/${pkgName}`);return`${quote}@stacksjs/${pkgName}/${subpath}.js${quote}`}),packages,rewritten}}async function unvendorFramework(force){const coreDir=path.frameworkPath("core"),rel=(p)=>p.replace(`${process.cwd()}/`,"");if(!existsSync(coreDir)){log.info("No storage/framework/core in this project - already on the installed packages.");return}const corePkgPath=resolve(coreDir,"package.json");if(!existsSync(corePkgPath)){await log.error(`${rel(coreDir)} has no package.json, so its version cannot be determined.`);log.info("Unvendor the packages individually with `buddy unpublish:core <pkg>` instead.");await log.flush();process.exit(ExitCode.FatalError)}const corePkg=JSON.parse(await fs.promises.readFile(corePkgPath,"utf-8")),version=corePkg.version;if(!version){await log.error(`${rel(corePkgPath)} has no version field.`);process.exit(ExitCode.FatalError)}await assertNoUncommittedChanges(coreDir,force);const depName=corePkg.name??"stacks",range=`^${await resolvePublishedVersion(depName,version)}`,rootPkgPath=resolve(process.cwd(),"package.json"),rootPkgRaw=await fs.promises.readFile(rootPkgPath,"utf-8"),rootPkg=JSON.parse(rootPkgRaw),provided=new Set([depName,...Object.keys(corePkg.dependencies??{}).filter((name)=>name.startsWith("@stacksjs/"))]);for(const entry of await readdir(coreDir,{withFileTypes:!0}))if(entry.isDirectory())provided.add(`@stacksjs/${entry.name}`);let repointed=0;const repointWorkspaceRanges=(pkg)=>{let touched=!1;for(const field of["dependencies","devDependencies","peerDependencies"]){const deps=pkg[field];if(!deps)continue;for(const[name,spec]of Object.entries(deps)){if(!spec.startsWith("workspace:")||!provided.has(name))continue;deps[name]=range;touched=!0;repointed++}}return touched};repointWorkspaceRanges(rootPkg);if(!rootPkg.dependencies?.[depName]&&!rootPkg.devDependencies?.[depName])rootPkg.dependencies={...rootPkg.dependencies,[depName]:range};let rewrittenScripts=0;for(const[name,script]of Object.entries(rootPkg.scripts??{})){if(!/storage\/framework\/core\/buddy\/src\/cli\.ts/.test(script))continue;rootPkg.scripts[name]=script.replace(/\bbunx?\s+(?:--bun\s+)?\.?\/?storage\/framework\/core\/buddy\/src\/cli\.ts/g,"./buddy");rewrittenScripts++}if(Array.isArray(rootPkg.workspaces)){rootPkg.workspaces=rootPkg.workspaces.filter((glob)=>!isCoreWorkspaceGlob(glob));if(rootPkg.workspaces.length===0)delete rootPkg.workspaces}await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
|
|
5
5
|
`);for(const glob of rootPkg.workspaces??[])for(const memberPkgPath of globSync(`${glob.replace(/\/$/,"")}/package.json`,{cwd:process.cwd(),absolute:!0})){const raw=await fs.promises.readFile(memberPkgPath,"utf-8"),memberPkg=JSON.parse(raw);if(repointWorkspaceRanges(memberPkg))await fs.promises.writeFile(memberPkgPath,`${JSON.stringify(memberPkg,null,2)}
|
|
6
|
-
`)}const survivingManifests=await rewriteSurvivingFrameworkManifests(process.cwd(),provided,range),bunfigPath=resolve(process.cwd(),"bunfig.toml");let rewrittenPreloads=0;if(existsSync(bunfigPath)){const bunfig=await fs.promises.readFile(bunfigPath,"utf-8"),next
|
|
6
|
+
`)}const survivingManifests=await rewriteSurvivingFrameworkManifests(process.cwd(),provided,range),bunfigPath=resolve(process.cwd(),"bunfig.toml");let rewrittenPreloads=0;if(existsSync(bunfigPath)){const bunfig=await fs.promises.readFile(bunfigPath,"utf-8"),{next,packages:preloadedPackages,rewritten}=rewriteBunfigPreloads(bunfig);rewrittenPreloads+=rewritten;if(next!==bunfig)await fs.promises.writeFile(bunfigPath,next);let declaredForPreload=0;for(const pkgName of preloadedPackages){if(rootPkg.dependencies?.[pkgName]||rootPkg.devDependencies?.[pkgName])continue;rootPkg.dependencies={...rootPkg.dependencies,[pkgName]:range};declaredForPreload++}if(declaredForPreload>0)await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
|
|
7
|
+
`)}const tsconfigPath=resolve(process.cwd(),"tsconfig.json");let rewroteTsconfig=!1,rewroteTypecheck=!1;if(existsSync(tsconfigPath)){const raw=await fs.promises.readFile(tsconfigPath,"utf-8"),next=raw.replace(/"extends"\s*:\s*"\.\/storage\/framework\/core\/tsconfig\.json"/,'"extends": "./storage/framework/tsconfig.app.json"');if(next!==raw){await fs.promises.writeFile(tsconfigPath,next);rewroteTsconfig=!0}}const splitTypecheck=splitFrameworkTypecheckScript(rootPkg.scripts??{});if(splitTypecheck){rootPkg.scripts=splitTypecheck;rewroteTypecheck=!0;await fs.promises.writeFile(rootPkgPath,`${JSON.stringify(rootPkg,null,2)}
|
|
7
8
|
`)}const rewrittenCommands=await rewriteCoreCommandPaths(process.cwd()),prunedWorkflows=await pruneVendoredCoreFromWorkflows(process.cwd()),rewrittenImports=await rewriteCoreSourceImports(process.cwd());await fs.promises.rm(coreDir,{recursive:!0,force:!0});const pantryLock=resolve(process.cwd(),"pantry.lock"),removedPantryLock=existsSync(pantryLock);await fs.promises.rm(pantryLock,{force:!0});let danglingRemoved=0;for(const depsDir of["node_modules","pantry"]){const scopedDir=resolve(process.cwd(),depsDir,"@stacksjs");if(!existsSync(scopedDir))continue;for(const entry of await readdir(scopedDir,{withFileTypes:!0})){if(!entry.isSymbolicLink())continue;const link=resolve(scopedDir,entry.name);if(existsSync(link))continue;await fs.promises.rm(link,{force:!0});danglingRemoved++}const rootLink=resolve(process.cwd(),depsDir,depName);if(existsSync(dirname(rootLink))&&isDanglingLink(rootLink)){await fs.promises.rm(rootLink,{force:!0});danglingRemoved++}}log.success(`Removed ${italic(rel(coreDir))}`);log.info(`package.json now depends on ${depName}@${range}`);if(repointed>0)log.info(`Repointed ${repointed} workspace: range${repointed===1?"":"s"} to ${range}`);if(survivingManifests.ranges>0)log.info(`Repointed ${survivingManifests.ranges} workspace: range${survivingManifests.ranges===1?"":"s"} across ${survivingManifests.files.length} surviving framework manifest${survivingManifests.files.length===1?"":"s"}`);if(removedPantryLock)log.info("Removed the legacy Pantry workspace lock; the install will resolve a package-layout graph");if(danglingRemoved>0)log.info(`Removed ${danglingRemoved} node_modules symlink${danglingRemoved===1?"":"s"} left pointing into it`);if(rewrittenScripts>0)log.info(`Repointed ${rewrittenScripts} package.json script${rewrittenScripts===1?"":"s"} to ./buddy`);if(rewrittenPreloads>0)log.info(`Rewrote ${rewrittenPreloads} bunfig.toml preload path${rewrittenPreloads===1?"":"s"} to package specifiers`);if(rewroteTsconfig)log.info("tsconfig.json now extends storage/framework/tsconfig.app.json");if(rewroteTypecheck)log.info("`typecheck` now checks this app as well as the framework files it still ships");for(const pruned of prunedWorkflows){const parts=[pruned.removedJobs.length>0?`${pruned.removedJobs.length} job${pruned.removedJobs.length===1?"":"s"} (${pruned.removedJobs.join(", ")})`:"",pruned.removedSteps>0?`${pruned.removedSteps} step${pruned.removedSteps===1?"":"s"}`:""].filter(Boolean);log.info(`${pruned.file}: removed ${parts.join(" and ")} that ran against the vendored core`)}if(rewrittenCommands.length>0){log.info(`Repointed vendored-CLI commands to ./buddy in ${rewrittenCommands.length} file${rewrittenCommands.length===1?"":"s"}:`);for(const file of rewrittenCommands)log.info(` ${file}`)}if(rewrittenImports.length>0){log.info(`Repointed vendored-source imports to package specifiers in ${rewrittenImports.length} file${rewrittenImports.length===1?"":"s"}:`);for(const file of rewrittenImports)log.info(` ${file}`)}const stragglers=await findCoreReferences(process.cwd());if(stragglers.length>0){log.warn(`${stragglers.length} file${stragglers.length===1?"":"s"} still reference storage/framework/core, which no longer exists:`);for(const{file,line,text}of stragglers)log.warn(` ${file}:${line} ${text}`);log.info("Run those through `./buddy <command>` or a package specifier before deploying.")}const installer=detectInstaller(process.cwd());log.info(`Installing the published packages with \`${installer.join(" ")}\`...`);if(await Bun.spawn(installer,{cwd:process.cwd(),stdout:"inherit",stderr:"inherit"}).exited!==0){await log.error(`\`${installer.join(" ")}\` failed. package.json and bunfig.toml were updated; re-run the install once the failure is resolved.`);process.exit(ExitCode.FatalError)}log.success("This project now runs on the published Stacks packages.");log.info("Vendor an individual package again any time with `buddy publish:core <pkg>`.")}async function resolvePublishedVersion(depName,vendored){let published;try{published=await fetchPublishedVersions(depName)}catch(error){log.warn(`Could not reach the npm registry to check ${depName} versions (${error instanceof Error?error.message:String(error)}).`);log.info(`Pinning the vendored version, ${depName}@^${vendored}.`);return vendored}if(published.versions.has(vendored))return vendored;if(!published.latest){log.warn(`${depName}@${vendored} is not published and the registry reports no latest version.`);return vendored}log.warn(`${depName}@${vendored} is not published yet - the vendored copy is ahead of npm.`);log.info(`Pinning the newest published version instead, ${depName}@^${published.latest}.`);return published.latest}function normalizeCoreName(pkg){const shortName=pkg.replace(/^@stacksjs\//,"").replace(/^core\//,"");if(!shortName||shortName.includes("/")||shortName.includes("..")){process.stderr.write(`Invalid package name: ${pkg}
|
|
8
9
|
`);process.stderr.write(" Use a short name like `router` or the fully qualified `@stacksjs/router`.\n");process.exit(ExitCode.FatalError)}return shortName}function isCoreWorkspaceGlob(glob){const normalized=glob.replace(/^\.\//,"").replace(/\/$/,"");return normalized==="storage/framework/core"||normalized.startsWith("storage/framework/core/")}async function assertNoUncommittedChanges(dir,force){if(force)return;try{const proc=Bun.spawn(["git","status","--porcelain","--",dir],{cwd:process.cwd(),stdout:"pipe",stderr:"ignore"}),output=await new Response(proc.stdout).text();if(await proc.exited!==0)return;const changed=output.split(`
|
|
9
10
|
`).filter(Boolean);if(changed.length===0)return;log.error(`${changed.length} uncommitted change${changed.length===1?"":"s"} under ${italic(dir.replace(`${process.cwd()}/`,""))}:`);for(const line of changed.slice(0,10))log.info(` ${line}`);if(changed.length>10)log.info(` ... and ${changed.length-10} more`);log.info("Commit or stash them first, or pass --force to delete them anyway.");await log.flush();process.exit(ExitCode.FatalError)}catch{}}
|
package/dist/commands/share.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{getErrorMessage}from"@stacksjs/utils";import process from"node:process";import{runApiDevServer,runDashboardDevServer,runDesktopDevServer,runDocsDevServer,runFrontendDevServer}from"@stacksjs/actions";import{bold,cyan,dim,green,intro,log,outro,spinner}from"@stacksjs/cli";import{ports as configPorts}from"@stacksjs/config";import{ExitCode}from"@stacksjs/types";const originalStdoutWrite=process.stdout.write.bind(process.stdout),originalStderrWrite=process.stderr.write.bind(process.stderr),originalConsoleLog
|
|
1
|
+
import{getErrorMessage}from"@stacksjs/utils";import process from"node:process";import{runApiDevServer,runDashboardDevServer,runDesktopDevServer,runDocsDevServer,runFrontendDevServer}from"@stacksjs/actions";import{bold,cyan,dim,green,intro,log,outro,spinner}from"@stacksjs/cli";import{ports as configPorts}from"@stacksjs/config";import{ExitCode}from"@stacksjs/types";const originalStdoutWrite=process.stdout.write.bind(process.stdout),originalStderrWrite=process.stderr.write.bind(process.stderr),{log:originalConsoleLog,error:originalConsoleError,warn:originalConsoleWarn,info:originalConsoleInfo}=console;let _muted=!1,_verboseBuffer=[];function muteOutput(){_muted=!0;_verboseBuffer=[];const filter=(fn)=>{return function(chunk,...args){if(_muted){_verboseBuffer.push(String(chunk));return!0}return fn(chunk,...args)}};process.stdout.write=filter(originalStdoutWrite);process.stderr.write=filter(originalStderrWrite);console.log=(...args)=>{if(!_muted)originalConsoleLog(...args)};console.error=(...args)=>{if(!_muted)originalConsoleError(...args)};console.warn=(...args)=>{if(!_muted)originalConsoleWarn(...args)};console.info=(...args)=>{if(!_muted)originalConsoleInfo(...args)}}function unmuteOutput(){_muted=!1;process.stdout.write=originalStdoutWrite;process.stderr.write=originalStderrWrite;console.log=originalConsoleLog;console.error=originalConsoleError;console.warn=originalConsoleWarn;console.info=originalConsoleInfo}async function waitForPort(port,host="localhost",timeoutMs=30000){const start=Date.now();while(Date.now()-start<timeoutMs)try{await(await fetch(`http://${host}:${port}`,{signal:AbortSignal.timeout(1000)})).arrayBuffer();return}catch{await Bun.sleep(300)}throw Error(`Timed out waiting for server on port ${port}`)}const devServerRunners={frontend:runFrontendDevServer,api:runApiDevServer,backend:runApiDevServer,admin:runDashboardDevServer,dashboard:runDashboardDevServer,desktop:runDesktopDevServer,docs:runDocsDevServer},companionServices={frontend:[{port:configPorts?.api||3008,suffix:"api",label:"API",runner:runApiDevServer},{port:configPorts?.docs||3006,suffix:"docs",label:"Docs",runner:runDocsDevServer}]};function capitalize(s){return s.charAt(0).toUpperCase()+s.slice(1)}export function share(buddy){buddy.command("share [type]","Share your local development server via a public tunnel").option("-p, --port <port>","Local port to share").option("--server <url>","Tunnel server URL",{default:"api.localtunnel.dev"}).option("--subdomain <name>","Request a specific subdomain").option("--verbose","Enable verbose output",{default:!1}).action(async(type,options)=>{const perf=await intro("buddy share"),serviceType=type||"frontend",defaultPorts={frontend:configPorts?.frontend||3000,api:configPorts?.api||3008,backend:configPorts?.backend||3001,admin:configPorts?.admin||3002,dashboard:configPorts?.admin||3002,library:configPorts?.library||3003,desktop:configPorts?.desktop||3004,email:configPorts?.email||3005,docs:configPorts?.docs||3006,inspect:configPorts?.inspect||3007},port=options.port?Number.parseInt(options.port,10):defaultPorts[serviceType]||3000;if(Number.isNaN(port)||port<1||port>65535){await log.error(`Invalid port: ${options.port}`);process.exit(ExitCode.InvalidArgument)}const server=options.server||"api.localtunnel.dev",tunnels=[],companions=companionServices[serviceType]||[],s=spinner();try{const{localTunnel}=await import("@stacksjs/tunnel");console.log();const runner=devServerRunners[serviceType];if(runner){s.start(`Starting ${serviceType} dev server...`);muteOutput();runner({verbose:options.verbose??!1}).catch(()=>{});await waitForPort(port);unmuteOutput();s.succeed(`${bold(capitalize(serviceType))} ready ${dim(`on :${port}`)}`)}const startedCompanions=[];if(companions.length>0){s.start("Starting companion services...");muteOutput();for(const companion of companions)companion.runner({verbose:options.verbose??!1}).catch(()=>{});const results=await Promise.allSettled(companions.map((c)=>waitForPort(c.port,"localhost",60000)));unmuteOutput();for(let i=0;i<companions.length;i++){const result=results[i],companion=companions[i];if(!result||!companion)continue;if(result.status==="fulfilled"){startedCompanions.push(companion);s.succeed(`${bold(companion.label)} ready ${dim(`on :${companion.port}`)}`)}else s.fail(`${companion.label} failed to start ${dim(`on :${companion.port}`)}`)}}console.log();s.start("Creating tunnel...");const primaryTunnel=await localTunnel({port,server,subdomain:options.subdomain,verbose:options.verbose,onRequest:(req)=>{if(options.verbose)console.log(` ${dim(`${req.method} ${req.url}`)}`)},onReconnecting:(info)=>{s.start(`Reconnecting... (attempt ${info.attempt})`)}});tunnels.push(primaryTunnel);const baseSubdomain=primaryTunnel.subdomain;for(const companion of startedCompanions)try{const companionTunnel=await localTunnel({port:companion.port,server,subdomain:`${baseSubdomain}-${companion.suffix}`,verbose:options.verbose,onReconnecting:(info)=>{s.start(`${companion.label}: reconnecting... (attempt ${info.attempt})`)}});tunnels.push(companionTunnel)}catch{}s.succeed(`Connected ${dim(`to ${server}`)}`);const entries=[{label:capitalize(serviceType),url:primaryTunnel.url,local:`localhost:${port}`}];for(const companion of startedCompanions){const tunnel=tunnels.find((t)=>t.subdomain===`${baseSubdomain}-${companion.suffix}`);if(tunnel)entries.push({label:companion.label,url:tunnel.url,local:`localhost:${companion.port}`})}const maxLabel=Math.max(...entries.map((e)=>e.label.length));console.log();for(const entry of entries){const padding=" ".repeat(maxLabel-entry.label.length+1);console.log(` ${green("\u279C")} ${bold(entry.label)}${padding} ${cyan(entry.url)}`)}console.log();for(const entry of entries){const padding=" ".repeat(maxLabel-entry.label.length+1);console.log(` ${dim(entry.label)}${dim(padding)}${dim(entry.url)} ${dim("\u2192")} ${dim(entry.local)}`)}console.log();console.log(` ${dim("press Ctrl+C to stop")}`);console.log();const cleanup=async()=>{console.log();s.start("Closing tunnels...");await Promise.all(tunnels.map((t)=>t.close()));s.succeed("Tunnels closed");outro("Stopped sharing",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)};process.on("SIGINT",()=>cleanup());process.on("SIGTERM",()=>cleanup());await new Promise(()=>{})}catch(error){unmuteOutput();s.fail(getErrorMessage(error));const caught=error instanceof Error?error:Error(String(error));if(caught.message.includes("timeout")||caught.message.includes("ECONNREFUSED")){log.error(`Could not reach tunnel server at ${server}`);log.info(`Verify with: curl -sk https://${server}/status`)}else log.error(`Failed to create tunnel: ${caught.message}`);if(options.verbose)log.error(caught.stack);for(const t of tunnels)t.close();await outro("Share failed",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}})}
|
package/dist/deploy-notify.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import process from"node:process";import{sendToDiscord,sendToSlack,sendToTeams}from"@stacksjs/chat";import{log}from"@stacksjs/logging";import{ExitCode}from"@stacksjs/types";export function driverForWebhookUrl(url){let host="";try{host=new URL(url).hostname.toLowerCase()}catch{return"slack"}if(host.endsWith("discord.com")||host.endsWith("discordapp.com"))return"discord";if(host.endsWith("office.com")||host.endsWith("office365.com")||host.endsWith("outlook.com"))return"teams";return"slack"}export function resolveDeployNotifyConfig(env=process.env){const url=env.DEPLOY_WEBHOOK_URL?.trim();if(!url)return{};const rawDriver=env.DEPLOY_WEBHOOK_DRIVER?.trim().toLowerCase();return{url,driver:(rawDriver==="slack"||rawDriver==="discord"||rawDriver==="teams"?rawDriver:void 0)??driverForWebhookUrl(url),notifyOn:env.DEPLOY_WEBHOOK_NOTIFY?.trim().toLowerCase()==="all"?"all":"failure"}}function summarizeError(error,limit=1200){if(error==null)return"No error detail was captured.";const trimmed=(error instanceof Error?error.stack||error.message:String(error)).trim();return trimmed.length>limit?`${trimmed.slice(0,limit)}
|
|
2
|
-
\u2026 (truncated)`:trimmed}function formatDuration(ms){if(typeof ms!=="number"||!Number.isFinite(ms)||ms<0)return null;const total=Math.round(ms/1000);return total<60?`${total}s`:`${Math.floor(total/60)}m ${total%60}s`}function ciContext(env=process.env){const lines=[],repo
|
|
2
|
+
\u2026 (truncated)`:trimmed}function formatDuration(ms){if(typeof ms!=="number"||!Number.isFinite(ms)||ms<0)return null;const total=Math.round(ms/1000);return total<60?`${total}s`:`${Math.floor(total/60)}m ${total%60}s`}function ciContext(env=process.env){const lines=[],{GITHUB_REPOSITORY:repo,GITHUB_SHA:sha}=env;if(repo&&sha)lines.push(`Commit: ${repo}@${sha.slice(0,7)}`);else if(sha)lines.push(`Commit: ${sha.slice(0,7)}`);if(repo&&env.GITHUB_RUN_ID)lines.push(`Run: ${env.GITHUB_SERVER_URL||"https://github.com"}/${repo}/actions/runs/${env.GITHUB_RUN_ID}`);return lines}export function buildDeployMessage(outcome,env=process.env){const failed=outcome.status==="failed",target=[outcome.project,outcome.environment].filter(Boolean).join(" \xB7 ")||"project",lines=[failed?`\uD83D\uDD34 Deploy FAILED - ${target}`:`\u2705 Deploy succeeded - ${target}`];if(outcome.provider)lines.push(`Provider: ${outcome.provider}`);const duration=formatDuration(outcome.durationMs);if(duration)lines.push(`Duration: ${duration}`);lines.push(...ciContext(env));if(failed){lines.push("");lines.push("```");lines.push(summarizeError(outcome.error));lines.push("```");const envLabel=outcome.environment||"The target environment";lines.push(`${envLabel==="production"?"Production":envLabel} is still serving the previous release.`)}return lines.join(`
|
|
3
3
|
`)}export async function notifyDeployOutcome(outcome,config=resolveDeployNotifyConfig()){try{if(!config.url){log.debug("[deploy] no DEPLOY_WEBHOOK_URL set - skipping deploy notification");return!1}const notifyOn=config.notifyOn??"failure";if(outcome.status==="succeeded"&¬ifyOn!=="all"){log.debug('[deploy] deploy succeeded and DEPLOY_WEBHOOK_NOTIFY is not "all" - not notifying');return!1}const driver=config.driver??driverForWebhookUrl(config.url),message=buildDeployMessage(outcome),result=driver==="discord"?await sendToDiscord(config.url,message):driver==="teams"?await sendToTeams(config.url,message):await sendToSlack(config.url,message);if(!result?.success){log.warn(`[deploy] deploy notification to ${driver} failed: ${result?.message??"unknown error"}`);return!1}log.debug(`[deploy] deploy ${outcome.status} notification sent via ${driver}`);return!0}catch(error){log.warn(`[deploy] deploy notification could not be sent: ${error instanceof Error?error.message:String(error)}`);return!1}}export function getDeployErrorMessage(error){if(error instanceof Error&&error.message)return error.message;if(typeof error==="string"&&error)return error;try{const rendered=JSON.stringify(error);if(rendered&&rendered!=="{}"&&rendered!=="null")return rendered}catch{}return`${String(error)} (no message; re-run with --verbose for the stack)`}export function withDeployNotification(action){return async(...args)=>{const startedAt=Date.now(),environment=typeof args[0]==="string"&&args[0]?args[0]:"production",context={environment,provider:process.env.CLOUD_PROVIDER||void 0,project:process.env.APP_NAME||void 0};try{await action(...args)}catch(error){log.error(`Deploy to ${environment} failed: ${getDeployErrorMessage(error)}`);const stack=error instanceof Error?error.stack:void 0;if(stack)log.debug(stack);await notifyDeployOutcome({...context,status:"failed",durationMs:Date.now()-startedAt,error});process.exit(ExitCode.FatalError)}await notifyDeployOutcome({...context,status:"succeeded",durationMs:Date.now()-startedAt})}}
|
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.
|
|
5
|
+
"version": "0.74.21",
|
|
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.
|
|
99
|
-
"@stacksjs/ai": "^0.74.
|
|
100
|
-
"@stacksjs/alias": "^0.74.
|
|
101
|
-
"@stacksjs/analytics": "^0.74.
|
|
102
|
-
"@stacksjs/api": "^0.74.
|
|
103
|
-
"@stacksjs/arrays": "^0.74.
|
|
104
|
-
"@stacksjs/auth": "^0.74.
|
|
105
|
-
"@stacksjs/browser-extension": "^0.74.
|
|
106
|
-
"@stacksjs/build": "^0.74.
|
|
107
|
-
"@stacksjs/cache": "^0.74.
|
|
108
|
-
"@stacksjs/chat": "^0.74.
|
|
98
|
+
"@stacksjs/actions": "^0.74.21",
|
|
99
|
+
"@stacksjs/ai": "^0.74.21",
|
|
100
|
+
"@stacksjs/alias": "^0.74.21",
|
|
101
|
+
"@stacksjs/analytics": "^0.74.21",
|
|
102
|
+
"@stacksjs/api": "^0.74.21",
|
|
103
|
+
"@stacksjs/arrays": "^0.74.21",
|
|
104
|
+
"@stacksjs/auth": "^0.74.21",
|
|
105
|
+
"@stacksjs/browser-extension": "^0.74.21",
|
|
106
|
+
"@stacksjs/build": "^0.74.21",
|
|
107
|
+
"@stacksjs/cache": "^0.74.21",
|
|
108
|
+
"@stacksjs/chat": "^0.74.21",
|
|
109
109
|
"@stacksjs/clapp": "^0.2.12",
|
|
110
|
-
"@stacksjs/cli": "^0.74.
|
|
111
|
-
"@stacksjs/cloud": "^0.74.
|
|
112
|
-
"@stacksjs/cms": "^0.74.
|
|
113
|
-
"@stacksjs/collections": "^0.74.
|
|
114
|
-
"@stacksjs/config": "^0.74.
|
|
115
|
-
"@stacksjs/database": "^0.74.
|
|
116
|
-
"@stacksjs/desktop-build": "^0.74.
|
|
117
|
-
"@stacksjs/dns": "^0.74.
|
|
110
|
+
"@stacksjs/cli": "^0.74.21",
|
|
111
|
+
"@stacksjs/cloud": "^0.74.21",
|
|
112
|
+
"@stacksjs/cms": "^0.74.21",
|
|
113
|
+
"@stacksjs/collections": "^0.74.21",
|
|
114
|
+
"@stacksjs/config": "^0.74.21",
|
|
115
|
+
"@stacksjs/database": "^0.74.21",
|
|
116
|
+
"@stacksjs/desktop-build": "^0.74.21",
|
|
117
|
+
"@stacksjs/dns": "^0.74.21",
|
|
118
118
|
"@stacksjs/dnsx": "^0.2.3",
|
|
119
|
-
"@stacksjs/email": "^0.74.
|
|
120
|
-
"@stacksjs/enums": "^0.74.
|
|
121
|
-
"@stacksjs/env": "^0.74.
|
|
122
|
-
"@stacksjs/error-handling": "^0.74.
|
|
123
|
-
"@stacksjs/events": "^0.74.
|
|
124
|
-
"@stacksjs/features": "^0.74.
|
|
125
|
-
"@stacksjs/git": "^0.74.
|
|
119
|
+
"@stacksjs/email": "^0.74.21",
|
|
120
|
+
"@stacksjs/enums": "^0.74.21",
|
|
121
|
+
"@stacksjs/env": "^0.74.21",
|
|
122
|
+
"@stacksjs/error-handling": "^0.74.21",
|
|
123
|
+
"@stacksjs/events": "^0.74.21",
|
|
124
|
+
"@stacksjs/features": "^0.74.21",
|
|
125
|
+
"@stacksjs/git": "^0.74.21",
|
|
126
126
|
"@stacksjs/gitit": "^0.2.5",
|
|
127
|
-
"@stacksjs/health": "^0.74.
|
|
127
|
+
"@stacksjs/health": "^0.74.21",
|
|
128
128
|
"@stacksjs/httx": "^0.1.10",
|
|
129
|
-
"@stacksjs/image": "^0.74.
|
|
130
|
-
"@stacksjs/lint": "^0.74.
|
|
131
|
-
"@stacksjs/logging": "^0.74.
|
|
132
|
-
"@stacksjs/notifications": "^0.74.
|
|
133
|
-
"@stacksjs/objects": "^0.74.
|
|
134
|
-
"@stacksjs/orm": "^0.74.
|
|
135
|
-
"@stacksjs/path": "^0.74.
|
|
136
|
-
"@stacksjs/payments": "^0.74.
|
|
137
|
-
"@stacksjs/realtime": "^0.74.
|
|
138
|
-
"@stacksjs/router": "^0.74.
|
|
129
|
+
"@stacksjs/image": "^0.74.21",
|
|
130
|
+
"@stacksjs/lint": "^0.74.21",
|
|
131
|
+
"@stacksjs/logging": "^0.74.21",
|
|
132
|
+
"@stacksjs/notifications": "^0.74.21",
|
|
133
|
+
"@stacksjs/objects": "^0.74.21",
|
|
134
|
+
"@stacksjs/orm": "^0.74.21",
|
|
135
|
+
"@stacksjs/path": "^0.74.21",
|
|
136
|
+
"@stacksjs/payments": "^0.74.21",
|
|
137
|
+
"@stacksjs/realtime": "^0.74.21",
|
|
138
|
+
"@stacksjs/router": "^0.74.21",
|
|
139
139
|
"@stacksjs/rpx": "^0.11.42",
|
|
140
|
-
"@stacksjs/scheduler": "^0.74.
|
|
141
|
-
"@stacksjs/search-engine": "^0.74.
|
|
142
|
-
"@stacksjs/security": "^0.74.
|
|
143
|
-
"@stacksjs/server": "^0.74.
|
|
144
|
-
"@stacksjs/sites": "^0.74.
|
|
145
|
-
"@stacksjs/skills": "^0.74.
|
|
146
|
-
"@stacksjs/storage": "^0.74.
|
|
147
|
-
"@stacksjs/strings": "^0.74.
|
|
148
|
-
"@stacksjs/stx": "^0.2.
|
|
149
|
-
"@stacksjs/testing": "^0.74.
|
|
150
|
-
"@stacksjs/tinker": "^0.74.
|
|
140
|
+
"@stacksjs/scheduler": "^0.74.21",
|
|
141
|
+
"@stacksjs/search-engine": "^0.74.21",
|
|
142
|
+
"@stacksjs/security": "^0.74.21",
|
|
143
|
+
"@stacksjs/server": "^0.74.21",
|
|
144
|
+
"@stacksjs/sites": "^0.74.21",
|
|
145
|
+
"@stacksjs/skills": "^0.74.21",
|
|
146
|
+
"@stacksjs/storage": "^0.74.21",
|
|
147
|
+
"@stacksjs/strings": "^0.74.21",
|
|
148
|
+
"@stacksjs/stx": "^0.2.274",
|
|
149
|
+
"@stacksjs/testing": "^0.74.21",
|
|
150
|
+
"@stacksjs/tinker": "^0.74.21",
|
|
151
151
|
"@stacksjs/tlsx": "^0.13.19",
|
|
152
152
|
"@stacksjs/ts-cloud": "^0.12.10",
|
|
153
|
-
"@stacksjs/tunnel": "^0.74.
|
|
154
|
-
"@stacksjs/types": "^0.74.
|
|
155
|
-
"@stacksjs/ui": "^0.74.
|
|
156
|
-
"@stacksjs/utils": "^0.74.
|
|
157
|
-
"@stacksjs/validation": "^0.74.
|
|
153
|
+
"@stacksjs/tunnel": "^0.74.21",
|
|
154
|
+
"@stacksjs/types": "^0.74.21",
|
|
155
|
+
"@stacksjs/ui": "^0.74.21",
|
|
156
|
+
"@stacksjs/utils": "^0.74.21",
|
|
157
|
+
"@stacksjs/validation": "^0.74.21",
|
|
158
158
|
"ajv": "^8.20.0",
|
|
159
159
|
"ajv-formats": "^3.0.1",
|
|
160
|
-
"bun-plugin-stx": "^0.2.
|
|
160
|
+
"bun-plugin-stx": "^0.2.274",
|
|
161
161
|
"ts-pantry": "^0.11.35"
|
|
162
162
|
},
|
|
163
163
|
"devDependencies": {
|