@stacksjs/buddy 0.70.264 → 0.70.266
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 +2 -2
- package/dist/commands/auth.js +1 -1
- package/dist/commands/build.js +1 -1
- package/dist/commands/changelog.js +1 -1
- package/dist/commands/clean.js +1 -1
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/configure.js +2 -2
- package/dist/commands/create.js +1 -1
- package/dist/commands/deploy.js +2 -2
- package/dist/commands/desktop-apple.js +2 -2
- package/dist/commands/dev.js +1 -1
- package/dist/commands/domains.js +2 -2
- package/dist/commands/fresh.js +1 -1
- package/dist/commands/generate.js +2 -2
- package/dist/commands/install.js +1 -1
- package/dist/commands/key.js +1 -1
- package/dist/commands/migrate.js +5 -5
- package/dist/commands/queue.js +1 -1
- package/dist/commands/release.js +1 -1
- package/dist/commands/route.js +1 -1
- package/dist/commands/saas.js +1 -1
- package/dist/commands/schedule.js +1 -1
- package/dist/commands/search.js +1 -1
- package/dist/commands/setup.js +1 -1
- package/dist/commands/test.js +1 -1
- package/dist/commands/upgrade.js +1 -1
- package/dist/result.d.ts +18 -0
- package/dist/result.js +1 -0
- package/package.json +44 -44
package/dist/commands/dev.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{execSync}from"node:child_process";import{existsSync,readdirSync,readFileSync}from"node:fs";import readline from"node:readline";import process from"node:process";import{bold,cyan,dim,green,intro,log,onUnknownSubcommand,outro,prompts,runCommand,yellow}from"@stacksjs/cli";import{homedir}from"node:os";import{dirname,join,relative}from"node:path";import{fileURLToPath}from"node:url";import{Action}from"@stacksjs/enums";import{libsPath,projectPath,stxPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{version}from"../../package.json";const DEV_BOOT_STARTING_LINE_COUNT=3;function eraseDevBootStartingLines(){if(!process.stdout.isTTY)return;for(let i=0;i<DEV_BOOT_STARTING_LINE_COUNT;i++){readline.moveCursor(process.stdout,0,-1);readline.clearLine(process.stdout,0)}}let developmentRpx;function resolveRpxEntryPath(){return[projectPath("node_modules/@stacksjs/rpx/dist/index.js"),join(homedir(),"Code/Tools/rpx/packages/rpx/dist/index.js"),projectPath("pantry/@stacksjs/rpx/dist/src/index.js")].find((entry)=>existsSync(entry))??null}async function importDevelopmentRpx(){if(developmentRpx)return developmentRpx;const entry=resolveRpxEntryPath();developmentRpx=entry?await import(entry):await import("@stacksjs/rpx");return developmentRpx}const activeRpxRegistryIds=[];let _actions;async function actions(){if(!_actions)_actions=await import("@stacksjs/actions");return _actions}export const interactiveDevChoices=[{value:"all",title:"All"},{value:"frontend",title:"Frontend"},{value:"api",title:"Backend"},{value:"dashboard",title:"Dashboard"},{value:"desktop",title:"Desktop"},{value:"native",title:"Native App"},{value:"components",title:"Components"},{value:"docs",title:"Documentation"}];export async function dispatchInteractiveDevSelection(selection,runners){if(!Object.hasOwn(runners,selection))return!1;await runners[selection]();return!0}export function resolvePrettyDevDomain(appUrl,nativeMode=!1){if(!appUrl||nativeMode)return null;try{const hostname=new URL(/^https?:\/\//i.test(appUrl)?appUrl:`https://${appUrl}`).hostname.toLowerCase();if(hostname==="localhost"||hostname==="127.0.0.1"||hostname==="::1"||hostname==="0.0.0.0")return null;return hostname}catch{return null}}export function shouldUsePrettyDevUrls(input){return input.domain!==null&&!input.localhostOnly&&(input.proxyManagedExternally||input.systemAuthorized)}const APPLICATION_VIEW_CANDIDATES=["app/index","app","dashboard/index","dashboard","home/index","home","composer","workspace/index","workspace","feed","portal","admin","account/index","account","profile/index","profile"],SITE_LAYOUT_NAMES=new Set(["default","guest","marketing","site"]);export function normalizeDevelopmentEntryPath(path){const value=path?.trim();if(!value||value==="/")return"/";try{const parsed=new URL(value,"http://stacks.localhost");return`${parsed.pathname}${parsed.search}${parsed.hash}`}catch{return"/"}}function configuredApplicationPath(root){if(process.env.APP_PATH)return process.env.APP_PATH;try{return readFileSync(join(root,"config/app.ts"),"utf8").match(/^[ \t]*appPath\s*:\s*(['"`])([^'"`]+)\1/m)?.[2]}catch{return}}function configuredDevelopmentLaunch(root){const environmentValue=process.env.STACKS_DEV_LAUNCH?.trim().toLowerCase();if(environmentValue==="browser"||environmentValue==="native")return environmentValue;try{const value=readFileSync(join(root,"config/app.ts"),"utf8").match(/\bdevLaunch\s*:\s*(['"`])(browser|native)\1/)?.[2];return value==="browser"||value==="native"?value:void 0}catch{return}}export function resolveDevelopmentLaunch(input={}){if(input.browser||input.site)return"browser";if(input.native)return"native";const root=input.root??projectPath();return input.configuredLaunch??configuredDevelopmentLaunch(root)??"browser"}function viewSource(root,route){const base=join(root,"resources/views",route);for(const extension of["stx","vue","html"]){const path=`${base}.${extension}`;if(existsSync(path))try{return readFileSync(path,"utf8")}catch{return""}}return}function usesApplicationLayout(source){const layout=source.match(/@extends\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1]??source.match(/@layout\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1];if(!layout)return!1;return!SITE_LAYOUT_NAMES.has(layout.split("/").at(-1)??layout)}export function resolveDevelopmentEntryPath(input={}){if(input.site)return"/";const root=input.root??projectPath(),configuredPath=input.configuredPath??configuredApplicationPath(root);if(configuredPath)return normalizeDevelopmentEntryPath(configuredPath);const loginExists=viewSource(root,"login")!==void 0||viewSource(root,"auth/login")!==void 0,candidates=APPLICATION_VIEW_CANDIDATES.map((route)=>({route,source:viewSource(root,route)})).filter((candidate)=>candidate.source!==void 0),appCandidate=candidates.find((candidate)=>usesApplicationLayout(candidate.source))??(loginExists?candidates[0]:void 0);if(appCandidate)return normalizeDevelopmentEntryPath(`/${appCandidate.route.replace(/\/index$/,"")}`);return loginExists?"/login":"/"}export function developmentUrl(baseUrl,entryPath){const normalized=normalizeDevelopmentEntryPath(entryPath);if(normalized==="/")return baseUrl.replace(/\/$/,"");return new URL(normalized,`${baseUrl.replace(/\/$/,"")}/`).toString()}export function developmentBrowserCommand(url,platform=process.platform){if(platform==="darwin")return["open",url];if(platform==="win32")return["cmd","/c","start","",url];return["xdg-open",url]}function openDevelopmentBrowser(url){if(!process.stdout.isTTY||process.env.CI||process.env.STACKS_DEV_NO_OPEN==="1")return;try{Bun.spawn(developmentBrowserCommand(url),{stdin:"ignore",stdout:"ignore",stderr:"ignore"}).unref()}catch{}}async function canStartPrettyDevProxy(){if(await waitForHttpsProxy(443,150))return!0;try{const{authorizeSystemAccess}=await importDevelopmentRpx();return authorizeSystemAccess({interactive:!1})}catch{return!1}}export function dev(buddy){const descriptions={dev:"Start development server",frontend:"Start the frontend development server",components:"Start the Components development server",desktop:"Start the Desktop App development server",native:"Start the app in a native Craft window",dashboard:"Start the Dashboard development server",api:"Start the local API development server",docs:"Start the Documentation development server",systemTray:"Start the System Tray development server",interactive:"Get asked which development server to start",select:"Which development server are you trying to start?",withLocalhost:"Include the localhost URL in the output",browser:"Open the application in a browser instead of its configured native window",site:"Open the marketing site instead of the application",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("dev [server]",descriptions.dev).option("-f, --frontend",descriptions.frontend).option("-a, --api",descriptions.api).option("-c, --components",descriptions.components).option("-d, --dashboard",descriptions.dashboard).option("-k, --desktop",descriptions.desktop).option("-n, --native",descriptions.native).option("-o, --docs",descriptions.docs).option("-s, --system-tray",descriptions.systemTray).option("-i, --interactive",descriptions.interactive,{default:!1}).option("-l, --with-localhost",descriptions.withLocalhost,{default:!1}).option("--browser",descriptions.browser,{default:!1}).option("--site",descriptions.site,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(server,options)=>{const perf=Bun.nanoseconds();process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});const target=server||(options.frontend?"frontend":void 0)||(options.api?"api":void 0)||(options.components?"components":void 0)||(options.dashboard?"dashboard":void 0)||(options.desktop?"desktop":void 0)||(options.native?"native":void 0)||(options.systemTray||options["system-tray"]?"system-tray":void 0)||(options.docs?"docs":void 0);if(target){const serverOptions={...options},a=await actions();switch(target){case"native":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"frontend":await a.runFrontendDevServer(serverOptions);break;case"api":await a.runApiDevServer(serverOptions);break;case"components":await a.runComponentsDevServer(serverOptions);break;case"dashboard":await a.runDashboardDevServer(serverOptions);break;case"desktop":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"system-tray":await a.runSystemTrayDevServer(serverOptions);break;case"docs":await a.runDocsDevServer(serverOptions);break;default:log.error(`Unknown server: ${target}`);process.exit(ExitCode.InvalidArgument)}}else if(wantsInteractive(options)){const selectedValue=(await prompts({type:"select",name:"value",message:descriptions.select,choices:interactiveDevChoices})).value,a=await actions();if(!await dispatchInteractiveDevSelection(selectedValue,{all:()=>startDevelopmentServer(options,perf),frontend:()=>a.runFrontendDevServer(options),api:()=>a.runApiDevServer(options),dashboard:()=>a.runDashboardDevServer(options),desktop:()=>startDevelopmentServer({...options,native:!0},perf),native:()=>startDevelopmentServer({...options,native:!0},perf),components:()=>a.runComponentsDevServer(options),docs:()=>a.runDocsDevServer(options)})){log.error("Invalid option during interactive mode");process.exit(ExitCode.InvalidArgument)}}else await startDevelopmentServer(options,perf);outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:components",descriptions.components).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:components"),result=await runCommand("bun run dev",{cwd:libsPath("components/stx")});if(options.verbose)log.info("buddy dev:components result",result);if(result.isErr){await outro("While running the dev:components command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:docs",descriptions.docs).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:docs"),result=await(await actions()).runAction(Action.DevDocs,options);if(result.isErr){await outro("While running the dev:docs command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:native",descriptions.native).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:native");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:desktop",descriptions.desktop).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:desktop");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:api",descriptions.api).option("-p, --project [project]",descriptions.project,{default:!1}).option("--no-watch-types","Skip the model/config type-regeneration watcher",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const a=await actions();if(options.watchTypes!==!1)a.watchTypes(options).catch((err)=>{log.warn("[dev:api] type watcher exited:",err)});await a.runApiDevServer(options)});buddy.command("dev:frontend",descriptions.frontend).alias("dev:pages").alias("dev:views").option("-p, --project [project]",descriptions.project,{default:!1}).option("--site",descriptions.site,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});await(await actions()).runFrontendDevServer(options)});buddy.command("dev:dashboard",descriptions.dashboard).alias("dev:admin").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runDashboardDevServer(options)});buddy.command("dev:system-tray",descriptions.systemTray).alias("dev:tray").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runSystemTrayDevServer(options)});onUnknownSubcommand(buddy,"dev")}export async function startDevelopmentServer(_options,_startTime){const options=_options,startedAt=_startTime,appUrl=process.env.APP_URL??"stacks.localhost",nativeMode=resolveDevelopmentLaunch({browser:options.browser,native:options.native,site:options.site})==="native",entryPath=resolveDevelopmentEntryPath({site:options.site});process.env.STACKS_DEV_ENTRY_PATH=entryPath;const proxyManagedExternally=process.env.STACKS_PROXY_MANAGED==="1",preferredFrontendPort=Number(process.env.PORT)||3000,preferredApiPort=Number(process.env.PORT_API)||3008,preferredDocsPort=Number(process.env.PORT_DOCS)||3006,preferredDashboardPort=Number(process.env.PORT_ADMIN)||3002;if(process.env.STACKS_DEV_NO_KILL!=="1")await cleanupStaleDevProcesses([preferredFrontendPort,preferredApiPort,preferredDocsPort,preferredDashboardPort]);const portShifts=[],claimPort=async(label,preferred)=>{if(proxyManagedExternally)return preferred;const port=await findAvailablePort(preferred);if(port!==preferred)portShifts.push(`${label} :${preferred} \u2192 :${port}`);return port},frontendPort=await claimPort("Frontend",preferredFrontendPort),apiPort=await claimPort("API",preferredApiPort),docsPort=await claimPort("Docs",preferredDocsPort),dashboardPort=await claimPort("Dashboard",preferredDashboardPort);process.env.PORT=String(frontendPort);process.env.PORT_API=String(apiPort);process.env.PORT_DOCS=String(docsPort);process.env.PORT_ADMIN=String(dashboardPort);const includeDashboard=process.env.STACKS_DEV_DASHBOARD==="1",domain=resolvePrettyDevDomain(appUrl,nativeMode),appLooksCustom=domain!==null,localhostOnly=process.env.STACKS_DEV_LOCALHOST==="1",prettyUrlsRequested=appLooksCustom&&!localhostOnly,systemAuthorized=!prettyUrlsRequested||proxyManagedExternally?!0:await canStartPrettyDevProxy(),usePrettyUrls=shouldUsePrettyDevUrls({domain,localhostOnly,proxyManagedExternally,systemAuthorized}),prettySetupRequired=prettyUrlsRequested&&!usePrettyUrls,hasCustomDomain=usePrettyUrls&&!proxyManagedExternally,displayedDomain=usePrettyUrls?domain:null,dashboardDomain=displayedDomain?`dashboard.${displayedDomain}`:null,frontendBaseUrl=displayedDomain?`https://${displayedDomain}`:`http://localhost:${frontendPort}`,frontendUrl=developmentUrl(frontendBaseUrl,entryPath),apiUrl=displayedDomain?`https://${displayedDomain}/api`:`http://localhost:${apiPort}`,docsUrl=displayedDomain?`https://${displayedDomain}/docs`:`http://localhost:${docsPort}`,dashboardUrl=dashboardDomain?`https://${dashboardDomain}`:`http://localhost:${dashboardPort}`;process.env.STACKS_PROXY_MANAGED="1";process.env.STACKS_DEV_QUIET="1";console.log();if(prettySetupRequired){console.log(` ${yellow("\u26A0")} ${yellow("Pretty URL setup required")} ${dim("- using localhost for this session")}`);console.log(` ${dim(" ")}${dim("Run `./buddy setup:ssl` once, then restart `./buddy dev`.")}`);console.log()}if(portShifts.length>0){console.log(` ${yellow("\u26A0")} ${yellow("Ports in use by another process")} ${dim(`- ${portShifts.join(", ")}`)}`);console.log()}console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);console.log();const rpxTlsPreflight=hasCustomDomain&&domain?prepareRpxTlsForDev({domain,includeDashboard,options}):Promise.resolve();if(localhostOnly&&domain&&!proxyManagedExternally)removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1).catch(()=>{});rpxTlsPreflight.catch(()=>{});let isExiting=!1,closeNativeApp;const SHUTDOWN_GRACE_MS=1500,cleanup=()=>{if(isExiting)return;isExiting=!0;closeNativeApp?.();const rpxTeardown=(async()=>{try{await unregisterRpxProxies(activeRpxRegistryIds);activeRpxRegistryIds.length=0;if(hasCustomDomain&&domain)await removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1)}catch{}})(),teardownDeadline=new Promise((resolve)=>setTimeout(resolve,SHUTDOWN_GRACE_MS));Promise.race([rpxTeardown,teardownDeadline]).finally(()=>{try{process.kill(-process.pid,"SIGTERM")}catch{try{process.kill(0,"SIGTERM")}catch{}}setTimeout(()=>{try{process.kill(0,"SIGKILL")}catch{process.exit(1)}},SHUTDOWN_GRACE_MS).unref()})};process.on("SIGINT",cleanup);process.on("SIGTERM",cleanup);process.on("SIGHUP",cleanup);const quietOpts={...options,quiet:!0},a=await actions(),ports=[{name:"API",port:apiPort}],readinessTimeoutMs=30000;let readyAnnounced=!1;const nativeUrl=developmentUrl(`http://localhost:${frontendPort}`,entryPath),nativeWindowReady=nativeMode?waitForPort(frontendPort,readinessTimeoutMs).then(async(ready)=>{if(!ready){log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs/1000}s`);return}closeNativeApp=await launchNativeAppWindow(nativeUrl,options)}):Promise.resolve();Promise.all(ports.map((p)=>waitForPort(p.port,readinessTimeoutMs))).then(async(results)=>{if(readyAnnounced)return;readyAnnounced=!0;const failed=results.map((ok,i)=>ok?null:ports[i]?.name??null).filter((x)=>x!==null);let proxyReachable=!1;if(hasCustomDomain&&domain){(async()=>{try{await rpxTlsPreflight;await registerRpxProxiesForDomain({domain,frontendPort,apiPort,docsPort,dashboardPort,includeDashboard,options})}catch{}})();proxyReachable=await waitForHttpsProxy(443,16000);if(!proxyReachable){console.log(` ${yellow("\u26A0")} ${yellow("HTTPS proxy not reachable on :443")} \u2014 serving ${cyan(`http://localhost:${frontendPort}`)} instead`);console.log(` ${dim(" ")}${dim("rpx needs a valid SUDO_PASSWORD in .env to bind :443; trust the local CA, then restart `./buddy dev`.")}`);if(options.verbose)console.log(` ${dim(" ")}${dim(`Trust CA: sh ${join(RPX_SSL_DIR,"trust-rpx-cert.sh")}`)}`)}}eraseDevBootStartingLines();printDevReadyBanner({options,nativeMode,hasCustomDomain:!!appLooksCustom&&!localhostOnly,proxyReachable:proxyManagedExternally?!0:proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain});if(!nativeMode){const browserUrl=hasCustomDomain&&(proxyManagedExternally||proxyReachable)?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath);openDevelopmentBrowser(browserUrl)}if(startedAt){const elapsedMs=(Bun.nanoseconds()-startedAt)/1e6,summary=failed.length?`ready in ${(elapsedMs/1000).toFixed(1)}s \u2014 ${failed.join(", ")} did not bind within ${readinessTimeoutMs/1000}s`:`ready in ${(elapsedMs/1000).toFixed(1)}s`;console.log(` ${dim(summary)}`);console.log();printDevEngineNotes();console.log()}if(process.env.STACKS_PRINT_ROUTES==="1")await printRegisteredRoutes(apiPort).catch(()=>{})}).catch(()=>{});await Promise.all([a.runFrontendDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Frontend: ${error}`)}),a.runApiDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`API: ${error}`)}),a.runDocsDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Docs: ${error}`)}),includeDashboard?a.runDashboardDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Dashboard: ${error}`)}):Promise.resolve(),nativeWindowReady.catch((error)=>{if(options.verbose)log.warn(`Native window: ${error}`)})])}function isComingSoonMode(){if(existsSync(projectPath("storage/framework/coming-soon")))return!0;try{const idx=projectPath("resources/views/index.stx");return existsSync(idx)&&/siteMode\s*=\s*['"]coming-soon['"]/.test(readFileSync(idx,"utf8"))}catch{return!1}}function blogIsConfigured(){try{const dir=projectPath("content/blog");return existsSync(dir)&&readdirSync(dir).some((f)=>f.endsWith(".md"))}catch{return!1}}function printDevReadyBanner(input){const{options,nativeMode,hasCustomDomain,proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain}=input,verbose=options.verbose??!1,useProxy=hasCustomDomain&&proxyReachable,feUrl=useProxy?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath),feBaseUrl=useProxy?frontendBaseUrl:`http://localhost:${frontendPort}`,apUrl=useProxy?apiUrl:`http://localhost:${apiPort}`,dcUrl=useProxy?docsUrl:`http://localhost:${docsPort}`,dbUrl=useProxy?dashboardUrl:`http://localhost:${dashboardPort}`,blogUrl=`${feBaseUrl}/blog`;console.log();const frontendLabel=entryPath==="/"?"Site":"App";console.log(` ${green("\u279C")} ${bold(frontendLabel.padEnd(8))}: ${cyan(feUrl)}`);if(entryPath!=="/")console.log(` ${dim("\u279C")} ${dim("Site")}: ${dim(feBaseUrl)}`);if(nativeMode)console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);if(blogIsConfigured())console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);if(includeDashboard)console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);if(useProxy){console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`${developmentUrl(`http://localhost:${frontendPort}`,entryPath)} (bypasses the proxy)`)}`);if(includeDashboard&&dashboardDomain)console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`)}if(isComingSoonMode()){console.log();console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("\u2014 visitors see the holding page; bypass with the coming-soon secret.")}`)}if(verbose&&domain){console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${frontendPort} \u2192 ${domain}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/api \u2192 localhost:${apiPort}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/docs \u2192 localhost:${docsPort}`)}`);if(includeDashboard)console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`)}console.log()}function printDevEngineNotes(){const routesFile=stxPath("routes.ts"),routesLabel=relative(projectPath(),routesFile),crosswindConfig=join(projectPath(),"config/crosswind.ts");if(existsSync(crosswindConfig)||existsSync(join(projectPath(),"crosswind.config.ts")))console.log(` ${green("[Crosswind]")} ${dim("CSS engine loaded")}`);if(existsSync(routesFile))try{const routeCount=(readFileSync(routesFile,"utf8").match(/pattern:/g)??[]).length;if(routeCount>0)console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192 ${routesLabel}`)}`)}catch{console.log(` ${green("[stx]")} ${dim(`Routes manifest \u2192 ${routesLabel}`)}`)}}async function launchNativeAppWindow(url,options){let createApp;try{createApp=(await importCraftSdk()).createApp}catch(error){if(options.verbose)log.warn(`Native window unavailable: ${error}`);console.log(` ${dim("Native window unavailable. Install craft-native or set CRAFT_BIN to a Craft binary.")}
|
|
1
|
+
import{execSync}from"node:child_process";import{existsSync,readdirSync,readFileSync}from"node:fs";import readline from"node:readline";import process from"node:process";import{bold,cyan,dim,green,intro,log,onUnknownSubcommand,outro,prompts,runCommand,yellow}from"@stacksjs/cli";import{homedir}from"node:os";import{dirname,join,relative}from"node:path";import{fileURLToPath}from"node:url";import{Action}from"@stacksjs/enums";import{libsPath,projectPath,stxPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{version}from"../../package.json";import{resultFailed}from"../result";const DEV_BOOT_STARTING_LINE_COUNT=3;function eraseDevBootStartingLines(){if(!process.stdout.isTTY)return;for(let i=0;i<DEV_BOOT_STARTING_LINE_COUNT;i++){readline.moveCursor(process.stdout,0,-1);readline.clearLine(process.stdout,0)}}let developmentRpx;function resolveRpxEntryPath(){return[projectPath("node_modules/@stacksjs/rpx/dist/index.js"),join(homedir(),"Code/Tools/rpx/packages/rpx/dist/index.js"),projectPath("pantry/@stacksjs/rpx/dist/src/index.js")].find((entry)=>existsSync(entry))??null}async function importDevelopmentRpx(){if(developmentRpx)return developmentRpx;const entry=resolveRpxEntryPath();developmentRpx=entry?await import(entry):await import("@stacksjs/rpx");return developmentRpx}const activeRpxRegistryIds=[];let _actions;async function actions(){if(!_actions)_actions=await import("@stacksjs/actions");return _actions}export const interactiveDevChoices=[{value:"all",title:"All"},{value:"frontend",title:"Frontend"},{value:"api",title:"Backend"},{value:"dashboard",title:"Dashboard"},{value:"desktop",title:"Desktop"},{value:"native",title:"Native App"},{value:"components",title:"Components"},{value:"docs",title:"Documentation"}];export async function dispatchInteractiveDevSelection(selection,runners){if(!Object.hasOwn(runners,selection))return!1;await runners[selection]();return!0}export function resolvePrettyDevDomain(appUrl,nativeMode=!1){if(!appUrl||nativeMode)return null;try{const hostname=new URL(/^https?:\/\//i.test(appUrl)?appUrl:`https://${appUrl}`).hostname.toLowerCase();if(hostname==="localhost"||hostname==="127.0.0.1"||hostname==="::1"||hostname==="0.0.0.0")return null;return hostname}catch{return null}}export function shouldUsePrettyDevUrls(input){return input.domain!==null&&!input.localhostOnly&&(input.proxyManagedExternally||input.systemAuthorized)}const APPLICATION_VIEW_CANDIDATES=["app/index","app","dashboard/index","dashboard","home/index","home","composer","workspace/index","workspace","feed","portal","admin","account/index","account","profile/index","profile"],SITE_LAYOUT_NAMES=new Set(["default","guest","marketing","site"]);export function normalizeDevelopmentEntryPath(path){const value=path?.trim();if(!value||value==="/")return"/";try{const parsed=new URL(value,"http://stacks.localhost");return`${parsed.pathname}${parsed.search}${parsed.hash}`}catch{return"/"}}function configuredApplicationPath(root){if(process.env.APP_PATH)return process.env.APP_PATH;try{return readFileSync(join(root,"config/app.ts"),"utf8").match(/^[ \t]*appPath\s*:\s*(['"`])([^'"`]+)\1/m)?.[2]}catch{return}}function configuredDevelopmentLaunch(root){const environmentValue=process.env.STACKS_DEV_LAUNCH?.trim().toLowerCase();if(environmentValue==="browser"||environmentValue==="native")return environmentValue;try{const value=readFileSync(join(root,"config/app.ts"),"utf8").match(/\bdevLaunch\s*:\s*(['"`])(browser|native)\1/)?.[2];return value==="browser"||value==="native"?value:void 0}catch{return}}export function resolveDevelopmentLaunch(input={}){if(input.browser||input.site)return"browser";if(input.native)return"native";const root=input.root??projectPath();return input.configuredLaunch??configuredDevelopmentLaunch(root)??"browser"}function viewSource(root,route){const base=join(root,"resources/views",route);for(const extension of["stx","vue","html"]){const path=`${base}.${extension}`;if(existsSync(path))try{return readFileSync(path,"utf8")}catch{return""}}return}function usesApplicationLayout(source){const layout=source.match(/@extends\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1]??source.match(/@layout\(\s*['"]layouts\/([^'"]+)['"]\s*\)/)?.[1];if(!layout)return!1;return!SITE_LAYOUT_NAMES.has(layout.split("/").at(-1)??layout)}export function resolveDevelopmentEntryPath(input={}){if(input.site)return"/";const root=input.root??projectPath(),configuredPath=input.configuredPath??configuredApplicationPath(root);if(configuredPath)return normalizeDevelopmentEntryPath(configuredPath);const loginExists=viewSource(root,"login")!==void 0||viewSource(root,"auth/login")!==void 0,candidates=APPLICATION_VIEW_CANDIDATES.map((route)=>({route,source:viewSource(root,route)})).filter((candidate)=>candidate.source!==void 0),appCandidate=candidates.find((candidate)=>usesApplicationLayout(candidate.source))??(loginExists?candidates[0]:void 0);if(appCandidate)return normalizeDevelopmentEntryPath(`/${appCandidate.route.replace(/\/index$/,"")}`);return loginExists?"/login":"/"}export function developmentUrl(baseUrl,entryPath){const normalized=normalizeDevelopmentEntryPath(entryPath);if(normalized==="/")return baseUrl.replace(/\/$/,"");return new URL(normalized,`${baseUrl.replace(/\/$/,"")}/`).toString()}export function developmentBrowserCommand(url,platform=process.platform){if(platform==="darwin")return["open",url];if(platform==="win32")return["cmd","/c","start","",url];return["xdg-open",url]}function openDevelopmentBrowser(url){if(!process.stdout.isTTY||process.env.CI||process.env.STACKS_DEV_NO_OPEN==="1")return;try{Bun.spawn(developmentBrowserCommand(url),{stdin:"ignore",stdout:"ignore",stderr:"ignore"}).unref()}catch{}}async function canStartPrettyDevProxy(){if(await waitForHttpsProxy(443,150))return!0;try{const{authorizeSystemAccess}=await importDevelopmentRpx();return authorizeSystemAccess({interactive:!1})}catch{return!1}}export function dev(buddy){const descriptions={dev:"Start development server",frontend:"Start the frontend development server",components:"Start the Components development server",desktop:"Start the Desktop App development server",native:"Start the app in a native Craft window",dashboard:"Start the Dashboard development server",api:"Start the local API development server",docs:"Start the Documentation development server",systemTray:"Start the System Tray development server",interactive:"Get asked which development server to start",select:"Which development server are you trying to start?",withLocalhost:"Include the localhost URL in the output",browser:"Open the application in a browser instead of its configured native window",site:"Open the marketing site instead of the application",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("dev [server]",descriptions.dev).option("-f, --frontend",descriptions.frontend).option("-a, --api",descriptions.api).option("-c, --components",descriptions.components).option("-d, --dashboard",descriptions.dashboard).option("-k, --desktop",descriptions.desktop).option("-n, --native",descriptions.native).option("-o, --docs",descriptions.docs).option("-s, --system-tray",descriptions.systemTray).option("-i, --interactive",descriptions.interactive,{default:!1}).option("-l, --with-localhost",descriptions.withLocalhost,{default:!1}).option("--browser",descriptions.browser,{default:!1}).option("--site",descriptions.site,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(server,options)=>{const perf=Bun.nanoseconds();process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});const target=server||(options.frontend?"frontend":void 0)||(options.api?"api":void 0)||(options.components?"components":void 0)||(options.dashboard?"dashboard":void 0)||(options.desktop?"desktop":void 0)||(options.native?"native":void 0)||(options.systemTray||options["system-tray"]?"system-tray":void 0)||(options.docs?"docs":void 0);if(target){const serverOptions={...options},a=await actions();switch(target){case"native":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"frontend":await a.runFrontendDevServer(serverOptions);break;case"api":await a.runApiDevServer(serverOptions);break;case"components":await a.runComponentsDevServer(serverOptions);break;case"dashboard":await a.runDashboardDevServer(serverOptions);break;case"desktop":await startDevelopmentServer({...serverOptions,native:!0},perf);break;case"system-tray":await a.runSystemTrayDevServer(serverOptions);break;case"docs":await a.runDocsDevServer(serverOptions);break;default:log.error(`Unknown server: ${target}`);process.exit(ExitCode.InvalidArgument)}}else if(wantsInteractive(options)){const selectedValue=(await prompts({type:"select",name:"value",message:descriptions.select,choices:interactiveDevChoices})).value,a=await actions();if(!await dispatchInteractiveDevSelection(selectedValue,{all:()=>startDevelopmentServer(options,perf),frontend:()=>a.runFrontendDevServer(options),api:()=>a.runApiDevServer(options),dashboard:()=>a.runDashboardDevServer(options),desktop:()=>startDevelopmentServer({...options,native:!0},perf),native:()=>startDevelopmentServer({...options,native:!0},perf),components:()=>a.runComponentsDevServer(options),docs:()=>a.runDocsDevServer(options)})){log.error("Invalid option during interactive mode");process.exit(ExitCode.InvalidArgument)}}else await startDevelopmentServer(options,perf);outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:components",descriptions.components).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:components"),result=await runCommand("bun run dev",{cwd:libsPath("components/stx")});if(options.verbose)log.info("buddy dev:components result",result);if(resultFailed(result)){await outro("While running the dev:components command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:docs",descriptions.docs).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:docs"),result=await(await actions()).runAction(Action.DevDocs,options);if(resultFailed(result)){await outro("While running the dev:docs command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}console.log("");await outro("Exited",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("dev:native",descriptions.native).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:native");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:desktop",descriptions.desktop).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy dev:desktop");await startDevelopmentServer({...options,native:!0},perf)});buddy.command("dev:api",descriptions.api).option("-p, --project [project]",descriptions.project,{default:!1}).option("--no-watch-types","Skip the model/config type-regeneration watcher",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const a=await actions();if(options.watchTypes!==!1)a.watchTypes(options).catch((err)=>{log.warn("[dev:api] type watcher exited:",err)});await a.runApiDevServer(options)});buddy.command("dev:frontend",descriptions.frontend).alias("dev:pages").alias("dev:views").option("-p, --project [project]",descriptions.project,{default:!1}).option("--site",descriptions.site,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{process.env.STACKS_DEV_ENTRY_PATH=resolveDevelopmentEntryPath({site:options.site});await(await actions()).runFrontendDevServer(options)});buddy.command("dev:dashboard",descriptions.dashboard).alias("dev:admin").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runDashboardDevServer(options)});buddy.command("dev:system-tray",descriptions.systemTray).alias("dev:tray").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await(await actions()).runSystemTrayDevServer(options)});onUnknownSubcommand(buddy,"dev")}export async function startDevelopmentServer(_options,_startTime){const options=_options,startedAt=_startTime,appUrl=process.env.APP_URL??"stacks.localhost",nativeMode=resolveDevelopmentLaunch({browser:options.browser,native:options.native,site:options.site})==="native",entryPath=resolveDevelopmentEntryPath({site:options.site});process.env.STACKS_DEV_ENTRY_PATH=entryPath;const proxyManagedExternally=process.env.STACKS_PROXY_MANAGED==="1",preferredFrontendPort=Number(process.env.PORT)||3000,preferredApiPort=Number(process.env.PORT_API)||3008,preferredDocsPort=Number(process.env.PORT_DOCS)||3006,preferredDashboardPort=Number(process.env.PORT_ADMIN)||3002;if(process.env.STACKS_DEV_NO_KILL!=="1")await cleanupStaleDevProcesses([preferredFrontendPort,preferredApiPort,preferredDocsPort,preferredDashboardPort]);const portShifts=[],claimPort=async(label,preferred)=>{if(proxyManagedExternally)return preferred;const port=await findAvailablePort(preferred);if(port!==preferred)portShifts.push(`${label} :${preferred} \u2192 :${port}`);return port},frontendPort=await claimPort("Frontend",preferredFrontendPort),apiPort=await claimPort("API",preferredApiPort),docsPort=await claimPort("Docs",preferredDocsPort),dashboardPort=await claimPort("Dashboard",preferredDashboardPort);process.env.PORT=String(frontendPort);process.env.PORT_API=String(apiPort);process.env.PORT_DOCS=String(docsPort);process.env.PORT_ADMIN=String(dashboardPort);const includeDashboard=process.env.STACKS_DEV_DASHBOARD==="1",domain=resolvePrettyDevDomain(appUrl,nativeMode),appLooksCustom=domain!==null,localhostOnly=process.env.STACKS_DEV_LOCALHOST==="1",prettyUrlsRequested=appLooksCustom&&!localhostOnly,systemAuthorized=!prettyUrlsRequested||proxyManagedExternally?!0:await canStartPrettyDevProxy(),usePrettyUrls=shouldUsePrettyDevUrls({domain,localhostOnly,proxyManagedExternally,systemAuthorized}),prettySetupRequired=prettyUrlsRequested&&!usePrettyUrls,hasCustomDomain=usePrettyUrls&&!proxyManagedExternally,displayedDomain=usePrettyUrls?domain:null,dashboardDomain=displayedDomain?`dashboard.${displayedDomain}`:null,frontendBaseUrl=displayedDomain?`https://${displayedDomain}`:`http://localhost:${frontendPort}`,frontendUrl=developmentUrl(frontendBaseUrl,entryPath),apiUrl=displayedDomain?`https://${displayedDomain}/api`:`http://localhost:${apiPort}`,docsUrl=displayedDomain?`https://${displayedDomain}/docs`:`http://localhost:${docsPort}`,dashboardUrl=dashboardDomain?`https://${dashboardDomain}`:`http://localhost:${dashboardPort}`;process.env.STACKS_PROXY_MANAGED="1";process.env.STACKS_DEV_QUIET="1";console.log();if(prettySetupRequired){console.log(` ${yellow("\u26A0")} ${yellow("Pretty URL setup required")} ${dim("- using localhost for this session")}`);console.log(` ${dim(" ")}${dim("Run `./buddy setup:ssl` once, then restart `./buddy dev`.")}`);console.log()}if(portShifts.length>0){console.log(` ${yellow("\u26A0")} ${yellow("Ports in use by another process")} ${dim(`- ${portShifts.join(", ")}`)}`);console.log()}console.log(` ${bold(cyan("stacks"))} ${dim(`v${version}`)} ${dim("starting\u2026")}`);console.log();const rpxTlsPreflight=hasCustomDomain&&domain?prepareRpxTlsForDev({domain,includeDashboard,options}):Promise.resolve();if(localhostOnly&&domain&&!proxyManagedExternally)removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1).catch(()=>{});rpxTlsPreflight.catch(()=>{});let isExiting=!1,closeNativeApp;const SHUTDOWN_GRACE_MS=1500,cleanup=()=>{if(isExiting)return;isExiting=!0;closeNativeApp?.();const rpxTeardown=(async()=>{try{await unregisterRpxProxies(activeRpxRegistryIds);activeRpxRegistryIds.length=0;if(hasCustomDomain&&domain)await removeStalePublicDomainOverrides(domain,includeDashboard,options.verbose??!1)}catch{}})(),teardownDeadline=new Promise((resolve)=>setTimeout(resolve,SHUTDOWN_GRACE_MS));Promise.race([rpxTeardown,teardownDeadline]).finally(()=>{try{process.kill(-process.pid,"SIGTERM")}catch{try{process.kill(0,"SIGTERM")}catch{}}setTimeout(()=>{try{process.kill(0,"SIGKILL")}catch{process.exit(1)}},SHUTDOWN_GRACE_MS).unref()})};process.on("SIGINT",cleanup);process.on("SIGTERM",cleanup);process.on("SIGHUP",cleanup);const quietOpts={...options,quiet:!0},a=await actions(),ports=[{name:"API",port:apiPort}],readinessTimeoutMs=30000;let readyAnnounced=!1;const nativeUrl=developmentUrl(`http://localhost:${frontendPort}`,entryPath),nativeWindowReady=nativeMode?waitForPort(frontendPort,readinessTimeoutMs).then(async(ready)=>{if(!ready){log.warn(`Native window skipped because the frontend did not answer within ${readinessTimeoutMs/1000}s`);return}closeNativeApp=await launchNativeAppWindow(nativeUrl,options)}):Promise.resolve();Promise.all(ports.map((p)=>waitForPort(p.port,readinessTimeoutMs))).then(async(results)=>{if(readyAnnounced)return;readyAnnounced=!0;const failed=results.map((ok,i)=>ok?null:ports[i]?.name??null).filter((x)=>x!==null);let proxyReachable=!1;if(hasCustomDomain&&domain){(async()=>{try{await rpxTlsPreflight;await registerRpxProxiesForDomain({domain,frontendPort,apiPort,docsPort,dashboardPort,includeDashboard,options})}catch{}})();proxyReachable=await waitForHttpsProxy(443,16000);if(!proxyReachable){console.log(` ${yellow("\u26A0")} ${yellow("HTTPS proxy not reachable on :443")} \u2014 serving ${cyan(`http://localhost:${frontendPort}`)} instead`);console.log(` ${dim(" ")}${dim("rpx needs a valid SUDO_PASSWORD in .env to bind :443; trust the local CA, then restart `./buddy dev`.")}`);if(options.verbose)console.log(` ${dim(" ")}${dim(`Trust CA: sh ${join(RPX_SSL_DIR,"trust-rpx-cert.sh")}`)}`)}}eraseDevBootStartingLines();printDevReadyBanner({options,nativeMode,hasCustomDomain:!!appLooksCustom&&!localhostOnly,proxyReachable:proxyManagedExternally?!0:proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain});if(!nativeMode){const browserUrl=hasCustomDomain&&(proxyManagedExternally||proxyReachable)?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath);openDevelopmentBrowser(browserUrl)}if(startedAt){const elapsedMs=(Bun.nanoseconds()-startedAt)/1e6,summary=failed.length?`ready in ${(elapsedMs/1000).toFixed(1)}s \u2014 ${failed.join(", ")} did not bind within ${readinessTimeoutMs/1000}s`:`ready in ${(elapsedMs/1000).toFixed(1)}s`;console.log(` ${dim(summary)}`);console.log();printDevEngineNotes();console.log()}if(process.env.STACKS_PRINT_ROUTES==="1")await printRegisteredRoutes(apiPort).catch(()=>{})}).catch(()=>{});await Promise.all([a.runFrontendDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Frontend: ${error}`)}),a.runApiDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`API: ${error}`)}),a.runDocsDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Docs: ${error}`)}),includeDashboard?a.runDashboardDevServer(quietOpts).catch((error)=>{if(options.verbose)log.error(`Dashboard: ${error}`)}):Promise.resolve(),nativeWindowReady.catch((error)=>{if(options.verbose)log.warn(`Native window: ${error}`)})])}function isComingSoonMode(){if(existsSync(projectPath("storage/framework/coming-soon")))return!0;try{const idx=projectPath("resources/views/index.stx");return existsSync(idx)&&/siteMode\s*=\s*['"]coming-soon['"]/.test(readFileSync(idx,"utf8"))}catch{return!1}}function blogIsConfigured(){try{const dir=projectPath("content/blog");return existsSync(dir)&&readdirSync(dir).some((f)=>f.endsWith(".md"))}catch{return!1}}function printDevReadyBanner(input){const{options,nativeMode,hasCustomDomain,proxyReachable,frontendUrl,frontendBaseUrl,entryPath,apiUrl,docsUrl,dashboardUrl,frontendPort,apiPort,docsPort,includeDashboard,domain,dashboardPort,dashboardDomain}=input,verbose=options.verbose??!1,useProxy=hasCustomDomain&&proxyReachable,feUrl=useProxy?frontendUrl:developmentUrl(`http://localhost:${frontendPort}`,entryPath),feBaseUrl=useProxy?frontendBaseUrl:`http://localhost:${frontendPort}`,apUrl=useProxy?apiUrl:`http://localhost:${apiPort}`,dcUrl=useProxy?docsUrl:`http://localhost:${docsPort}`,dbUrl=useProxy?dashboardUrl:`http://localhost:${dashboardPort}`,blogUrl=`${feBaseUrl}/blog`;console.log();const frontendLabel=entryPath==="/"?"Site":"App";console.log(` ${green("\u279C")} ${bold(frontendLabel.padEnd(8))}: ${cyan(feUrl)}`);if(entryPath!=="/")console.log(` ${dim("\u279C")} ${dim("Site")}: ${dim(feBaseUrl)}`);if(nativeMode)console.log(` ${green("\u279C")} ${bold("Native")}: ${cyan(`Craft \u2192 ${feUrl}`)}`);console.log(` ${green("\u279C")} ${bold("API")}: ${cyan(apUrl)}`);console.log(` ${green("\u279C")} ${bold("Docs")}: ${cyan(dcUrl)}`);if(blogIsConfigured())console.log(` ${green("\u279C")} ${bold("Blog")}: ${cyan(blogUrl)}`);if(includeDashboard)console.log(` ${green("\u279C")} ${bold("Dashboard")}: ${cyan(dbUrl)}`);if(useProxy){console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`${developmentUrl(`http://localhost:${frontendPort}`,entryPath)} (bypasses the proxy)`)}`);if(includeDashboard&&dashboardDomain)console.log(` ${dim("\u279C")} ${dim("Direct")}: ${dim(`http://localhost:${dashboardPort} (dashboard, bypasses the proxy)`)}`)}if(isComingSoonMode()){console.log();console.log(` ${yellow("\u25CF")} ${bold(yellow("Coming soon mode"))} ${dim("\u2014 visitors see the holding page; bypass with the coming-soon secret.")}`)}if(verbose&&domain){console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${frontendPort} \u2192 ${domain}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/api \u2192 localhost:${apiPort}`)}`);console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`${frontendBaseUrl}/docs \u2192 localhost:${docsPort}`)}`);if(includeDashboard)console.log(` ${dim("\u279C")} ${dim("Proxy")}: ${dim(`localhost:${dashboardPort} \u2192 ${dashboardDomain}`)}`)}console.log()}function printDevEngineNotes(){const routesFile=stxPath("routes.ts"),routesLabel=relative(projectPath(),routesFile),crosswindConfig=join(projectPath(),"config/crosswind.ts");if(existsSync(crosswindConfig)||existsSync(join(projectPath(),"crosswind.config.ts")))console.log(` ${green("[Crosswind]")} ${dim("CSS engine loaded")}`);if(existsSync(routesFile))try{const routeCount=(readFileSync(routesFile,"utf8").match(/pattern:/g)??[]).length;if(routeCount>0)console.log(` ${green("[stx]")} ${dim(`Generated ${routeCount} routes \u2192 ${routesLabel}`)}`)}catch{console.log(` ${green("[stx]")} ${dim(`Routes manifest \u2192 ${routesLabel}`)}`)}}async function launchNativeAppWindow(url,options){let createApp;try{createApp=(await importCraftSdk()).createApp}catch(error){if(options.verbose)log.warn(`Native window unavailable: ${error}`);console.log(` ${dim("Native window unavailable. Install craft-native or set CRAFT_BIN to a Craft binary.")}
|
|
2
2
|
`);return}if(!createApp){console.log(` ${dim("Native window unavailable. The Craft SDK did not export createApp.")}
|
|
3
3
|
`);return}const craftBinaryPath=resolveCraftBinaryPath();if(!craftBinaryPath){console.log(` ${dim("Native window unavailable. Set CRAFT_BIN to a Craft binary, or install Craft in ~/Code/Tools/craft.")}
|
|
4
4
|
`);return}const appIconPath=resolveNativeAppIconPath(),app=createApp({url,quiet:!options.verbose,craftPath:craftBinaryPath,window:{title:await resolveNativeAppTitle(),width:1280,height:860,titlebarHidden:!0,webSidebarMaterial:!0,webSidebarWidth:286,webSidebarMaterialOpacity:0.78,...appIconPath&&{icon:appIconPath}}});app.show().catch((error)=>{if(options.verbose)log.warn(`Native window exited: ${error}`)});return()=>app.close()}async function importCraftSdk(){const localCraftSdk=process.env.HOME?`${process.env.HOME}/Code/Tools/craft/packages/typescript/src/index.ts`:void 0;if(localCraftSdk&&existsSync(localCraftSdk))return await import(localCraftSdk);const packageNames=["craft-native","@craft-native/craft","@stacksjs/ts-craft"];let primaryError;for(const packageName of packageNames)try{return await import(packageName)}catch(error){primaryError??=error}throw primaryError}async function cleanupStaleDevProcesses(ports){const projectRoot=projectPath(),actionDevPath=projectPath("storage/framework/core/actions/src/dev/"),pids=new Set;for(const port of ports)for(const pid of await listenerPids(port)){if(pid===process.pid)continue;const command=await commandForPid(pid);if(!command.includes(projectRoot))continue;if(command.includes("storage/framework/core/buddy/src/cli.ts dev")||command.includes("storage/framework/core/actions/src/dev/")||command.includes(actionDevPath))pids.add(pid)}if(pids.size===0)return;for(const pid of pids)try{process.kill(pid,"SIGTERM")}catch{}await new Promise((r)=>setTimeout(r,250));for(const pid of pids)try{process.kill(pid,"SIGKILL")}catch{}await new Promise((r)=>setTimeout(r,150))}async function listenerPids(port){try{const proc=Bun.spawn(["lsof","-nP",`-iTCP:${port}`,"-sTCP:LISTEN","-t"],{stdout:"pipe",stderr:"ignore"}),output=await new Response(proc.stdout).text();await proc.exited;return output.split(/\s+/).map((value)=>Number(value)).filter((pid)=>Number.isInteger(pid)&&pid>0)}catch{return[]}}async function commandForPid(pid){try{const proc=Bun.spawn(["ps","-p",String(pid),"-o","command="],{stdout:"pipe",stderr:"ignore"}),output=await new Response(proc.stdout).text();await proc.exited;return output.trim()}catch{return""}}function resolveCraftBinaryPath(){const home=process.env.HOME;return[process.env.CRAFT_BIN,home?`${home}/Code/Tools/craft/craft`:void 0,home?`${home}/Code/Tools/craft/bin/craft`:void 0,home?`${home}/Code/Tools/craft/packages/zig/zig-out/bin/craft`:void 0,home?`${home}/Documents/Projects/craft/packages/zig/zig-out/bin/craft`:void 0].filter((candidate)=>Boolean(candidate)).find((candidate)=>existsSync(candidate))}function resolveNativeAppIconPath(){return[projectPath("resources/assets/images/app-icon.png"),projectPath("resources/assets/images/icon.png"),projectPath("public/icon.png")].find((candidate)=>existsSync(candidate))}async function resolveNativeAppTitle(){try{const pkg=await Bun.file(projectPath("package.json")).json(),name=typeof pkg.productName==="string"?pkg.productName:pkg.name;if(typeof name==="string"&&name.length>0)return name.split(/[-_\s]+/).filter(Boolean).map((part)=>part.charAt(0).toUpperCase()+part.slice(1)).join(" ")}catch{}return"Stacks App"}const METHOD_COLORS={GET:cyan,POST:green,PUT:cyan,PATCH:cyan,DELETE:cyan,OPTIONS:dim};async function printRegisteredRoutes(apiPort){try{const ac=new AbortController,t=setTimeout(()=>ac.abort(),1500),res=await fetch(`http://127.0.0.1:${apiPort}/__routes`,{signal:ac.signal}).catch(()=>null);clearTimeout(t);if(!res||!res.ok)return;const routes=await res.json();if(!Array.isArray(routes)||routes.length===0)return;const sorted=[...routes].sort((a,b)=>a.path.localeCompare(b.path)||a.method.localeCompare(b.method)),longestPath=Math.max(...sorted.map((r)=>r.path.length));console.log(` ${dim("Registered routes")}
|
package/dist/commands/domains.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import process from"node:process";import{runAction}from"@stacksjs/actions";import{bgCyan,bold,intro,italic,log,onUnknownSubcommand,outro,prompts}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{addDomain}from"@stacksjs/dns";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function domains(buddy){const descriptions={purchase:"Purchase a domain",add:"Add a domain to your cloud",remove:"Remove a domain from your cloud",skip:"Skip the confirmation prompt",project:"Target a specific project",verbose:"Enable verbose output"},c=config.dns.contactInfo;buddy.command("domains:purchase <domain>",descriptions.purchase).option("--years <years>","Number of years to purchase the domain for",{default:1}).option("--privacy","Enable privacy protection",{default:!0}).option("--auto-renew","Enable auto-renew",{default:!0}).option("--first-name <firstName>","Registrant first name",{default:c?.firstName}).option("--last-name <lastName>","Registrant last name",{default:c?.lastName}).option("--organization <organization>","Registrant organization name",{default:c?.organizationName}).option("--address-line1 <address>","Registrant address line 1",{default:c?.addressLine1}).option("--address-line2 <address>","Registrant address line 2",{default:c?.addressLine2}).option("--city <city>","Registrant city",{default:c?.city}).option("--state <state>","Registrant state",{default:c?.state}).option("--country <country>","Registrant country code",{default:c?.countryCode}).option("--zip <zip>","Registrant zip",{default:c?.zip}).option("--phone <phone>","Registrant phone",{default:c?.phoneNumber}).option("--email <email>","Registrant email",{default:c?.email}).option("--admin-first-name <firstName>","Admin first name",{default:c?.admin?.firstName||c?.firstName}).option("--admin-last-name <lastName>","Admin last name",{default:c?.admin?.lastName||c?.lastName}).option("--admin-organization <organization>","Admin organization",{default:c?.admin?.organizationName||c?.organizationName}).option("--admin-address-line1 <address>","Admin address line 1",{default:c?.admin?.addressLine1||c?.addressLine1}).option("--admin-address-line2 <address>","Admin address line 2",{default:c?.admin?.addressLine2||c?.addressLine2}).option("--admin-city <city>","Admin city",{default:c?.admin?.city||c?.city}).option("--admin-state <state>","Admin state",{default:c?.admin?.state||c?.state}).option("--admin-country <country>","Admin country code",{default:c?.admin?.countryCode||c?.countryCode}).option("--admin-zip <zip>","Admin zip",{default:c?.admin?.zip||c?.zip}).option("--admin-phone <phone>","Admin phone number",{default:c?.admin?.phoneNumber||c?.phoneNumber}).option("--admin-email <email>","Admin email",{default:c?.admin?.email||c?.email}).option("--tech-first-name <firstName>","Tech first name",{default:c?.tech?.firstName||c?.firstName}).option("--tech-last-name <lastName>","Tech last name",{default:c?.tech?.lastName||c?.lastName}).option("--tech-organization <organization>","Tech organization name",{default:c?.tech?.organizationName||c?.organizationName}).option("--tech-address-line1 <address>","Tech address line 1",{default:c?.tech?.addressLine1||c?.addressLine1}).option("--tech-address-line2 <address>","Tech address line 2",{default:c?.tech?.addressLine2||c?.addressLine2}).option("--tech-city <city>","Tech city",{default:c?.tech?.city||c?.city}).option("--tech-state <state>","Tech state",{default:c?.tech?.state||c?.state}).option("--tech-country <country>","Tech country",{default:c?.tech?.countryCode||c?.countryCode}).option("--tech-zip <zip>","Tech zip",{default:c?.tech?.zip||c?.zip}).option("--tech-phone <phone>","Tech phone",{default:c?.tech?.phoneNumber||c?.phoneNumber}).option("--tech-email <email>","Tech email",{default:c?.tech?.email||c?.email}).option("--privacy-admin","Enable privacy protection for admin",{default:c?.privacyAdmin||c?.privacy||!0}).option("--privacy-tech","Enable privacy protection for tech",{default:c?.privacyTech||c?.privacy||!0}).option("--privacy-registrant","Enable privacy protection for registrant",{default:c?.privacyRegistrant||c?.privacy||!0}).option("--contact-type <type>","Contact type",{default:"person"}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{log.debug("Running `buddy domains:purchase <domain>` ...",options);options.domain=domain;const startTime=await intro("buddy domains:purchase"),result=await runAction(Action.DomainsPurchase,options);if(result
|
|
1
|
+
import process from"node:process";import{runAction}from"@stacksjs/actions";import{bgCyan,bold,intro,italic,log,onUnknownSubcommand,outro,prompts}from"@stacksjs/cli";import{config}from"@stacksjs/config";import{addDomain}from"@stacksjs/dns";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function domains(buddy){const descriptions={purchase:"Purchase a domain",add:"Add a domain to your cloud",remove:"Remove a domain from your cloud",skip:"Skip the confirmation prompt",project:"Target a specific project",verbose:"Enable verbose output"},c=config.dns.contactInfo;buddy.command("domains:purchase <domain>",descriptions.purchase).option("--years <years>","Number of years to purchase the domain for",{default:1}).option("--privacy","Enable privacy protection",{default:!0}).option("--auto-renew","Enable auto-renew",{default:!0}).option("--first-name <firstName>","Registrant first name",{default:c?.firstName}).option("--last-name <lastName>","Registrant last name",{default:c?.lastName}).option("--organization <organization>","Registrant organization name",{default:c?.organizationName}).option("--address-line1 <address>","Registrant address line 1",{default:c?.addressLine1}).option("--address-line2 <address>","Registrant address line 2",{default:c?.addressLine2}).option("--city <city>","Registrant city",{default:c?.city}).option("--state <state>","Registrant state",{default:c?.state}).option("--country <country>","Registrant country code",{default:c?.countryCode}).option("--zip <zip>","Registrant zip",{default:c?.zip}).option("--phone <phone>","Registrant phone",{default:c?.phoneNumber}).option("--email <email>","Registrant email",{default:c?.email}).option("--admin-first-name <firstName>","Admin first name",{default:c?.admin?.firstName||c?.firstName}).option("--admin-last-name <lastName>","Admin last name",{default:c?.admin?.lastName||c?.lastName}).option("--admin-organization <organization>","Admin organization",{default:c?.admin?.organizationName||c?.organizationName}).option("--admin-address-line1 <address>","Admin address line 1",{default:c?.admin?.addressLine1||c?.addressLine1}).option("--admin-address-line2 <address>","Admin address line 2",{default:c?.admin?.addressLine2||c?.addressLine2}).option("--admin-city <city>","Admin city",{default:c?.admin?.city||c?.city}).option("--admin-state <state>","Admin state",{default:c?.admin?.state||c?.state}).option("--admin-country <country>","Admin country code",{default:c?.admin?.countryCode||c?.countryCode}).option("--admin-zip <zip>","Admin zip",{default:c?.admin?.zip||c?.zip}).option("--admin-phone <phone>","Admin phone number",{default:c?.admin?.phoneNumber||c?.phoneNumber}).option("--admin-email <email>","Admin email",{default:c?.admin?.email||c?.email}).option("--tech-first-name <firstName>","Tech first name",{default:c?.tech?.firstName||c?.firstName}).option("--tech-last-name <lastName>","Tech last name",{default:c?.tech?.lastName||c?.lastName}).option("--tech-organization <organization>","Tech organization name",{default:c?.tech?.organizationName||c?.organizationName}).option("--tech-address-line1 <address>","Tech address line 1",{default:c?.tech?.addressLine1||c?.addressLine1}).option("--tech-address-line2 <address>","Tech address line 2",{default:c?.tech?.addressLine2||c?.addressLine2}).option("--tech-city <city>","Tech city",{default:c?.tech?.city||c?.city}).option("--tech-state <state>","Tech state",{default:c?.tech?.state||c?.state}).option("--tech-country <country>","Tech country",{default:c?.tech?.countryCode||c?.countryCode}).option("--tech-zip <zip>","Tech zip",{default:c?.tech?.zip||c?.zip}).option("--tech-phone <phone>","Tech phone",{default:c?.tech?.phoneNumber||c?.phoneNumber}).option("--tech-email <email>","Tech email",{default:c?.tech?.email||c?.email}).option("--privacy-admin","Enable privacy protection for admin",{default:c?.privacyAdmin||c?.privacy||!0}).option("--privacy-tech","Enable privacy protection for tech",{default:c?.privacyTech||c?.privacy||!0}).option("--privacy-registrant","Enable privacy protection for registrant",{default:c?.privacyRegistrant||c?.privacy||!0}).option("--contact-type <type>","Contact type",{default:"person"}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(domain,options)=>{log.debug("Running `buddy domains:purchase <domain>` ...",options);options.domain=domain;const startTime=await intro("buddy domains:purchase"),result=await runAction(Action.DomainsPurchase,options);if(resultFailed(result)){await outro("While running the domains:purchase command, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}const{confirm}=await prompts({name:"confirm",type:"confirm",message:`Would you like to set ${domain} as your APP_URL?`});if(!confirm){await outro(`Alrighty! ${italic(domain)} was added to your account.`,{startTime,useSeconds:!0,type:"success"});log.info(`Please note, you may need to validate your email address. Check your ${italic(options.registrantEmail)} inbox.`);process.exit(ExitCode.Success)}const{writeEnv}=await import("@stacksjs/env");writeEnv("APP_URL",domain);let message=`Great! ${italic(domain)} was added to your account.`;message+=`
|
|
2
2
|
|
|
3
3
|
And your APP_URL has been set to ${italic(domain)}.
|
|
4
4
|
|
|
@@ -6,4 +6,4 @@ Please note, this change has not been deployed yet.
|
|
|
6
6
|
|
|
7
7
|
The next time you run ${bgCyan(italic(bold(" buddy deploy ")))}, your app will deploy to ${italic(domain)}.
|
|
8
8
|
|
|
9
|
-
${italic("You may need to deploy 2-3 times for the changes to take effect. Issue tracked here: https://github.com/stacksjs/stacks/issues/685")}`;await outro(message,{startTime,useSeconds:!0,type:"info"});process.exit(ExitCode.Success)});buddy.command("domains:add <domain>",descriptions.add).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy domains:add <domain>` ...",options);const startTime=await intro("buddy domains:add"),result=await addDomain({...options,startTime});if(result
|
|
9
|
+
${italic("You may need to deploy 2-3 times for the changes to take effect. Issue tracked here: https://github.com/stacksjs/stacks/issues/685")}`;await outro(message,{startTime,useSeconds:!0,type:"info"});process.exit(ExitCode.Success)});buddy.command("domains:add <domain>",descriptions.add).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy domains:add <domain>` ...",options);const startTime=await intro("buddy domains:add"),result=await addDomain({...options,startTime});if(resultFailed(result)){await outro("While running the `buddy deploy`, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Added your domain.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("domains:remove <domain>",descriptions.remove).option("--yes",descriptions.skip,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy domains:remove <domain>` ...",options);const domain=options.domain||config.app.url,opts={...options,domain},startTime=await intro("buddy domains:remove");if(!opts.yes){const{confirm}=await prompts({name:"confirm",type:"confirm",message:`Are you sure you want to remove ${domain}?`});if(!confirm){await outro("Cancelled the domains:remove command",{startTime,useSeconds:!0,type:"info"});process.exit(ExitCode.Success)}}const result=await runAction(Action.DomainsRemove,opts);if(resultFailed(result)){await outro("While running the domains:remove command, there was an issue",{startTime,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Removed your domain DNS records.",{startTime,useSeconds:!0});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"domains")}
|
package/dist/commands/fresh.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{hasTTY,isCI}from"@stacksjs/env";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function fresh(buddy){const descriptions={fresh:"Re-installs your npm dependencies",project:"Target a specific project",force:"Skip the confirmation prompt (required in CI/non-interactive shells)",verbose:"Enable verbose output"};buddy.command("fresh",descriptions.fresh).option("-p, --project [project]",descriptions.project,{default:!1}).option("-f, --force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy fresh` ...",options);const skipConfirm=options.force===!0||Boolean(buddy.isForce)||Boolean(buddy.isNoInteraction);if(!skipConfirm&&(isCI||!hasTTY||!process.stdin.isTTY)){log.syncError("Refusing to run `buddy fresh` from a non-interactive shell without confirmation.");log.fatal(" \u27A1\uFE0F Re-run with `--force` to proceed: `buddy fresh --force`")}if(!skipConfirm){const{confirm}=await import("@stacksjs/cli");if(!await confirm({message:"This will remove and reinstall all dependencies. Continue?",initial:!1})){log.info("Fresh install cancelled");process.exit(ExitCode.Success)}}const perf=await intro("buddy fresh"),result=await runAction(Action.Fresh,{...options,stdout:"inherit"});if(result
|
|
1
|
+
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{hasTTY,isCI}from"@stacksjs/env";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function fresh(buddy){const descriptions={fresh:"Re-installs your npm dependencies",project:"Target a specific project",force:"Skip the confirmation prompt (required in CI/non-interactive shells)",verbose:"Enable verbose output"};buddy.command("fresh",descriptions.fresh).option("-p, --project [project]",descriptions.project,{default:!1}).option("-f, --force",descriptions.force,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy fresh` ...",options);const skipConfirm=options.force===!0||Boolean(buddy.isForce)||Boolean(buddy.isNoInteraction);if(!skipConfirm&&(isCI||!hasTTY||!process.stdin.isTTY)){log.syncError("Refusing to run `buddy fresh` from a non-interactive shell without confirmation.");log.fatal(" \u27A1\uFE0F Re-run with `--force` to proceed: `buddy fresh --force`")}if(!skipConfirm){const{confirm}=await import("@stacksjs/cli");if(!await confirm({message:"This will remove and reinstall all dependencies. Continue?",initial:!1})){log.info("Fresh install cancelled");process.exit(ExitCode.Success)}}const perf=await intro("buddy fresh"),result=await runAction(Action.Fresh,{...options,stdout:"inherit"});if(resultFailed(result)){await outro("While running `buddy fresh`, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Freshly reinstalled your dependencies",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"fresh")}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import process from"node:process";import{generateComponentMeta,generateCoreSymlink,generateIdeHelpers,generateLibEntries,generateOpenApiSpec,generatePantryConfig,generateProjectImages,generateTypes,generateVsCodeCustomData,generateWebTypes,invoke as startGenerationProcess,watchTypes}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";export function generate(buddy){const descriptions={command:"Automagically build any of your libraries/packages for production use. Select any of the following packages",types:"Generate your TypeScript types",entries:"Generate your function & Component Library Entry Points",webTypes:"Generate web-types.json for IDEs",customData:"Generate VS Code custom data (custom-elements.json) for IDEs",ideHelpers:"Generate IDE helpers",componentMeta:"Generate component meta information",coreSymlink:"Generate symlink of the core framework to the project root",pantry:"Generate the pantry configuration file",openApi:"Generate the OpenAPI specification",images:"Generate every image declared in config/images.ts",og:"Generate the social cards used by link previews",appStore:"Generate the App Store screenshot set",appIcons:"Generate the app icon and favicon sets",select:"What are you trying to generate?",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("generate",descriptions.command).option("-t, --types",descriptions.types).option("-e, --entries",descriptions.entries).option("-w, --web-types",descriptions.webTypes).option("-c, --custom-data",descriptions.customData).option("-i, --ide-helpers",descriptions.ideHelpers).option("-c, --component-meta",descriptions.componentMeta).option("-p, --pantry",descriptions.pantry).option("-o, --openapi",descriptions.openApi).option("--images",descriptions.images).option("-p, --project [project]",descriptions.project,{default:!1}).option("--core-symlink",descriptions.coreSymlink).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate` ...",options);await startGenerationProcess(options);process.exit(ExitCode.Success)});buddy.command("generate:types",descriptions.types).option("-p, --project [project]",descriptions.project,{default:!1}).option("-w, --watch","Re-run on changes to models/ and config/",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).alias("types:generate").action(async(options)=>{log.debug("Running `buddy generate:types` ...",options);await generateTypes(options);try{const{buildDatabaseSchema}=await import("@stacksjs/orm");await buildDatabaseSchema()}catch(err){log.warn(`[generate:db-types] skipped: ${err.message}`)}if(options.watch)await watchTypes(options)});buddy.command("generate:db-types","Refresh database/types.d.ts for db.selectFrom autocomplete (stacksjs/stacks#1923)").option("--dry-run","Print the would-be file content without writing",{default:!1}).action(async(options)=>{const{buildDatabaseSchema}=await import("@stacksjs/orm"),result=await buildDatabaseSchema({dryRun:options.dryRun});if(options.dryRun)console.log(result.content);for(const e of result.errors)log.warn(`[generate:db-types] ${e.file}: ${e.error}`);log.info(`[generate:db-types] resolved ${result.tables.length} table(s)`)});buddy.command("generate:vschema","Derive a Vitess VSchema from your models (writes database/vschema.json)").option("--dry-run","Print the VSchema without writing it",{default:!1}).option("--out [path]","Where to write the VSchema",{default:"database/vschema.json"}).action(async(options)=>{const{generateVSchema}=await import("@stacksjs/actions"),result=await generateVSchema({dryRun:options.dryRun,out:options.out});if(!result.ok){console.error(`
|
|
1
|
+
import process from"node:process";import{generateComponentMeta,generateCoreSymlink,generateIdeHelpers,generateLibEntries,generateOpenApiSpec,generatePantryConfig,generateProjectImages,generateTypes,generateVsCodeCustomData,generateWebTypes,invoke as startGenerationProcess,watchTypes}from"@stacksjs/actions";import{intro,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";import{resultError,resultFailed}from"../result";export function generate(buddy){const descriptions={command:"Automagically build any of your libraries/packages for production use. Select any of the following packages",types:"Generate your TypeScript types",entries:"Generate your function & Component Library Entry Points",webTypes:"Generate web-types.json for IDEs",customData:"Generate VS Code custom data (custom-elements.json) for IDEs",ideHelpers:"Generate IDE helpers",componentMeta:"Generate component meta information",coreSymlink:"Generate symlink of the core framework to the project root",pantry:"Generate the pantry configuration file",openApi:"Generate the OpenAPI specification",images:"Generate every image declared in config/images.ts",og:"Generate the social cards used by link previews",appStore:"Generate the App Store screenshot set",appIcons:"Generate the app icon and favicon sets",select:"What are you trying to generate?",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("generate",descriptions.command).option("-t, --types",descriptions.types).option("-e, --entries",descriptions.entries).option("-w, --web-types",descriptions.webTypes).option("-c, --custom-data",descriptions.customData).option("-i, --ide-helpers",descriptions.ideHelpers).option("-c, --component-meta",descriptions.componentMeta).option("-p, --pantry",descriptions.pantry).option("-o, --openapi",descriptions.openApi).option("--images",descriptions.images).option("-p, --project [project]",descriptions.project,{default:!1}).option("--core-symlink",descriptions.coreSymlink).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate` ...",options);await startGenerationProcess(options);process.exit(ExitCode.Success)});buddy.command("generate:types",descriptions.types).option("-p, --project [project]",descriptions.project,{default:!1}).option("-w, --watch","Re-run on changes to models/ and config/",{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).alias("types:generate").action(async(options)=>{log.debug("Running `buddy generate:types` ...",options);await generateTypes(options);try{const{buildDatabaseSchema}=await import("@stacksjs/orm");await buildDatabaseSchema()}catch(err){log.warn(`[generate:db-types] skipped: ${err.message}`)}if(options.watch)await watchTypes(options)});buddy.command("generate:db-types","Refresh database/types.d.ts for db.selectFrom autocomplete (stacksjs/stacks#1923)").option("--dry-run","Print the would-be file content without writing",{default:!1}).action(async(options)=>{const{buildDatabaseSchema}=await import("@stacksjs/orm"),result=await buildDatabaseSchema({dryRun:options.dryRun});if(options.dryRun)console.log(result.content);for(const e of result.errors)log.warn(`[generate:db-types] ${e.file}: ${e.error}`);log.info(`[generate:db-types] resolved ${result.tables.length} table(s)`)});buddy.command("generate:vschema","Derive a Vitess VSchema from your models (writes database/vschema.json)").option("--dry-run","Print the VSchema without writing it",{default:!1}).option("--out [path]","Where to write the VSchema",{default:"database/vschema.json"}).action(async(options)=>{const{generateVSchema}=await import("@stacksjs/actions"),result=await generateVSchema({dryRun:options.dryRun,out:options.out});if(!result.ok){console.error(`
|
|
2
2
|
\u274C ${result.error}
|
|
3
|
-
`);process.exit(ExitCode.FatalError)}console.log(result.report);if(options.dryRun)console.log(JSON.stringify(result.vschema,null,2));else log.success(`Wrote ${result.path} (${result.tableCount} tables)`)});buddy.command("generate:entries",descriptions.entries).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:entries` ...",options);await generateLibEntries(options)});buddy.command("generate:web-types",descriptions.webTypes).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:web-types` ...",options);await generateWebTypes(options)});buddy.command("generate:vscode-custom-data",descriptions.customData).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:vscode-custom-data` ...",options);await generateVsCodeCustomData(options)});buddy.command("generate:ide-helpers",descriptions.ideHelpers).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:ide-helpers` ...",options);await generateIdeHelpers(options)});buddy.command("generate:component-meta",descriptions.componentMeta).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:component-meta` ...",options);await generateComponentMeta()});buddy.command("generate:pantry-config",descriptions.pantry).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:pantry-config` ...",options);await generatePantryConfig()});buddy.command("generate:openapi-spec",descriptions.openApi).alias("generate:openapi").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:openapi-spec` ...",options);const perf=await intro("buddy generate:openapi-spec");await generateOpenApiSpec();await outro("Generated OpenAPI specification",{startTime:perf,useSeconds:!0})});buddy.command("generate:migrations","Generate Migrations").action(async(options)=>{log.debug("Running `buddy generate:migrations` ...",options);const{generateMigrations}=await import("@stacksjs/database"),result=await generateMigrations();if(result
|
|
3
|
+
`);process.exit(ExitCode.FatalError)}console.log(result.report);if(options.dryRun)console.log(JSON.stringify(result.vschema,null,2));else log.success(`Wrote ${result.path} (${result.tableCount} tables)`)});buddy.command("generate:entries",descriptions.entries).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:entries` ...",options);await generateLibEntries(options)});buddy.command("generate:web-types",descriptions.webTypes).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:web-types` ...",options);await generateWebTypes(options)});buddy.command("generate:vscode-custom-data",descriptions.customData).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:vscode-custom-data` ...",options);await generateVsCodeCustomData(options)});buddy.command("generate:ide-helpers",descriptions.ideHelpers).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:ide-helpers` ...",options);await generateIdeHelpers(options)});buddy.command("generate:component-meta",descriptions.componentMeta).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:component-meta` ...",options);await generateComponentMeta()});buddy.command("generate:pantry-config",descriptions.pantry).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:pantry-config` ...",options);await generatePantryConfig()});buddy.command("generate:openapi-spec",descriptions.openApi).alias("generate:openapi").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:openapi-spec` ...",options);const perf=await intro("buddy generate:openapi-spec");await generateOpenApiSpec();await outro("Generated OpenAPI specification",{startTime:perf,useSeconds:!0})});buddy.command("generate:migrations","Generate Migrations").action(async(options)=>{log.debug("Running `buddy generate:migrations` ...",options);const{generateMigrations}=await import("@stacksjs/database"),result=await generateMigrations();if(resultFailed(result)){log.error(resultError(result,"generateMigrations failed"));process.exit(ExitCode.FatalError)}});buddy.command("generate:core-symlink","Symlink `.framework` -> storage/framework. A shortcut for core developers.").action(async(options)=>{log.debug("Running `buddy core-symlink` ...",options);await generateCoreSymlink()});buddy.command("generate:images",descriptions.images).alias("images:generate").option("--social","Only build the social cards").option("--app-store","Only build the App Store screenshots").option("--app-icons","Only build the app icons and favicons").option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:images` ...",options);const perf=await intro("buddy generate:images"),only=[];if(options.social)only.push("social");if(options.appStore)only.push("app-store");if(options.appIcons)only.push("app-icons");await generateProjectImages({only,verbose:options.verbose});await outro("Generated images",{startTime:perf,useSeconds:!0})});buddy.command("generate:og",descriptions.og).alias("generate:social").option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:og` ...",options);const perf=await intro("buddy generate:og");await generateProjectImages({only:["social"],verbose:options.verbose});await outro("Generated social cards",{startTime:perf,useSeconds:!0})});buddy.command("generate:app-store",descriptions.appStore).alias("generate:screenshots").option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:app-store` ...",options);const perf=await intro("buddy generate:app-store");await generateProjectImages({only:["app-store"],verbose:options.verbose});await outro("Generated App Store screenshots",{startTime:perf,useSeconds:!0})});buddy.command("generate:app-icons",descriptions.appIcons).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy generate:app-icons` ...",options);const perf=await intro("buddy generate:app-icons");await generateProjectImages({only:["app-icons"],verbose:options.verbose});await outro("Generated app icons",{startTime:perf,useSeconds:!0})});onUnknownSubcommand(buddy,"generate")}
|
package/dist/commands/install.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{log,onUnknownSubcommand,runCommand}from"@stacksjs/cli";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";export function install(buddy){const descriptions={install:"Install your dependencies",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("install",descriptions.install).option("-p, --project [project]",descriptions.project,{default:!1}).option("-v, --verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy install` ...",options);
|
|
1
|
+
import process from"node:process";import{log,onUnknownSubcommand,runCommand}from"@stacksjs/cli";import{path as p}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function install(buddy){const descriptions={install:"Install your dependencies",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("install",descriptions.install).option("-p, --project [project]",descriptions.project,{default:!1}).option("-v, --verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy install` ...",options);const result=await runCommand("bun install",{...options,cwd:p.projectPath()});if(resultFailed(result)){log.error("bun install failed");process.exit(ExitCode.FatalError)}});onUnknownSubcommand(buddy,"install")}
|
package/dist/commands/key.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";export function key(buddy){const descriptions={command:"Generate & set the application key.",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("key:generate",descriptions.command).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy key:generate` ...",options);await intro("buddy key:generate");const result=await runAction(Action.KeyGenerate,options);if(result
|
|
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 key(buddy){const descriptions={command:"Generate & set the application key.",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("key:generate",descriptions.command).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy key:generate` ...",options);await intro("buddy key:generate");const result=await runAction(Action.KeyGenerate,options);if(resultFailed(result)){log.error("Failed to set random application key.",result.error);process.exit(ExitCode.FatalError)}await outro("Random application key set.")});onUnknownSubcommand(buddy,"key")}
|
package/dist/commands/migrate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{closeSync,existsSync,mkdirSync,openSync,readdirSync,readFileSync,rmSync}from"node:fs";import process from"node:process";import{confirm,intro,log,onUnknownSubcommand,outro,text}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{hasTTY,isCI}from"@stacksjs/env";import{appPath,frameworkPath,frameworkRuntimePath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{preflightDatabase}from"../database-preflight";import{DDL_CONSTRAINT_OVERRIDE_ENV,DIALECT_OVERRIDE_ENV,auditDdlConstraints,auditMigrationCorpus,dialectCapabilities,formatDdlConstraintError,formatMigrationDialectError,stripSqlNoise}from"@stacksjs/database";let _runAction;async function runAction(...args){if(!_runAction)_runAction=(await import("@stacksjs/actions")).runAction;return _runAction(...args)}function acquireMigrationLock(){const lockDir=frameworkRuntimePath(),lockFile=`${lockDir}/migrations.lock`;try{if(!existsSync(lockDir))mkdirSync(lockDir,{recursive:!0});const fd=openSync(lockFile,"wx");try{closeSync(fd)}catch{}return{acquired:!0,release:()=>{try{rmSync(lockFile,{force:!0})}catch{}}}}catch{return{acquired:!1,release:()=>{}}}}async function ensureDatabaseOrExit(){try{const{ensureDatabaseReady}=await import("@stacksjs/database");await ensureDatabaseReady()}catch(error){log.syncError(error instanceof Error?error.message:String(error));process.exit(ExitCode.FatalError)}}function readMigrateMarker(){const file=frameworkRuntimePath("last-migrate-result.json");if(!existsSync(file))return null;try{const raw=readFileSync(file,"utf8"),parsed=JSON.parse(raw),n=typeof parsed.appliedCount==="number"?parsed.appliedCount:null;if(n===null||!Number.isFinite(n))return null;return{appliedCount:Math.max(0,Math.floor(n))}}catch{return null}finally{try{rmSync(file,{force:!0})}catch{}}}function countModelFiles(dir){if(!existsSync(dir))return 0;let count=0;const entries=readdirSync(dir,{withFileTypes:!0});for(const entry of entries)if(entry.isDirectory())count+=countModelFiles(`${dir}/${entry.name}`);else if(entry.name.endsWith(".ts")&&!entry.name.startsWith(".")&&!entry.name.startsWith("index"))count++;return count}function validateModelsExist(){const userModelsPath=appPath("Models"),defaultModelsPath=frameworkPath("defaults/app/Models"),userModelCount=countModelFiles(userModelsPath),defaultModelCount=countModelFiles(defaultModelsPath);if(userModelCount===0&&defaultModelCount===0)return{valid:!1,error:"No models found. Please create models in app/Models or ensure framework defaults exist."};return{valid:!0}}function validateMigrationDialect(){const driver=String(process.env.DB_CONNECTION||"sqlite").toLowerCase(),dir=projectPath("database/migrations");if(process.env[DIALECT_OVERRIDE_ENV]!=="1"){const caps=dialectCapabilities(driver),target=caps.wire==="mysql"?"mysql":caps.wire==="postgres"?"postgres":"sqlite";if(driver==="sqlite"||driver==="postgres"||caps.wire==="mysql"){const audit=auditMigrationCorpus({dir,target});if(!audit.empty&&audit.incompatible.length>0)return{valid:!1,error:formatMigrationDialectError(audit,target,"database/migrations")}}}if(process.env[DDL_CONSTRAINT_OVERRIDE_ENV]!=="1"){const constraints=auditDdlConstraints({dir,dialect:driver});if(!constraints.empty&&constraints.violations.length>0)return{valid:!1,error:formatDdlConstraintError(constraints,driver,"database/migrations")}}return{valid:!0}}async function reportMissingForeignKeys(){try{const{auditForeignKeys}=await import("@stacksjs/database"),result=await auditForeignKeys();if(result.missing.length===0)return;const sample=result.missing.slice(0,5).map((fk)=>` \u2022 ${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn} (${fk.model})`).join(`
|
|
1
|
+
import{closeSync,existsSync,mkdirSync,openSync,readdirSync,readFileSync,rmSync}from"node:fs";import process from"node:process";import{confirm,intro,log,onUnknownSubcommand,outro,text}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{hasTTY,isCI}from"@stacksjs/env";import{appPath,frameworkPath,frameworkRuntimePath,projectPath}from"@stacksjs/path";import{ExitCode}from"@stacksjs/types";import{preflightDatabase}from"../database-preflight";import{DDL_CONSTRAINT_OVERRIDE_ENV,DIALECT_OVERRIDE_ENV,auditDdlConstraints,auditMigrationCorpus,dialectCapabilities,formatDdlConstraintError,formatMigrationDialectError,stripSqlNoise}from"@stacksjs/database";import{resultFailed}from"../result";let _runAction;async function runAction(...args){if(!_runAction)_runAction=(await import("@stacksjs/actions")).runAction;return _runAction(...args)}function acquireMigrationLock(){const lockDir=frameworkRuntimePath(),lockFile=`${lockDir}/migrations.lock`;try{if(!existsSync(lockDir))mkdirSync(lockDir,{recursive:!0});const fd=openSync(lockFile,"wx");try{closeSync(fd)}catch{}return{acquired:!0,release:()=>{try{rmSync(lockFile,{force:!0})}catch{}}}}catch{return{acquired:!1,release:()=>{}}}}async function ensureDatabaseOrExit(){try{const{ensureDatabaseReady}=await import("@stacksjs/database");await ensureDatabaseReady()}catch(error){log.syncError(error instanceof Error?error.message:String(error));process.exit(ExitCode.FatalError)}}function readMigrateMarker(){const file=frameworkRuntimePath("last-migrate-result.json");if(!existsSync(file))return null;try{const raw=readFileSync(file,"utf8"),parsed=JSON.parse(raw),n=typeof parsed.appliedCount==="number"?parsed.appliedCount:null;if(n===null||!Number.isFinite(n))return null;return{appliedCount:Math.max(0,Math.floor(n))}}catch{return null}finally{try{rmSync(file,{force:!0})}catch{}}}function countModelFiles(dir){if(!existsSync(dir))return 0;let count=0;const entries=readdirSync(dir,{withFileTypes:!0});for(const entry of entries)if(entry.isDirectory())count+=countModelFiles(`${dir}/${entry.name}`);else if(entry.name.endsWith(".ts")&&!entry.name.startsWith(".")&&!entry.name.startsWith("index"))count++;return count}function validateModelsExist(){const userModelsPath=appPath("Models"),defaultModelsPath=frameworkPath("defaults/app/Models"),userModelCount=countModelFiles(userModelsPath),defaultModelCount=countModelFiles(defaultModelsPath);if(userModelCount===0&&defaultModelCount===0)return{valid:!1,error:"No models found. Please create models in app/Models or ensure framework defaults exist."};return{valid:!0}}function validateMigrationDialect(){const driver=String(process.env.DB_CONNECTION||"sqlite").toLowerCase(),dir=projectPath("database/migrations");if(process.env[DIALECT_OVERRIDE_ENV]!=="1"){const caps=dialectCapabilities(driver),target=caps.wire==="mysql"?"mysql":caps.wire==="postgres"?"postgres":"sqlite";if(driver==="sqlite"||driver==="postgres"||caps.wire==="mysql"){const audit=auditMigrationCorpus({dir,target});if(!audit.empty&&audit.incompatible.length>0)return{valid:!1,error:formatMigrationDialectError(audit,target,"database/migrations")}}}if(process.env[DDL_CONSTRAINT_OVERRIDE_ENV]!=="1"){const constraints=auditDdlConstraints({dir,dialect:driver});if(!constraints.empty&&constraints.violations.length>0)return{valid:!1,error:formatDdlConstraintError(constraints,driver,"database/migrations")}}return{valid:!0}}async function reportMissingForeignKeys(){try{const{auditForeignKeys}=await import("@stacksjs/database"),result=await auditForeignKeys();if(result.missing.length===0)return;const sample=result.missing.slice(0,5).map((fk)=>` \u2022 ${fk.fromTable}.${fk.fromColumn} \u2192 ${fk.toTable}.${fk.toColumn} (${fk.model})`).join(`
|
|
2
2
|
`),more=result.missing.length>5?`
|
|
3
3
|
+ ${result.missing.length-5} more \u2014 run \`./buddy doctor\` for the full list.`:"";log.warn(`${result.missing.length} of ${result.declared.length} declared foreign keys are missing from the live schema:
|
|
4
4
|
${sample}${more}
|
|
@@ -10,14 +10,14 @@ ${sample}${more}
|
|
|
10
10
|
\u274C Error: ${validation.error}
|
|
11
11
|
`);process.exit(ExitCode.FatalError)}const dialectCheck=validateMigrationDialect();if(!dialectCheck.valid){console.error(`
|
|
12
12
|
\u274C Error: ${dialectCheck.error}
|
|
13
|
-
`);process.exit(ExitCode.FatalError)}const applyRenames=options.rename===!1?!1:void 0;if(options.fromDb)process.env.STACKS_MIGRATE_FROM_DB="1";if(applyRenames===!1)process.env.STACKS_MIGRATE_NO_RENAME="1";if(options.diff){try{const{previewPendingMigrations}=await import("@stacksjs/database"),ops=await previewPendingMigrations({fromDb:options.fromDb,applyRenames});if(ops.length===0)log.info("No pending schema changes \u2014 your models match the database.");else{log.info(`${ops.length} pending change${ops.length===1?"":"s"}:`);for(const op of ops)log.info(` \u2022 ${describeOp(op)}${op.destructive?" [destructive]":""}`)}}catch(error){await log.error("Failed to preview migrations:",error)}await outro("Diff complete \u2014 no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate"});if((await resolveMigrationGuards()).confirmMigrate&&!options.force)if(isCI||!hasTTY)log.debug("[migrate] confirmMigrate guard skipped \u2014 non-interactive environment.");else{const APP_ENV=process.env.APP_ENV||"local";await log.flush();if(!await confirm({message:`Run migrations against the ${APP_ENV} database "${currentDatabaseLabel()}"?`,initial:!0})){await outro("Migration cancelled \u2014 no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await ensureDatabaseOrExit();const lock=acquireMigrationLock();if(!lock.acquired){log.syncError("Another migration is already running (storage/framework/runtime/migrations.lock exists). Wait for it to finish, or remove the lockfile if it is stale.");process.exit(ExitCode.FatalError)}if(!await confirmDestructiveMigrations({force:options.force,fromDb:options.fromDb,applyRenames})){lock.release();await outro("Migration cancelled \u2014 no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(options.auth!==!1){log.debug("Migrating auth tables...");try{const{migrateAuthTables}=await import("@stacksjs/database"),authResult=await migrateAuthTables({verbose:options.verbose});if(!authResult.success)log.error(`Failed to migrate auth tables: ${authResult.error}`)}catch(error){log.error("Failed to migrate auth tables:",error)}}const result=await runAction(Action.Migrate,options).finally(()=>lock.release());if(result
|
|
13
|
+
`);process.exit(ExitCode.FatalError)}const applyRenames=options.rename===!1?!1:void 0;if(options.fromDb)process.env.STACKS_MIGRATE_FROM_DB="1";if(applyRenames===!1)process.env.STACKS_MIGRATE_NO_RENAME="1";if(options.diff){try{const{previewPendingMigrations}=await import("@stacksjs/database"),ops=await previewPendingMigrations({fromDb:options.fromDb,applyRenames});if(ops.length===0)log.info("No pending schema changes \u2014 your models match the database.");else{log.info(`${ops.length} pending change${ops.length===1?"":"s"}:`);for(const op of ops)log.info(` \u2022 ${describeOp(op)}${op.destructive?" [destructive]":""}`)}}catch(error){await log.error("Failed to preview migrations:",error)}await outro("Diff complete \u2014 no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate"});if((await resolveMigrationGuards()).confirmMigrate&&!options.force)if(isCI||!hasTTY)log.debug("[migrate] confirmMigrate guard skipped \u2014 non-interactive environment.");else{const APP_ENV=process.env.APP_ENV||"local";await log.flush();if(!await confirm({message:`Run migrations against the ${APP_ENV} database "${currentDatabaseLabel()}"?`,initial:!0})){await outro("Migration cancelled \u2014 no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await ensureDatabaseOrExit();const lock=acquireMigrationLock();if(!lock.acquired){log.syncError("Another migration is already running (storage/framework/runtime/migrations.lock exists). Wait for it to finish, or remove the lockfile if it is stale.");process.exit(ExitCode.FatalError)}if(!await confirmDestructiveMigrations({force:options.force,fromDb:options.fromDb,applyRenames})){lock.release();await outro("Migration cancelled \u2014 no changes applied.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(options.auth!==!1){log.debug("Migrating auth tables...");try{const{migrateAuthTables}=await import("@stacksjs/database"),authResult=await migrateAuthTables({verbose:options.verbose});if(!authResult.success)log.error(`Failed to migrate auth tables: ${authResult.error}`)}catch(error){log.error("Failed to migrate auth tables:",error)}}const result=await runAction(Action.Migrate,options).finally(()=>lock.release());if(resultFailed(result))log.error("Model migrations failed \u2014 applying notification/RBAC table guarantees before exiting.");if(options.auth!==!1)try{const{ensureUtcDatetimeColumns,migrateNotificationTables,migrateRbacTables,migrateTraitTables}=await import("@stacksjs/database"),notifResult=await migrateNotificationTables({verbose:options.verbose});if(!notifResult.success)log.error(`Failed to migrate notification tables: ${notifResult.error}`);const rbacResult=await migrateRbacTables({verbose:options.verbose});if(!rbacResult.success)log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);const traitResult=await migrateTraitTables({verbose:options.verbose});if(!traitResult.success)log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`);const datetimeResult=await ensureUtcDatetimeColumns({verbose:options.verbose});if(!datetimeResult.success)log.error(`Failed to convert TIMESTAMP columns to DATETIME: ${datetimeResult.error}`)}catch(error){log.error("Failed to migrate notification/RBAC/trait tables:",error)}if(resultFailed(result)){await outro("While running the migrate command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}if(options.auth!==!1)try{const{ensureUsersAuthColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUsersAuthColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure users auth columns post-migration:",error)}try{const{ensureUuidColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUuidColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure uuid columns post-migration:",error)}await reportMissingForeignKeys();await reportFkOrphans();const APP_ENV=process.env.APP_ENV||"local",marker=readMigrateMarker(),authSuffix=options.auth!==!1?" (including auth tables)":"",outroMessage=marker==null?`Migrated your ${APP_ENV} database.${authSuffix}`:marker.appliedCount===0?`Nothing to migrate \u2014 your ${APP_ENV} database is already up to date.${authSuffix}`:`Applied ${marker.appliedCount} migration${marker.appliedCount===1?"":"s"} to your ${APP_ENV} database.${authSuffix}`;await outro(outroMessage,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:fresh",descriptions.fresh).alias("db:fresh").option("-d, --diff","Show the SQL that would be run",{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("-s, --seed","Run database seeders after migration",{default:!1}).option("-a, --auth",descriptions.auth,{default:!0}).option("--no-auth","Skip auth/oauth table migrations").option("--create-database","Create the database if it does not exist, without asking",{default:!1}).option("-f, --force",'Skip the drop-database confirmation (only honored when the migrateFresh guard is "allow")',{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate:fresh` ...",options);const perf=await intro("buddy migrate:fresh"),validation=validateModelsExist();if(!validation.valid){console.error(`
|
|
14
14
|
\u274C Error: ${validation.error}
|
|
15
15
|
`);process.exit(ExitCode.FatalError)}const dialectCheck=validateMigrationDialect();if(!dialectCheck.valid){console.error(`
|
|
16
16
|
\u274C Error: ${dialectCheck.error}
|
|
17
17
|
`);process.exit(ExitCode.FatalError)}const guards=await resolveMigrationGuards(),dbLabel=currentDatabaseLabel(),APP_ENV=process.env.APP_ENV||"local";if(guards.migrateFresh==="disabled"){await log.error(`\`buddy migrate:fresh\` is disabled by your migration safety guards (it DROPS every table).
|
|
18
18
|
Target: ${APP_ENV} database "${dbLabel}"
|
|
19
19
|
To allow it, set database.safety.migrateFresh to 'allow' in config/database.ts,
|
|
20
|
-
or run once with: DB_MIGRATE_FRESH=allow ./buddy migrate:fresh`);await outro('migrate:fresh refused \u2014 the migrateFresh guard is set to "disabled".',{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}if(!(guards.migrateFresh==="allow"&&options.force===!0)){if(isCI||!hasTTY){const hint=guards.migrateFresh==="confirm"?'Guard is "confirm": migrate:fresh must be run interactively.':"Re-run with --force to drop the database non-interactively.";await log.error(`Refusing to drop the ${APP_ENV} database "${dbLabel}" in a non-interactive environment. ${hint}`);await outro("migrate:fresh cancelled.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}log.warn(`This will DROP ALL TABLES in the ${APP_ENV} database "${dbLabel}" and rebuild them from scratch. All data will be lost.`);await log.flush();if((await text({message:`Type the database name "${dbLabel}" to confirm (blank to cancel):`})).trim()!==dbLabel){await outro("migrate:fresh cancelled \u2014 confirmation did not match.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate:fresh"});await ensureDatabaseOrExit();const result=await runAction(Action.MigrateFresh,options);if(result
|
|
20
|
+
or run once with: DB_MIGRATE_FRESH=allow ./buddy migrate:fresh`);await outro('migrate:fresh refused \u2014 the migrateFresh guard is set to "disabled".',{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}if(!(guards.migrateFresh==="allow"&&options.force===!0)){if(isCI||!hasTTY){const hint=guards.migrateFresh==="confirm"?'Guard is "confirm": migrate:fresh must be run interactively.':"Re-run with --force to drop the database non-interactively.";await log.error(`Refusing to drop the ${APP_ENV} database "${dbLabel}" in a non-interactive environment. ${hint}`);await outro("migrate:fresh cancelled.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}log.warn(`This will DROP ALL TABLES in the ${APP_ENV} database "${dbLabel}" and rebuild them from scratch. All data will be lost.`);await log.flush();if((await text({message:`Type the database name "${dbLabel}" to confirm (blank to cancel):`})).trim()!==dbLabel){await outro("migrate:fresh cancelled \u2014 confirmation did not match.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}await preflightDatabase({createDatabase:options.createDatabase,command:"migrate:fresh"});await ensureDatabaseOrExit();const result=await runAction(Action.MigrateFresh,options);if(resultFailed(result))log.error("Model migrations failed \u2014 applying auth/notification/RBAC table guarantees before exiting.");if(options.auth!==!1){log.debug("Migrating auth tables...");try{const{migrateAuthTables,migrateNotificationTables,migrateRbacTables,migrateTraitTables}=await import("@stacksjs/database"),authResult=await migrateAuthTables({verbose:options.verbose});if(!authResult.success)log.error(`Failed to migrate auth tables: ${authResult.error}`);const notifResult=await migrateNotificationTables({verbose:options.verbose});if(!notifResult.success)log.error(`Failed to migrate notification tables: ${notifResult.error}`);const rbacResult=await migrateRbacTables({verbose:options.verbose});if(!rbacResult.success)log.error(`Failed to migrate RBAC tables: ${rbacResult.error}`);const traitResult=await migrateTraitTables({verbose:options.verbose});if(!traitResult.success)log.error(`Failed to migrate polymorphic trait tables: ${traitResult.error}`)}catch(error){log.error("Failed to migrate auth/notification/RBAC/trait tables:",error)}}try{const{ensureUuidColumns,sqlHelpers}=await import("@stacksjs/database"),driver=process.env.DB_CONNECTION||"sqlite";await ensureUuidColumns(sqlHelpers(driver),{verbose:options.verbose})}catch(error){log.error("Failed to ensure uuid columns post-migration:",error)}if(resultFailed(result)){await outro("While running the migrate:fresh command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await reportMissingForeignKeys();if(options.seed){log.info("Running database seeders...");try{const{seed}=await import("@stacksjs/database"),seedResult=await seed({verbose:options.verbose,fresh:!0});if(seedResult.failed>0){log.warn(`Seeding completed with ${seedResult.failed} failure(s)`);for(const r of seedResult.results)if(!r.success)log.error(` - ${r.model}: ${r.error}`)}else log.success(`Seeded ${seedResult.successful} model(s)`)}catch(error){log.error("Failed to run seeders:",error)}}const parts=[];if(options.auth!==!1)parts.push("auth tables");if(options.seed)parts.push("seeded");const suffix=parts.length>0?` & ${parts.join(" & ")}`:"",marker=readMigrateMarker(),countPhrase=marker==null?"":marker.appliedCount===0?" (0 applied \u2014 no migration files found?)":` (${marker.appliedCount} migration${marker.appliedCount===1?"":"s"} applied)`;await outro(`All tables dropped successfully & migrated successfully${countPhrase}${suffix}`,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:dns",descriptions.migrate).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy migrate:dns` ...",options);const perf=await intro("buddy migrate:dns"),result=await runAction(Action.MigrateDns,{...options});if(resultFailed(result)){await outro("While running the migrate:dns command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}const APP_URL=process.env.APP_URL||"undefined";await outro(`Migrated your ${APP_URL} DNS.`,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:switch <driver>","Pre-flight check + plan for switching DB_CONNECTION between sqlite / mysql / postgres").action(async(driver)=>{log.debug(`Running \`buddy migrate:switch ${driver}\` ...`);const perf=await intro("buddy migrate:switch"),target=driver.toLowerCase();if(!new Set(["sqlite","mysql","postgres"]).has(target)){console.log(`
|
|
21
21
|
Unknown target driver "${driver}". Allowed: sqlite, mysql, postgres.
|
|
22
22
|
`);await outro("Aborted.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.FatalError)}const current=(process.env.DB_CONNECTION||"sqlite").toLowerCase();if(current===target){console.log(`
|
|
23
23
|
DB_CONNECTION is already "${target}". Nothing to switch.
|
|
@@ -41,7 +41,7 @@ ${sample}${more}
|
|
|
41
41
|
2. (Optional) Export data from the current ${current} database.
|
|
42
42
|
3. ${switchBlocked?`Regenerate the migration files for ${target} FIRST \u2014 \`./buddy migrate\` will refuse until then.`:"Run `./buddy migrate` (or `migrate:fresh` to start clean)."}
|
|
43
43
|
4. The post-migrate FK audit will report any constraints that didn't replay.
|
|
44
|
-
`);await outro("Plan rendered. Re-run after updating .env to actually switch.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:regenerate [dialect]","Rebuild database/migrations from your models for a given dialect").option("--dry-run","Show what would change without writing anything",{default:!1}).option("-f, --force","Regenerate even though the database already has migrations recorded",{default:!1}).action(async(dialect,options)=>{const perf=await intro("buddy migrate:regenerate"),target=(dialect||process.env.DB_CONNECTION||"sqlite").toLowerCase();if(!new Set(["sqlite","mysql","postgres","singlestore"]).has(target)){log.syncError(`Unknown dialect "${target}". Allowed: sqlite, mysql, postgres, singlestore.`);process.exit(ExitCode.FatalError)}const{countAppliedMigrations,regenerateMigrationCorpus}=await import("@stacksjs/database");let applied=0;try{applied=await countAppliedMigrations()}catch{applied=0}if(applied>0&&!options.force&&!options.dryRun){log.syncError(`This database already has ${applied} migration(s) recorded.`);log.syncError("Regenerating renumbers every file, and the migrations table keys on the filename,");log.syncError("so already-applied migrations would look pending and run a second time.");log.syncError(" Point at an empty database, or re-run with --force if you know it is safe.");process.exit(ExitCode.FatalError)}const plan=await regenerateMigrationCorpus({dialect:target,dryRun:!0});if(plan
|
|
44
|
+
`);await outro("Plan rendered. Re-run after updating .env to actually switch.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:regenerate [dialect]","Rebuild database/migrations from your models for a given dialect").option("--dry-run","Show what would change without writing anything",{default:!1}).option("-f, --force","Regenerate even though the database already has migrations recorded",{default:!1}).action(async(dialect,options)=>{const perf=await intro("buddy migrate:regenerate"),target=(dialect||process.env.DB_CONNECTION||"sqlite").toLowerCase();if(!new Set(["sqlite","mysql","postgres","singlestore"]).has(target)){log.syncError(`Unknown dialect "${target}". Allowed: sqlite, mysql, postgres, singlestore.`);process.exit(ExitCode.FatalError)}const{countAppliedMigrations,regenerateMigrationCorpus}=await import("@stacksjs/database");let applied=0;try{applied=await countAppliedMigrations()}catch{applied=0}if(applied>0&&!options.force&&!options.dryRun){log.syncError(`This database already has ${applied} migration(s) recorded.`);log.syncError("Regenerating renumbers every file, and the migrations table keys on the filename,");log.syncError("so already-applied migrations would look pending and run a second time.");log.syncError(" Point at an empty database, or re-run with --force if you know it is safe.");process.exit(ExitCode.FatalError)}const plan=await regenerateMigrationCorpus({dialect:target,dryRun:!0});if(resultFailed(plan)){log.syncError(plan.error.message);process.exit(ExitCode.FatalError)}const{files,removed,models}=plan.value;console.log(`
|
|
45
45
|
Regenerate plan: ${target}
|
|
46
46
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
47
47
|
\u2022 ${models} model(s) read from app/Models and the framework defaults
|
|
@@ -49,7 +49,7 @@ ${sample}${more}
|
|
|
49
49
|
\u2022 ${removed.length} existing file(s) will be removed
|
|
50
50
|
\u2022 These files are tracked in git, so review with \`git diff\` afterwards
|
|
51
51
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
52
|
-
`);if(options.dryRun){await outro("Dry run. Nothing was written.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(!isCI&&hasTTY&&process.stdin.isTTY){await log.flush();if(!await confirm({message:`Replace ${removed.length} migration file(s) with ${files.length} generated for ${target}?`,initial:!1})){await outro("Cancelled. Nothing was written.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}const result=await regenerateMigrationCorpus({dialect:target});if(result
|
|
52
|
+
`);if(options.dryRun){await outro("Dry run. Nothing was written.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(!isCI&&hasTTY&&process.stdin.isTTY){await log.flush();if(!await confirm({message:`Replace ${removed.length} migration file(s) with ${files.length} generated for ${target}?`,initial:!1})){await outro("Cancelled. Nothing was written.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}const result=await regenerateMigrationCorpus({dialect:target});if(resultFailed(result)){log.syncError(result.error.message);process.exit(ExitCode.FatalError)}log.success(`Wrote ${result.value.files.length} ${target} migration file(s) to database/migrations.`);if(applied>0){const{reconcileMigrationLedger}=await import("@stacksjs/database"),fixed=await reconcileMigrationLedger();if(fixed.remapped.length>0)log.info(`Repointed ${fixed.remapped.length} ledger row(s) at their renumbered file.`);if(fixed.recorded.length>0)log.info(`Recorded ${fixed.recorded.length} migration(s) already present in the schema.`);if(fixed.skipped.length>0)log.warn(`${fixed.skipped.length} ledger entr(ies) need a look \u2014 run \`./buddy migrate:status\`.`)}log.info("Review the change with `git diff`, then run `./buddy migrate`.");await outro("Regenerated.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("migrate:status","Compare database/migrations, the migrations ledger, and the live schema").option("--reconcile","Repair the ledger where the schema proves what happened",{default:!1}).option("--include-partial","With --reconcile, also record half-applied migrations",{default:!1}).option("--json","Emit the audit as JSON",{default:!1}).action(async(options)=>{const perf=options.json?void 0:await intro("buddy migrate:status"),{auditMigrationLedger,reconcileMigrationLedger}=await import("@stacksjs/database"),audit=await auditMigrationLedger();if(options.json){console.log(JSON.stringify(audit,(_k,v)=>v instanceof Set?[...v]:v,2));process.exit(audit.drift?ExitCode.FatalError:ExitCode.Success)}if(!audit.supported){log.info(`Dialect "${audit.dialect}" is not audited. Nothing to compare.`);await outro("Skipped.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const{counts,entries,orphans}=audit,list=(status)=>entries.filter((e)=>e.status===status).map((e)=>e.file),report=[],section=(heading,files)=>{if(files.length===0)return;if(heading)report.push(` ${heading}`);for(const file of files.slice(0,8))report.push(` ${file}`);if(files.length>8)report.push(` \u2026 +${files.length-8} more`);report.push("")};report.push("");report.push(` Migration status: ${audit.dialect}`);report.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");report.push(` ${entries.length} file(s) on disk \xB7 ${audit.recordedCount} recorded in the ledger`);report.push("");if(counts.applied>0)report.push(` ${counts.applied} applied - recorded, and present in the schema.`,"");if(counts.unverifiable>0)report.push(` ${counts.unverifiable} unverifiable - data migrations with no schema trace to check.`,"");section(`${counts.pending} pending - not applied yet, will run on the next \`buddy migrate\`:`,list("pending"));if(counts.stranded>0){report.push(` ${counts.stranded} STRANDED - already applied to the schema, but missing from the ledger.`);report.push(" These re-run on the next `buddy migrate`, which is unsafe for anything not idempotent.");section("",list("stranded"))}section(`${counts.partial} PARTIAL - some effects present, some missing. Needs a human:`,list("partial"));section(`${counts.reverted} REVERTED - recorded as applied, but the effects are gone from the schema:`,list("reverted"));if(orphans.length>0)section(`${orphans.length} orphaned ledger row(s) - recorded, but no such file on disk:`,orphans.map((o)=>`${o.migration}${o.renamedTo?` -> renumbered to ${o.renamedTo}`:" (no counterpart; migration deleted?)"}`));console.log(report.join(`
|
|
53
53
|
`));if(!audit.drift){await outro("Ledger matches the corpus and the schema.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(!options.reconcile){console.log(`
|
|
54
54
|
Repair with: ./buddy migrate:status --reconcile
|
|
55
55
|
That repoints renumbered ledger rows and records migrations the schema
|
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";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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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(result.isErr){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 \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")}
|
package/dist/commands/release.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{execFileSync}from"node:child_process";import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,italic,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";const descriptions={release:"Release a new version of your libraries/packages",project:"Target a specific project",dryRun:"Run the release without actually releasing",bump:"Non-interactive bump: patch | minor | major | prepatch | preminor | premajor | prerelease | x.y.z",verbose:"Enable verbose output"};export function release(buddy){buddy.command("release",descriptions.release).option("--dry-run",descriptions.dryRun,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--bump <type>",descriptions.bump).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy release` ...",options);if(options.dryRun)log.warn("Dry run enabled. No changes will be made or committed.");const startTime=await intro("buddy release"),result=await runAction(Action.Release,options);if(result
|
|
1
|
+
import{execFileSync}from"node:child_process";import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,italic,log,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";const descriptions={release:"Release a new version of your libraries/packages",project:"Target a specific project",dryRun:"Run the release without actually releasing",bump:"Non-interactive bump: patch | minor | major | prepatch | preminor | premajor | prerelease | x.y.z",verbose:"Enable verbose output"};export function release(buddy){buddy.command("release",descriptions.release).option("--dry-run",descriptions.dryRun,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).option("--bump <type>",descriptions.bump).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy release` ...",options);if(options.dryRun)log.warn("Dry run enabled. No changes will be made or committed.");const startTime=await intro("buddy release"),result=await runAction(Action.Release,options);if(resultFailed(result)){log.error("Failed to release",result.error);process.exit(ExitCode.FatalError)}await outro("Triggered CI/CD Release via GitHub Actions",{startTime,useSeconds:!0});log.info(`Follow along: ${italic(resolveGitHubActionsUrl(readOriginRemote()))}`)});onUnknownSubcommand(buddy,"release")}export function resolveGitHubActionsUrl(remoteUrl,repository=process.env.GITHUB_REPOSITORY,serverUrl=process.env.GITHUB_SERVER_URL??"https://github.com"){const repo=repository?.trim()||remoteUrl?.trim().match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?$/)?.[1];return repo?`${serverUrl.replace(/\/$/,"")}/${repo}/actions`:"https://github.com/stacksjs/stacks/actions"}function readOriginRemote(){try{return execFileSync("git",["config","--get","remote.origin.url"],{encoding:"utf8"}).trim()}catch{return}}
|
package/dist/commands/route.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function route(buddy){const descriptions={route:"Lists your routes",verbose:"Enable verbose output"};buddy.command("route:list",descriptions.route).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy route:list"),result=await runAction(Action.RouteList,options);if(result
|
|
1
|
+
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,onUnknownSubcommand,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function route(buddy){const descriptions={route:"Lists your routes",verbose:"Enable verbose output"};buddy.command("route:list",descriptions.route).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{const perf=await intro("buddy route:list"),result=await runAction(Action.RouteList,options);if(resultFailed(result)){await outro("While running the route:list command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Successfully listed routes");process.exit(ExitCode.Success)});onUnknownSubcommand(buddy,"route")}
|
package/dist/commands/saas.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function saas(buddy){const descriptions={stripe:"Sets up stripe products in the dashboard",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("stripe:setup",descriptions.stripe).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy stripe:setup` ...",options);const perf=await intro("buddy stripe:setup"),result=await runAction(Action.StripeSetup,options);if(result
|
|
1
|
+
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function saas(buddy){const descriptions={stripe:"Sets up stripe products in the dashboard",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("stripe:setup",descriptions.stripe).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy stripe:setup` ...",options);const perf=await intro("buddy stripe:setup"),result=await runAction(Action.StripeSetup,options);if(resultFailed(result)){await outro("While running the stripe:setup command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Stripe products created successfully",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function schedule(buddy){const descriptions={project:"Target a specific project",schedule:"Run the scheduler",verbose:"Enable verbose output",list:"List all registered scheduled tasks with their next run time",status:"Show currently-held overlap locks (this-process only)"};buddy.command("schedule:run",descriptions.schedule).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy schedule:run` ...",options);const perf=await intro("buddy schedule:run"),result=await runAction(Action.ScheduleRun,options);if(result
|
|
1
|
+
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function schedule(buddy){const descriptions={project:"Target a specific project",schedule:"Run the scheduler",verbose:"Enable verbose output",list:"List all registered scheduled tasks with their next run time",status:"Show currently-held overlap locks (this-process only)"};buddy.command("schedule:run",descriptions.schedule).option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{log.debug("Running `buddy schedule:run` ...",options);const perf=await intro("buddy schedule:run"),result=await runAction(Action.ScheduleRun,options);if(resultFailed(result)){await outro("While running the schedule:run command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("schedule:list",descriptions.list).action(async()=>{try{await import(`${process.cwd()}/routes/scheduler.ts`).catch(()=>{log.warn("[schedule:list] no routes/scheduler.ts found \u2014 registry will be empty unless tasks were registered elsewhere")});const{Schedule}=await import("@stacksjs/scheduler"),jobs=Schedule.listJobs();if(jobs.length===0){log.info("No scheduled tasks registered.");process.exit(ExitCode.Success)}const nameWidth=Math.max(4,...jobs.map((j)=>j.name.length)),patternWidth=Math.max(7,...jobs.map((j)=>(j.pattern??"").length));log.info(`${"NAME".padEnd(nameWidth)} ${"PATTERN".padEnd(patternWidth)} TIMEZONE NEXT RUN (UTC)`);for(const j of jobs){const next=j.nextRun?j.nextRun.toISOString():"\u2014";log.info(`${j.name.padEnd(nameWidth)} ${(j.pattern??"").padEnd(patternWidth)} ${(j.timezone??"UTC").padEnd(25)} ${next}`)}process.exit(ExitCode.Success)}catch(err){log.error(`[schedule:list] failed: ${err instanceof Error?err.message:String(err)}`);process.exit(ExitCode.FatalError)}});buddy.command("schedule:status",descriptions.status).action(async()=>{try{const{Schedule}=await import("@stacksjs/scheduler"),held=Schedule.listLocks();if(held.length===0)log.info("No in-process scheduler locks held.");else{log.info(`Held locks (${held.length}):`);for(const name of held)log.info(` \u2022 ${name}`)}process.exit(ExitCode.Success)}catch(err){log.error(`[schedule:status] failed: ${err instanceof Error?err.message:String(err)}`);process.exit(ExitCode.FatalError)}})}
|
package/dist/commands/search.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";export function search(buddy){const descriptions={import:"Indexes database data to search engine",flush:"Flushes all data from search engine",settings:"Update index settings",list:"List index settings",model:"Target a specific model",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("search-engine:update",descriptions.import).option("-m, --model [model]",descriptions.model,{default:""}).option("-f, --flush [flush]",descriptions.flush,{default:!1}).option("-s, --settings [settings]",descriptions.settings,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).action(async(options)=>{log.debug("Running `search-engine:update` ...",options);let actionString=Action.SearchEngineImport,introString="search-engine:update";if(options.model){actionString=Action.SearchEngineImport;introString=`search-engine:update --model ${options.model}`}if(options.settings){actionString=Action.SearchEnginePushSettings;introString="search-engine:update --settings"}if(options.flush){actionString=Action.SearchEngineFlush;introString="search-engine:update --flush"}const perf=await intro(introString),result=await runAction(actionString,options);if(result
|
|
1
|
+
import process from"node:process";import{runAction}from"@stacksjs/actions";import{intro,log,outro}from"@stacksjs/cli";import{Action}from"@stacksjs/enums";import{ExitCode}from"@stacksjs/types";import{resultFailed}from"../result";export function search(buddy){const descriptions={import:"Indexes database data to search engine",flush:"Flushes all data from search engine",settings:"Update index settings",list:"List index settings",model:"Target a specific model",project:"Target a specific project",verbose:"Enable verbose output"};buddy.command("search-engine:update",descriptions.import).option("-m, --model [model]",descriptions.model,{default:""}).option("-f, --flush [flush]",descriptions.flush,{default:!1}).option("-s, --settings [settings]",descriptions.settings,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).option("-p, --project [project]",descriptions.project,{default:!1}).action(async(options)=>{log.debug("Running `search-engine:update` ...",options);let actionString=Action.SearchEngineImport,introString="search-engine:update";if(options.model){actionString=Action.SearchEngineImport;introString=`search-engine:update --model ${options.model}`}if(options.settings){actionString=Action.SearchEnginePushSettings;introString="search-engine:update --settings"}if(options.flush){actionString=Action.SearchEngineFlush;introString="search-engine:update --flush"}const perf=await intro(introString),result=await runAction(actionString,options);if(resultFailed(result)){await outro("While running the search-engine:update command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Successfully imported model data to search engine.",{startTime:perf,useSeconds:!0})});buddy.command("search-engine:settings",descriptions.list).option("-m, --model [model]",descriptions.model,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{if(!options.model)log.error("Missing required option --model");log.debug("Running `search-engine:settings` ...",options);const perf=await intro("search-engine:settings"),result=await runAction(Action.SearchEngineListSettings,options);if(resultFailed(result)){await outro("While running the search-engine:settings command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro("Successfully listed search engine index settings.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("search:reindex [model]","Re-index a model (Scout-style alias for search-engine:update)").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(model,options)=>{const opts={...options,model:model??""};log.debug("Running `search:reindex` ...",opts);const perf=await intro(`search:reindex${model?` ${model}`:""}`),result=await runAction(Action.SearchEngineImport,opts);if(resultFailed(result)){await outro("While running the search:reindex command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro(model?`Reindexed ${model}.`:"Reindexed all searchable models.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)});buddy.command("search:flush [model]","Delete all documents from a model index (Scout-style alias)").option("-p, --project [project]",descriptions.project,{default:!1}).option("--verbose",descriptions.verbose,{default:!1}).action(async(model,options)=>{const opts={...options,model:model??"",flush:!0};log.debug("Running `search:flush` ...",opts);const perf=await intro(`search:flush${model?` ${model}`:""}`),result=await runAction(Action.SearchEngineFlush,opts);if(resultFailed(result)){await outro("While running the search:flush command, there was an issue",{startTime:perf,useSeconds:!0},result.error);process.exit(ExitCode.FatalError)}await outro(model?`Flushed ${model} from search.`:"Flushed all model indices.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)})}
|