@stacksjs/buddy 0.72.9 → 0.72.11

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.
@@ -1,2 +1,32 @@
1
+ import type { BackupTarget } from '../database-backup';
1
2
  import type { CLI } from '@stacksjs/types';
3
+ /*.ts` asynchronously and only re-binds its
4
+ * exports once `overridesReady` resolves, so reading `config.database` before
5
+ * then answers with the FRAMEWORK DEFAULT rather than the project's config
6
+ * (stacksjs/stacks#2333). There is no barrier anywhere above this: the CLI
7
+ * does not await it, so this function has to.
8
+ *
9
+ * That matters most where it is least visible. `applyPreMigrationBackup`
10
+ * splices `db:backup --before-migrations` into a site's `preStart` ahead of
11
+ * `migrate`, so an early read means dumping one database while `migrate`
12
+ * changes another - and reporting success. A backup of the wrong database is
13
+ * worse than no backup, because it is one you would restore from.
14
+ *
15
+ * The rejection is swallowed on purpose. `overridesReady` rejects when boot
16
+ * validation finds ANY issue in ANY config file, but that check runs after
17
+ * every config module has already merged into `overrides` in place, so the
18
+ * values here are complete either way. Letting it propagate would mean a typo
19
+ * in an unrelated file - `ports.frontend`, say - aborts the pre-migration
20
+ * backup and therefore the deploy, which is a worse failure than the one it
21
+ * would be reporting. The validator has already printed the issues itself.
22
+ *
23
+ * Deliberately untested, which is worth stating rather than hiding. In this
24
+ * repo `database.default` comes from `DB_CONNECTION` in the environment,
25
+ * available synchronously, so early and late reads agree and no assertion can
26
+ * tell them apart - measured over three runs, and an ordering assertion also
27
+ * passed with the barrier removed. Reproducing the divergence needs a
28
+ * `config/database.ts` that is not env-derived or that carries a top-level
29
+ * await. A test that passes either way would only look like coverage.
30
+ */
31
+ export declare function backupTarget(): Promise<BackupTarget | null>;
2
32
  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,describeCommand,dumpCommand,dumpSqlite,isBackupFileName,prunableBackups,resolveBackupTarget,restoreCommand,restoreSqlite,toolFailureDetail,withoutPassword}from"../database-backup";const DEFAULT_BACKUP_DIR="storage/backups/database",DEFAULT_RETAIN=7;async function backupTarget(){const{config}=await import("@stacksjs/config");return resolveBackupTarget(config?.database)}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("--verbose","Enable verbose output",{default:!1}).example("buddy db:backup").example("buddy db:backup --out /var/backups/app --retain 30").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}.`);log.info("This dump is on the same disk as the database. Copy it off the box for a real backup.");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}.`);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`)}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,describeCommand,dumpCommand,dumpSqlite,isBackupFileName,prunableBackups,resolveBackupTarget,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)}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("--verbose","Enable verbose output",{default:!1}).example("buddy db:backup").example("buddy db:backup --out /var/backups/app --retain 30").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}.`);log.info("This dump is on the same disk as the database. Copy it off the box for a real backup.");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}.`);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`)}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)})}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/buddy",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.72.9",
5
+ "version": "0.72.11",
6
6
  "description": "Meet Buddy. The Stacks runtime.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -95,55 +95,55 @@
95
95
  "prepublishOnly": "bun run build"
96
96
  },
