@stacksjs/buddy 0.75.383 → 0.75.387

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.
@@ -52,4 +52,10 @@ export declare function backupTarget(): Promise<BackupTarget | null>;
52
52
  * that configured nothing, so the dump quietly stays on one disk.
53
53
  */
54
54
  export declare function backupDestination(override?: string): Promise<BackupDestination | null>;
55
+ /**
56
+ * How many dumps to keep, behind the same config barrier as
57
+ * {@link backupTarget}: read early, `backups.retain` in `config/database.ts`
58
+ * would look unset and the default would quietly win.
59
+ */
60
+ export declare function backupRetain(flag?: string): Promise<number>;
55
61
  export declare function db(buddy: CLI): void;
@@ -1 +1 @@
1
- import{existsSync,mkdirSync,readdirSync,rmSync,statSync}from"node:fs";import{isAbsolute,join,resolve}from"node:path";import process from"node:process";import{confirmOrNull,intro,log,outro}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";import{backupFileName,backupObjectKey,describeCommand,dumpCommand,dumpSqlite,isBackupFileName,prunableBackups,resolveBackupDestination,resolveBackupTarget,parseBackupDestination,restoreCommand,restoreSqlite,toolFailureDetail,withoutPassword}from"../database-backup";const DEFAULT_BACKUP_DIR="storage/backups/database",DEFAULT_RETAIN=7;export async function backupTarget(){const{config,overridesReady}=await import("@stacksjs/config");await overridesReady.catch(()=>{});return resolveBackupTarget(config?.database)}export async function backupDestination(override){if(override?.trim())return parseBackupDestination(override);const{config,overridesReady}=await import("@stacksjs/config");await overridesReady.catch(()=>{});return resolveBackupDestination(config?.database,process.env)}async function uploadBackup(destination,file,fileName){const{Storage}=await import("@stacksjs/storage"),key=backupObjectKey(destination,fileName),diskName=destination.kind==="disk"?destination.target:"s3",disk=Storage.disk(diskName);if(typeof disk.putStream!=="function")throw TypeError(`The '${diskName}' disk cannot stream uploads, so a database dump cannot be copied to it. Use an S3-backed disk for \`backups.destination\`.`);await disk.putStream(key,Bun.file(file).stream());return destination.kind==="disk"?`disk://${destination.target}/${key}`:`s3://${destination.target}/${key}`}function backupDir(out){const dir=out?.trim()||DEFAULT_BACKUP_DIR;return isAbsolute(dir)?dir:resolve(process.cwd(),dir)}function existingBackups(dir){if(!existsSync(dir))return[];return readdirSync(dir).filter(isBackupFileName).sort()}function sqliteDatabaseExists(target){const path=sqliteFile(target);return existsSync(path)&&statSync(path).size>0}function sqliteFile(target){return isAbsolute(target.database)?target.database:resolve(process.cwd(),target.database)}async function runTool(command,password){const proc=Bun.spawn([command.bin,...command.args],{env:{...process.env,...command.env},stdout:"pipe",stderr:"pipe"}),[code,stderr]=await Promise.all([proc.exited,new Response(proc.stderr).text()]);if(code===0)return;const detail=toolFailureDetail(stderr,command.bin)||`exit code ${code}`;throw Error(withoutPassword(`${command.bin}: ${detail}`,password))}function prune(dir,retain){const removed=prunableBackups(existingBackups(dir),retain);for(const name of removed)rmSync(join(dir,name),{force:!0});return removed}export function db(buddy){buddy.command("db:backup","Dump the application database to a file").option("--out [dir]",`Where to write the dump (default: ${DEFAULT_BACKUP_DIR})`).option("--retain [count]","How many dumps to keep",{default:String(DEFAULT_RETAIN)}).option("--before-migrations","Deploy mode: succeed quietly when there is no database yet",{default:!1}).option("--destination [uri]","Copy the dump offsite: s3://bucket/prefix or disk://name/prefix").option("--verbose","Enable verbose output",{default:!1}).example("buddy db:backup").example("buddy db:backup --out /var/backups/app --retain 30").example("buddy db:backup --destination disk://backups/daily").action(async(options)=>{const perf=await intro("buddy db:backup");try{const target=await backupTarget();if(!target){await outro("No dumpable database is configured; nothing to back up.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(target.engine==="sqlite"&&!sqliteDatabaseExists(target)){const message=`No database at ${target.database} yet; nothing to back up.`;if(options.beforeMigrations){await outro(message,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}log.warn(message);await outro("Nothing backed up.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const dir=backupDir(options.out);mkdirSync(dir,{recursive:!0});const name=backupFileName(target,new Date),destination=join(dir,name),command=dumpCommand(target,destination);if(command){if(options.verbose)log.info(describeCommand(command));await runTool(command,target.password)}else await dumpSqlite(sqliteFile(target),destination);const size=existsSync(destination)?statSync(destination).size:0;log.success(`Wrote ${destination} (${(size/1024).toFixed(1)} KB)`);const retain=Number.parseInt(String(options.retain??DEFAULT_RETAIN),10),removed=prune(dir,retain);if(removed.length)log.info(`Pruned ${removed.length} older dump(s), keeping ${retain}.`);const offsite=await backupDestination(options.destination);if(offsite){const uploaded=await uploadBackup(offsite,destination,name);log.success(`Copied to ${uploaded}`)}else log.info("This dump is on the same disk as the database. Set `backups.destination` in config/database.ts (or DB_BACKUP_DESTINATION) to copy it off the box.");await outro("Database backed up",{startTime:perf,useSeconds:!0})}catch(error){await log.error("Database backup failed:",error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("db:backups","List the database dumps that have been taken").option("--out [dir]",`Where dumps are kept (default: ${DEFAULT_BACKUP_DIR})`).example("buddy db:backups").action(async(options)=>{const dir=backupDir(options.out),backups=existingBackups(dir);if(!backups.length){log.info(`No dumps in ${dir}.`);await log.flush();process.exit(ExitCode.Success)}log.info(`${backups.length} dump(s) in ${dir}, oldest first:`);for(const name of backups){const size=statSync(join(dir,name)).size;log.info(` ${name} ${(size/1024).toFixed(1)} KB`)}await log.flush();process.exit(ExitCode.Success)});buddy.command("db:restore [file]","Restore the application database from a dump").option("--out [dir]",`Where dumps are kept (default: ${DEFAULT_BACKUP_DIR})`).option("--force","Skip the confirmation prompt",{default:!1}).option("--verbose","Enable verbose output",{default:!1}).example("buddy db:restore").example("buddy db:restore 2026-08-13T09-00-00-000.sqlite.sqlite --force").action(async(file,options)=>{const perf=await intro("buddy db:restore");try{const target=await backupTarget();if(!target){await log.error("No restorable database is configured.");process.exit(ExitCode.FatalError)}const dir=backupDir(options.out),backups=existingBackups(dir),name=file??backups[backups.length-1];if(!name){await log.error(`No dumps found in ${dir}.`);process.exit(ExitCode.FatalError)}const source=isAbsolute(name)?name:join(dir,name);if(!existsSync(source)){await log.error(`No such dump: ${source}`);process.exit(ExitCode.FatalError)}if(!options.force){if(await confirmOrNull({message:`Replace ${target.engine} database "${target.database}" with ${name}? The current contents are lost.`,initial:!1})!==!0){await outro("Restore cancelled.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}const command=restoreCommand(target,source);if(command){if(options.verbose)log.info(describeCommand(command));await runTool(command,target.password)}else{const displaced=await restoreSqlite(source,sqliteFile(target),Date.now());if(displaced)log.info(`The database that was there is kept at ${displaced}`)}log.success(`Restored ${target.database} from ${name}`);await outro("Database restored",{startTime:perf,useSeconds:!0})}catch(error){await log.error("Database restore failed:",error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)})}
1
+ import{existsSync,mkdirSync,readdirSync,rmSync,statSync}from"node:fs";import{isAbsolute,join,resolve}from"node:path";import process from"node:process";import{confirmOrNull,intro,log,outro}from"@stacksjs/cli";import{ExitCode}from"@stacksjs/types";import{backupFileName,backupObjectKey,DEFAULT_RETAIN,describeCommand,dumpCommand,dumpSqlite,isBackupFileName,prunableBackups,resolveBackupDestination,resolveBackupTarget,parseBackupDestination,readDump,resolveBackupRetain,restoreCommand,restoreSqlite,writeDump}from"../database-backup";const DEFAULT_BACKUP_DIR="storage/backups/database";export async function backupTarget(){const{config,overridesReady}=await import("@stacksjs/config");await overridesReady.catch(()=>{});return resolveBackupTarget(config?.database)}export async function backupDestination(override){if(override?.trim())return parseBackupDestination(override);const{config,overridesReady}=await import("@stacksjs/config");await overridesReady.catch(()=>{});return resolveBackupDestination(config?.database,process.env)}export async function backupRetain(flag){const{config,overridesReady}=await import("@stacksjs/config");await overridesReady.catch(()=>{});return resolveBackupRetain(config?.database,process.env,flag)}async function uploadBackup(destination,file,fileName){const{Storage}=await import("@stacksjs/storage"),key=backupObjectKey(destination,fileName),diskName=destination.kind==="disk"?destination.target:"s3",disk=Storage.disk(diskName);if(typeof disk.putStream!=="function")throw TypeError(`The '${diskName}' disk cannot stream uploads, so a database dump cannot be copied to it. Use an S3-backed disk for \`backups.destination\`.`);await disk.putStream(key,Bun.file(file).stream());return destination.kind==="disk"?`disk://${destination.target}/${key}`:`s3://${destination.target}/${key}`}function backupDir(out){const dir=out?.trim()||DEFAULT_BACKUP_DIR;return isAbsolute(dir)?dir:resolve(process.cwd(),dir)}function existingBackups(dir){if(!existsSync(dir))return[];return readdirSync(dir).filter(isBackupFileName).sort()}function sqliteDatabaseExists(target){const path=sqliteFile(target);return existsSync(path)&&statSync(path).size>0}function sqliteFile(target){return isAbsolute(target.database)?target.database:resolve(process.cwd(),target.database)}function prune(dir,retain){const removed=prunableBackups(existingBackups(dir),retain);for(const name of removed)rmSync(join(dir,name),{force:!0});return removed}export function db(buddy){buddy.command("db:backup","Dump the application database to a file").option("--out [dir]",`Where to write the dump (default: ${DEFAULT_BACKUP_DIR})`).option("--retain [count]",`How many dumps to keep (default: DB_BACKUP_RETAIN, backups.retain, or ${DEFAULT_RETAIN})`).option("--before-migrations","Deploy mode: succeed quietly when there is no database yet",{default:!1}).option("--destination [uri]","Copy the dump offsite: s3://bucket/prefix or disk://name/prefix").option("--verbose","Enable verbose output",{default:!1}).example("buddy db:backup").example("buddy db:backup --out /var/backups/app --retain 30").example("buddy db:backup --destination disk://backups/daily").action(async(options)=>{const perf=await intro("buddy db:backup");try{const target=await backupTarget();if(!target){await outro("No dumpable database is configured; nothing to back up.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}if(target.engine==="sqlite"&&!sqliteDatabaseExists(target)){const message=`No database at ${target.database} yet; nothing to back up.`;if(options.beforeMigrations){await outro(message,{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}log.warn(message);await outro("Nothing backed up.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}const retain=await backupRetain(options.retain),dir=backupDir(options.out);mkdirSync(dir,{recursive:!0});const name=backupFileName(target,new Date),destination=join(dir,name),command=dumpCommand(target);if(command){if(options.verbose)log.info(describeCommand(command));await writeDump(command,destination,target.password)}else await dumpSqlite(sqliteFile(target),destination);const size=existsSync(destination)?statSync(destination).size:0;log.success(`Wrote ${destination} (${(size/1024).toFixed(1)} KB)`);const removed=prune(dir,retain);if(removed.length)log.info(`Pruned ${removed.length} older dump(s), keeping ${retain}.`);const offsite=await backupDestination(options.destination);if(offsite){const uploaded=await uploadBackup(offsite,destination,name);log.success(`Copied to ${uploaded}`)}else log.info("This dump is on the same disk as the database. Set `backups.destination` in config/database.ts (or DB_BACKUP_DESTINATION) to copy it off the box.");await outro("Database backed up",{startTime:perf,useSeconds:!0})}catch(error){await log.error("Database backup failed:",error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)});buddy.command("db:backups","List the database dumps that have been taken").option("--out [dir]",`Where dumps are kept (default: ${DEFAULT_BACKUP_DIR})`).example("buddy db:backups").action(async(options)=>{const dir=backupDir(options.out),backups=existingBackups(dir);if(!backups.length){log.info(`No dumps in ${dir}.`);await log.flush();process.exit(ExitCode.Success)}log.info(`${backups.length} dump(s) in ${dir}, oldest first:`);for(const name of backups){const size=statSync(join(dir,name)).size;log.info(` ${name} ${(size/1024).toFixed(1)} KB`)}await log.flush();process.exit(ExitCode.Success)});buddy.command("db:restore [file]","Restore the application database from a dump").option("--out [dir]",`Where dumps are kept (default: ${DEFAULT_BACKUP_DIR})`).option("--force","Skip the confirmation prompt",{default:!1}).option("--verbose","Enable verbose output",{default:!1}).example("buddy db:restore").example("buddy db:restore 2026-08-13T09-00-00-000.sqlite.sqlite --force").action(async(file,options)=>{const perf=await intro("buddy db:restore");try{const target=await backupTarget();if(!target){await log.error("No restorable database is configured.");process.exit(ExitCode.FatalError)}const dir=backupDir(options.out),backups=existingBackups(dir),name=file??backups[backups.length-1];if(!name){await log.error(`No dumps found in ${dir}.`);process.exit(ExitCode.FatalError)}const source=isAbsolute(name)?name:join(dir,name);if(!existsSync(source)){await log.error(`No such dump: ${source}`);process.exit(ExitCode.FatalError)}if(!options.force){if(await confirmOrNull({message:`Replace ${target.engine} database "${target.database}" with ${name}? The current contents are lost.`,initial:!1})!==!0){await outro("Restore cancelled.",{startTime:perf,useSeconds:!0});process.exit(ExitCode.Success)}}const command=restoreCommand(target);if(command){if(options.verbose)log.info(describeCommand(command));await readDump(command,source,target.password)}else{const displaced=await restoreSqlite(source,sqliteFile(target),Date.now());if(displaced)log.info(`The database that was there is kept at ${displaced}`)}log.success(`Restored ${target.database} from ${name}`);await outro("Database restored",{startTime:perf,useSeconds:!0})}catch(error){await log.error("Database restore failed:",error);process.exit(ExitCode.FatalError)}process.exit(ExitCode.Success)})}
@@ -1,4 +1,15 @@
1
1
  import type { CLI, DevOptions } from '@stacksjs/types';
2
+ /**
3
+ * The line a boot prints while it waits, or null when it should say nothing.
4
+ *
5
+ * A warm boot is ready in a few seconds and prints nothing between the header
6
+ * and the banner, which is right. A slow one used to print nothing either, for
7
+ * as long as it took, so "still building" looked exactly like "dead" (#3077).
8
+ * Every ten seconds it now says how long and what it is waiting for. A server
9
+ * that crashed under `bun --watch` does not exit, so the count climbing is the
10
+ * only sign of it, and its own output is hidden unless `--verbose` is on.
11
+ */
12
+ export declare function devBootProgressLine(elapsedSeconds: number, waitingOn: Array<{ name: string, port: number }>, verbose?: boolean): string | null;
2
13
  export declare function dispatchInteractiveDevSelection(selection: string, runners: InteractiveDevRunners): Promise<boolean>;
3
14
  export declare function resolvePrettyDevDomain(appUrl: string | undefined, nativeMode?: boolean): string | null;
4
15
  export declare function shouldUsePrettyDevUrls(input: {
@@ -34,6 +45,8 @@ export declare function setupPrettyDevEnvironment(input: {
34
45
  skipTrust?: boolean
35
46
  verbose?: boolean
36
47
  }): Promise<boolean>;
48
+ /** Seconds between the lines a slow boot prints, and before the first one. */
49
+ export declare const DEV_BOOT_PROGRESS_EVERY_SECONDS: 10;
37
50
  export declare const interactiveDevChoices: readonly [{
38
51
  value: 'all';
39
52
  title: 'All'
@@ -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{inspectDefaultsProvenance,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}async function checkFrameworkDefaults(){let skew;try{skew=inspectDefaultsProvenance(projectPath())}catch{return}if(skew.status==="not-applicable"||skew.status==="current")return;if(skew.status==="stale")throw Error(`Framework defaults version mismatch: storage/framework/defaults is from ${skew.vendored}, but @stacksjs/defaults ${skew.installed} is installed. Run \`buddy upgrade\` to synchronize them before starting development servers.`);try{const{measureDefaultsDrift}=await actions(),drift=measureDefaultsDrift(projectPath(),{shallow:!0});if(!drift||drift.length===0)return;log.warn(`storage/framework/defaults does not match the installed @stacksjs/defaults ${skew.installed}.`);log.warn("The app runs the vendored copy. Run `buddy upgrade` to sync it, or `buddy doctor` for the file counts.")}catch{}}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});await checkFrameworkDefaults();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:await log.error(`Unknown server: ${target}`);process.exit(ExitCode.InvalidArgument)}}else if(wantsInteractive(options)){const selectedValue=await prompts.select({message:descriptions.select,choices:interactiveDevChoices.map((choice)=>({value:choice.value,label:choice.title}))}),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)})){await 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("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await checkFrameworkDefaults();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)=>{await checkFrameworkDefaults();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)=>{await checkFrameworkDefaults();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)=>{await checkFrameworkDefaults();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").option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await checkFrameworkDefaults();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)=>{await checkFrameworkDefaults();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 checkFrameworkDefaults();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 checkFrameworkDefaults();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=(signal)=>{if(isExiting)return;isExiting=!0;console.log();console.log(` ${bold(cyan("stacks"))} ${dim(`received ${signal} - shutting down the dev server`)}`);if(signal==="SIGHUP")console.log(` ${dim(" ")}${dim("The launching shell went away (a closed terminal, or a CI step that ended). nohup does not prevent this.")}`);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")} - 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 - ${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("- 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);if(["config/css.ts","css.config.ts","config/crosswind.ts","crosswind.config.ts"].some((config)=>existsSync(join(projectPath(),config))))console.log(` ${green("[ts-css]")} ${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{inspectDefaultsProvenance,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(progressLines=0){if(!process.stdout.isTTY)return;for(let i=0;i<DEV_BOOT_STARTING_LINE_COUNT+progressLines;i++){readline.moveCursor(process.stdout,0,-1);readline.clearLine(process.stdout,0)}}export const DEV_BOOT_PROGRESS_EVERY_SECONDS=10;const DEV_BOOT_VERBOSE_HINT_SECONDS=30;export function devBootProgressLine(elapsedSeconds,waitingOn,verbose=!1){if(!waitingOn.length||elapsedSeconds<DEV_BOOT_PROGRESS_EVERY_SECONDS)return null;const servers=waitingOn.map((server)=>`the ${server.name} on :${server.port}`).join(", "),line=`still starting, ${Math.floor(elapsedSeconds)}s so far, waiting for ${servers}`;return!verbose&&elapsedSeconds>=DEV_BOOT_VERBOSE_HINT_SECONDS?`${line} (\`./buddy dev --verbose\` shows its output)`:line}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}async function checkFrameworkDefaults(){let skew;try{skew=inspectDefaultsProvenance(projectPath())}catch{return}if(skew.status==="not-applicable"||skew.status==="current")return;if(skew.status==="stale")throw Error(`Framework defaults version mismatch: storage/framework/defaults is from ${skew.vendored}, but @stacksjs/defaults ${skew.installed} is installed. Run \`buddy upgrade\` to synchronize them before starting development servers.`);try{const{measureDefaultsDrift}=await actions(),drift=measureDefaultsDrift(projectPath(),{shallow:!0});if(!drift||drift.length===0)return;log.warn(`storage/framework/defaults does not match the installed @stacksjs/defaults ${skew.installed}.`);log.warn("The app runs the vendored copy. Run `buddy upgrade` to sync it, or `buddy doctor` for the file counts.")}catch{}}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});await checkFrameworkDefaults();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:await log.error(`Unknown server: ${target}`);process.exit(ExitCode.InvalidArgument)}}else if(wantsInteractive(options)){const selectedValue=await prompts.select({message:descriptions.select,choices:interactiveDevChoices.map((choice)=>({value:choice.value,label:choice.title}))}),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)})){await 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("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await checkFrameworkDefaults();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)=>{await checkFrameworkDefaults();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)=>{await checkFrameworkDefaults();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)=>{await checkFrameworkDefaults();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").option("--verbose",descriptions.verbose,{default:!1}).action(async(options)=>{await checkFrameworkDefaults();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)=>{await checkFrameworkDefaults();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 checkFrameworkDefaults();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 checkFrameworkDefaults();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=(signal)=>{if(isExiting)return;isExiting=!0;console.log();console.log(` ${bold(cyan("stacks"))} ${dim(`received ${signal} - shutting down the dev server`)}`);if(signal==="SIGHUP")console.log(` ${dim(" ")}${dim("The launching shell went away (a closed terminal, or a CI step that ended). nohup does not prevent this.")}`);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(),waitingOn=new Set(ports),bootStartedMs=Date.now();let progressLines=0,nextProgressAt=DEV_BOOT_PROGRESS_EVERY_SECONDS;const progress=setInterval(()=>{const elapsed=(Date.now()-bootStartedMs)/1000;if(elapsed<nextProgressAt)return;nextProgressAt+=DEV_BOOT_PROGRESS_EVERY_SECONDS;const line=devBootProgressLine(elapsed,[...waitingOn],options.verbose);if(!line)return;console.log(` ${dim(line)}`);progressLines++},1000);progress.unref?.();Promise.all(ports.map((p)=>waitForPort(p.port,Number.POSITIVE_INFINITY).then(()=>waitingOn.delete(p)))).then(async()=>{clearInterval(progress);if(readyAnnounced)return;readyAnnounced=!0;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")} - 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(progressLines);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;console.log(` ${dim(`ready in ${(elapsedMs/1000).toFixed(1)}s`)}`);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("- 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);if(["config/css.ts","css.config.ts","config/crosswind.ts","crosswind.config.ts"].some((config)=>existsSync(join(projectPath(),config))))console.log(` ${green("[ts-css]")} ${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")}
@@ -16,10 +16,19 @@ export declare function resolveBackupTarget(databaseConfig: unknown): BackupTarg
16
16
  * stat of every file.
17
17
  */
18
18
  export declare function backupFileName(target: BackupTarget, at: Date): string;
19
- /** Does this name look like something {@link backupFileName} produced? */
19
+ /**
20
+ * Does this name look like something {@link backupFileName} produced?
21
+ *
22
+ * Uncompressed `.sql` is still recognised: dumps taken before #3079 have to
23
+ * keep counting toward retention and stay restorable.
24
+ */
20
25
  export declare function isBackupFileName(name: string): boolean;
26
+ /** Is this dump gzipped, so it has to be inflated on its way back in? */
27
+ export declare function isCompressedBackup(name: string): boolean;
21
28
  /**
22
- * The external command that writes a dump of `target` to `destination`.
29
+ * The external command that writes a dump of `target` to its stdout, for
30
+ * {@link writeDump} to compress on the way to disk. Neither tool can gzip
31
+ * a plain dump itself on every version we meet, and mysqldump not at all.
23
32
  *
24
33
  * `null` for SQLite, which is copied in-process with `VACUUM INTO` rather than
25
34
  * shelled out to a `sqlite3` binary that may not be installed.
@@ -29,14 +38,30 @@ export declare function isBackupFileName(name: string): boolean;
29
38
  * the production database password to the process table on every release would
30
39
  * be a worse bug than the one this file is fixing.
31
40
  */
32
- export declare function dumpCommand(target: BackupTarget, destination: string): DumpCommand | null;
41
+ export declare function dumpCommand(target: BackupTarget): DumpCommand | null;
33
42
  /**
34
- * The command that reads a dump back in.
43
+ * The command that reads a dump back in, from its stdin, which
44
+ * {@link readDump} feeds (inflating a gzipped dump on the way).
35
45
  *
36
46
  * Restoring SQLite is a file copy, so this returns `null` for it, exactly as
37
47
  * {@link dumpCommand} does.
38
48
  */
39
- export declare function restoreCommand(target: BackupTarget, source: string): DumpCommand | null;
49
+ export declare function restoreCommand(target: BackupTarget): DumpCommand | null;
50
+ /**
51
+ * Run `command` and gzip what it prints into `destination`.
52
+ *
53
+ * Written beside the destination under a `.partial` name and renamed only once
54
+ * the tool has exited cleanly, so a dump that fails halfway never takes the
55
+ * place of the newest good one, which is the one a hurried restore picks.
56
+ * Streamed, because a dump is the whole database and the box is sized for the
57
+ * web process.
58
+ */
59
+ export declare function writeDump(command: DumpCommand, destination: string, password?: string): Promise<void>;
60
+ /**
61
+ * Feed the dump at `source` to `command`'s stdin, inflating it first when it
62
+ * is gzipped, so dumps taken before #3079 restore exactly as they did.
63
+ */
64
+ export declare function readDump(command: DumpCommand, source: string, password?: string): Promise<void>;
40
65
  /**
41
66
  * Which dumps to delete to keep the `retain` newest.
42
67
  *
@@ -96,6 +121,17 @@ export declare function toolFailureDetail(stderr: string | Buffer, bin?: string)
96
121
  export declare function parseBackupDestination(uri: string): BackupDestination;
97
122
  /** The key a dump is stored under at its destination. */
98
123
  export declare function backupObjectKey(destination: BackupDestination, fileName: string): string;
124
+ /**
125
+ * How many dumps to keep: the `--retain` flag, then `DB_BACKUP_RETAIN`, then
126
+ * `config/database.ts`'s `backups.retain`, then {@link DEFAULT_RETAIN}.
127
+ *
128
+ * The same precedence {@link resolveBackupDestination} uses, so the deploy's
129
+ * pre-migration backup, which passes no flag, can be tuned per app (#3079).
130
+ * A value that is not a whole number of at least one throws, as an unreadable
131
+ * destination does: a deploy that silently kept the default after being told
132
+ * otherwise would be the disk filling up again.
133
+ */
134
+ export declare function resolveBackupRetain(databaseConfig: unknown, env?: Record<string, string | undefined>, flag?: string): number;
99
135
  /**
100
136
  * The configured offsite destination, or null.
101
137
  *
@@ -104,6 +140,8 @@ export declare function backupObjectKey(destination: BackupDestination, fileName
104
140
  * case and is not an error — it is what the deploy warning is about.
105
141
  */
106
142
  export declare function resolveBackupDestination(databaseConfig: unknown, env?: Record<string, string | undefined>): BackupDestination | null;
143
+ /** How many dumps are kept when nothing says otherwise. */
144
+ export declare const DEFAULT_RETAIN: 7;
107
145
  /** Everything needed to dump one database. */
108
146
  export declare interface BackupTarget {
109
147
  engine: BackupEngine
@@ -1,2 +1,2 @@
1
- const ENGINES={sqlite:"sqlite",postgres:"postgres",postgresql:"postgres",mysql:"mysql",mariadb:"mysql"};export function resolveBackupTarget(databaseConfig){const cfg=databaseConfig,dialect=String(cfg?.default??"").trim().toLowerCase(),engine=ENGINES[dialect];if(!engine)return null;const connection=cfg?.connections?.[dialect];if(!connection)return null;if(engine==="sqlite"){const database=String(connection.database??"").trim();return database?{engine,database}:null}const database=String(connection.name??connection.database??"").trim();if(!database)return null;return{engine,database,host:String(connection.host??"127.0.0.1"),port:Number(connection.port)||(engine==="postgres"?5432:3306),username:String(connection.username??""),password:String(connection.password??"")}}export function backupFileName(target,at){const stamp=at.toISOString().replace(/[:.]/g,"-").replace("Z",""),extension=target.engine==="sqlite"?"sqlite":"sql";return`${stamp}.${target.engine}.${extension}`}export function isBackupFileName(name){return/^\d{4}-\d{2}-\d{2}T[\d-]+\.(?:sqlite|postgres|mysql)\.(?:sqlite|sql)$/.test(name)}export function dumpCommand(target,destination){if(target.engine==="sqlite")return null;if(target.engine==="postgres")return{bin:"pg_dump",args:["--host",String(target.host),"--port",String(target.port),"--username",String(target.username),"--no-owner","--no-acl","--file",destination,target.database],env:target.password?{PGPASSWORD:target.password}:{}};return{bin:"mysqldump",args:[`--host=${target.host}`,`--port=${target.port}`,`--user=${target.username}`,"--single-transaction",`--result-file=${destination}`,target.database],env:target.password?{MYSQL_PWD:target.password}:{}}}export function restoreCommand(target,source){if(target.engine==="sqlite")return null;if(target.engine==="postgres")return{bin:"psql",args:["--host",String(target.host),"--port",String(target.port),"--username",String(target.username),"--set","ON_ERROR_STOP=1","--file",source,target.database],env:target.password?{PGPASSWORD:target.password}:{}};return{bin:"mysql",args:[`--host=${target.host}`,`--port=${target.port}`,`--user=${target.username}`,`--database=${target.database}`,`--execute=source ${source}`],env:target.password?{MYSQL_PWD:target.password}:{}}}export function prunableBackups(existing,retain){if(!Number.isFinite(retain)||retain<1)return[];const backups=existing.filter(isBackupFileName).sort(),excess=backups.length-retain;return excess>0?backups.slice(0,excess):[]}export async function dumpSqlite(source,destination){const{Database}=await import("bun:sqlite"),database=new Database(source,{readonly:!0});try{database.exec(`VACUUM INTO '${destination.replace(/'/g,"''")}'`)}finally{database.close()}}export async function restoreSqlite(source,live,stamp){const{existsSync,renameSync}=await import("node:fs");let displaced=null;if(existsSync(live)){displaced=`${live}.replaced-${stamp}`;renameSync(live,displaced)}await Bun.write(live,Bun.file(source));return displaced}export function withoutPassword(text,password){return password?text.split(password).join("***"):text}export function describeCommand(command){return[command.bin,...command.args].join(" ")}export function toolFailureDetail(stderr,bin){const lines=String(stderr).split(`
2
- `).map((l)=>l.trim()).filter(Boolean).map((l)=>bin&&l.startsWith(`${bin}: `)?l.slice(bin.length+2):l);if(!lines.length)return"";const errorAt=lines.findIndex((l)=>/^error\b|\berror:/i.test(l));if(errorAt===-1)return lines[lines.length-1]??"";const kept=[lines[errorAt]];for(const line of lines.slice(errorAt+1))if(/^(?:detail|hint):/i.test(line))kept.push(line);return kept.join(" ")}export function parseBackupDestination(uri){const match=uri.trim().match(/^(s3|disk):\/\/([^/]+)(?:\/(.*))?$/);if(!match)throw Error(`Backup destination must be \`s3://bucket/prefix\` or \`disk://name/prefix\`, got ${JSON.stringify(uri)}. `+"A local path is not accepted here \u2014 `--out` already writes locally, and a second copy on the same disk "+"is not a backup of the box.");const[,kind,target,rawPrefix=""]=match,prefix=rawPrefix.replace(/^\/+|\/+$/g,"");if(!target)throw Error(`Backup destination ${JSON.stringify(uri)} names no ${kind==="s3"?"bucket":"disk"}.`);return{kind,target,prefix}}export function backupObjectKey(destination,fileName){return destination.prefix?`${destination.prefix}/${fileName}`:fileName}export function resolveBackupDestination(databaseConfig,env={}){const configured=env.DB_BACKUP_DESTINATION??databaseConfig?.backups?.destination;if(typeof configured!=="string"||!configured.trim())return null;return parseBackupDestination(configured)}
1
+ import process from"node:process";const ENGINES={sqlite:"sqlite",postgres:"postgres",postgresql:"postgres",mysql:"mysql",mariadb:"mysql"};export function resolveBackupTarget(databaseConfig){const cfg=databaseConfig,dialect=String(cfg?.default??"").trim().toLowerCase(),engine=ENGINES[dialect];if(!engine)return null;const connection=cfg?.connections?.[dialect];if(!connection)return null;if(engine==="sqlite"){const database=String(connection.database??"").trim();return database?{engine,database}:null}const database=String(connection.name??connection.database??"").trim();if(!database)return null;return{engine,database,host:String(connection.host??"127.0.0.1"),port:Number(connection.port)||(engine==="postgres"?5432:3306),username:String(connection.username??""),password:String(connection.password??"")}}export function backupFileName(target,at){const stamp=at.toISOString().replace(/[:.]/g,"-").replace("Z",""),extension=target.engine==="sqlite"?"sqlite":"sql.gz";return`${stamp}.${target.engine}.${extension}`}export function isBackupFileName(name){return/^\d{4}-\d{2}-\d{2}T[\d-]+\.(?:sqlite|postgres|mysql)\.(?:sqlite|sql|sql\.gz)$/.test(name)}export function isCompressedBackup(name){return name.endsWith(".gz")}export function dumpCommand(target){if(target.engine==="sqlite")return null;if(target.engine==="postgres")return{bin:"pg_dump",args:["--host",String(target.host),"--port",String(target.port),"--username",String(target.username),"--no-owner","--no-acl",target.database],env:target.password?{PGPASSWORD:target.password}:{}};return{bin:"mysqldump",args:[`--host=${target.host}`,`--port=${target.port}`,`--user=${target.username}`,"--single-transaction",target.database],env:target.password?{MYSQL_PWD:target.password}:{}}}export function restoreCommand(target){if(target.engine==="sqlite")return null;if(target.engine==="postgres")return{bin:"psql",args:["--host",String(target.host),"--port",String(target.port),"--username",String(target.username),"--set","ON_ERROR_STOP=1",target.database],env:target.password?{PGPASSWORD:target.password}:{}};return{bin:"mysql",args:[`--host=${target.host}`,`--port=${target.port}`,`--user=${target.username}`,`--database=${target.database}`],env:target.password?{MYSQL_PWD:target.password}:{}}}export async function writeDump(command,destination,password){const{renameSync,rmSync}=await import("node:fs"),partial=`${destination}.partial`,proc=Bun.spawn([command.bin,...command.args],{env:{...process.env,...command.env},stdout:"pipe",stderr:"pipe"});try{const[,code,stderr]=await Promise.all([Bun.write(partial,new Response(proc.stdout.pipeThrough(new CompressionStream("gzip")))),proc.exited,new Response(proc.stderr).text()]);if(code!==0)throw Error(withoutPassword(`${command.bin}: ${toolFailureDetail(stderr,command.bin)||`exit code ${code}`}`,password));renameSync(partial,destination)}finally{rmSync(partial,{force:!0})}}export async function readDump(command,source,password){const dump=Bun.file(source).stream(),proc=Bun.spawn([command.bin,...command.args],{env:{...process.env,...command.env},stdin:isCompressedBackup(source)?dump.pipeThrough(new DecompressionStream("gzip")):dump,stdout:"pipe",stderr:"pipe"}),[code,stderr]=await Promise.all([proc.exited,new Response(proc.stderr).text()]);if(code!==0)throw Error(withoutPassword(`${command.bin}: ${toolFailureDetail(stderr,command.bin)||`exit code ${code}`}`,password))}export function prunableBackups(existing,retain){if(!Number.isFinite(retain)||retain<1)return[];const backups=existing.filter(isBackupFileName).sort(),excess=backups.length-retain;return excess>0?backups.slice(0,excess):[]}export async function dumpSqlite(source,destination){const{Database}=await import("bun:sqlite"),database=new Database(source,{readonly:!0});try{database.exec(`VACUUM INTO '${destination.replace(/'/g,"''")}'`)}finally{database.close()}}export async function restoreSqlite(source,live,stamp){const{existsSync,renameSync}=await import("node:fs");let displaced=null;if(existsSync(live)){displaced=`${live}.replaced-${stamp}`;renameSync(live,displaced)}await Bun.write(live,Bun.file(source));return displaced}export function withoutPassword(text,password){return password?text.split(password).join("***"):text}export function describeCommand(command){return[command.bin,...command.args].join(" ")}export function toolFailureDetail(stderr,bin){const lines=String(stderr).split(`
2
+ `).map((l)=>l.trim()).filter(Boolean).map((l)=>bin&&l.startsWith(`${bin}: `)?l.slice(bin.length+2):l);if(!lines.length)return"";const errorAt=lines.findIndex((l)=>/^error\b|\berror:/i.test(l));if(errorAt===-1)return lines[lines.length-1]??"";const kept=[lines[errorAt]];for(const line of lines.slice(errorAt+1))if(/^(?:detail|hint):/i.test(line))kept.push(line);return kept.join(" ")}export function parseBackupDestination(uri){const match=uri.trim().match(/^(s3|disk):\/\/([^/]+)(?:\/(.*))?$/);if(!match)throw Error(`Backup destination must be \`s3://bucket/prefix\` or \`disk://name/prefix\`, got ${JSON.stringify(uri)}. `+"A local path is not accepted here \u2014 `--out` already writes locally, and a second copy on the same disk "+"is not a backup of the box.");const[,kind,target,rawPrefix=""]=match,prefix=rawPrefix.replace(/^\/+|\/+$/g,"");if(!target)throw Error(`Backup destination ${JSON.stringify(uri)} names no ${kind==="s3"?"bucket":"disk"}.`);return{kind,target,prefix}}export function backupObjectKey(destination,fileName){return destination.prefix?`${destination.prefix}/${fileName}`:fileName}export const DEFAULT_RETAIN=7;export function resolveBackupRetain(databaseConfig,env={},flag){const configured=databaseConfig?.backups?.retain,sources=[["--retain",flag],["DB_BACKUP_RETAIN",env.DB_BACKUP_RETAIN],["backups.retain",configured]];for(const[name,value]of sources){if(value===void 0||value===null||String(value).trim()==="")continue;const count=Number(String(value).trim());if(!Number.isInteger(count)||count<1)throw Error(`${name} must be a whole number of dumps to keep, at least 1; got ${JSON.stringify(value)}.`);return count}return DEFAULT_RETAIN}export function resolveBackupDestination(databaseConfig,env={}){const configured=env.DB_BACKUP_DESTINATION??databaseConfig?.backups?.destination;if(typeof configured!=="string"||!configured.trim())return null;return parseBackupDestination(configured)}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.383",
5
+ "version": "0.75.387",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,69 +95,69 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "0.75.383",
99
- "@stacksjs/ai": "0.75.383",
100
- "@stacksjs/alias": "0.75.383",
101
- "@stacksjs/analytics": "0.75.383",
102
- "@stacksjs/api": "0.75.383",
103
- "@stacksjs/arrays": "0.75.383",
104
- "@stacksjs/auth": "0.75.383",
105
- "@stacksjs/browser-extension": "0.75.383",
106
- "@stacksjs/build": "0.75.383",
98
+ "@stacksjs/actions": "0.75.387",
99
+ "@stacksjs/ai": "0.75.387",
100
+ "@stacksjs/alias": "0.75.387",
101
+ "@stacksjs/analytics": "0.75.387",
102
+ "@stacksjs/api": "0.75.387",
103
+ "@stacksjs/arrays": "0.75.387",
104
+ "@stacksjs/auth": "0.75.387",
105
+ "@stacksjs/browser-extension": "0.75.387",
106
+ "@stacksjs/build": "0.75.387",
107
107
  "@stacksjs/bumpx": "^0.2.26",
108
- "@stacksjs/cache": "0.75.383",
109
- "@stacksjs/chat": "0.75.383",
108
+ "@stacksjs/cache": "0.75.387",
109
+ "@stacksjs/chat": "0.75.387",
110
110
  "@stacksjs/clapp": "^0.2.16",
111
- "@stacksjs/cli": "0.75.383",
112
- "@stacksjs/cloud": "0.75.383",
113
- "@stacksjs/cms": "0.75.383",
114
- "@stacksjs/collections": "0.75.383",
115
- "@stacksjs/commerce": "0.75.383",
116
- "@stacksjs/config": "0.75.383",
117
- "@stacksjs/database": "0.75.383",
118
- "@stacksjs/desktop-build": "0.75.383",
119
- "@stacksjs/dns": "0.75.383",
111
+ "@stacksjs/cli": "0.75.387",
112
+ "@stacksjs/cloud": "0.75.387",
113
+ "@stacksjs/cms": "0.75.387",
114
+ "@stacksjs/collections": "0.75.387",
115
+ "@stacksjs/commerce": "0.75.387",
116
+ "@stacksjs/config": "0.75.387",
117
+ "@stacksjs/database": "0.75.387",
118
+ "@stacksjs/desktop-build": "0.75.387",
119
+ "@stacksjs/dns": "0.75.387",
120
120
  "@stacksjs/dnsx": "^0.2.3",
121
- "@stacksjs/email": "0.75.383",
122
- "@stacksjs/enums": "0.75.383",
123
- "@stacksjs/env": "0.75.383",
124
- "@stacksjs/error-handling": "0.75.383",
125
- "@stacksjs/events": "0.75.383",
126
- "@stacksjs/features": "0.75.383",
127
- "@stacksjs/git": "0.75.383",
121
+ "@stacksjs/email": "0.75.387",
122
+ "@stacksjs/enums": "0.75.387",
123
+ "@stacksjs/env": "0.75.387",
124
+ "@stacksjs/error-handling": "0.75.387",
125
+ "@stacksjs/events": "0.75.387",
126
+ "@stacksjs/features": "0.75.387",
127
+ "@stacksjs/git": "0.75.387",
128
128
  "@stacksjs/gitit": "^0.2.5",
129
- "@stacksjs/health": "0.75.383",
129
+ "@stacksjs/health": "0.75.387",
130
130
  "@stacksjs/httx": "^0.1.10",
131
- "@stacksjs/image": "0.75.383",
132
- "@stacksjs/lint": "0.75.383",
133
- "@stacksjs/logging": "0.75.383",
134
- "@stacksjs/notifications": "0.75.383",
135
- "@stacksjs/objects": "0.75.383",
136
- "@stacksjs/orm": "0.75.383",
137
- "@stacksjs/path": "0.75.383",
138
- "@stacksjs/payments": "0.75.383",
139
- "@stacksjs/realtime": "0.75.383",
140
- "@stacksjs/router": "0.75.383",
131
+ "@stacksjs/image": "0.75.387",
132
+ "@stacksjs/lint": "0.75.387",
133
+ "@stacksjs/logging": "0.75.387",
134
+ "@stacksjs/notifications": "0.75.387",
135
+ "@stacksjs/objects": "0.75.387",
136
+ "@stacksjs/orm": "0.75.387",
137
+ "@stacksjs/path": "0.75.387",
138
+ "@stacksjs/payments": "0.75.387",
139
+ "@stacksjs/realtime": "0.75.387",
140
+ "@stacksjs/router": "0.75.387",
141
141
  "@stacksjs/rpx": "^0.11.54",
142
- "@stacksjs/scheduler": "0.75.383",
143
- "@stacksjs/search-engine": "0.75.383",
144
- "@stacksjs/security": "0.75.383",
145
- "@stacksjs/server": "0.75.383",
146
- "@stacksjs/sites": "0.75.383",
147
- "@stacksjs/skills": "0.75.383",
148
- "@stacksjs/socials": "0.75.383",
149
- "@stacksjs/storage": "0.75.383",
150
- "@stacksjs/strings": "0.75.383",
142
+ "@stacksjs/scheduler": "0.75.387",
143
+ "@stacksjs/search-engine": "0.75.387",
144
+ "@stacksjs/security": "0.75.387",
145
+ "@stacksjs/server": "0.75.387",
146
+ "@stacksjs/sites": "0.75.387",
147
+ "@stacksjs/skills": "0.75.387",
148
+ "@stacksjs/socials": "0.75.387",
149
+ "@stacksjs/storage": "0.75.387",
150
+ "@stacksjs/strings": "0.75.387",
151
151
  "@stacksjs/stx": "0.2.752",
152
- "@stacksjs/testing": "0.75.383",
153
- "@stacksjs/tinker": "0.75.383",
152
+ "@stacksjs/testing": "0.75.387",
153
+ "@stacksjs/tinker": "0.75.387",
154
154
  "@stacksjs/tlsx": "^0.13.19",
155
155
  "@stacksjs/ts-cloud": "^0.16.89",
156
- "@stacksjs/tunnel": "0.75.383",
157
- "@stacksjs/types": "0.75.383",
158
- "@stacksjs/ui": "0.75.383",
159
- "@stacksjs/utils": "0.75.383",
160
- "@stacksjs/validation": "0.75.383",
156
+ "@stacksjs/tunnel": "0.75.387",
157
+ "@stacksjs/types": "0.75.387",
158
+ "@stacksjs/ui": "0.75.387",
159
+ "@stacksjs/utils": "0.75.387",
160
+ "@stacksjs/validation": "0.75.387",
161
161
  "ajv": "^8.20.0",
162
162
  "ajv-formats": "^3.0.1",
163
163
  "bun-plugin-stx": "0.2.752",