@stacksjs/buddy 0.72.38 → 0.72.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/dist/commands/cd.js +1 -1
- package/dist/commands/config-migrate.js +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/deploy.js +6 -6
- package/dist/commands/dev.js +3 -3
- package/dist/commands/dns.js +1 -1
- package/dist/commands/docs/buddy-commands.js +1 -1
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/email.js +1 -1
- package/dist/commands/env.js +1 -1
- package/dist/commands/extension.js +2 -2
- package/dist/commands/features.js +2 -2
- package/dist/commands/link.js +1 -1
- package/dist/commands/lint.js +1 -1
- package/dist/commands/mail.js +2 -2
- package/dist/commands/migrate-project.js +1 -1
- package/dist/commands/migrate.js +10 -10
- package/dist/commands/publish.js +4 -4
- package/dist/commands/queue.js +1 -1
- package/dist/commands/seed.js +1 -1
- package/dist/commands/setup.js +2 -2
- package/dist/deploy-notify.js +2 -2
- package/dist/migrators/index.js +1 -1
- package/dist/migrators/laravel/index.js +1 -1
- package/dist/migrators/laravel/models.js +3 -3
- package/dist/migrators/rails/index.js +1 -1
- package/dist/production-server.js +1 -1
- package/package.json +46 -46
package/dist/commands/queue.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function queue(buddy){const descriptions={project:"Target a specific project",queue:"Specify queue name",verbose:"Enable verbose output",id:"Job or batch ID",all:"Apply to all items",force:"Force the operation without confirmation",connection:"Queue connection to use",concurrency:"Number of concurrent workers"};buddy.command("queue:work","Start processing jobs on the queue").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("-c, --connection [connection]",descriptions.connection,{default:!1}).option("--concurrency [concurrency]",descriptions.concurrency,{default:"1"}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:work` ...",options);const PARENT_BACKSTOP_MS=(Number(process.env.STACKS_QUEUE_SHUTDOWN_GRACE_MS)||1e4)+5000;let shuttingDown=!1;const onSignal=(signal)=>{if(shuttingDown)return;shuttingDown=!0;log.info(`[queue] Received ${signal}; waiting for the worker to drain\u2026`);setTimeout(()=>{log.warn("[queue] Worker drain overran its window \u2014 forcing shutdown.");process.exit(ExitCode.FatalError)},PARENT_BACKSTOP_MS).unref()};process.on("SIGINT",()=>onSignal("SIGINT"));process.on("SIGTERM",()=>onSignal("SIGTERM"));const perf=await intro("buddy queue:work"),result=await runAction(Action.QueueWork,options);if(resultFailed(result)){await outro("While running the queue:work command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("queue:failed","List all failed jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--since [duration]","Only show jobs failed since (e.g. 1h, 30m, 2d)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:failed` ...",options);const perf=await intro("buddy queue:failed"),result=await runAction(Action.QueueFailed,options);if(resultFailed(result)){await outro("While running the queue:failed command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Listed all failed jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:retry [id]","Retry failed jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--all",descriptions.all,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(id,options)=>{log.debug("Running `buddy queue:retry` ...",options);const perf=await intro("buddy queue:retry"),result=await runAction(Action.QueueRetry,{...options,id});if(resultFailed(result)){await outro("While running the queue:retry command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}const message=options.all?"Retried all failed jobs":`Retried failed job ${id}`;await outro(message,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:clear","Clear all jobs from the queue").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:clear` ...",options);const perf=await intro("buddy queue:clear"),result=await runAction(Action.QueueClear,options);if(resultFailed(result)){await outro("While running the queue:clear command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}const queueName=options.queue||"default";await outro(`Cleared all jobs from the "${queueName}" queue`,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:list","List queued jobs (flat row view, filterable by queue/status)").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--status [status]","Filter by pending | reserved | delayed",{default:!1}).option("--limit [limit]","Maximum rows to display (default 50)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:list` ...",options);const perf=await intro("buddy queue:list"),result=await runAction(Action.QueueList,options);if(resultFailed(result)){await outro("While running the queue:list command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Listed queued jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:status","Display the status of queue workers").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:status` ...",options);const perf=await intro("buddy queue:status"),result=await runAction(Action.QueueStatus,options);if(resultFailed(result)){await outro("While running the queue:status command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Queue status displayed",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:flush","Delete all failed jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:flush` ...",options);const perf=await intro("buddy queue:flush"),result=await runAction(Action.QueueFlush,options);if(resultFailed(result)){await outro("While running the queue:flush command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Flushed all failed jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:dlq","List dead-letter jobs (jobs that re-failed after retry)").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--reason [reason]","Filter by reason (repeat-failure | poison-detected | circuit-broken | manual)",{default:!1}).option("--since [duration]","Only show rows dead-lettered since (e.g. 1h, 30m, 2d)",{default:!1}).option("--limit [limit]","Maximum rows to display (default 100)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:dlq` ...",options);const perf=await intro("buddy queue:dlq"),result=await runAction(Action.QueueDlq,options);if(resultFailed(result)){await outro("While running queue:dlq there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Listed dead-letter jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:dlq:retry","Re-enqueue a dead-letter job back into its queue").option("--id [id]","Dead-letter row id to retry",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:dlq:retry` ...",options);const perf=await intro("buddy queue:dlq:retry"),result=await runAction(Action.QueueDlqRetry,options);if(resultFailed(result)){await outro("While running queue:dlq:retry there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Re-enqueued dead-letter job",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:dlq:purge","Delete dead-letter rows older than a retention window").option("--older-than-days [days]","Retention window in days (default 30)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:dlq:purge` ...",options);const perf=await intro("buddy queue:dlq:purge"),result=await runAction(Action.QueueDlqPurge,options);if(resultFailed(result)){await outro("While running queue:dlq:purge there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Purged dead-letter rows",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:quarantine","List quarantined jobs (or --add a manual quarantine)").option("--add [jobName]","Manually quarantine a job class",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:quarantine` ...",options);const perf=await intro("buddy queue:quarantine"),result=await runAction(Action.QueueQuarantine,options);if(resultFailed(result)){await outro("While running queue:quarantine there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("queue:quarantine done",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:unquarantine","Lift the quarantine on a job class").option("--name [name]","Job class name to unquarantine",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:unquarantine` ...",options);const perf=await intro("buddy queue:unquarantine"),result=await runAction(Action.QueueUnquarantine,options);if(resultFailed(result)){await outro("While running queue:unquarantine there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Unquarantined",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:pause","Manually pause a queue (circuit-breaker)").option("--queue [name]","Queue name to pause",{default:!1}).option("--for [seconds]","Pause duration in seconds (default 300)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:pause` ...",options);const perf=await intro("buddy queue:pause"),result=await runAction(Action.QueuePause,options);if(resultFailed(result)){await outro("While running queue:pause there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Paused queue",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:resume","Resume a paused queue (circuit-breaker)").option("--queue [name]","Queue name to resume",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:resume` ...",options);const perf=await intro("buddy queue:resume"),result=await runAction(Action.QueueResume,options);if(resultFailed(result)){await outro("While running queue:resume there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Resumed queue",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:table","Create a migration for the jobs database table").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:table` ...",options);const perf=await intro("buddy queue:table"),result=await runAction(Action.QueueTable,options);if(resultFailed(result)){await outro("While running the queue:table command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Jobs table migration created",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:monitor","Monitor queue status in real-time").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("-i, --interval [interval]","Refresh interval in milliseconds",{default:"2000"}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:monitor` ...",options);const result=await runAction(Action.QueueMonitor,options);if(resultFailed(result)){log.error("While running the queue:monitor command, there was an issue",result.error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("queue:inspect [id]","Inspect a specific job").option("-p, --project [project]",descriptions.project,{default:!1}).option("--failed","Inspect a failed job",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(id,options)=>{log.debug("Running `buddy queue:inspect` ...",options);const perf=await intro("buddy queue:inspect"),result=await runAction(Action.QueueInspect,{...options,id});if(resultFailed(result)){await outro("While running the queue:inspect command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Job inspection complete",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:schedule","Start the job scheduler for cron-based jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:schedule` ...",options);const result=await runAction(Action.QueueSchedule,options);if(resultFailed(result)){log.error("While running the queue:schedule command, there was an issue",result.error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("queue:schedule:list","List all scheduled jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:schedule:list` ...",options);const perf=await intro("buddy queue:schedule:list"),result=await runAction(Action.QueueScheduleList,options);if(resultFailed(result)){await outro("While running the queue:schedule:list command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Listed all scheduled jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"queue")}
|
|
1
|
+
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function queue(buddy){const descriptions={project:"Target a specific project",queue:"Specify queue name",verbose:"Enable verbose output",id:"Job or batch ID",all:"Apply to all items",force:"Force the operation without confirmation",connection:"Queue connection to use",concurrency:"Number of concurrent workers"};buddy.command("queue:work","Start processing jobs on the queue").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("-c, --connection [connection]",descriptions.connection,{default:!1}).option("--concurrency [concurrency]",descriptions.concurrency,{default:"1"}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:work` ...",options);const PARENT_BACKSTOP_MS=(Number(process.env.STACKS_QUEUE_SHUTDOWN_GRACE_MS)||1e4)+5000;let shuttingDown=!1;const onSignal=(signal)=>{if(shuttingDown)return;shuttingDown=!0;log.info(`[queue] Received ${signal}; waiting for the worker to drain\u2026`);setTimeout(()=>{log.warn("[queue] Worker drain overran its window - forcing shutdown.");process.exit(ExitCode.FatalError)},PARENT_BACKSTOP_MS).unref()};process.on("SIGINT",()=>onSignal("SIGINT"));process.on("SIGTERM",()=>onSignal("SIGTERM"));const perf=await intro("buddy queue:work"),result=await runAction(Action.QueueWork,options);if(resultFailed(result)){await outro("While running the queue:work command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("queue:failed","List all failed jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--since [duration]","Only show jobs failed since (e.g. 1h, 30m, 2d)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:failed` ...",options);const perf=await intro("buddy queue:failed"),result=await runAction(Action.QueueFailed,options);if(resultFailed(result)){await outro("While running the queue:failed command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Listed all failed jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:retry [id]","Retry failed jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--all",descriptions.all,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(id,options)=>{log.debug("Running `buddy queue:retry` ...",options);const perf=await intro("buddy queue:retry"),result=await runAction(Action.QueueRetry,{...options,id});if(resultFailed(result)){await outro("While running the queue:retry command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}const message=options.all?"Retried all failed jobs":`Retried failed job ${id}`;await outro(message,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:clear","Clear all jobs from the queue").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:clear` ...",options);const perf=await intro("buddy queue:clear"),result=await runAction(Action.QueueClear,options);if(resultFailed(result)){await outro("While running the queue:clear command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}const queueName=options.queue||"default";await outro(`Cleared all jobs from the "${queueName}" queue`,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:list","List queued jobs (flat row view, filterable by queue/status)").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--status [status]","Filter by pending | reserved | delayed",{default:!1}).option("--limit [limit]","Maximum rows to display (default 50)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:list` ...",options);const perf=await intro("buddy queue:list"),result=await runAction(Action.QueueList,options);if(resultFailed(result)){await outro("While running the queue:list command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Listed queued jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:status","Display the status of queue workers").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:status` ...",options);const perf=await intro("buddy queue:status"),result=await runAction(Action.QueueStatus,options);if(resultFailed(result)){await outro("While running the queue:status command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Queue status displayed",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:flush","Delete all failed jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:flush` ...",options);const perf=await intro("buddy queue:flush"),result=await runAction(Action.QueueFlush,options);if(resultFailed(result)){await outro("While running the queue:flush command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Flushed all failed jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:dlq","List dead-letter jobs (jobs that re-failed after retry)").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("--reason [reason]","Filter by reason (repeat-failure | poison-detected | circuit-broken | manual)",{default:!1}).option("--since [duration]","Only show rows dead-lettered since (e.g. 1h, 30m, 2d)",{default:!1}).option("--limit [limit]","Maximum rows to display (default 100)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:dlq` ...",options);const perf=await intro("buddy queue:dlq"),result=await runAction(Action.QueueDlq,options);if(resultFailed(result)){await outro("While running queue:dlq there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Listed dead-letter jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:dlq:retry","Re-enqueue a dead-letter job back into its queue").option("--id [id]","Dead-letter row id to retry",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:dlq:retry` ...",options);const perf=await intro("buddy queue:dlq:retry"),result=await runAction(Action.QueueDlqRetry,options);if(resultFailed(result)){await outro("While running queue:dlq:retry there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Re-enqueued dead-letter job",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:dlq:purge","Delete dead-letter rows older than a retention window").option("--older-than-days [days]","Retention window in days (default 30)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:dlq:purge` ...",options);const perf=await intro("buddy queue:dlq:purge"),result=await runAction(Action.QueueDlqPurge,options);if(resultFailed(result)){await outro("While running queue:dlq:purge there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Purged dead-letter rows",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:quarantine","List quarantined jobs (or --add a manual quarantine)").option("--add [jobName]","Manually quarantine a job class",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:quarantine` ...",options);const perf=await intro("buddy queue:quarantine"),result=await runAction(Action.QueueQuarantine,options);if(resultFailed(result)){await outro("While running queue:quarantine there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("queue:quarantine done",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:unquarantine","Lift the quarantine on a job class").option("--name [name]","Job class name to unquarantine",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:unquarantine` ...",options);const perf=await intro("buddy queue:unquarantine"),result=await runAction(Action.QueueUnquarantine,options);if(resultFailed(result)){await outro("While running queue:unquarantine there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Unquarantined",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:pause","Manually pause a queue (circuit-breaker)").option("--queue [name]","Queue name to pause",{default:!1}).option("--for [seconds]","Pause duration in seconds (default 300)",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:pause` ...",options);const perf=await intro("buddy queue:pause"),result=await runAction(Action.QueuePause,options);if(resultFailed(result)){await outro("While running queue:pause there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Paused queue",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:resume","Resume a paused queue (circuit-breaker)").option("--queue [name]","Queue name to resume",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:resume` ...",options);const perf=await intro("buddy queue:resume"),result=await runAction(Action.QueueResume,options);if(resultFailed(result)){await outro("While running queue:resume there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Resumed queue",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:table","Create a migration for the jobs database table").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:table` ...",options);const perf=await intro("buddy queue:table"),result=await runAction(Action.QueueTable,options);if(resultFailed(result)){await outro("While running the queue:table command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Jobs table migration created",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:monitor","Monitor queue status in real-time").option("-p, --project [project]",descriptions.project,{default:!1}).option("-q, --queue [queue]",descriptions.queue,{default:!1}).option("-i, --interval [interval]","Refresh interval in milliseconds",{default:"2000"}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:monitor` ...",options);const result=await runAction(Action.QueueMonitor,options);if(resultFailed(result)){log.error("While running the queue:monitor command, there was an issue",result.error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("queue:inspect [id]","Inspect a specific job").option("-p, --project [project]",descriptions.project,{default:!1}).option("--failed","Inspect a failed job",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(id,options)=>{log.debug("Running `buddy queue:inspect` ...",options);const perf=await intro("buddy queue:inspect"),result=await runAction(Action.QueueInspect,{...options,id});if(resultFailed(result)){await outro("While running the queue:inspect command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Job inspection complete",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("queue:schedule","Start the job scheduler for cron-based jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:schedule` ...",options);const result=await runAction(Action.QueueSchedule,options);if(resultFailed(result)){log.error("While running the queue:schedule command, there was an issue",result.error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("queue:schedule:list","List all scheduled jobs").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy queue:schedule:list` ...",options);const perf=await intro("buddy queue:schedule:list"),result=await runAction(Action.QueueScheduleList,options);if(resultFailed(result)){await outro("While running the queue:schedule:list command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Listed all scheduled jobs",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"queue")}
|
package/dist/commands/seed.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";export function seed(buddy){const descriptions={seed:"Seed your database",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("seed",descriptions.seed).alias("db:seed").option("-p, --project [project]",descriptions.project,{default:!1}).option("--only [models]","Comma-separated list of models to seed",{default:""}).option("--except [models]","Comma-separated list of models to skip",{default:""}).option("--include-defaults","Also seed the framework's built-in models",{default:!1}).option("--allow-protected","Seed auth/oauth models even on a non-fresh DB (will invalidate live tokens)",{default:!1}).option("--fresh","Truncate tables before seeding",{default:!1}).option("--append","Add rows to tables that already have some, instead of skipping them",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy seed` ...",options);const perf=await intro("buddy seed"),{injectGlobalAutoImports}=await import("@stacksjs/server");await injectGlobalAutoImports();const{runApplicationSeeders,seed:seedDatabase}=await import("@stacksjs/database"),list=(value)=>value?value.split(",").map((entry)=>entry.trim()).filter(Boolean):void 0,summary=await seedDatabase({verbose:options.verbose,fresh:options.fresh,append:options.append,only:list(options.only),except:list(options.except),includeDefaults:options.includeDefaults,allowProtected:options.allowProtected}),applicationSummary=await runApplicationSeeders({verbose:options.verbose}),APP_ENV=process.env.APP_ENV||"local";if(summary.total===0&&applicationSummary.total===0){await outro("No models declare a `useSeeder` trait and no application seeders were found.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const failures=summary.failed+applicationSummary.failed;await outro(`Seeded your ${APP_ENV} database. ${summary.successful}/${summary.total} model(s) and ${applicationSummary.successful}/${applicationSummary.total} application seeder(s) completed${failures>0?`, ${failures} failed`:""}.`,{startTime:perf,useSeconds:!0});process.exit(failures>0?ExitCode.FatalError:ExitCode.Success)});buddy.command("seed:roles","Seed default RBAC role packs (admin, dev, client)").alias("roles:seed").action(async()=>{const perf=await intro("buddy seed:roles");try{const{seedDefaultRoles}=await import("@stacksjs/auth"),result=await seedDefaultRoles();if(result.created.length===0&&result.skipped.length>0)await outro(`All ${result.skipped.length} default role packs already exist
|
|
1
|
+
import process from"node:process";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";export function seed(buddy){const descriptions={seed:"Seed your database",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("seed",descriptions.seed).alias("db:seed").option("-p, --project [project]",descriptions.project,{default:!1}).option("--only [models]","Comma-separated list of models to seed",{default:""}).option("--except [models]","Comma-separated list of models to skip",{default:""}).option("--include-defaults","Also seed the framework's built-in models",{default:!1}).option("--allow-protected","Seed auth/oauth models even on a non-fresh DB (will invalidate live tokens)",{default:!1}).option("--fresh","Truncate tables before seeding",{default:!1}).option("--append","Add rows to tables that already have some, instead of skipping them",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy seed` ...",options);const perf=await intro("buddy seed"),{injectGlobalAutoImports}=await import("@stacksjs/server");await injectGlobalAutoImports();const{runApplicationSeeders,seed:seedDatabase}=await import("@stacksjs/database"),list=(value)=>value?value.split(",").map((entry)=>entry.trim()).filter(Boolean):void 0,summary=await seedDatabase({verbose:options.verbose,fresh:options.fresh,append:options.append,only:list(options.only),except:list(options.except),includeDefaults:options.includeDefaults,allowProtected:options.allowProtected}),applicationSummary=await runApplicationSeeders({verbose:options.verbose}),APP_ENV=process.env.APP_ENV||"local";if(summary.total===0&&applicationSummary.total===0){await outro("No models declare a `useSeeder` trait and no application seeders were found.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const failures=summary.failed+applicationSummary.failed;await outro(`Seeded your ${APP_ENV} database. ${summary.successful}/${summary.total} model(s) and ${applicationSummary.successful}/${applicationSummary.total} application seeder(s) completed${failures>0?`, ${failures} failed`:""}.`,{startTime:perf,useSeconds:!0});process.exit(failures>0?ExitCode.FatalError:ExitCode.Success)});buddy.command("seed:roles","Seed default RBAC role packs (admin, dev, client)").alias("roles:seed").action(async()=>{const perf=await intro("buddy seed:roles");try{const{seedDefaultRoles}=await import("@stacksjs/auth"),result=await seedDefaultRoles();if(result.created.length===0&&result.skipped.length>0)await outro(`All ${result.skipped.length} default role packs already exist - nothing to do.`,{startTime:perf,useSeconds:!0});else{const createdNames=result.created.map((r)=>r.name).join(", ");await outro(`Created ${result.created.length} role pack(s): ${createdNames}. Skipped ${result.skipped.length} existing.`,{startTime:perf,useSeconds:!0})}process.exit(ExitCode.Success)}catch(err){await outro("Failed to seed default roles. Most often: migrations haven't run yet (try `./buddy migrate` first).",{startTime:perf,useSeconds:!0},err);process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"seed")}
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import{cpSync,existsSync,readFileSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{join}from"node:path";import process from"node:process";import{runAction}from"@stacksjs/actions";import{log,onUnknownSubcommand,runCommand}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{handleError}from"@stacksjs/error-handling";import{path as p}from"@stacksjs/path";import{copyFile,storage}from"@stacksjs/storage";import{ExitCode}from"@stacksjs/types";import{setupPrettyDevEnvironment}from"./dev";import{resultFailed}from"../result";function getTimeoutMs(envVar,fallbackMs){const value=Number(process.env[envVar]);if(Number.isFinite(value)&&value>0)return value;return fallbackMs}const PANTRY_CHECK_TIMEOUT_MS=getTimeoutMs("PANTRY_CHECK_TIMEOUT_MS",15000),PANTRY_INSTALL_TIMEOUT_MS=getTimeoutMs("PANTRY_INSTALL_TIMEOUT_MS",600000),PANTRY_DEPENDENCIES_TIMEOUT_MS=getTimeoutMs("PANTRY_DEPENDENCIES_TIMEOUT_MS",1200000),KEYGEN_TIMEOUT_MS=getTimeoutMs("KEYGEN_TIMEOUT_MS",120000),AWS_CONFIG_TIMEOUT_MS=getTimeoutMs("AWS_CONFIG_TIMEOUT_MS",900000);export function setup(buddy){const descriptions={setup:"This command ensures your project is setup correctly",ssl:"Setup SSL certificates and hosts file for HTTPS development",ai:"Set the project up for an AI coding agent (Claude Code, Codex, Cursor, Copilot, Gemini)",copy:"Copy the agent files instead of symlinking them, so they can be edited per project",force:"Overwrite files that already exist",ohMyZsh:"Enable Oh My Zsh",aws:"Ensures AWS is connected to the project",project:"Target a specific project",verbose:"Enable verbose output",domain:"Custom domain to setup (defaults to APP_URL)",skipHosts:"Skip adding domain to hosts file",skipTrust:"Skip trusting the certificate",skipAws:"Skip AWS configuration during setup",skipKeygen:"Skip generating an application key during setup"};buddy.command("setup",descriptions.setup).alias("ensure").option("-p, --project [project]",descriptions.project,{default:!1}).option("--skip-aws",descriptions.skipAws,{default:!1}).option("--skip-keygen",descriptions.skipKeygen,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy setup` ...",options);await ensurePantryInstalled();await optimizePantryDeps();await initializeProject(options)});buddy.command("setup:ssl",descriptions.ssl).alias("ssl:setup").option("-d, --domain [domain]",descriptions.domain).option("--skip-hosts",descriptions.skipHosts,{default:!1}).option("--skip-trust",descriptions.skipTrust,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy setup:ssl` ...",options);if(!await setupPrettyDevEnvironment({domain:options.domain,skipHosts:options.skipHosts,skipTrust:options.skipTrust,verbose:options.verbose})){log.warn("SSL setup completed with warnings");log.info("You may need to manually trust certificates or update hosts file")}});buddy.command("setup:ai [provider]",descriptions.ai).alias("ai:setup").option("--copy",descriptions.copy,{default:!1}).option("--force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(provider,options)=>{log.debug("Running `buddy setup:ai` ...",options);const{AI_PROVIDERS,isAiProvider,reportAiSetup,setupAiProvider}=await import("./setup-ai");let id=provider;if(!id){const{select}=await import("@stacksjs/cli");id=await select({message:"Which AI coding agent do you use?",choices:AI_PROVIDERS.map((entry)=>({value:entry.id,label:entry.label})),initial:0})}if(!id||!isAiProvider(id)){log.error(`Unknown AI provider: ${id}. Expected one of: ${AI_PROVIDERS.map((entry)=>entry.id).join(", ")}`);process.exit(ExitCode.InvalidArgument)}const definition=AI_PROVIDERS.find((entry)=>entry.id===id);reportAiSetup(definition,setupAiProvider(id,{copy:options.copy,force:options.force}))});buddy.command("setup:oh-my-zsh",descriptions.ohMyZsh).alias("upgrade:oh-my-zsh").option("--verbose",descriptions.verbose,{default:!1}).action(async(_options)=>{log.debug("Running `buddy setup:oh-my-zsh` ...",_options);const result=await runAction(Action.UpgradeShell);if(resultFailed(result)){log.error(result.error);process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"setup")}async function isPantryInstalled(){try{return(await runCommand("pantry --version",{silent:!0,timeoutMs:PANTRY_CHECK_TIMEOUT_MS})).isOk}catch{return!1}}async function installPantry(){const bundledInstaller=p.frameworkPath("scripts/pantry-install"),command=existsSync(bundledInstaller)?[bundledInstaller]:["sh","-c","curl -fsSL https://pantry.dev | bash"],result=await runCommand(command,{timeoutMs:PANTRY_INSTALL_TIMEOUT_MS}),localBin=join(homedir(),".local","bin");if(!process.env.PATH?.split(":").includes(localBin))process.env.PATH=`${localBin}:${process.env.PATH||""}`;if(result.isOk&&await isPantryInstalled())return;if(resultFailed(result))handleError(result.error);else log.error("Pantry installed but is not available on PATH. Open a new shell and run `buddy setup` again.");process.exit(ExitCode.FatalError)}export async function ensurePantryInstalled(){if(await isPantryInstalled())return;log.info("Pantry is required. Installing it from https://pantry.dev...");await installPantry()}export async function ensurePantryDependencies(cwd){await ensurePantryInstalled();log.info("Installing project dependencies with Pantry...");const result=await runCommand("pantry install",{cwd,timeoutMs:PANTRY_DEPENDENCIES_TIMEOUT_MS});if(resultFailed(result)){handleError(result.error);process.exit(ExitCode.FatalError)}if(existsSync(join(cwd,"package.json"))&&!existsSync(join(cwd,"node_modules"))){log.error("Pantry completed without installing the project JavaScript dependencies.");process.exit(ExitCode.FatalError)}log.success("Installed project dependencies with Pantry")}function hasAppKey(cwd){const envPath=join(cwd,".env");if(!existsSync(envPath))return!1;return/^APP_KEY=.+$/m.test(readFileSync(envPath,"utf-8"))}export async function ensureAppKey(cwd){if(hasAppKey(cwd)||process.env.APP_KEY&&process.env.APP_KEY.length>0){log.success("APP_KEY existed");return}const keyResult=await runCommand("./buddy key:generate",{cwd,timeoutMs:KEYGEN_TIMEOUT_MS});if(resultFailed(keyResult)){handleError(keyResult.error);process.exit(ExitCode.FatalError)}log.success("Generated application key")}async function runInitialMigration(cwd){const appEnv=(process.env.APP_ENV||process.env.NODE_ENV||"local").toLowerCase();if(!["local","development","dev","test"].includes(appEnv)){log.info(`Skipping initial migration in the ${appEnv} environment`);return}log.info("Running initial database migration...");try{const result=await runAction(Action.Migrate,{cwd});if(resultFailed(result)){log.warn("Initial migration did not complete - you can run it later via ./buddy migrate");log.debug(result.error);return}log.success("Database is migrated")}catch(error){log.warn("Initial migration did not complete - you can run it later via ./buddy migrate");log.debug(error)}}async function initializeProject(options){const cwd=options.cwd||p.projectPath();await ensurePantryDependencies(cwd);await ensureEnvIsSet(options);if(!options.skipKeygen)await ensureAppKey(cwd);await runInitialMigration(cwd);ensureIdeSettings(cwd);if(!options.skipAws){log.info("Ensuring AWS is connected...");try{const awsResult=await runCommand("./buddy configure:aws",{cwd,timeoutMs:AWS_CONFIG_TIMEOUT_MS});if(resultFailed(awsResult)){log.warn("AWS not configured - you can do this later via ./buddy configure:aws");log.debug(awsResult.error)}else log.success("Configured AWS")}catch(error){log.warn("AWS not configured - you can do this later via ./buddy configure:aws");log.debug(error)}}log.success("Project is setup");log.info("Run `./buddy doctor` anytime to check your setup. Happy coding! \uD83D\uDC99")}export function ensureIdeSettings(cwd){const source=p.frameworkPath("defaults/ide/vscode/.vscode"),destination=join(cwd,".vscode");if(existsSync(destination)){log.debug(".vscode already exists; keeping the project settings");return}if(!existsSync(source)){log.debug("No bundled VS Code settings found; skipping IDE setup");return}cpSync(source,destination,{recursive:!0});log.success("Installed project VS Code settings")}const DB_CONNECTION_PACKAGES={postgres:{name:"postgresql.org",version:"^17.10",service:"postgres"},mysql:{name:"mysql.com",version:"^9.2",service:"mysql"},sqlite:{name:"sqlite.org",version:"^3.47.2"}};function databaseAliases(connection,pkg){return[connection,pkg.name]}export function pantryDatabasePackage(connection){return DB_CONNECTION_PACKAGES[connection]}function detectDbPackage(cwd){const envPath=join(cwd,".env"),envExamplePath=join(cwd,".env.example"),filePath=existsSync(envPath)?envPath:existsSync(envExamplePath)?envExamplePath:void 0;if(!filePath)return;const match=readFileSync(filePath,"utf-8").match(/^DB_CONNECTION=(.+)$/m);if(!match)return;const value=match[1].trim().replace(/['"]/g,"");return pantryDatabasePackage(value)}export async function optimizePantryDeps(){const cwd=p.projectPath(),depsConfigPath=join(cwd,"config","deps.ts");if(!existsSync(depsConfigPath)){log.debug("No config/deps.ts found, skipping dependency optimization");return}let configDeps={},configServices=[],configDefined={};try{const mod=await import(depsConfigPath),config=mod.config||mod.default;if(config?.dependencies)configDeps={...config.dependencies};if(Array.isArray(config?.services?.autoStart))configServices=config.services.autoStart.filter((name)=>typeof name==="string");if(config?.services?.define&&typeof config.services.define==="object")configDefined=config.services.define}catch(err){log.debug("Could not load config/deps.ts, skipping dependency optimization");return}const dbPackage=detectDbPackage(cwd),autoStart=[...configServices];if(dbPackage){const selected=new Set(Object.entries(DB_CONNECTION_PACKAGES).filter(([,pkg])=>pkg.name===dbPackage.name).flatMap(([connection,pkg])=>databaseAliases(connection,pkg))),unused=new Set(Object.entries(DB_CONNECTION_PACKAGES).flatMap(([connection,pkg])=>databaseAliases(connection,pkg)).filter((alias)=>!selected.has(alias)));for(const pkg of Object.keys(configDeps)){const domain=pkg.split("/")[0];if(unused.has(domain)){log.info(`DB_CONNECTION selects ${dbPackage.name}, dropping unused ${pkg}`);delete configDeps[pkg]}}if(!Object.keys(configDeps).some((key)=>{const domain=key.split("/")[0];return selected.has(domain)})){log.info(`Detected DB_CONNECTION requires ${dbPackage.name}, adding to dependencies`);configDeps[dbPackage.name]=dbPackage.version}if(dbPackage.service&&!autoStart.includes(dbPackage.service))autoStart.push(dbPackage.service)}const lines=["# Auto-generated from config/deps.ts and .env sniffing.","# This file is regenerated on each `buddy setup` run.","#","# To learn more, please visit:","# https://stacksjs.com/docs/dependency-management","","dependencies:"];for(const[pkg,version]of Object.entries(configDeps))lines.push(` ${pkg}: ${version}`);const defined=Object.entries(configDefined);if(autoStart.length>0||defined.length>0){lines.push("","services:"," enabled: true");if(autoStart.length>0){lines.push(" autoStart:");for(const service of autoStart)lines.push(` - ${service}`)}if(defined.length>0){lines.push(" define:");for(const[name,definition]of defined){if(!definition||typeof definition!=="object")continue;lines.push(` ${name}:`);for(const[key,value]of Object.entries(definition)){if(value===void 0||value===null)continue;lines.push(` ${key}: ${String(value)}`)}}}}const depsYamlPath=join(cwd,"deps.yaml");writeFileSync(depsYamlPath,`${lines.join(`
|
|
2
2
|
`)}
|
|
3
3
|
`);log.success("Generated deps.yaml from config/deps.ts")}export async function ensureEnvIsSet(options){log.info("Ensuring .env exists...");const cwd=options.cwd||p.projectPath(),envPath=`${cwd}/.env`,envExamplePath=`${cwd}/.env.example`;if(storage.doesNotExist(envPath)){try{copyFile(envExamplePath,envPath)}catch(error){handleError(error);process.exit(ExitCode.FatalError)}log.success(".env created")}else log.success(".env existed")}function envValues(contents){return contents.split(`
|
|
4
|
-
`).map((line)=>line.trim()).filter((line)=>line.length>0&&!line.startsWith("#")&&!line.startsWith("DOTENV_PUBLIC_KEY")).map((line)=>line.slice(line.indexOf("=")+1).trim()).map((value)=>value.replace(/^['"]/,""))}function isCiphertext(value){return value.startsWith("encrypted:")||value.startsWith("enc:")}export function isEnvFileEncrypted(contents){return envValues(contents).every(isCiphertext)}function deployEnvTemplate(environment){return[`# Secrets for the ${environment} environment.`,"#","# Values here are encrypted with the public key below and decrypted at","# deploy time with the matching private key in .env.keys, which is NOT","# committed. This file is
|
|
5
|
-
`)}export async function ensureDeployEnvIsSet(cwd,environment){if(["development","dev","local","test"].includes(environment))return;const fileName=`.env.${environment}`,filePath=join(cwd,fileName),created=!existsSync(filePath);if(created)writeFileSync(filePath,deployEnvTemplate(environment));const contents=readFileSync(filePath,"utf-8");if(!created&&isEnvFileEncrypted(contents)){log.success(`${fileName} existed`);return}const{encryptEnv}=await import("@stacksjs/env"),result=encryptEnv({file:fileName,cwd});if(!result.success){log.error(`Could not encrypt ${fileName}: ${result.error??"unknown error"}`);process.exit(ExitCode.FatalError)}if(created){log.success(`${fileName} created (empty, encrypted
|
|
4
|
+
`).map((line)=>line.trim()).filter((line)=>line.length>0&&!line.startsWith("#")&&!line.startsWith("DOTENV_PUBLIC_KEY")).map((line)=>line.slice(line.indexOf("=")+1).trim()).map((value)=>value.replace(/^['"]/,""))}function isCiphertext(value){return value.startsWith("encrypted:")||value.startsWith("enc:")}export function isEnvFileEncrypted(contents){return envValues(contents).every(isCiphertext)}function deployEnvTemplate(environment){return[`# Secrets for the ${environment} environment.`,"#","# Values here are encrypted with the public key below and decrypted at","# deploy time with the matching private key in .env.keys, which is NOT","# committed. This file is - that is the point: the ciphertext is reviewable","# and diffable, and losing a laptop does not leak production.","#","# Add one with:",`# buddy env:set STRIPE_SECRET_KEY sk_live_\u2026 --env ${environment}`,"#","# Left empty on purpose. Nothing was copied out of your .env: that file","# describes a laptop, and a server is not one.",""].join(`
|
|
5
|
+
`)}export async function ensureDeployEnvIsSet(cwd,environment){if(["development","dev","local","test"].includes(environment))return;const fileName=`.env.${environment}`,filePath=join(cwd,fileName),created=!existsSync(filePath);if(created)writeFileSync(filePath,deployEnvTemplate(environment));const contents=readFileSync(filePath,"utf-8");if(!created&&isEnvFileEncrypted(contents)){log.success(`${fileName} existed`);return}const{encryptEnv}=await import("@stacksjs/env"),result=encryptEnv({file:fileName,cwd});if(!result.success){log.error(`Could not encrypt ${fileName}: ${result.error??"unknown error"}`);process.exit(ExitCode.FatalError)}if(created){log.success(`${fileName} created (empty, encrypted - add secrets with \`buddy env:set KEY value --env ${environment}\`)`);return}const encrypted=envValues(contents).filter((value)=>!isCiphertext(value)).length;log.success(`${fileName} encrypted (${encrypted} value${encrypted===1?"":"s"})`)}
|
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=env.GITHUB_REPOSITORY,sha=env.GITHUB_SHA;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
|
|
3
|
-
`)}export async function notifyDeployOutcome(outcome,config=resolveDeployNotifyConfig()){try{if(!config.url){log.debug("[deploy] no DEPLOY_WEBHOOK_URL set
|
|
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=env.GITHUB_REPOSITORY,sha=env.GITHUB_SHA;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
|
+
`)}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 withDeployNotification(action){return async(...args)=>{const startedAt=Date.now(),context={environment:typeof args[0]==="string"&&args[0]?args[0]:"production",provider:process.env.CLOUD_PROVIDER||void 0,project:process.env.APP_NAME||void 0};try{await action(...args)}catch(error){await notifyDeployOutcome({...context,status:"failed",durationMs:Date.now()-startedAt,error});process.exit(ExitCode.FatalError)}await notifyDeployOutcome({...context,status:"succeeded",durationMs:Date.now()-startedAt})}}
|
package/dist/migrators/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{laravelDriver}from"./laravel";import{railsDriver}from"./rails";export const DRIVERS={laravel:laravelDriver,rails:railsDriver};export async function runMigrator(req){const driver=DRIVERS[req.from];if(!driver)throw Error(`Unknown source framework: ${req.from}. Supported: ${Object.keys(DRIVERS).join(", ")}`);const startedAt=new Date().toISOString(),entries=await driver.migrate(req),finishedAt=new Date().toISOString();return{source:req.source,target:req.target,from:req.from,startedAt,finishedAt,entries}}export function renderReport(report){const lines=[];lines.push(`# Migration report
|
|
1
|
+
import{laravelDriver}from"./laravel";import{railsDriver}from"./rails";export const DRIVERS={laravel:laravelDriver,rails:railsDriver};export async function runMigrator(req){const driver=DRIVERS[req.from];if(!driver)throw Error(`Unknown source framework: ${req.from}. Supported: ${Object.keys(DRIVERS).join(", ")}`);const startedAt=new Date().toISOString(),entries=await driver.migrate(req),finishedAt=new Date().toISOString();return{source:req.source,target:req.target,from:req.from,startedAt,finishedAt,entries}}export function renderReport(report){const lines=[];lines.push(`# Migration report - ${report.from} \u2192 Stacks`);lines.push("");lines.push(`- **Source**: \`${report.source}\``);lines.push(`- **Target**: \`${report.target}\``);lines.push(`- **Started**: ${report.startedAt}`);lines.push(`- **Finished**: ${report.finishedAt}`);lines.push("");const groups=groupEntriesByStatus(report.entries);for(const[status,label]of[["translated","Translated"],["copied","Copied verbatim"],["skipped","Skipped (hand-port required)"],["failed","Failed"]]){const group=groups[status]??[];if(group.length===0)continue;lines.push(`## ${label} (${group.length})`);lines.push("");for(const e of group){const arrow=e.target?` \u2192 \`${e.target}\``:"",note=e.note?` - ${e.note}`:"";lines.push(`- \`${e.source}\`${arrow}${note}`)}lines.push("")}lines.push("---");lines.push("");lines.push("Re-run `./buddy migrate:project <name> --from="+report.from+"` after fixing the source project to refresh emitted files. Translated outputs are overwritten; skipped entries are not.");return lines.join(`
|
|
2
2
|
`)}function groupEntriesByStatus(entries){const out={};for(const e of entries){if(!out[e.status])out[e.status]=[];out[e.status].push(e)}return out}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync,readdirSync}from"node:fs";import{copyFile,mkdir,readFile,writeFile}from"node:fs/promises";import{basename,join}from"node:path";import{laravelFilenameToStacks,parseLaravelMigration}from"./migrations";import{parseLaravelModel}from"./models";export const laravelDriver={name:"laravel",async migrate(req){const entries=[];entries.push(...await migrateEnv(req));entries.push(...await migrateMigrations(req));entries.push(...await migrateModels(req));entries.push(...summariseDeferred(req));return entries}};async function migrateEnv(req){const out=[];for(const file of[".env",".env.example"]){const sourcePath=join(req.source,file);if(!existsSync(sourcePath))continue;const targetPath=join(req.target,file);if(!req.dryRun){await mkdir(req.target,{recursive:!0});await copyFile(sourcePath,targetPath)}out.push({source:file,target:file,status:"copied",note:"Laravel-specific keys (BROADCAST_DRIVER, FILESYSTEM_DISK names) may need renaming; Stacks reads APP_*/DB_*/MAIL_* keys as-is."})}return out}async function migrateMigrations(req){const dir=join(req.source,"database","migrations");if(!existsSync(dir))return[];const files=readdirSync(dir).filter((f)=>f.endsWith(".php")).sort(),targetDir=join(req.target,"database","migrations");if(!req.dryRun)await mkdir(targetDir,{recursive:!0});const out=[];for(let i=0;i<files.length;i++){const file=files[i],rel=`database/migrations/${file}`;try{const body=await readFile(join(dir,file),"utf8"),parsed=parseLaravelMigration(body);if(!parsed){out.push({source:rel,target:"",status:"skipped",note:"No `Schema::create(...)` block found (likely an alter / drop / data-only migration)."});continue}const sequence=i+1,outRel=`database/migrations/${laravelFilenameToStacks(file,sequence,parsed.table)}`;if(!req.dryRun)await writeFile(join(req.target,outRel),parsed.sql);const skippedNote=parsed.skipped.length>0?` Skipped ${parsed.skipped.length} line(s) inside the create block
|
|
1
|
+
import{existsSync,readdirSync}from"node:fs";import{copyFile,mkdir,readFile,writeFile}from"node:fs/promises";import{basename,join}from"node:path";import{laravelFilenameToStacks,parseLaravelMigration}from"./migrations";import{parseLaravelModel}from"./models";export const laravelDriver={name:"laravel",async migrate(req){const entries=[];entries.push(...await migrateEnv(req));entries.push(...await migrateMigrations(req));entries.push(...await migrateModels(req));entries.push(...summariseDeferred(req));return entries}};async function migrateEnv(req){const out=[];for(const file of[".env",".env.example"]){const sourcePath=join(req.source,file);if(!existsSync(sourcePath))continue;const targetPath=join(req.target,file);if(!req.dryRun){await mkdir(req.target,{recursive:!0});await copyFile(sourcePath,targetPath)}out.push({source:file,target:file,status:"copied",note:"Laravel-specific keys (BROADCAST_DRIVER, FILESYSTEM_DISK names) may need renaming; Stacks reads APP_*/DB_*/MAIL_* keys as-is."})}return out}async function migrateMigrations(req){const dir=join(req.source,"database","migrations");if(!existsSync(dir))return[];const files=readdirSync(dir).filter((f)=>f.endsWith(".php")).sort(),targetDir=join(req.target,"database","migrations");if(!req.dryRun)await mkdir(targetDir,{recursive:!0});const out=[];for(let i=0;i<files.length;i++){const file=files[i],rel=`database/migrations/${file}`;try{const body=await readFile(join(dir,file),"utf8"),parsed=parseLaravelMigration(body);if(!parsed){out.push({source:rel,target:"",status:"skipped",note:"No `Schema::create(...)` block found (likely an alter / drop / data-only migration)."});continue}const sequence=i+1,outRel=`database/migrations/${laravelFilenameToStacks(file,sequence,parsed.table)}`;if(!req.dryRun)await writeFile(join(req.target,outRel),parsed.sql);const skippedNote=parsed.skipped.length>0?` Skipped ${parsed.skipped.length} line(s) inside the create block - hand-port from the source migration.`:"";out.push({source:rel,target:outRel,status:"translated",note:`CREATE TABLE \`${parsed.table}\` emitted.${skippedNote}`})}catch(err){out.push({source:rel,target:"",status:"failed",note:`Parse error: ${err instanceof Error?err.message:String(err)}`})}}return out}async function migrateModels(req){const dir=join(req.source,"app","Models");if(!existsSync(dir))return[];const files=readdirSync(dir).filter((f)=>f.endsWith(".php")),targetDir=join(req.target,"app","Models");if(!req.dryRun)await mkdir(targetDir,{recursive:!0});const out=[];for(const file of files){const rel=`app/Models/${file}`;try{const body=await readFile(join(dir,file),"utf8"),parsed=parseLaravelModel(body);if(!parsed){out.push({source:rel,target:"",status:"skipped",note:"No class declaration found."});continue}const outRel=`app/Models/${`${parsed.className}.ts`}`;if(!req.dryRun)await writeFile(join(req.target,outRel),parsed.tsSource);const notes=[`Attributes: ${parsed.fillable.length} fillable, ${Object.keys(parsed.casts).length} cast`,parsed.relationships.length>0?`Relationships: ${parsed.relationships.map((r)=>`${r.name}(${r.kind})`).join(", ")}`:null,...parsed.notes].filter(Boolean).join(" | ");out.push({source:rel,target:outRel,status:"translated",note:notes||`Model ${parsed.className} emitted.`})}catch(err){out.push({source:rel,target:"",status:"failed",note:`Parse error: ${err instanceof Error?err.message:String(err)}`})}}return out}const DEFERRED_SURFACES=[{path:"routes",note:"routes/web.php + routes/api.php - port to routes/web.ts + routes/api.ts using the Stacks router DSL."},{path:"app/Http/Controllers",note:"Controllers \u2192 Stacks Actions in app/Actions/. Each controller method maps to one Action file."},{path:"resources/views",note:"Blade templates \u2192 stx components in resources/views/."},{path:"database/seeders",note:"Seeders \u2192 database/seeders/*.ts using Stacks SeederContract."},{path:"app/Console/Commands",note:"Artisan commands \u2192 app/Commands/*.ts in your Stacks project."},{path:"app/Jobs",note:"Queue jobs \u2192 app/Jobs/*.ts; pair with config/queue.ts driver settings."},{path:"composer.json",note:"Dependency map - Laravel packages don't translate 1:1; the Stacks docs cover the closest @stacksjs/* equivalents."}];function summariseDeferred(req){const out=[];for(const surface of DEFERRED_SURFACES){const sourcePath=join(req.source,surface.path);if(!existsSync(sourcePath))continue;out.push({source:surface.path,target:"",status:"skipped",note:surface.note})}return out}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
const RELATION_KINDS=["belongsTo","hasMany","hasOne","belongsToMany"];export function parseLaravelModel(source){const classMatch=source.match(/\bclass\s+([A-Z][A-Za-z0-9_]*)\b/);if(!classMatch?.[1])return null;const className=classMatch[1],table=extractStringProperty(source,"table")??snakeCasePlural(className),fillable=extractArrayProperty(source,"fillable"),hidden=extractArrayProperty(source,"hidden"),casts=extractKeyedArrayProperty(source,"casts"),relationships=extractRelationships(source),notes=[];if(/protected\s+\$appends\s*=/.test(source))notes.push("$appends accessors require a custom Stacks computed-attribute setup");if(/protected\s+\$dates\s*=/.test(source))notes.push("$dates is implicit in Stacks
|
|
1
|
+
const RELATION_KINDS=["belongsTo","hasMany","hasOne","belongsToMany"];export function parseLaravelModel(source){const classMatch=source.match(/\bclass\s+([A-Z][A-Za-z0-9_]*)\b/);if(!classMatch?.[1])return null;const className=classMatch[1],table=extractStringProperty(source,"table")??snakeCasePlural(className),fillable=extractArrayProperty(source,"fillable"),hidden=extractArrayProperty(source,"hidden"),casts=extractKeyedArrayProperty(source,"casts"),relationships=extractRelationships(source),notes=[];if(/protected\s+\$appends\s*=/.test(source))notes.push("$appends accessors require a custom Stacks computed-attribute setup");if(/protected\s+\$dates\s*=/.test(source))notes.push("$dates is implicit in Stacks - datetime columns are already typed via validation rules");if(/\buse\b[^;]*\bSoftDeletes\b/.test(source))notes.push("SoftDeletes trait \u2192 enable `useDeletedAt` (or matching trait) in the Stacks model traits block");if(/\buse\b[^;]*\bHasFactory\b/.test(source))notes.push("HasFactory \u2192 Stacks generates factories automatically when `useSeeder` is enabled");const tsSource=emitStacksModel({className,table,fillable,hidden,casts,relationships});return{className,table,fillable,hidden,casts,relationships,tsSource,notes}}function extractStringProperty(source,name){const re=new RegExp(`protected\\s+\\$${name}\\s*=\\s*['"]([^'"]+)['"]`);return source.match(re)?.[1]??null}function extractArrayProperty(source,name){const re=new RegExp(`protected\\s+\\$${name}\\s*=\\s*\\[([^\\]]*)\\]`,"s"),m=source.match(re);if(!m?.[1])return[];return m[1].split(",").map((s)=>s.trim().replace(/^['"]|['"]$/g,"")).filter(Boolean)}function extractKeyedArrayProperty(source,name){const re=new RegExp(`protected\\s+\\$${name}\\s*=\\s*\\[([^\\]]*)\\]`,"s"),m=source.match(re);if(!m?.[1])return{};const out={},pairs=m[1].split(",");for(const pair of pairs){const kv=pair.match(/['"]([^'"]+)['"]\s*=>\s*['"]([^'"]+)['"]/);if(kv?.[1]&&kv[2]!==void 0)out[kv[1]]=kv[2]}return out}function extractRelationships(source){const found=[],methodRegex=/public\s+function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\([^)]*\)\s*(?::\s*[^\{]+)?\{[\s\S]*?return\s+\$this\s*->\s*(belongsTo|hasMany|hasOne|belongsToMany)\s*\(\s*([A-Z][A-Za-z0-9_]*)::class/g;let m;while((m=methodRegex.exec(source))!==null){const name=m[1],kind=m[2],target=m[3];if(!name||!kind||!target)continue;found.push({name,kind,target})}return found}function emitStacksModel(args){const attributeNames=unique([...args.fillable,...Object.keys(args.casts)]),attrBlock=attributeNames.length===0?" attributes: {},":` attributes: {
|
|
2
2
|
${attributeNames.map((n)=>emitAttribute(n,args)).join(`
|
|
3
3
|
`)}
|
|
4
4
|
},`,relationsBlock=args.relationships.length===0?"":`
|
|
5
|
-
// Relationships translated from Eloquent
|
|
5
|
+
// Relationships translated from Eloquent - refine targets / foreign keys to match your schema.
|
|
6
6
|
relations: {
|
|
7
7
|
${args.relationships.map((r)=>` ${r.name}: { type: '${r.kind}', target: '${r.target}' },`).join(`
|
|
8
8
|
`)}
|
|
@@ -22,7 +22,7 @@ export default defineModel({
|
|
|
22
22
|
${relationsBlock}
|
|
23
23
|
${attrBlock}
|
|
24
24
|
} as const)
|
|
25
|
-
`}function emitAttribute(name,args){const rule=ruleForCast(args.casts[name])??"schema.string()",fillable=args.fillable.includes(name),hidden=args.hidden.includes(name),notes=[];if(hidden)notes.push("hidden from API responses in source
|
|
25
|
+
`}function emitAttribute(name,args){const rule=ruleForCast(args.casts[name])??"schema.string()",fillable=args.fillable.includes(name),hidden=args.hidden.includes(name),notes=[];if(hidden)notes.push("hidden from API responses in source - handle via your Stacks resource/transformer");const noteLine=notes.length>0?` // ${notes.join("; ")}
|
|
26
26
|
`:"";return` ${name}: {
|
|
27
27
|
fillable: ${fillable},
|
|
28
28
|
${noteLine} validation: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const railsDriver={name:"rails",async migrate(_req){return[{source:".",target:"",status:"skipped",note:"Rails driver is not yet implemented (stacksjs/stacks#1241
|
|
1
|
+
export const railsDriver={name:"rails",async migrate(_req){return[{source:".",target:"",status:"skipped",note:"Rails driver is not yet implemented (stacksjs/stacks#1241 - Laravel driver shipped first). Track follow-up at github.com/stacksjs/stacks/issues/1241."}]}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{existsSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";import process from"node:process";import{installRequestContext,parseCookieHeader}from"@stacksjs/config";import{log}from"@stacksjs/logging";function productionRequestSnapshot(){const{__stxServeContext:snapshot,__stxServeCookies:legacyCookies,__stxServeSearch:legacySearch,__stxServeSite:site}=globalThis;if(!snapshot&&!legacyCookies&&legacySearch===void 0)return;return{...snapshot,cookies:snapshot?.cookies??legacyCookies??{},url:snapshot?.url||snapshot?.search||legacySearch||"",search:snapshot?.search??legacySearch??"",site:snapshot?.site??site??null}}installRequestContext(productionRequestSnapshot);function parseCookies(req){return parseCookieHeader(req.headers.get("cookie"))}export function resolveUserPartialsPath(cwd=process.cwd(),configuredDir){if(configuredDir){const configured=[join(cwd,"resources",configuredDir),join(cwd,configuredDir)].find((candidate)=>existsSync(candidate));if(configured)return configured}const existing=["resources/partials","resources/views/partials","partials","resources/components"].filter((candidate)=>existsSync(join(cwd,candidate)));if(existing.length===0)return;const populated=existing.find((candidate)=>containsTemplates(join(cwd,candidate)));return join(cwd,populated??existing[0])}function containsTemplates(dir){try{return[...new Bun.Glob("**/*.stx").scanSync({cwd:dir,onlyFiles:!0})].length>0}catch{return!1}}export async function loadStxPartialsDir(cwd=process.cwd()){const configPath=join(cwd,"config/stx.ts");if(!existsSync(configPath))return;try{const dir=(await import(configPath)).default?.partialsDir;return typeof dir==="string"&&dir.length>0?dir:void 0}catch{return}}function resolveCsrfMiddlewarePath(){const rel="app/Middleware/Csrf.ts",vendored=join(process.cwd(),"storage/framework/defaults",rel);if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return`${pkgJson.slice(0,pkgJson.lastIndexOf("/"))}/${rel}`}catch{return vendored}}export async function startProductionServer(options){if(options?.port)process.env.PORT=String(options.port);process.env.APP_ENV=process.env.APP_ENV||"production";const port=Number(process.env.PORT)||3000,{config,overridesReady,resolveViewPatterns}=await import("@stacksjs/config");await overridesReady;const{applyViewSecurityHeaders,describeApiProxyRules,describeRedirectRules,injectGlobalAutoImports,resolveApiBase,resolveApiProxyRules,resolveEmbeddableRules,resolveRedirectRules}=await import("@stacksjs/server"),{resolveDefaultsResources}=await import("@stacksjs/actions/dev/defaults-resources"),{stxPageAuthMiddleware}=await import("@stacksjs/auth");await injectGlobalAutoImports();let stxServe;const serveCandidates=[join(homedir(),"Code/Tools/stx/packages/bun-plugin/dist/serve.js"),join(process.cwd(),"pantry/bun-plugin-stx/dist/serve.js")];for(const entry of serveCandidates)try{if(existsSync(entry)){({serve:stxServe}=await import(entry));break}}catch{}if(!stxServe)({serve:stxServe}=await import("bun-plugin-stx/serve"));const stxModule=await resolveVendoredStxModule(),{site:siteConfig,i18n:i18nConfig}=await loadStxSiteConfig(),userViewsPath="resources/views",defaultsResources=resolveDefaultsResources(),defaultViewsPath=join(defaultsResources,"views"),userLayoutsPath=existsSync("resources/views/layouts")?"resources/views/layouts":"resources/layouts",userPartialsPath=resolveUserPartialsPath(process.cwd(),await loadStxPartialsDir()),apiBase=resolveApiBase(config.ports?.api),viewPatterns=resolveViewPatterns(userViewsPath,defaultViewsPath,config?.ui?.defaultViews);for(const name of viewPatterns.missing)log.warn(`ui.defaultViews lists "${name}", which does not exist under ${defaultViewsPath}
|
|
1
|
+
import{existsSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";import process from"node:process";import{installRequestContext,parseCookieHeader}from"@stacksjs/config";import{log}from"@stacksjs/logging";function productionRequestSnapshot(){const{__stxServeContext:snapshot,__stxServeCookies:legacyCookies,__stxServeSearch:legacySearch,__stxServeSite:site}=globalThis;if(!snapshot&&!legacyCookies&&legacySearch===void 0)return;return{...snapshot,cookies:snapshot?.cookies??legacyCookies??{},url:snapshot?.url||snapshot?.search||legacySearch||"",search:snapshot?.search??legacySearch??"",site:snapshot?.site??site??null}}installRequestContext(productionRequestSnapshot);function parseCookies(req){return parseCookieHeader(req.headers.get("cookie"))}export function resolveUserPartialsPath(cwd=process.cwd(),configuredDir){if(configuredDir){const configured=[join(cwd,"resources",configuredDir),join(cwd,configuredDir)].find((candidate)=>existsSync(candidate));if(configured)return configured}const existing=["resources/partials","resources/views/partials","partials","resources/components"].filter((candidate)=>existsSync(join(cwd,candidate)));if(existing.length===0)return;const populated=existing.find((candidate)=>containsTemplates(join(cwd,candidate)));return join(cwd,populated??existing[0])}function containsTemplates(dir){try{return[...new Bun.Glob("**/*.stx").scanSync({cwd:dir,onlyFiles:!0})].length>0}catch{return!1}}export async function loadStxPartialsDir(cwd=process.cwd()){const configPath=join(cwd,"config/stx.ts");if(!existsSync(configPath))return;try{const dir=(await import(configPath)).default?.partialsDir;return typeof dir==="string"&&dir.length>0?dir:void 0}catch{return}}function resolveCsrfMiddlewarePath(){const rel="app/Middleware/Csrf.ts",vendored=join(process.cwd(),"storage/framework/defaults",rel);if(existsSync(vendored))return vendored;try{const pkgJson=Bun.resolveSync("@stacksjs/defaults/package.json",process.cwd());return`${pkgJson.slice(0,pkgJson.lastIndexOf("/"))}/${rel}`}catch{return vendored}}export async function startProductionServer(options){if(options?.port)process.env.PORT=String(options.port);process.env.APP_ENV=process.env.APP_ENV||"production";const port=Number(process.env.PORT)||3000,{config,overridesReady,resolveViewPatterns}=await import("@stacksjs/config");await overridesReady;const{applyViewSecurityHeaders,describeApiProxyRules,describeRedirectRules,injectGlobalAutoImports,resolveApiBase,resolveApiProxyRules,resolveEmbeddableRules,resolveRedirectRules}=await import("@stacksjs/server"),{resolveDefaultsResources}=await import("@stacksjs/actions/dev/defaults-resources"),{stxPageAuthMiddleware}=await import("@stacksjs/auth");await injectGlobalAutoImports();let stxServe;const serveCandidates=[join(homedir(),"Code/Tools/stx/packages/bun-plugin/dist/serve.js"),join(process.cwd(),"pantry/bun-plugin-stx/dist/serve.js")];for(const entry of serveCandidates)try{if(existsSync(entry)){({serve:stxServe}=await import(entry));break}}catch{}if(!stxServe)({serve:stxServe}=await import("bun-plugin-stx/serve"));const stxModule=await resolveVendoredStxModule(),{site:siteConfig,i18n:i18nConfig}=await loadStxSiteConfig(),userViewsPath="resources/views",defaultsResources=resolveDefaultsResources(),defaultViewsPath=join(defaultsResources,"views"),userLayoutsPath=existsSync("resources/views/layouts")?"resources/views/layouts":"resources/layouts",userPartialsPath=resolveUserPartialsPath(process.cwd(),await loadStxPartialsDir()),apiBase=resolveApiBase(config.ports?.api),viewPatterns=resolveViewPatterns(userViewsPath,defaultViewsPath,config?.ui?.defaultViews);for(const name of viewPatterns.missing)log.warn(`ui.defaultViews lists "${name}", which does not exist under ${defaultViewsPath} - ignoring.`);const apiProxyRules=resolveApiProxyRules(config.server?.proxy);if(apiProxyRules.paths.length>0||apiProxyRules.prefixes.length>1)log.info(`API proxy: ${describeApiProxyRules(apiProxyRules)}`);const redirectRules=resolveRedirectRules(config.server?.redirects);if(redirectRules.size>0)log.info(`Redirects: ${describeRedirectRules(redirectRules)}`);const embeddableRules=resolveEmbeddableRules(config.server?.security?.embeddable);if(embeddableRules.paths.length>0||embeddableRules.prefixes.length>0)log.info(`Frameable by other origins: ${[...embeddableRules.paths,...embeddableRules.prefixes].join(" ")}`);log.info(`Starting production server on port ${port}...`);await stxServe({patterns:viewPatterns.patterns,port,autoIncrementPort:!1,reusePort:["production","staging","development"].includes((process.env.APP_ENV||"").toLowerCase()),componentsDir:join(defaultsResources,"components"),layoutsDir:userLayoutsPath,...userPartialsPath&&{partialsDir:userPartialsPath},fallbackLayoutsDir:join(defaultsResources,"layouts"),fallbackPartialsDir:defaultViewsPath,quiet:options?.verbose!==!0,...stxModule&&{stxModule},...i18nConfig&&{i18n:i18nConfig},...siteConfig?.url&&{site:siteConfig},middleware:stxPageAuthMiddleware(),onRequest:async(req)=>{const{maintenanceGate,isApiBoundRequest:isApiBound,proxyToBackend,resolveRedirect}=await import("@stacksjs/server"),gated=await maintenanceGate(req);if(gated)return gated;const url=new URL(req.url),redirected=resolveRedirect(url,redirectRules);if(redirected)return redirected;if(isApiBound(req,url.pathname,apiProxyRules)){if(!apiBase){log.error(`No API target configured for ${url.pathname}. This app shares its host with other deployments, so there is no safe default port to guess - refusing to proxy. Set PORT_API (or API_URL) for this site, and deploy an \`api\` site on its own port.`);return new Response("Bad Gateway",{status:502})}try{return await proxyToBackend(req,apiBase)}catch(error){log.error(`API proxy to ${apiBase} failed: ${error.message}`);return new Response("Bad Gateway",{status:502})}}if(existsSync(join(process.cwd(),"resources/views/blog.stx"))){const{renderBlogFeed}=await import("@stacksjs/actions/blog"),feed=await renderBlogFeed(req);if(feed)return feed}globalThis.__stxServeSearch=url.search;globalThis.__stxServeCookies=parseCookies(req);if(config.analytics?.capturePageviews)import("@stacksjs/analytics").then(({recordPageview})=>recordPageview(req)).catch(()=>{});if(config.sites?.enabled){const sites=await import("@stacksjs/sites"),resolved=await sites.resolveSiteByHost(sites.requestHost(req.headers,sites.sitesOptions()));sites.setCurrentSite(resolved);globalThis.__stxServeSite=sites.toSiteSnapshot(resolved)}else globalThis.__stxServeSite=null;return},onResponse:async(req,response)=>{const secured=applyViewSecurityHeaders(req,response,embeddableRules),method=req.method.toUpperCase();if(method!=="GET"&&method!=="HEAD")return secured;const baseline=secured??response;let current=baseline;if(current.status===404&&config.sites?.enabled)try{const{cmsNotFoundFallback}=await import("@stacksjs/cms"),cmsResponse=await cmsNotFoundFallback(req);if(cmsResponse)current=cmsResponse}catch(error){log.debug(`CMS fallback skipped: ${error.message}`)}try{const{seedCsrfCookieIfMissing}=await import(resolveCsrfMiddlewarePath());return await seedCsrfCookieIfMissing(req,current)??(current===baseline?secured:current)}catch(error){log.debug(`CSRF cookie seeding skipped: ${error.message}`);return current===baseline?secured:current}}});log.success(`Production server listening on http://0.0.0.0:${port}`)}async function resolveVendoredStxModule(){const candidates=[join(homedir(),"Code/Tools/stx/packages/stx/dist/index.js"),join(process.cwd(),"pantry/@stacksjs/stx/dist/index.js")];for(const entry of candidates)try{if(existsSync(entry))return await import(entry)}catch{}try{return await import("@stacksjs/stx")}catch{}return}function fallbackI18nFromSite(site){const locales=site.i18n.locales,defaultLocale=site.i18n.defaultLocale??locales[0];return{locales,defaultLocale,labels:site.i18n.labels??Object.fromEntries(locales.map((c)=>[c,c.toUpperCase()])),translations:{},pickerSelector:site.i18n.pickerSelector??"#lang-picker"}}async function resolveSiteI18n(site){const resolverPaths=[join(homedir(),"Code/Tools/stx/packages/stx/src/site-builder/i18n.ts"),join(homedir(),"Code/Tools/stx/packages/stx/dist/index.js"),join(process.cwd(),"pantry/@stacksjs/stx/dist/index.js")];for(const resolverPath of resolverPaths)try{if(!existsSync(resolverPath))continue;const resolved=await import(resolverPath);if(typeof resolved.resolveI18n!=="function")continue;const i18n=resolved.resolveI18n(site,process.cwd());if(i18n)return i18n}catch{}try{const resolved=await import("@stacksjs/stx");if(typeof resolved.resolveI18n==="function"){const i18n=resolved.resolveI18n(site,process.cwd());if(i18n)return i18n}}catch{}return fallbackI18nFromSite(site)}async function loadStxSiteConfig(){const sitePath=join(process.cwd(),"site.config.ts");if(!existsSync(sitePath))return{};try{const site=(await import(sitePath)).default;if(!site)return{};if(!site.i18n)return{site};const i18n=await resolveSiteI18n(site);return{site,i18n}}catch{}return{}}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/buddy",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.72.
|
|
5
|
+
"version": "0.72.39",
|
|
6
6
|
"description": "Meet Buddy. The Stacks runtime.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -95,57 +95,57 @@
|
|
|
95
95
|
"prepublishOnly": "bun run build"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@stacksjs/actions": "^0.72.
|
|
99
|
-
"@stacksjs/ai": "^0.72.
|
|
100
|
-
"@stacksjs/alias": "^0.72.
|
|
101
|
-
"@stacksjs/arrays": "^0.72.
|
|
102
|
-
"@stacksjs/auth": "^0.72.
|
|
103
|
-
"@stacksjs/build": "^0.72.
|
|
104
|
-
"@stacksjs/cache": "^0.72.
|
|
105
|
-
"@stacksjs/chat": "^0.72.
|
|
98
|
+
"@stacksjs/actions": "^0.72.39",
|
|
99
|
+
"@stacksjs/ai": "^0.72.39",
|
|
100
|
+
"@stacksjs/alias": "^0.72.39",
|
|
101
|
+
"@stacksjs/arrays": "^0.72.39",
|
|
102
|
+
"@stacksjs/auth": "^0.72.39",
|
|
103
|
+
"@stacksjs/build": "^0.72.39",
|
|
104
|
+
"@stacksjs/cache": "^0.72.39",
|
|
105
|
+
"@stacksjs/chat": "^0.72.39",
|
|
106
106
|
"@stacksjs/clapp": "^0.2.12",
|
|
107
|
-
"@stacksjs/cli": "^0.72.
|
|
108
|
-
"@stacksjs/cloud": "^0.72.
|
|
109
|
-
"@stacksjs/cms": "^0.72.
|
|
110
|
-
"@stacksjs/collections": "^0.72.
|
|
111
|
-
"@stacksjs/config": "^0.72.
|
|
112
|
-
"@stacksjs/database": "^0.72.
|
|
113
|
-
"@stacksjs/desktop-build": "^0.72.
|
|
114
|
-
"@stacksjs/dns": "^0.72.
|
|
107
|
+
"@stacksjs/cli": "^0.72.39",
|
|
108
|
+
"@stacksjs/cloud": "^0.72.39",
|
|
109
|
+
"@stacksjs/cms": "^0.72.39",
|
|
110
|
+
"@stacksjs/collections": "^0.72.39",
|
|
111
|
+
"@stacksjs/config": "^0.72.39",
|
|
112
|
+
"@stacksjs/database": "^0.72.39",
|
|
113
|
+
"@stacksjs/desktop-build": "^0.72.39",
|
|
114
|
+
"@stacksjs/dns": "^0.72.39",
|
|
115
115
|
"@stacksjs/dnsx": "^0.2.3",
|
|
116
|
-
"@stacksjs/email": "^0.72.
|
|
117
|
-
"@stacksjs/enums": "^0.72.
|
|
118
|
-
"@stacksjs/error-handling": "^0.72.
|
|
119
|
-
"@stacksjs/events": "^0.72.
|
|
120
|
-
"@stacksjs/git": "^0.72.
|
|
116
|
+
"@stacksjs/email": "^0.72.39",
|
|
117
|
+
"@stacksjs/enums": "^0.72.39",
|
|
118
|
+
"@stacksjs/error-handling": "^0.72.39",
|
|
119
|
+
"@stacksjs/events": "^0.72.39",
|
|
120
|
+
"@stacksjs/git": "^0.72.39",
|
|
121
121
|
"@stacksjs/gitit": "^0.2.5",
|
|
122
|
-
"@stacksjs/health": "^0.72.
|
|
122
|
+
"@stacksjs/health": "^0.72.39",
|
|
123
123
|
"@stacksjs/httx": "^0.1.10",
|
|
124
|
-
"@stacksjs/image": "^0.72.
|
|
125
|
-
"@stacksjs/lint": "^0.72.
|
|
126
|
-
"@stacksjs/logging": "^0.72.
|
|
127
|
-
"@stacksjs/notifications": "^0.72.
|
|
128
|
-
"@stacksjs/objects": "^0.72.
|
|
129
|
-
"@stacksjs/orm": "^0.72.
|
|
130
|
-
"@stacksjs/path": "^0.72.
|
|
131
|
-
"@stacksjs/payments": "^0.72.
|
|
132
|
-
"@stacksjs/realtime": "^0.72.
|
|
133
|
-
"@stacksjs/router": "^0.72.
|
|
124
|
+
"@stacksjs/image": "^0.72.39",
|
|
125
|
+
"@stacksjs/lint": "^0.72.39",
|
|
126
|
+
"@stacksjs/logging": "^0.72.39",
|
|
127
|
+
"@stacksjs/notifications": "^0.72.39",
|
|
128
|
+
"@stacksjs/objects": "^0.72.39",
|
|
129
|
+
"@stacksjs/orm": "^0.72.39",
|
|
130
|
+
"@stacksjs/path": "^0.72.39",
|
|
131
|
+
"@stacksjs/payments": "^0.72.39",
|
|
132
|
+
"@stacksjs/realtime": "^0.72.39",
|
|
133
|
+
"@stacksjs/router": "^0.72.39",
|
|
134
134
|
"@stacksjs/rpx": "^0.11.42",
|
|
135
|
-
"@stacksjs/search-engine": "^0.72.
|
|
136
|
-
"@stacksjs/security": "^0.72.
|
|
137
|
-
"@stacksjs/server": "^0.72.
|
|
138
|
-
"@stacksjs/sites": "^0.72.
|
|
139
|
-
"@stacksjs/skills": "^0.72.
|
|
140
|
-
"@stacksjs/storage": "^0.72.
|
|
141
|
-
"@stacksjs/strings": "^0.72.
|
|
142
|
-
"@stacksjs/testing": "^0.72.
|
|
135
|
+
"@stacksjs/search-engine": "^0.72.39",
|
|
136
|
+
"@stacksjs/security": "^0.72.39",
|
|
137
|
+
"@stacksjs/server": "^0.72.39",
|
|
138
|
+
"@stacksjs/sites": "^0.72.39",
|
|
139
|
+
"@stacksjs/skills": "^0.72.39",
|
|
140
|
+
"@stacksjs/storage": "^0.72.39",
|
|
141
|
+
"@stacksjs/strings": "^0.72.39",
|
|
142
|
+
"@stacksjs/testing": "^0.72.39",
|
|
143
143
|
"@stacksjs/ts-cloud": "^0.9.4",
|
|
144
|
-
"@stacksjs/tunnel": "^0.72.
|
|
145
|
-
"@stacksjs/types": "^0.72.
|
|
146
|
-
"@stacksjs/ui": "^0.72.
|
|
147
|
-
"@stacksjs/utils": "^0.72.
|
|
148
|
-
"@stacksjs/validation": "^0.72.
|
|
144
|
+
"@stacksjs/tunnel": "^0.72.39",
|
|
145
|
+
"@stacksjs/types": "^0.72.39",
|
|
146
|
+
"@stacksjs/ui": "^0.72.39",
|
|
147
|
+
"@stacksjs/utils": "^0.72.39",
|
|
148
|
+
"@stacksjs/validation": "^0.72.39",
|
|
149
149
|
"ajv": "^8.20.0",
|
|
150
150
|
"ajv-formats": "^3.0.1",
|
|
151
151
|
"ts-pantry": "^0.11.35"
|