97
97
  "dependencies": {
98
- "@stacksjs/actions": "^0.72.9",
99
- "@stacksjs/ai": "^0.72.9",
100
- "@stacksjs/alias": "^0.72.9",
101
- "@stacksjs/arrays": "^0.72.9",
102
- "@stacksjs/auth": "^0.72.9",
103
- "@stacksjs/build": "^0.72.9",
104
- "@stacksjs/cache": "^0.72.9",
105
- "@stacksjs/cli": "^0.72.9",
98
+ "@stacksjs/actions": "^0.72.11",
99
+ "@stacksjs/ai": "^0.72.11",
100
+ "@stacksjs/alias": "^0.72.11",
101
+ "@stacksjs/arrays": "^0.72.11",
102
+ "@stacksjs/auth": "^0.72.11",
103
+ "@stacksjs/build": "^0.72.11",
104
+ "@stacksjs/cache": "^0.72.11",
105
+ "@stacksjs/cli": "^0.72.11",
106
106
  "@stacksjs/clapp": "^0.2.12",
107
- "@stacksjs/cloud": "^0.72.9",
108
- "@stacksjs/collections": "^0.72.9",
109
- "@stacksjs/config": "^0.72.9",
110
- "@stacksjs/database": "^0.72.9",
111
- "@stacksjs/desktop-build": "^0.72.9",
112
- "@stacksjs/dns": "^0.72.9",
113
- "@stacksjs/email": "^0.72.9",
114
- "@stacksjs/enums": "^0.72.9",
115
- "@stacksjs/error-handling": "^0.72.9",
116
- "@stacksjs/events": "^0.72.9",
117
- "@stacksjs/git": "^0.72.9",
107
+ "@stacksjs/cloud": "^0.72.11",
108
+ "@stacksjs/collections": "^0.72.11",
109
+ "@stacksjs/config": "^0.72.11",
110
+ "@stacksjs/database": "^0.72.11",
111
+ "@stacksjs/desktop-build": "^0.72.11",
112
+ "@stacksjs/dns": "^0.72.11",
113
+ "@stacksjs/email": "^0.72.11",
114
+ "@stacksjs/enums": "^0.72.11",
115
+ "@stacksjs/error-handling": "^0.72.11",
116
+ "@stacksjs/events": "^0.72.11",
117
+ "@stacksjs/git": "^0.72.11",
118
118
  "@stacksjs/gitit": "^0.2.5",
119
- "@stacksjs/health": "^0.72.9",
119
+ "@stacksjs/health": "^0.72.11",
120
120
  "@stacksjs/dnsx": "^0.2.3",
121
121
  "@stacksjs/httx": "^0.1.10",
122
- "@stacksjs/image": "^0.72.9",
123
- "@stacksjs/lint": "^0.72.9",
124
- "@stacksjs/logging": "^0.72.9",
125
- "@stacksjs/notifications": "^0.72.9",
126
- "@stacksjs/objects": "^0.72.9",
127
- "@stacksjs/orm": "^0.72.9",
128
- "@stacksjs/path": "^0.72.9",
129
- "@stacksjs/skills": "^0.72.9",
130
- "@stacksjs/payments": "^0.72.9",
131
- "@stacksjs/realtime": "^0.72.9",
132
- "@stacksjs/router": "^0.72.9",
122
+ "@stacksjs/image": "^0.72.11",
123
+ "@stacksjs/lint": "^0.72.11",
124
+ "@stacksjs/logging": "^0.72.11",
125
+ "@stacksjs/notifications": "^0.72.11",
126
+ "@stacksjs/objects": "^0.72.11",
127
+ "@stacksjs/orm": "^0.72.11",
128
+ "@stacksjs/path": "^0.72.11",
129
+ "@stacksjs/skills": "^0.72.11",
130
+ "@stacksjs/payments": "^0.72.11",
131
+ "@stacksjs/realtime": "^0.72.11",
132
+ "@stacksjs/router": "^0.72.11",
133
133
  "@stacksjs/rpx": "^0.11.42",
134
- "@stacksjs/search-engine": "^0.72.9",
135
- "@stacksjs/security": "^0.72.9",
136
- "@stacksjs/server": "^0.72.9",
137
- "@stacksjs/cms": "^0.72.9",
138
- "@stacksjs/sites": "^0.72.9",
139
- "@stacksjs/storage": "^0.72.9",
140
- "@stacksjs/strings": "^0.72.9",
141
- "@stacksjs/testing": "^0.72.9",
142
- "@stacksjs/tunnel": "^0.72.9",
143
- "@stacksjs/types": "^0.72.9",
144
- "@stacksjs/ui": "^0.72.9",
145
- "@stacksjs/utils": "^0.72.9",
146
- "@stacksjs/validation": "^0.72.9",
134
+ "@stacksjs/search-engine": "^0.72.11",
135
+ "@stacksjs/security": "^0.72.11",
136
+ "@stacksjs/server": "^0.72.11",
137
+ "@stacksjs/cms": "^0.72.11",
138
+ "@stacksjs/sites": "^0.72.11",
139
+ "@stacksjs/storage": "^0.72.11",
140
+ "@stacksjs/strings": "^0.72.11",
141
+ "@stacksjs/testing": "^0.72.11",
142
+ "@stacksjs/tunnel": "^0.72.11",
143
+ "@stacksjs/types": "^0.72.11",
144
+ "@stacksjs/ui": "^0.72.11",
145
+ "@stacksjs/utils": "^0.72.11",
146
+ "@stacksjs/validation": "^0.72.11",
147
147
  "@stacksjs/ts-cloud": "^0.8.3",
148
148
  "ajv": "^8.20.0",
149
149
  "ajv-formats": "^3.0.1",