@stacksjs/buddy 0.74.19 → 0.74.20
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 +1 -1
- package/dist/commands/share.js +1 -1
- package/dist/deploy-notify.js +1 -1
- package/package.json +55 -55
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}
|
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.20",
|
|
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.20",
|
|
99
|
+
"@stacksjs/ai": "^0.74.20",
|
|
100
|
+
"@stacksjs/alias": "^0.74.20",
|
|
101
|
+
"@stacksjs/analytics": "^0.74.20",
|
|
102
|
+
"@stacksjs/api": "^0.74.20",
|
|
103
|
+
"@stacksjs/arrays": "^0.74.20",
|
|
104
|
+
"@stacksjs/auth": "^0.74.20",
|
|
105
|
+
"@stacksjs/browser-extension": "^0.74.20",
|
|
106
|
+
"@stacksjs/build": "^0.74.20",
|
|
107
|
+
"@stacksjs/cache": "^0.74.20",
|
|
108
|
+
"@stacksjs/chat": "^0.74.20",
|
|
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.20",
|
|
111
|
+
"@stacksjs/cloud": "^0.74.20",
|
|
112
|
+
"@stacksjs/cms": "^0.74.20",
|
|
113
|
+
"@stacksjs/collections": "^0.74.20",
|
|
114
|
+
"@stacksjs/config": "^0.74.20",
|
|
115
|
+
"@stacksjs/database": "^0.74.20",
|
|
116
|
+
"@stacksjs/desktop-build": "^0.74.20",
|
|
117
|
+
"@stacksjs/dns": "^0.74.20",
|
|
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.20",
|
|
120
|
+
"@stacksjs/enums": "^0.74.20",
|
|
121
|
+
"@stacksjs/env": "^0.74.20",
|
|
122
|
+
"@stacksjs/error-handling": "^0.74.20",
|
|
123
|
+
"@stacksjs/events": "^0.74.20",
|
|
124
|
+
"@stacksjs/features": "^0.74.20",
|
|
125
|
+
"@stacksjs/git": "^0.74.20",
|
|
126
126
|
"@stacksjs/gitit": "^0.2.5",
|
|
127
|
-
"@stacksjs/health": "^0.74.
|
|
127
|
+
"@stacksjs/health": "^0.74.20",
|
|
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.20",
|
|
130
|
+
"@stacksjs/lint": "^0.74.20",
|
|
131
|
+
"@stacksjs/logging": "^0.74.20",
|
|
132
|
+
"@stacksjs/notifications": "^0.74.20",
|
|
133
|
+
"@stacksjs/objects": "^0.74.20",
|
|
134
|
+
"@stacksjs/orm": "^0.74.20",
|
|
135
|
+
"@stacksjs/path": "^0.74.20",
|
|
136
|
+
"@stacksjs/payments": "^0.74.20",
|
|
137
|
+
"@stacksjs/realtime": "^0.74.20",
|
|
138
|
+
"@stacksjs/router": "^0.74.20",
|
|
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.20",
|
|
141
|
+
"@stacksjs/search-engine": "^0.74.20",
|
|
142
|
+
"@stacksjs/security": "^0.74.20",
|
|
143
|
+
"@stacksjs/server": "^0.74.20",
|
|
144
|
+
"@stacksjs/sites": "^0.74.20",
|
|
145
|
+
"@stacksjs/skills": "^0.74.20",
|
|
146
|
+
"@stacksjs/storage": "^0.74.20",
|
|
147
|
+
"@stacksjs/strings": "^0.74.20",
|
|
148
|
+
"@stacksjs/stx": "^0.2.274",
|
|
149
|
+
"@stacksjs/testing": "^0.74.20",
|
|
150
|
+
"@stacksjs/tinker": "^0.74.20",
|
|
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.20",
|
|
154
|
+
"@stacksjs/types": "^0.74.20",
|
|
155
|
+
"@stacksjs/ui": "^0.74.20",
|
|
156
|
+
"@stacksjs/utils": "^0.74.20",
|
|
157
|
+
"@stacksjs/validation": "^0.74.20",
|
|
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": {
|