@developer-ayushanand/dbgit 0.1.0-beta.1

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.
Files changed (66) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +33 -0
  2. package/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  3. package/.github-issue-bug.md +57 -0
  4. package/.github-workflows-ci.yml +41 -0
  5. package/CODE_OF_CONDUCT.md +45 -0
  6. package/CONTRIBUTING.md +29 -0
  7. package/LICENSE +21 -0
  8. package/README.md +115 -0
  9. package/SECURITY.md +15 -0
  10. package/dbgit-0.1.0-beta.1.tgz +0 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +173 -0
  13. package/dist/commands/branch.d.ts +3 -0
  14. package/dist/commands/branch.js +34 -0
  15. package/dist/commands/checkout.d.ts +1 -0
  16. package/dist/commands/checkout.js +59 -0
  17. package/dist/commands/commit.d.ts +3 -0
  18. package/dist/commands/commit.js +86 -0
  19. package/dist/commands/diff.d.ts +1 -0
  20. package/dist/commands/diff.js +91 -0
  21. package/dist/commands/doctor.d.ts +1 -0
  22. package/dist/commands/doctor.js +115 -0
  23. package/dist/commands/init.d.ts +4 -0
  24. package/dist/commands/init.js +84 -0
  25. package/dist/commands/log.d.ts +1 -0
  26. package/dist/commands/log.js +18 -0
  27. package/dist/commands/merge.d.ts +1 -0
  28. package/dist/commands/merge.js +101 -0
  29. package/dist/commands/purge.d.ts +1 -0
  30. package/dist/commands/purge.js +85 -0
  31. package/dist/commands/restore-backup.d.ts +1 -0
  32. package/dist/commands/restore-backup.js +14 -0
  33. package/dist/commands/restore.d.ts +1 -0
  34. package/dist/commands/restore.js +33 -0
  35. package/dist/commands/rollback.d.ts +6 -0
  36. package/dist/commands/rollback.js +187 -0
  37. package/dist/core/backup.d.ts +5 -0
  38. package/dist/core/backup.js +40 -0
  39. package/dist/core/connector.d.ts +13 -0
  40. package/dist/core/connector.js +39 -0
  41. package/dist/core/dependencyGraph.d.ts +5 -0
  42. package/dist/core/dependencyGraph.js +87 -0
  43. package/dist/core/differ.d.ts +3 -0
  44. package/dist/core/differ.js +218 -0
  45. package/dist/core/generator.d.ts +3 -0
  46. package/dist/core/generator.js +104 -0
  47. package/dist/core/ignore.d.ts +3 -0
  48. package/dist/core/ignore.js +20 -0
  49. package/dist/core/schemaLock.d.ts +3 -0
  50. package/dist/core/schemaLock.js +18 -0
  51. package/dist/core/snapshot.d.ts +4 -0
  52. package/dist/core/snapshot.js +121 -0
  53. package/dist/core/store.d.ts +19 -0
  54. package/dist/core/store.js +102 -0
  55. package/dist/core/transaction.d.ts +2 -0
  56. package/dist/core/transaction.js +17 -0
  57. package/dist/core/validator.d.ts +4 -0
  58. package/dist/core/validator.js +18 -0
  59. package/dist/types/changes.d.ts +27 -0
  60. package/dist/types/changes.js +14 -0
  61. package/dist/types/commits.d.ts +21 -0
  62. package/dist/types/commits.js +1 -0
  63. package/dist/types/schema.d.ts +40 -0
  64. package/dist/types/schema.js +1 -0
  65. package/package.json +38 -0
  66. package/tsconfig.json +16 -0
