relq 1.0.2 → 1.0.4

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 (92) hide show
  1. package/dist/cjs/cli/commands/add.cjs +403 -27
  2. package/dist/cjs/cli/commands/branch.cjs +13 -23
  3. package/dist/cjs/cli/commands/checkout.cjs +16 -29
  4. package/dist/cjs/cli/commands/cherry-pick.cjs +3 -4
  5. package/dist/cjs/cli/commands/commit.cjs +21 -29
  6. package/dist/cjs/cli/commands/diff.cjs +28 -32
  7. package/dist/cjs/cli/commands/export.cjs +7 -7
  8. package/dist/cjs/cli/commands/fetch.cjs +15 -21
  9. package/dist/cjs/cli/commands/generate.cjs +28 -54
  10. package/dist/cjs/cli/commands/history.cjs +19 -40
  11. package/dist/cjs/cli/commands/import.cjs +34 -41
  12. package/dist/cjs/cli/commands/init.cjs +69 -59
  13. package/dist/cjs/cli/commands/introspect.cjs +4 -8
  14. package/dist/cjs/cli/commands/log.cjs +26 -32
  15. package/dist/cjs/cli/commands/merge.cjs +24 -41
  16. package/dist/cjs/cli/commands/migrate.cjs +12 -25
  17. package/dist/cjs/cli/commands/pull.cjs +216 -106
  18. package/dist/cjs/cli/commands/push.cjs +35 -75
  19. package/dist/cjs/cli/commands/remote.cjs +2 -1
  20. package/dist/cjs/cli/commands/reset.cjs +22 -43
  21. package/dist/cjs/cli/commands/resolve.cjs +12 -14
  22. package/dist/cjs/cli/commands/rollback.cjs +16 -38
  23. package/dist/cjs/cli/commands/stash.cjs +5 -7
  24. package/dist/cjs/cli/commands/status.cjs +5 -10
  25. package/dist/cjs/cli/commands/sync.cjs +30 -50
  26. package/dist/cjs/cli/commands/tag.cjs +3 -4
  27. package/dist/cjs/cli/index.cjs +72 -9
  28. package/dist/cjs/cli/utils/change-tracker.cjs +107 -3
  29. package/dist/cjs/cli/utils/cli-utils.cjs +217 -0
  30. package/dist/cjs/cli/utils/config-loader.cjs +34 -8
  31. package/dist/cjs/cli/utils/fast-introspect.cjs +109 -3
  32. package/dist/cjs/cli/utils/git-utils.cjs +42 -161
  33. package/dist/cjs/cli/utils/pool-manager.cjs +156 -0
  34. package/dist/cjs/cli/utils/project-root.cjs +56 -5
  35. package/dist/cjs/cli/utils/relqignore.cjs +1 -0
  36. package/dist/cjs/cli/utils/repo-manager.cjs +47 -0
  37. package/dist/cjs/cli/utils/schema-comparator.cjs +301 -11
  38. package/dist/cjs/cli/utils/schema-diff.cjs +202 -1
  39. package/dist/cjs/cli/utils/schema-hash.cjs +2 -1
  40. package/dist/cjs/cli/utils/schema-introspect.cjs +7 -3
  41. package/dist/cjs/cli/utils/snapshot-manager.cjs +1 -0
  42. package/dist/cjs/cli/utils/spinner.cjs +14 -106
  43. package/dist/cjs/cli/utils/sql-generator.cjs +10 -2
  44. package/dist/cjs/cli/utils/type-generator.cjs +28 -16
  45. package/dist/config.d.ts +16 -6
  46. package/dist/esm/cli/commands/add.js +372 -29
  47. package/dist/esm/cli/commands/branch.js +14 -24
  48. package/dist/esm/cli/commands/checkout.js +16 -29
  49. package/dist/esm/cli/commands/cherry-pick.js +3 -4
  50. package/dist/esm/cli/commands/commit.js +22 -30
  51. package/dist/esm/cli/commands/diff.js +6 -10
  52. package/dist/esm/cli/commands/export.js +8 -8
  53. package/dist/esm/cli/commands/fetch.js +14 -20
  54. package/dist/esm/cli/commands/generate.js +28 -54
  55. package/dist/esm/cli/commands/history.js +11 -32
  56. package/dist/esm/cli/commands/import.js +35 -42
  57. package/dist/esm/cli/commands/init.js +65 -55
  58. package/dist/esm/cli/commands/introspect.js +4 -8
  59. package/dist/esm/cli/commands/log.js +6 -12
  60. package/dist/esm/cli/commands/merge.js +20 -37
  61. package/dist/esm/cli/commands/migrate.js +12 -25
  62. package/dist/esm/cli/commands/pull.js +204 -94
  63. package/dist/esm/cli/commands/push.js +21 -61
  64. package/dist/esm/cli/commands/remote.js +2 -1
  65. package/dist/esm/cli/commands/reset.js +16 -37
  66. package/dist/esm/cli/commands/resolve.js +13 -15
  67. package/dist/esm/cli/commands/rollback.js +16 -38
  68. package/dist/esm/cli/commands/stash.js +6 -8
  69. package/dist/esm/cli/commands/status.js +6 -11
  70. package/dist/esm/cli/commands/sync.js +30 -50
  71. package/dist/esm/cli/commands/tag.js +3 -4
  72. package/dist/esm/cli/index.js +72 -9
  73. package/dist/esm/cli/utils/change-tracker.js +107 -3
  74. package/dist/esm/cli/utils/cli-utils.js +169 -0
  75. package/dist/esm/cli/utils/config-loader.js +34 -8
  76. package/dist/esm/cli/utils/fast-introspect.js +109 -3
  77. package/dist/esm/cli/utils/git-utils.js +2 -124
  78. package/dist/esm/cli/utils/pool-manager.js +114 -0
  79. package/dist/esm/cli/utils/project-root.js +55 -5
  80. package/dist/esm/cli/utils/relqignore.js +1 -0
  81. package/dist/esm/cli/utils/repo-manager.js +42 -0
  82. package/dist/esm/cli/utils/schema-comparator.js +301 -11
  83. package/dist/esm/cli/utils/schema-diff.js +202 -1
  84. package/dist/esm/cli/utils/schema-hash.js +2 -1
  85. package/dist/esm/cli/utils/schema-introspect.js +7 -3
  86. package/dist/esm/cli/utils/snapshot-manager.js +1 -0
  87. package/dist/esm/cli/utils/spinner.js +1 -101
  88. package/dist/esm/cli/utils/sql-generator.js +10 -2
  89. package/dist/esm/cli/utils/type-generator.js +28 -16
  90. package/dist/index.d.ts +25 -8
  91. package/dist/schema-builder.d.ts +18 -7
  92. package/package.json +1 -1
