@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.
- package/dist/commands/db.d.ts +30 -0
- package/dist/commands/db.js +1 -1
- package/package.json +45 -45
package/dist/commands/db.d.ts
CHANGED
|
@@ -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;
|
package/dist/commands/db.js
CHANGED
|
@@ -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.
|
|
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.
|
|
99
|
-
"@stacksjs/ai": "^0.72.
|
|
100
|
-
"@stacksjs/alias": "^0.72.
|
|
101
|
-
"@stacksjs/arrays": "^0.72.
|
|
102
|
-
"@stacksjs/auth": "^0.72.
|
|
103
|
-
"@stacksjs/build": "^0.72.
|
|
104
|
-
"@stacksjs/cache": "^0.72.
|
|
105
|
-
"@stacksjs/cli": "^0.72.
|
|
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.
|
|
108
|
-
"@stacksjs/collections": "^0.72.
|
|
109
|
-
"@stacksjs/config": "^0.72.
|
|
110
|
-
"@stacksjs/database": "^0.72.
|
|
111
|
-
"@stacksjs/desktop-build": "^0.72.
|
|
112
|
-
"@stacksjs/dns": "^0.72.
|
|
113
|
-
"@stacksjs/email": "^0.72.
|
|
114
|
-
"@stacksjs/enums": "^0.72.
|
|
115
|
-
"@stacksjs/error-handling": "^0.72.
|
|
116
|
-
"@stacksjs/events": "^0.72.
|
|
117
|
-
"@stacksjs/git": "^0.72.
|
|
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.
|
|
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.
|
|
123
|
-
"@stacksjs/lint": "^0.72.
|
|
124
|
-
"@stacksjs/logging": "^0.72.
|
|
125
|
-
"@stacksjs/notifications": "^0.72.
|
|
126
|
-
"@stacksjs/objects": "^0.72.
|
|
127
|
-
"@stacksjs/orm": "^0.72.
|
|
128
|
-
"@stacksjs/path": "^0.72.
|
|
129
|
-
"@stacksjs/skills": "^0.72.
|
|
130
|
-
"@stacksjs/payments": "^0.72.
|
|
131
|
-
"@stacksjs/realtime": "^0.72.
|
|
132
|
-
"@stacksjs/router": "^0.72.
|
|
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.
|
|
135
|
-
"@stacksjs/security": "^0.72.
|
|
136
|
-
"@stacksjs/server": "^0.72.
|
|
137
|
-
"@stacksjs/cms": "^0.72.
|
|
138
|
-
"@stacksjs/sites": "^0.72.
|
|
139
|
-
"@stacksjs/storage": "^0.72.
|
|
140
|
-
"@stacksjs/strings": "^0.72.
|
|
141
|
-
"@stacksjs/testing": "^0.72.
|
|
142
|
-
"@stacksjs/tunnel": "^0.72.
|
|
143
|
-
"@stacksjs/types": "^0.72.
|
|
144
|
-
"@stacksjs/ui": "^0.72.
|
|
145
|
-
"@stacksjs/utils": "^0.72.
|
|
146
|
-
"@stacksjs/validation": "^0.72.
|
|
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",
|