@@ -0,0 +1,86 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import prompts from 'prompts';
4
+ import crypto from 'crypto';
5
+ import { isInitialized, getHead, loadSnapshot, saveSnapshot, saveCommit, saveBranch, loadBranch, setHead, loadCommit } from '../core/store.js';
6
+ import { createPool, getConnectionConfig } from '../core/connector.js';
7
+ import { captureSnapshot } from '../core/snapshot.js';
8
+ import { diffSnapshots } from '../core/differ.js';
9
+ import { loadIgnoreList } from '../core/ignore.js';
10
+ export async function commitCommand(options) {
11
+ if (!isInitialized()) {
12
+ console.error(chalk.red("Not a DBGit repository. Run 'dbgit init' first."));
13
+ process.exit(1);
14
+ }
15
+ const head = getHead();
16
+ if (head.branch === null) {
17
+ console.error(chalk.red("Cannot commit in detached HEAD state. Run 'dbgit checkout <branch>' first."));
18
+ process.exit(1);
19
+ }
20
+ const ignoreList = loadIgnoreList();
21
+ const config = getConnectionConfig();
22
+ const pool = createPool(config);
23
+ const spinner = ora('Capturing schema snapshot...').start();
24
+ try {
25
+ const newSnapshot = await captureSnapshot(pool, ignoreList);
26
+ let oldSnapshot = { tables: {}, capturedAt: '', schemaHash: '' };
27
+ if (head.commit) {
28
+ const lastCommit = loadCommit(head.commit);
29
+ oldSnapshot = loadSnapshot(lastCommit.snapshotHash);
30
+ }
31
+ const changeset = diffSnapshots(oldSnapshot, newSnapshot);
32
+ if (changeset.changes.length === 0) {
33
+ spinner.stop();
34
+ console.log(chalk.gray("Nothing to commit."));
35
+ await pool.end();
36
+ return;
37
+ }
38
+ spinner.stop();
39
+ if (changeset.hasDestructive) {
40
+ const destructiveChanges = changeset.changes.filter(c => c.isDestructive);
41
+ console.warn(chalk.yellow('\nWarning: Destructive changes detected:'));
42
+ destructiveChanges.forEach(c => {
43
+ console.warn(chalk.red(` - ${c.type}: ${c.table} ${c.objectName || ''}`));
44
+ });
45
+ const response = await prompts({
46
+ type: 'confirm',
47
+ name: 'value',
48
+ message: 'Continue?',
49
+ initial: false
50
+ });
51
+ if (!response.value) {
52
+ console.log(chalk.gray('Commit aborted.'));
53
+ await pool.end();
54
+ return;
55
+ }
56
+ }
57
+ const timestamp = new Date().toISOString();
58
+ const commitHash = crypto.createHash('sha256').update(options.message + timestamp + newSnapshot.schemaHash).digest('hex').substring(0, 7);
59
+ const commit = {
60
+ commitHash,
61
+ snapshotHash: newSnapshot.schemaHash,
62
+ parent: head.commit,
63
+ timestamp,
64
+ message: options.message,
65
+ branch: head.branch,
66
+ schemaHash: newSnapshot.schemaHash
67
+ };
68
+ saveSnapshot(newSnapshot.schemaHash, newSnapshot);
69
+ saveCommit(commit);
70
+ const branch = loadBranch(head.branch);
71
+ branch.headCommit = commitHash;
72
+ saveBranch(branch);
73
+ setHead({ ...head, commit: commitHash });
74
+ console.log(chalk.green(`\n✓ Commit ${commitHash} saved.`));
75
+ const tableCount = Object.keys(newSnapshot.tables).length;
76
+ const colCount = Object.values(newSnapshot.tables).reduce((sum, t) => sum + t.columns.length, 0);
77
+ const idxCount = Object.values(newSnapshot.tables).reduce((sum, t) => sum + t.indexes.length, 0);
78
+ console.log(chalk.gray(`${tableCount} tables · ${colCount} columns · ${idxCount} indexes`));
79
+ console.log(chalk.blue(`Branch: ${head.branch}`));
80
+ await pool.end();
81
+ }
82
+ catch (e) {
83
+ spinner.fail(chalk.red(`✗ Commit failed: ${e.message}`));
84
+ process.exit(1);
85
+ }
86
+ }
@@ -0,0 +1 @@
1
+ export declare function diffCommand(): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getHead, loadCommit, loadSnapshot } from '../core/store.js';
4
+ import { createPool, getConnectionConfig, query } from '../core/connector.js';
5
+ import { captureSnapshot } from '../core/snapshot.js';
6
+ import { diffSnapshots } from '../core/differ.js';
7
+ import { loadIgnoreList } from '../core/ignore.js';
8
+ import { ChangeType } from '../types/changes.js';
9
+ export async function diffCommand() {
10
+ const head = getHead();
11
+ const ignoreList = loadIgnoreList();
12
+ const config = getConnectionConfig();
13
+ const pool = createPool(config);
14
+ const spinner = ora('Computing diff...').start();
15
+ try {
16
+ const liveSnapshot = await captureSnapshot(pool, ignoreList);
17
+ let oldSnapshot = { tables: {}, capturedAt: '', schemaHash: '' };
18
+ if (head.commit) {
19
+ const lastCommit = loadCommit(head.commit);
20
+ oldSnapshot = loadSnapshot(lastCommit.snapshotHash);
21
+ }
22
+ const changeset = diffSnapshots(oldSnapshot, liveSnapshot);
23
+ spinner.stop();
24
+ if (changeset.changes.length === 0) {
25
+ console.log(chalk.gray("No changes since last commit."));
26
+ await pool.end();
27
+ return;
28
+ }
29
+ // Stable sort by table name then change type
30
+ const sortedChanges = [...changeset.changes].sort((a, b) => {
31
+ if (a.table !== b.table)
32
+ return a.table.localeCompare(b.table);
33
+ return a.type.localeCompare(b.type);
34
+ });
35
+ for (const change of sortedChanges) {
36
+ let impact = '';
37
+ if (change.type === ChangeType.DROP_TABLE) {
38
+ const rows = await query(pool, `SELECT count(*) FROM ${change.table}`);
39
+ impact = chalk.gray(` (${rows[0].count} rows)`);
40
+ }
41
+ else if (change.type === ChangeType.DROP_COLUMN) {
42
+ const rows = await query(pool, `SELECT count(*) FROM ${change.table} WHERE ${change.objectName} IS NOT NULL`);
43
+ impact = chalk.gray(` (${rows[0].count} non-null rows)`);
44
+ }
45
+ switch (change.type) {
46
+ case ChangeType.ADD_TABLE:
47
+ console.log(chalk.green(` + table ${change.table}`));
48
+ break;
49
+ case ChangeType.DROP_TABLE:
50
+ console.log(chalk.red(` - table ${change.table}${impact}`));
51
+ break;
52
+ case ChangeType.ADD_COLUMN:
53
+ console.log(chalk.green(` + column ${change.table}.${change.objectName} ${change.after.type}`));
54
+ break;
55
+ case ChangeType.DROP_COLUMN:
56
+ console.log(chalk.red(` - column ${change.table}.${change.objectName}${impact}`));
57
+ break;
58
+ case ChangeType.MODIFY_COLUMN:
59
+ console.log(chalk.yellow(` ~ column ${change.table}.${change.objectName} ${change.before.type} → ${change.after.type}`));
60
+ break;
61
+ case ChangeType.ADD_INDEX:
62
+ console.log(chalk.green(` + index ${change.objectName} on ${change.table}`));
63
+ break;
64
+ case ChangeType.DROP_INDEX:
65
+ console.log(chalk.red(` - index ${change.objectName} on ${change.table}`));
66
+ break;
67
+ case ChangeType.ADD_CONSTRAINT:
68
+ console.log(chalk.green(` + constr ${change.objectName} on ${change.table}`));
69
+ break;
70
+ case ChangeType.DROP_CONSTRAINT:
71
+ console.log(chalk.red(` - constr ${change.objectName} on ${change.table}`));
72
+ break;
73
+ case ChangeType.ADD_FOREIGN_KEY:
74
+ console.log(chalk.green(` + fk ${change.objectName} on ${change.table}`));
75
+ break;
76
+ case ChangeType.DROP_FOREIGN_KEY:
77
+ console.log(chalk.red(` - fk ${change.objectName} on ${change.table}`));
78
+ break;
79
+ default:
80
+ console.log(chalk.blue(` * ${change.type}: ${change.table} ${change.objectName || ''}`));
81
+ }
82
+ }
83
+ await pool.end();
84
+ }
85
+ catch (e) {
86
+ if (spinner.isSpinning)
87
+ spinner.stop();
88
+ console.error(chalk.red(`✗ Diff failed: ${e.message}`));
89
+ process.exit(1);
90
+ }
91
+ }
@@ -0,0 +1 @@
1
+ export declare function doctorCommand(): Promise<void>;
@@ -0,0 +1,115 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ import { isInitialized, getHead, loadCommit, loadSnapshot, loadConfig } from '../core/store.js';
4
+ import { createPool, getConnectionConfig, query } from '../core/connector.js';
5
+ import { listBackups } from '../core/backup.js';
6
+ import { captureSnapshot } from '../core/snapshot.js';
7
+ import { diffSnapshots } from '../core/differ.js';
8
+ import { computeSchemaHash } from '../core/validator.js';
9
+ import { loadIgnoreList } from '../core/ignore.js';
10
+ export async function doctorCommand() {
11
+ const repoInit = isInitialized();
12
+ if (!repoInit) {
13
+ console.log(chalk.red('\n✗ DBGit is not initialized in this directory.'));
14
+ console.log(chalk.yellow('Recommendation: Run \'dbgit init\' to initialize the repository.'));
15
+ process.exit(1);
16
+ }
17
+ let report = '';
18
+ report += `${chalk.green('✓')} Repository initialized\n`;
19
+ const ignoreList = loadIgnoreList();
20
+ const config = getConnectionConfig();
21
+ const pool = createPool(config);
22
+ let dbOk = false;
23
+ try {
24
+ await pool.query('SELECT 1');
25
+ report += `${chalk.green('✓')} Database connection: OK\n`;
26
+ dbOk = true;
27
+ }
28
+ catch (e) {
29
+ report += `${chalk.red('✗')} Database connection: FAILED (${e.message})\n`;
30
+ }
31
+ let riskScore = 0;
32
+ const head = getHead();
33
+ const dbConfig = loadConfig();
34
+ let drift = false;
35
+ let softDeletedCount = 0;
36
+ let hasPgDump = true; // We'll check this by trying to run it later if needed, but for now assume true
37
+ if (dbOk) {
38
+ const liveHash = await computeSchemaHash(pool, ignoreList);
39
+ if (head.commit) {
40
+ const lastCommit = loadCommit(head.commit);
41
+ if (liveHash !== lastCommit.schemaHash) {
42
+ drift = true;
43
+ riskScore += 40;
44
+ }
45
+ }
46
+ report += `Schema Drift: ${drift ? chalk.red('DETECTED') : chalk.green('None')}\n`;
47
+ const liveSnapshot = await captureSnapshot(pool, ignoreList);
48
+ let oldSnapshot = { tables: {}, capturedAt: '', schemaHash: '' };
49
+ if (head.commit) {
50
+ oldSnapshot = loadSnapshot(loadCommit(head.commit).snapshotHash);
51
+ }
52
+ const changeset = diffSnapshots(oldSnapshot, liveSnapshot);
53
+ report += `Untracked Changes: ${changeset.changes.length} changes\n`;
54
+ if (changeset.changes.some(c => c.isDestructive)) {
55
+ riskScore += 30;
56
+ }
57
+ const softDeletedTables = await query(pool, "SELECT table_name FROM information_schema.tables WHERE table_name LIKE '_dbgit_deleted_%'");
58
+ const softDeletedColumns = await query(pool, "SELECT column_name FROM information_schema.columns WHERE column_name LIKE '_dbgit_deleted_%'");
59
+ softDeletedCount = softDeletedTables.length + softDeletedColumns.length;
60
+ report += `Soft Deleted: ${softDeletedCount > 0 ? chalk.yellow(softDeletedCount + ' objects') : chalk.green('None')}\n`;
61
+ }
62
+ const backups = listBackups();
63
+ report += `Backups: ${backups.length > 0 ? chalk.green('Available (' + backups.length + ')') : chalk.yellow('Missing')}\n`;
64
+ if (backups.length === 0)
65
+ riskScore += 20;
66
+ if (head.commit) {
67
+ const lastCommit = loadCommit(head.commit);
68
+ const lastCommitDate = new Date(lastCommit.timestamp);
69
+ const daysSinceLastCommit = (Date.now() - lastCommitDate.getTime()) / (1000 * 60 * 60 * 24);
70
+ report += `Last Commit: ${chalk.blue(lastCommit.commitHash)} — ${lastCommit.message} (${Math.floor(daysSinceLastCommit)} days ago)\n`;
71
+ if (daysSinceLastCommit > 7)
72
+ riskScore += 10;
73
+ }
74
+ let safety = 'High';
75
+ if (riskScore > 30)
76
+ safety = 'Medium';
77
+ if (riskScore > 60)
78
+ safety = 'Low';
79
+ report += `Rollback Safety: ${safety === 'High' ? chalk.green(safety) : safety === 'Medium' ? chalk.yellow(safety) : chalk.red(safety)}\n`;
80
+ report += `Risk Score: ${riskScore}/100\n`;
81
+ console.log(boxen(report.trim(), { padding: 1, borderColor: 'cyan', title: 'DBGit Doctor Report' }));
82
+ console.log(chalk.bold('\nℹ Recommendations:'));
83
+ const recommendations = [];
84
+ if (drift) {
85
+ recommendations.push(chalk.yellow('- Schema Drift Detected: Run \'dbgit commit\' to capture current schema changes and resolve drift.'));
86
+ }
87
+ if (dbOk && !drift && head.commit) {
88
+ // Check if there are untracked changes that AREN'T drift (this shouldn't happen if drift check is correct, but good for clarity)
89
+ }
90
+ if (backups.length === 0) {
91
+ recommendations.push(chalk.yellow('- Missing Backups: Run \'dbgit rollback --safe\' on your next rollback to enable backup and restore support.'));
92
+ }
93
+ if (softDeletedCount > 0) {
94
+ recommendations.push(chalk.yellow(`- Soft Deleted Objects Exist: Run 'dbgit purge' to permanently remove ${softDeletedCount} archived objects.`));
95
+ }
96
+ if (riskScore > 50) {
97
+ recommendations.push(chalk.red('- Rollback Risk: High risk detected. Always use --safe before destructive rollbacks.'));
98
+ }
99
+ // Check for pg_dump availability
100
+ try {
101
+ const { execSync } = await import('child_process');
102
+ execSync('pg_dump --version', { stdio: 'ignore' });
103
+ }
104
+ catch (e) {
105
+ recommendations.push(chalk.red('- Missing pg_dump: Install PostgreSQL client tools to enable safe rollback and backup functionality.'));
106
+ }
107
+ if (recommendations.length === 0) {
108
+ console.log(chalk.green(' ✓ Your repository is in great shape!'));
109
+ }
110
+ else {
111
+ recommendations.forEach(rec => console.log(' ' + rec));
112
+ }
113
+ console.log('');
114
+ await pool.end();
115
+ }
@@ -0,0 +1,4 @@
1
+ export declare function initCommand(options: {
2
+ mode: string;
3
+ recover: boolean;
4
+ }): Promise<void>;
@@ -0,0 +1,84 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { initStore, isInitialized, setHead, saveBranch, saveConfig, saveCommit, saveSnapshot } from '../core/store.js';
4
+ import { createPool, getConnectionConfig } from '../core/connector.js';
5
+ import { captureSnapshot } from '../core/snapshot.js';
6
+ import crypto from 'crypto';
7
+ export async function initCommand(options) {
8
+ if (isInitialized() && !options.recover) {
9
+ console.error(chalk.red('Error: .dbgit/ already exists. Use --recover to reconstruct state.'));
10
+ process.exit(1);
11
+ }
12
+ initStore();
13
+ const config = getConnectionConfig();
14
+ const dbConfig = {
15
+ DBGIT_MODE: options.mode,
16
+ };
17
+ if (config.connectionString) {
18
+ dbConfig.databaseUrl = config.connectionString;
19
+ }
20
+ else {
21
+ dbConfig.DBGIT_HOST = config.host || 'localhost';
22
+ dbConfig.DBGIT_PORT = config.port?.toString() || '5432';
23
+ dbConfig.DBGIT_DATABASE = config.database || '';
24
+ dbConfig.DBGIT_USER = config.user || '';
25
+ dbConfig.DBGIT_SSL = config.ssl?.toString() || 'false';
26
+ if (config.password) {
27
+ dbConfig.DBGIT_PASSWORD = config.password;
28
+ }
29
+ }
30
+ saveConfig(dbConfig);
31
+ if (options.recover) {
32
+ const spinner = ora('Recovering state from live database...').start();
33
+ try {
34
+ const pool = createPool(config);
35
+ const snapshot = await captureSnapshot(pool, []);
36
+ const timestamp = new Date().toISOString();
37
+ const message = "Recovered initial state";
38
+ const commitHash = crypto.createHash('sha256').update(message + timestamp + snapshot.schemaHash).digest('hex').substring(0, 7);
39
+ const commit = {
40
+ commitHash,
41
+ snapshotHash: snapshot.schemaHash,
42
+ parent: null,
43
+ timestamp,
44
+ message,
45
+ branch: 'main',
46
+ schemaHash: snapshot.schemaHash
47
+ };
48
+ saveSnapshot(snapshot.schemaHash, snapshot);
49
+ saveCommit(commit);
50
+ saveBranch({
51
+ name: 'main',
52
+ headCommit: commitHash,
53
+ createdAt: timestamp,
54
+ basedOn: null
55
+ });
56
+ setHead({ branch: 'main', commit: commitHash });
57
+ spinner.succeed(chalk.green('✓ Recovered DBGit state from live database.'));
58
+ await pool.end();
59
+ }
60
+ catch (e) {
61
+ spinner.fail(chalk.red(`✗ Recovery failed: ${e.message}`));
62
+ process.exit(1);
63
+ }
64
+ }
65
+ else {
66
+ saveBranch({
67
+ name: 'main',
68
+ headCommit: '',
69
+ createdAt: new Date().toISOString(),
70
+ basedOn: null
71
+ });
72
+ setHead({ branch: 'main', commit: null });
73
+ console.log(chalk.green('✓ Initialized empty DBGit repository.'));
74
+ }
75
+ if (config.connectionString) {
76
+ console.log(chalk.blue('ℹ Connected using connection string (details hidden for security)'));
77
+ }
78
+ else {
79
+ console.log(chalk.blue(`ℹ Connected to: ${config.database}@${config.host}`));
80
+ }
81
+ if (options.mode === 'prod') {
82
+ console.warn(chalk.yellow('⚠ Production mode: destructive operations require --safe (for backups)'));
83
+ }
84
+ }
@@ -0,0 +1 @@
1
+ export declare function logCommand(): void;
@@ -0,0 +1,18 @@
1
+ import chalk from 'chalk';
2
+ import { getHead, listCommits } from '../core/store.js';
3
+ export function logCommand() {
4
+ const head = getHead();
5
+ const commits = listCommits(head.branch, head.commit);
6
+ if (commits.length === 0) {
7
+ console.log(chalk.gray("ℹ No commits found."));
8
+ return;
9
+ }
10
+ commits.forEach(commit => {
11
+ console.log(chalk.yellow(`commit ${commit.commitHash}`));
12
+ if (commit.branch) {
13
+ console.log(chalk.blue(`Branch: ${commit.branch}`));
14
+ }
15
+ console.log(chalk.gray(`Date: ${new Date(commit.timestamp).toLocaleString()}`));
16
+ console.log(`\n ${commit.message}\n`);
17
+ });
18
+ }
@@ -0,0 +1 @@
1
+ export declare function mergeCommand(branchName: string): Promise<void>;
@@ -0,0 +1,101 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import crypto from 'crypto';
4
+ import { getHead, loadBranch, loadCommit, loadSnapshot, saveCommit, saveSnapshot, saveBranch, setHead } from '../core/store.js';
5
+ import { createPool, getConnectionConfig } from '../core/connector.js';
6
+ import { captureSnapshot } from '../core/snapshot.js';
7
+ import { diffSnapshots } from '../core/differ.js';
8
+ import { orderChanges } from '../core/dependencyGraph.js';
9
+ import { generateForwardSQL } from '../core/generator.js';
10
+ import { runInTransaction } from '../core/transaction.js';
11
+ import { ChangeType } from '../types/changes.js';
12
+ export async function mergeCommand(branchName) {
13
+ const head = getHead();
14
+ if (!head.branch) {
15
+ console.error(chalk.red("Cannot merge in detached HEAD state."));
16
+ process.exit(1);
17
+ }
18
+ const config = getConnectionConfig();
19
+ const pool = createPool(config);
20
+ const spinner = ora(`Merging branch '${branchName}' into '${head.branch}'...`).start();
21
+ try {
22
+ const targetBranch = loadBranch(branchName);
23
+ const targetCommit = loadCommit(targetBranch.headCommit);
24
+ const targetSnapshot = loadSnapshot(targetCommit.snapshotHash);
25
+ const headCommit = loadCommit(head.commit);
26
+ const headSnapshot = loadSnapshot(headCommit.snapshotHash);
27
+ // Simplistic merge: diff target against head and apply to live (which should match head)
28
+ const changeset = diffSnapshots(headSnapshot, targetSnapshot);
29
+ if (changeset.changes.length === 0) {
30
+ spinner.succeed("Already up to date.");
31
+ await pool.end();
32
+ return;
33
+ }
34
+ // Conflict detection
35
+ const conflicts = [];
36
+ const headTableNames = Object.keys(headSnapshot.tables);
37
+ changeset.changes.forEach(change => {
38
+ // 1. Table modified on target but dropped on head
39
+ if (change.type !== ChangeType.ADD_TABLE && change.type !== ChangeType.DROP_TABLE) {
40
+ if (!headSnapshot.tables[change.table] && headTableNames.includes(change.table)) {
41
+ conflicts.push(`Table '${change.table}' was dropped on ${head.branch} but modified on ${branchName}`);
42
+ }
43
+ }
44
+ // 2. Table added on target but already exists on head (different schema)
45
+ if (change.type === ChangeType.ADD_TABLE) {
46
+ if (headSnapshot.tables[change.table]) {
47
+ conflicts.push(`Table '${change.table}' exists on both branches with different definitions.`);
48
+ }
49
+ }
50
+ // 3. Foreign key added on target referencing a table dropped on head
51
+ if (change.type === ChangeType.ADD_FOREIGN_KEY) {
52
+ const fk = change.after;
53
+ if (!headSnapshot.tables[fk.targetTable] && headTableNames.includes(fk.targetTable)) {
54
+ conflicts.push(`Foreign key '${fk.name}' on ${branchName} references table '${fk.targetTable}' which was dropped on ${head.branch}`);
55
+ }
56
+ }
57
+ // 4. Index added on target to a table dropped on head
58
+ if (change.type === ChangeType.ADD_INDEX) {
59
+ if (!headSnapshot.tables[change.table] && headTableNames.includes(change.table)) {
60
+ conflicts.push(`Index '${change.objectName}' on ${branchName} added to table '${change.table}' which was dropped on ${head.branch}`);
61
+ }
62
+ }
63
+ });
64
+ if (conflicts.length > 0) {
65
+ spinner.fail(chalk.red("Merge aborted. Resolve conflicts manually."));
66
+ conflicts.forEach(c => console.log(chalk.red(` Conflict: ${c}`)));
67
+ await pool.end();
68
+ process.exit(1);
69
+ }
70
+ const liveSnapshot = await captureSnapshot(pool, []);
71
+ const orderedChanges = orderChanges(changeset.changes, liveSnapshot);
72
+ const sqlStatements = generateForwardSQL({ ...changeset, changes: orderedChanges });
73
+ await runInTransaction(pool, sqlStatements);
74
+ // Create merge commit
75
+ const timestamp = new Date().toISOString();
76
+ const message = `Merge branch '${branchName}' into '${head.branch}'`;
77
+ const commitHash = crypto.createHash('sha256').update(message + timestamp + targetSnapshot.schemaHash).digest('hex').substring(0, 7);
78
+ const mergeCommit = {
79
+ commitHash,
80
+ snapshotHash: targetSnapshot.schemaHash,
81
+ parent: head.commit,
82
+ timestamp,
83
+ message,
84
+ branch: head.branch,
85
+ schemaHash: targetSnapshot.schemaHash
86
+ };
87
+ saveSnapshot(targetSnapshot.schemaHash, targetSnapshot);
88
+ saveCommit(mergeCommit);
89
+ const currentBranch = loadBranch(head.branch);
90
+ currentBranch.headCommit = commitHash;
91
+ saveBranch(currentBranch);
92
+ setHead({ ...head, commit: commitHash });
93
+ spinner.succeed(chalk.green(`Merged branch '${branchName}' into '${head.branch}'.`));
94
+ console.log(`Applied ${changeset.changes.length} changes.`);
95
+ await pool.end();
96
+ }
97
+ catch (e) {
98
+ spinner.fail(chalk.red(`Merge failed: ${e.message}`));
99
+ process.exit(1);
100
+ }
101
+ }
@@ -0,0 +1 @@
1
+ export declare function purgeCommand(): Promise<void>;
@@ -0,0 +1,85 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import prompts from 'prompts';
4
+ import { isInitialized, loadConfig } from '../core/store.js';
5
+ import { createPool, getConnectionConfig, query } from '../core/connector.js';
6
+ import { runInTransaction } from '../core/transaction.js';
7
+ export async function purgeCommand() {
8
+ if (!isInitialized()) {
9
+ console.error(chalk.red("Not a DBGit repository. Run 'dbgit init' first."));
10
+ process.exit(1);
11
+ }
12
+ const config = getConnectionConfig();
13
+ const pool = createPool(config);
14
+ const dbConfig = loadConfig();
15
+ const isProd = dbConfig.DBGIT_MODE === 'prod';
16
+ try {
17
+ const spinner = ora('Scanning for soft-deleted objects...').start();
18
+ // Find soft-deleted tables
19
+ const deletedTables = await query(pool, `
20
+ SELECT table_name FROM information_schema.tables
21
+ WHERE table_schema = 'public' AND table_name LIKE '_dbgit_deleted_%'
22
+ `);
23
+ // Find soft-deleted columns
24
+ const deletedColumns = await query(pool, `
25
+ SELECT table_name, column_name FROM information_schema.columns
26
+ WHERE table_schema = 'public' AND column_name LIKE '_dbgit_deleted_%'
27
+ `);
28
+ spinner.stop();
29
+ if (deletedTables.length === 0 && deletedColumns.length === 0) {
30
+ console.log(chalk.green('✓ No soft-deleted objects found.'));
31
+ await pool.end();
32
+ return;
33
+ }
34
+ console.log(chalk.yellow('ℹ Objects scheduled for permanent removal:'));
35
+ if (deletedTables.length > 0) {
36
+ console.log(chalk.red('\nTables:'));
37
+ deletedTables.forEach(t => console.log(chalk.red(` ${t.table_name}`)));
38
+ }
39
+ if (deletedColumns.length > 0) {
40
+ console.log(chalk.red('\nColumns:'));
41
+ deletedColumns.forEach(c => console.log(chalk.red(` ${c.table_name}.${c.column_name}`)));
42
+ }
43
+ if (isProd) {
44
+ console.warn(chalk.bgRed.white('\n WARNING: Production mode. Permanent deletion of data. '));
45
+ }
46
+ const response = await prompts({
47
+ type: 'confirm',
48
+ name: 'confirm',
49
+ message: 'Are you sure you want to permanently delete these objects?',
50
+ initial: false
51
+ });
52
+ if (!response.confirm) {
53
+ console.log(chalk.gray('Purge aborted.'));
54
+ await pool.end();
55
+ return;
56
+ }
57
+ if (isProd) {
58
+ const doubleCheck = await prompts({
59
+ type: 'text',
60
+ name: 'confirm',
61
+ message: 'Type PURGE to confirm permanent deletion:'
62
+ });
63
+ if (doubleCheck.confirm !== 'PURGE') {
64
+ console.log(chalk.gray('Purge aborted.'));
65
+ await pool.end();
66
+ return;
67
+ }
68
+ }
69
+ const sqlStatements = [];
70
+ deletedColumns.forEach(c => {
71
+ sqlStatements.push(`ALTER TABLE ${c.table_name} DROP COLUMN ${c.column_name}`);
72
+ });
73
+ deletedTables.forEach(t => {
74
+ sqlStatements.push(`DROP TABLE ${t.table_name}`);
75
+ });
76
+ const purgeSpinner = ora('Purging objects...').start();
77
+ await runInTransaction(pool, sqlStatements);
78
+ purgeSpinner.succeed(chalk.green('Purge complete. Soft-deleted objects permanently removed.'));
79
+ await pool.end();
80
+ }
81
+ catch (e) {
82
+ console.error(chalk.red(`Purge failed: ${e.message}`));
83
+ process.exit(1);
84
+ }
85
+ }
@@ -0,0 +1 @@
1
+ export declare function restoreBackupCommand(hash: string): void;
@@ -0,0 +1,14 @@
1
+ import chalk from 'chalk';
2
+ import { getRestoreCommand, backupExists } from '../core/backup.js';
3
+ import { getConnectionConfig } from '../core/connector.js';
4
+ export function restoreBackupCommand(hash) {
5
+ const config = getConnectionConfig();
6
+ if (!backupExists(hash)) {
7
+ console.error(chalk.red(`Error: No backup found for commit ${hash}.`));
8
+ process.exit(1);
9
+ }
10
+ const command = getRestoreCommand(hash, config);
11
+ console.log(chalk.green('\nTo restore the backup, run the following command manually:'));
12
+ console.log(chalk.cyan(`\n ${command}\n`));
13
+ console.log(chalk.yellow('Warning: This will overwrite data in your database.'));
14
+ }
@@ -0,0 +1 @@
1
+ export declare function restoreCommand(hash: string, tableName: string): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { loadSnapshot, loadCommit } from '../core/store.js';
4
+ import { createPool, getConnectionConfig } from '../core/connector.js';
5
+ import { captureSnapshot } from '../core/snapshot.js';
6
+ import { diffSnapshots } from '../core/differ.js';
7
+ import { generateForwardSQL } from '../core/generator.js';
8
+ import { runInTransaction } from '../core/transaction.js';
9
+ export async function restoreCommand(hash, tableName) {
10
+ const config = getConnectionConfig();
11
+ const pool = createPool(config);
12
+ const spinner = ora(`Restoring table '${tableName}' to state at ${hash}...`).start();
13
+ try {
14
+ const targetSnapshot = loadSnapshot(loadCommit(hash).snapshotHash);
15
+ const targetTable = targetSnapshot.tables[tableName];
16
+ if (!targetTable) {
17
+ throw new Error(`Table '${tableName}' not found in commit ${hash}.`);
18
+ }
19
+ const liveSnapshot = await captureSnapshot(pool, []);
20
+ const liveTable = liveSnapshot.tables[tableName];
21
+ const tempFromSnapshot = { tables: liveTable ? { [tableName]: liveTable } : {}, capturedAt: '', schemaHash: '' };
22
+ const tempToSnapshot = { tables: { [tableName]: targetTable }, capturedAt: '', schemaHash: '' };
23
+ const changeset = diffSnapshots(tempFromSnapshot, tempToSnapshot);
24
+ const sqlStatements = generateForwardSQL(changeset);
25
+ await runInTransaction(pool, sqlStatements);
26
+ spinner.succeed(chalk.green(`Table '${tableName}' restored to state at ${hash}`));
27
+ await pool.end();
28
+ }
29
+ catch (e) {
30
+ spinner.fail(chalk.red(`Restore failed: ${e.message}`));
31
+ process.exit(1);
32
+ }
33
+ }