@@ -1,31 +1,27 @@
1
1
  import * as crypto from 'crypto';
2
- import { colors } from "../utils/spinner.js";
3
- import { isInitialized, getHead, getStagedChanges, getUnstagedChanges, shortHash, } from "../utils/repo-manager.js";
2
+ import { fatal, hint } from "../utils/cli-utils.js";
3
+ import { isInitialized, getHead, getStagedChanges, getUnstagedChanges, shortHash, hashFileContent, saveFileHash, } from "../utils/repo-manager.js";
4
+ import { loadConfig } from "../../config/config.js";
4
5
  import { sortChangesByDependency, generateCombinedSQL, } from "../utils/change-tracker.js";
5
6
  import * as fs from 'fs';
6
7
  import * as path from 'path';
7
8
  export async function commitCommand(context) {
8
- const { config, flags, args } = context;
9
- const projectRoot = process.cwd();
9
+ const { config, flags, args, projectRoot } = context;
10
10
  const author = config?.author || 'Developer <dev@example.com>';
11
11
  console.log('');
12
12
  if (!isInitialized(projectRoot)) {
13
- console.log(`${colors.red('error:')} relq not initialized`);
14
- console.log('');
15
- console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('first.')}`);
16
- return;
13
+ fatal('not a relq repository (or any parent directories): .relq', "run 'relq init' to initialize");
17
14
  }
18
15
  const staged = getStagedChanges(projectRoot);
19
16
  if (staged.length === 0) {
20
- console.log(`${colors.yellow('Nothing to commit')}`);
21
- console.log('');
17
+ console.log('nothing to commit, working tree clean');
22
18
  const unstaged = getUnstagedChanges(projectRoot);
23
19
  if (unstaged.length > 0) {
24
- console.log(`${colors.muted(`${unstaged.length} unstaged change(s).`)}`);
25
- console.log(`${colors.muted('Use')} ${colors.cyan('relq add .')} ${colors.muted('to stage all changes.')}`);
20
+ console.log(`${unstaged.length} unstaged change(s).`);
21
+ hint("run 'relq add .' to stage all changes");
26
22
  }
27
23
  else {
28
- console.log(`${colors.muted('Use')} ${colors.cyan('relq add <table>')} ${colors.muted('to stage changes.')}`);
24
+ hint("run 'relq add <table>' to stage changes");
29
25
  }
30
26
  return;
31
27
  }
@@ -35,10 +31,7 @@ export async function commitCommand(context) {
35
31
  message = args.join(' ');
36
32
  }
37
33
  else {
38
- console.log(`${colors.red('error:')} commit message required`);
39
- console.log('');
40
- console.log(`${colors.muted('Usage:')} relq commit -m "message"`);
41
- return;
34
+ fatal('commit message required', "usage: relq commit -m '<message>'");
42
35
  }
43
36
  }
44
37
  const sortedChanges = sortChangesByDependency(staged);
@@ -77,7 +70,7 @@ export async function commitCommand(context) {
77
70
  fs.writeFileSync(path.join(commitsDir, `${hash}.json`), JSON.stringify(commit, null, 2), 'utf-8');
78
71
  fs.writeFileSync(path.join(projectRoot, '.relq', 'HEAD'), hash, 'utf-8');
79
72
  const workingPath = path.join(projectRoot, '.relq', 'working.json');
80
- const unstaged = getUnstagedChanges(projectRoot);
73
+ const unstaged = getUnstagedChanges(projectRoot).filter(c => c.objectType !== 'SCHEMA_FILE');
81
74
  if (unstaged.length > 0) {
82
75
  fs.writeFileSync(workingPath, JSON.stringify({
83
76
  timestamp: new Date().toISOString(),
@@ -90,20 +83,19 @@ export async function commitCommand(context) {
90
83
  fs.unlinkSync(workingPath);
91
84
  }
92
85
  }
93
- console.log(`${colors.yellow(`[${shortHash(hash)}]`)} ${message}`);
94
- console.log('');
95
- if (creates > 0) {
96
- console.log(` ${colors.green(`${creates} created`)}`);
97
- }
98
- if (alters > 0) {
99
- console.log(` ${colors.yellow(`${alters} altered`)}`);
100
- }
101
- if (drops > 0) {
102
- console.log(` ${colors.red(`${drops} dropped`)}`);
86
+ const commitConfig = await loadConfig();
87
+ const schemaPathRaw = typeof commitConfig.schema === 'string' ? commitConfig.schema : './db/schema.ts';
88
+ const schemaFilePath = path.resolve(projectRoot, schemaPathRaw);
89
+ if (fs.existsSync(schemaFilePath)) {
90
+ const currentContent = fs.readFileSync(schemaFilePath, 'utf-8');
91
+ const currentHash = hashFileContent(currentContent);
92
+ saveFileHash(currentHash, projectRoot);
103
93
  }
94
+ console.log(`[${shortHash(hash)}] ${message}`);
95
+ console.log(` ${creates} create(s), ${alters} alter(s), ${drops} drop(s)`);
104
96
  console.log('');
105
- console.log(`${colors.muted('Run')} ${colors.cyan('relq push')} ${colors.muted('to apply changes to database.')}`);
106
- console.log(`${colors.muted('Run')} ${colors.cyan('relq export')} ${colors.muted('to export as SQL file.')}`);
97
+ hint("run 'relq push' to apply changes to database");
98
+ hint("run 'relq export' to export as SQL file");
107
99
  console.log('');
108
100
  }
109
101
  export default commitCommand;
@@ -1,17 +1,14 @@
1
1
  import { requireValidConfig } from "../utils/config-loader.js";
2
2
  import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
3
3
  import { getConnectionDescription } from "../utils/env-loader.js";
4
- import { colors, createSpinner } from "../utils/spinner.js";
4
+ import { colors, createSpinner, fatal } from "../utils/cli-utils.js";
5
5
  import { isInitialized, loadSnapshot, getStagedChanges, getUnstagedChanges, } from "../utils/repo-manager.js";
6
6
  import { getChangeDisplayName, generateChangeSQL, sortChangesByDependency } from "../utils/change-tracker.js";
7
7
  export async function diffCommand(context) {
8
- const { config, args, flags } = context;
9
- const projectRoot = process.cwd();
8
+ const { config, args, flags, projectRoot } = context;
10
9
  console.log('');
11
10
  if (!isInitialized(projectRoot)) {
12
- console.log(`${colors.red('fatal:')} not a relq repository`);
13
- console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('to initialize.')}`);
14
- return;
11
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
15
12
  }
16
13
  const showSQL = flags['sql'] === true;
17
14
  const staged = flags['staged'] === true;
@@ -22,10 +19,9 @@ export async function diffCommand(context) {
22
19
  }
23
20
  if (target === 'remote/live' || target === 'remote' || target === 'live' || target === 'origin') {
24
21
  if (!config) {
25
- console.error('Error: No configuration found.');
26
- process.exit(1);
22
+ fatal('No configuration found', `Run ${colors.cyan('relq init')} to create one.`);
27
23
  }
28
- requireValidConfig(config);
24
+ await requireValidConfig(config, { calledFrom: 'diff' });
29
25
  await showOriginDiff(config, projectRoot, showSQL);
30
26
  return;
31
27
  }
@@ -111,7 +107,7 @@ async function showOriginDiff(config, projectRoot, showSQL) {
111
107
  const diffs = compareSchemas(snapshot, remoteDb);
112
108
  if (diffs.length === 0) {
113
109
  console.log('');
114
- console.log(`${colors.green('Local and remote are in sync.')}`);
110
+ console.log('Local and remote are in sync.');
115
111
  console.log('');
116
112
  return;
117
113
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { introspectDatabase } from "../utils/schema-introspect.js";
4
- import { createSpinner, colors } from "../utils/spinner.js";
4
+ import { createSpinner, colors, warning } from "../utils/spinner.js";
5
5
  import { isInitialized, getStagedChanges, getUnstagedChanges, loadSnapshot, } from "../utils/repo-manager.js";
6
6
  import { generateCombinedSQL, sortChangesByDependency, getChangeDisplayName, } from "../utils/change-tracker.js";
7
7
  import { generateFullSchemaSQL } from "../utils/sql-generator.js";
@@ -9,8 +9,7 @@ import { loadRelqignore, isTableIgnored, isColumnIgnored, isIndexIgnored, isCons
9
9
  import { loadConfig } from "../../config/config.js";
10
10
  export async function exportCommand(context) {
11
11
  const spinner = createSpinner();
12
- const { args, flags } = context;
13
- const projectRoot = process.cwd();
12
+ const { args, flags, projectRoot } = context;
14
13
  const changesOnly = Boolean(flags['changes']);
15
14
  const stagedOnly = Boolean(flags['staged']);
16
15
  const fromDb = Boolean(flags['db']);
@@ -75,7 +74,7 @@ async function exportFromSnapshot(projectRoot, absoluteOutputPath, options) {
75
74
  console.log(` ${colors.green('•')} Triggers: ${filteredSnapshot.triggers.length}`);
76
75
  }
77
76
  if (ignoredCount > 0) {
78
- console.log(` ${colors.muted(`ℹ ${ignoredCount} object(s) filtered by .relqignore`)}`);
77
+ console.log(` ${colors.muted(`${ignoredCount} object(s) filtered by .relqignore`)}`);
79
78
  }
80
79
  spinner.start('Generating SQL statements');
81
80
  const sqlContent = generateFullSQL(schema, options);
@@ -87,7 +86,7 @@ async function exportFromSnapshot(projectRoot, absoluteOutputPath, options) {
87
86
  fs.writeFileSync(absoluteOutputPath, sqlContent, 'utf-8');
88
87
  spinner.succeed(`Written ${options.output} (${(sqlContent.length / 1024).toFixed(1)} KB)`);
89
88
  console.log('');
90
- console.log(colors.green('Export completed!'));
89
+ console.log('Export completed');
91
90
  console.log(` Source: ${colors.muted('snapshot (local)')}`);
92
91
  console.log(` Output: ${colors.muted(options.output || '')}`);
93
92
  }
@@ -127,7 +126,7 @@ async function exportFromDatabase(context, absoluteOutputPath, options) {
127
126
  fs.writeFileSync(absoluteOutputPath, sqlContent, 'utf-8');
128
127
  spinner.succeed(`Written ${options.output} (${(sqlContent.length / 1024).toFixed(1)} KB)`);
129
128
  console.log('');
130
- console.log(colors.green('Export completed!'));
129
+ console.log('Export completed');
131
130
  console.log(` Source: ${colors.muted('database (live)')}`);
132
131
  console.log(` Output: ${colors.muted(options.output || '')}`);
133
132
  }
@@ -213,6 +212,7 @@ function normalizedToDbSchema(normalized) {
213
212
  cycle: s.cycle,
214
213
  ownedBy: s.ownedBy || undefined,
215
214
  })),
215
+ collations: [],
216
216
  extensions: normalized.extensions.map(e => e.name),
217
217
  functions: (normalized.functions || []).map(f => ({
218
218
  name: f.name,
@@ -260,7 +260,7 @@ async function exportChanges(projectRoot, flags, args, stagedOnly) {
260
260
  modeLabel = 'uncommitted';
261
261
  }
262
262
  if (changes.length === 0) {
263
- console.log(`${colors.yellow('⚠')} No ${modeLabel} changes to export`);
263
+ warning(`No ${modeLabel} changes to export`);
264
264
  console.log('');
265
265
  if (stagedOnly) {
266
266
  console.log(`${colors.muted('Run')} ${colors.cyan('relq add .')} ${colors.muted('to stage changes.')}`);
@@ -289,7 +289,7 @@ async function exportChanges(projectRoot, flags, args, stagedOnly) {
289
289
  fs.mkdirSync(outputDir, { recursive: true });
290
290
  }
291
291
  fs.writeFileSync(absoluteOutputPath, sqlContent, 'utf-8');
292
- console.log(`${colors.green('✓')} Exported ${changes.length} ${modeLabel} change(s) to ${outputPath}`);
292
+ console.log(`Exported ${changes.length} ${modeLabel} change(s) to ${outputPath}`);
293
293
  console.log(`${colors.muted(`${(sqlContent.length / 1024).toFixed(1)} KB`)}`);
294
294
  console.log('');
295
295
  }
@@ -1,22 +1,18 @@
1
1
  import { requireValidConfig } from "../utils/config-loader.js";
2
2
  import { getConnectionDescription } from "../utils/env-loader.js";
3
- import { colors, createSpinner } from "../utils/spinner.js";
3
+ import { colors, createSpinner, fatal, hint } from "../utils/cli-utils.js";
4
4
  import { isInitialized, getHead, loadCommit, saveCommit, setFetchHead, fetchRemoteCommits, ensureRemoteTable, } from "../utils/repo-manager.js";
5
5
  export async function fetchCommand(context) {
6
6
  const { config, flags } = context;
7
7
  if (!config) {
8
- console.error('Error: No configuration found.');
9
- process.exit(1);
8
+ fatal('No configuration found', `run ${colors.cyan('relq init')} to create a configuration file`);
10
9
  }
11
- requireValidConfig(config);
10
+ await requireValidConfig(config, { calledFrom: 'fetch' });
12
11
  const connection = config.connection;
13
- const projectRoot = process.cwd();
12
+ const { projectRoot } = context;
14
13
  console.log('');
15
14
  if (!isInitialized(projectRoot)) {
16
- console.log(`${colors.red('fatal:')} not a relq repository`);
17
- console.log('');
18
- console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('to initialize.')}`);
19
- return;
15
+ fatal('not a relq repository (or any parent directories): .relq', "run 'relq init' to initialize");
20
16
  }
21
17
  const spinner = createSpinner();
22
18
  spinner.start(`Fetching from ${getConnectionDescription(connection)}...`);
@@ -25,8 +21,7 @@ export async function fetchCommand(context) {
25
21
  const remoteCommits = await fetchRemoteCommits(connection, 100);
26
22
  if (remoteCommits.length === 0) {
27
23
  spinner.succeed('No remote commits found');
28
- console.log('');
29
- console.log(`${colors.muted('Run')} ${colors.cyan('relq push')} ${colors.muted('to push your commits.')}`);
24
+ hint("run 'relq push' to push your commits");
30
25
  return;
31
26
  }
32
27
  let newCommits = 0;
@@ -47,25 +42,24 @@ export async function fetchCommand(context) {
47
42
  const ahead = [...localCommits].filter(h => !remoteHashes.has(h)).length;
48
43
  console.log('');
49
44
  if (behind > 0 && ahead > 0) {
50
- console.log(`Your branch and 'origin/main' have diverged.`);
51
- console.log(` ${colors.yellow(String(ahead))} commit(s) ahead, ${colors.yellow(String(behind))} commit(s) behind`);
45
+ console.log(`Your branch and 'origin/main' have diverged,`);
46
+ console.log(`and have ${ahead} and ${behind} different commits each, respectively.`);
52
47
  }
53
48
  else if (behind > 0) {
54
- console.log(`Your branch is ${colors.yellow(String(behind))} commit(s) behind 'origin/main'.`);
55
- console.log(` ${colors.muted('Use')} ${colors.cyan('relq pull')} ${colors.muted('to update.')}`);
49
+ console.log(`Your branch is behind 'origin/main' by ${behind} commit(s).`);
50
+ hint("run 'relq pull' to update your local branch");
56
51
  }
57
52
  else if (ahead > 0) {
58
- console.log(`Your branch is ${colors.green(String(ahead))} commit(s) ahead of 'origin/main'.`);
59
- console.log(` ${colors.muted('Use')} ${colors.cyan('relq push')} ${colors.muted('to publish.')}`);
53
+ console.log(`Your branch is ahead of 'origin/main' by ${ahead} commit(s).`);
54
+ hint("run 'relq push' to publish your local commits");
60
55
  }
61
56
  else {
62
- console.log(`${colors.green('✓')} Your branch is up to date with 'origin/main'.`);
57
+ console.log("Your branch is up to date with 'origin/main'.");
63
58
  }
64
59
  }
65
60
  catch (error) {
66
61
  spinner.fail('Fetch failed');
67
- console.error(colors.red(`Error: ${error instanceof Error ? error.message : error}`));
68
- process.exit(1);
62
+ fatal('Fetch failed', error instanceof Error ? error.message : String(error));
69
63
  }
70
64
  console.log('');
71
65
  }
@@ -8,32 +8,7 @@ import { diffSchemas, filterDiff, hasDestructiveChanges, getDestructiveTables }
8
8
  import { normalizeSchema } from "../utils/schema-hash.js";
9
9
  import { generateMigrationFile, getNextMigrationNumber, generateTimestampedName } from "../utils/migration-generator.js";
10
10
  import { getConnectionDescription } from "../utils/env-loader.js";
11
- const colors = {
12
- reset: '\x1b[0m',
13
- bold: '\x1b[1m',
14
- dim: '\x1b[2m',
15
- red: '\x1b[31m',
16
- green: '\x1b[32m',
17
- yellow: '\x1b[33m',
18
- cyan: '\x1b[36m',
19
- };
20
- function askConfirm(question, defaultYes = false) {
21
- const rl = readline.createInterface({
22
- input: process.stdin,
23
- output: process.stdout,
24
- });
25
- const suffix = defaultYes ? '[Y/n]' : '[y/N]';
26
- return new Promise((resolve) => {
27
- rl.question(`${question} ${suffix}: `, (answer) => {
28
- rl.close();
29
- const a = answer.trim().toLowerCase();
30
- if (!a)
31
- resolve(defaultYes);
32
- else
33
- resolve(a === 'y' || a === 'yes');
34
- });
35
- });
36
- }
11
+ import { colors, confirm, fatal, warning } from "../utils/cli-utils.js";
37
12
  function askInput(question) {
38
13
  const rl = readline.createInterface({
39
14
  input: process.stdin,
@@ -49,10 +24,10 @@ function askInput(question) {
49
24
  export async function generateCommand(context) {
50
25
  const { config, args, flags } = context;
51
26
  if (!config) {
52
- console.error('Error: No configuration found.');
53
- process.exit(1);
27
+ fatal('No configuration found', `run ${colors.cyan('relq init')} to create a configuration file`);
28
+ return;
54
29
  }
55
- requireValidConfig(config);
30
+ await requireValidConfig(config, { calledFrom: 'generate' });
56
31
  const connection = config.connection;
57
32
  const migrationsDir = config.migrations?.directory || './migrations';
58
33
  const snapshotPath = config.sync?.snapshot || '.relq/snapshot.json';
@@ -75,10 +50,10 @@ export async function generateCommand(context) {
75
50
  migrationName = message.replace(/\s+/g, '_').toLowerCase();
76
51
  }
77
52
  if (!migrationName && !isEmpty) {
78
- migrationName = await askInput(`${colors.cyan}Migration name:${colors.reset} `);
53
+ migrationName = await askInput('Migration name: ');
79
54
  if (!migrationName) {
80
- console.error('Error: Migration name is required.');
81
- process.exit(1);
55
+ fatal('Migration name is required');
56
+ return;
82
57
  }
83
58
  }
84
59
  if (isEmpty) {
@@ -86,48 +61,48 @@ export async function generateCommand(context) {
86
61
  await createEmptyMigration(migrationsDir, migrationName, format, dryRun);
87
62
  return;
88
63
  }
89
- console.log(`${colors.bold}Generating migration...${colors.reset}`);
64
+ console.log('Generating migration...');
90
65
  console.log(` Connection: ${getConnectionDescription(connection)}`);
91
66
  console.log('');
92
67
  try {
93
68
  const dbSchema = await introspectDatabase(connection);
94
69
  const snapshot = loadSnapshot(snapshotPath);
95
70
  if (!snapshot) {
96
- console.log(`${colors.yellow}No snapshot found.${colors.reset}`);
97
- console.log(`Run "${colors.cyan}relq pull${colors.reset}" first to create initial snapshot.`);
71
+ warning('No snapshot found.');
72
+ console.log('Run "relq pull" first to create initial snapshot.');
98
73
  return;
99
74
  }
100
75
  const localSchema = snapshotToDatabaseSchema(snapshot);
101
76
  const diff = diffSchemas(normalizeSchema(localSchema), normalizeSchema(dbSchema));
102
77
  const filteredDiff = filterDiff(diff, ignorePatterns);
103
78
  if (!filteredDiff.hasChanges) {
104
- console.log(`${colors.green}No changes to generate.${colors.reset}`);
79
+ console.log('No changes to generate.');
105
80
  return;
106
81
  }
107
82
  const s = filteredDiff.summary;
108
- console.log(`${colors.bold}Changes to include:${colors.reset}`);
83
+ console.log('Changes to include:');
109
84
  if (s.tablesAdded > 0)
110
- console.log(` ${colors.green}+ ${s.tablesAdded} table(s)${colors.reset}`);
85
+ console.log(` + ${s.tablesAdded} table(s)`);
111
86
  if (s.tablesRemoved > 0)
112
- console.log(` ${colors.red}- ${s.tablesRemoved} table(s)${colors.reset}`);
87
+ console.log(` - ${s.tablesRemoved} table(s)`);
113
88
  if (s.tablesModified > 0)
114
- console.log(` ${colors.yellow}~ ${s.tablesModified} table(s) modified${colors.reset}`);
89
+ console.log(` ~ ${s.tablesModified} table(s) modified`);
115
90
  if (s.columnsAdded > 0)
116
- console.log(` ${colors.green}+ ${s.columnsAdded} column(s)${colors.reset}`);
91
+ console.log(` + ${s.columnsAdded} column(s)`);
117
92
  if (s.columnsRemoved > 0)
118
- console.log(` ${colors.red}- ${s.columnsRemoved} column(s)${colors.reset}`);
93
+ console.log(` - ${s.columnsRemoved} column(s)`);
119
94
  if (s.columnsModified > 0)
120
- console.log(` ${colors.yellow}~ ${s.columnsModified} column(s)${colors.reset}`);
95
+ console.log(` ~ ${s.columnsModified} column(s)`);
121
96
  console.log('');
122
97
  if (hasDestructiveChanges(filteredDiff)) {
123
98
  const tables = getDestructiveTables(filteredDiff);
124
- console.log(`${colors.red}${colors.bold}⚠️ Destructive changes:${colors.reset}`);
99
+ warning('Destructive changes:');
125
100
  for (const t of tables) {
126
- console.log(` ${colors.red}• ${t}${colors.reset}`);
101
+ console.log(` - ${t}`);
127
102
  }
128
103
  console.log('');
129
104
  if (!autoStage) {
130
- const proceed = await askConfirm('Include destructive changes?', false);
105
+ const proceed = await confirm('Include destructive changes?', false);
131
106
  if (!proceed) {
132
107
  console.log('Cancelled.');
133
108
  return;
@@ -149,7 +124,7 @@ export async function generateCommand(context) {
149
124
  }
150
125
  const filePath = path.join(migrationsDir, fileName);
151
126
  if (dryRun) {
152
- console.log(`${colors.cyan}[dry-run] Would create: ${filePath}${colors.reset}`);
127
+ console.log(`[dry-run] Would create: ${filePath}`);
153
128
  console.log('');
154
129
  console.log('--- Generated SQL ---');
155
130
  console.log(migrationFile.content);
@@ -160,16 +135,15 @@ export async function generateCommand(context) {
160
135
  fs.mkdirSync(migrationsDir, { recursive: true });
161
136
  }
162
137
  fs.writeFileSync(filePath, migrationFile.content, 'utf-8');
163
- console.log(`${colors.green}✓ Created:${colors.reset} ${filePath}`);
138
+ console.log(`Created: ${filePath}`);
164
139
  saveSnapshot(dbSchema, snapshotPath, connection.database);
165
- console.log(`${colors.dim}Updated snapshot.${colors.reset}`);
140
+ console.log('Updated snapshot.');
166
141
  }
167
142
  console.log('');
168
- console.log(`${colors.dim}Run "relq push" to apply this migration.${colors.reset}`);
143
+ console.log('Run "relq push" to apply this migration.');
169
144
  }
170
145
  catch (error) {
171
- console.error('Error:', error instanceof Error ? error.message : error);
172
- process.exit(1);
146
+ fatal('Generation failed', error instanceof Error ? error.message : String(error));
173
147
  }
174
148
  }
175
149
  async function createEmptyMigration(migrationsDir, name, format, dryRun) {
@@ -194,13 +168,13 @@ async function createEmptyMigration(migrationsDir, name, format, dryRun) {
194
168
 
195
169
  `;
196
170
  if (dryRun) {
197
- console.log(`${colors.cyan}[dry-run] Would create: ${filePath}${colors.reset}`);
171
+ console.log(`[dry-run] Would create: ${filePath}`);
198
172
  }
199
173
  else {
200
174
  if (!fs.existsSync(migrationsDir)) {
201
175
  fs.mkdirSync(migrationsDir, { recursive: true });
202
176
  }
203
177
  fs.writeFileSync(filePath, content, 'utf-8');
204
- console.log(`${colors.green}✓ Created empty migration:${colors.reset} ${filePath}`);
178
+ console.log(`Created empty migration: ${filePath}`);
205
179
  }
206
180
  }
@@ -1,27 +1,19 @@
1
1
  import * as fs from 'fs';
2
2
  import { requireValidConfig } from "../utils/config-loader.js";
3
3
  import { getConnectionDescription } from "../utils/env-loader.js";
4
- const colors = {
5
- reset: '\x1b[0m',
6
- bold: '\x1b[1m',
7
- dim: '\x1b[2m',
8
- red: '\x1b[31m',
9
- green: '\x1b[32m',
10
- yellow: '\x1b[33m',
11
- cyan: '\x1b[36m',
12
- };
4
+ import { colors, fatal } from "../utils/cli-utils.js";
5
+ import { withPool } from "../utils/pool-manager.js";
13
6
  export async function historyCommand(context) {
14
7
  const { config, flags } = context;
15
8
  if (!config) {
16
- console.error('Error: No configuration found.');
17
- process.exit(1);
9
+ fatal('No configuration found', `run ${colors.cyan('relq init')} to create a configuration file`);
18
10
  }
19
- requireValidConfig(config);
11
+ await requireValidConfig(config, { calledFrom: 'history' });
20
12
  const connection = config.connection;
21
13
  const migrationsDir = config.migrations?.directory || './migrations';
22
14
  const tableName = config.migrations?.tableName || '_relq_migrations';
23
15
  const limit = parseInt(flags['n']) || 20;
24
- console.log(`${colors.bold}Migration History${colors.reset}`);
16
+ console.log('Migration History');
25
17
  console.log(`Database: ${getConnectionDescription(connection)}`);
26
18
  console.log('');
27
19
  try {
@@ -40,8 +32,8 @@ export async function historyCommand(context) {
40
32
  const toShow = history.slice(0, limit);
41
33
  const pendingCount = history.filter(h => h.pending).length;
42
34
  const appliedCount = history.filter(h => !h.pending).length;
43
- console.log(`${colors.dim}Showing ${toShow.length} of ${history.length} migrations${colors.reset}`);
44
- console.log(`${colors.green}${appliedCount} applied${colors.reset}, ${colors.yellow}${pendingCount} pending${colors.reset}`);
35
+ console.log(`Showing ${toShow.length} of ${history.length} migrations`);
36
+ console.log(`${appliedCount} applied, ${pendingCount} pending`);
45
37
  console.log('');
46
38
  for (const record of toShow) {
47
39
  if (record.pending) {
@@ -74,8 +66,7 @@ export async function historyCommand(context) {
74
66
  }
75
67
  return;
76
68
  }
77
- console.error('Error:', error instanceof Error ? error.message : error);
78
- process.exit(1);
69
+ fatal('Failed to load history', error instanceof Error ? error.message : String(error));
79
70
  }
80
71
  }
81
72
  function getMigrationFiles(migrationsDir) {
@@ -87,29 +78,17 @@ function getMigrationFiles(migrationsDir) {
87
78
  .sort();
88
79
  }
89
80
  async function getAppliedMigrations(connection, tableName) {
90
- const { Pool } = await import("../../addon/pg/index.js");
91
- const pool = new Pool({
92
- host: connection.host,
93
- port: connection.port || 5432,
94
- database: connection.database,
95
- user: connection.user,
96
- password: connection.password,
97
- connectionString: connection.url,
98
- });
99
- try {
81
+ return withPool(connection, async (pool) => {
100
82
  const result = await pool.query(`
101
83
  SELECT name, applied_at
102
84
  FROM "${tableName}"
103
85
  ORDER BY id DESC;
104
86
  `);
105
- return result.rows.map(r => ({
87
+ return result.rows.map((r) => ({
106
88
  name: r.name,
107
89
  appliedAt: new Date(r.applied_at),
108
90
  }));
109
- }
110
- finally {
111
- await pool.end();
112
- }
91
+ });
113
92
  }
114
93
  function formatDate(date) {
115
94
  const now = new Date();