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,21 +1,19 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import * as readline from 'readline';
4
3
  import { requireValidConfig } from "../utils/config-loader.js";
5
4
  import { getConnectionDescription } from "../utils/env-loader.js";
6
- import { colors, createSpinner } from "../utils/spinner.js";
5
+ import { colors, createSpinner, fatal, confirm, warning } from "../utils/cli-utils.js";
7
6
  import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
8
7
  import { loadRelqignore, isTableIgnored, isColumnIgnored, isEnumIgnored, isDomainIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
9
8
  import { isInitialized, getHead, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, loadSnapshot, } from "../utils/repo-manager.js";
10
9
  export async function pushCommand(context) {
11
10
  const { config, flags } = context;
12
11
  if (!config) {
13
- console.error('Error: No configuration found.');
14
- process.exit(1);
12
+ fatal('No configuration found', `Run ${colors.cyan('relq init')} to create one.`);
15
13
  }
16
- requireValidConfig(config);
14
+ await requireValidConfig(config, { calledFrom: 'push' });
17
15
  const connection = config.connection;
18
- const projectRoot = process.cwd();
16
+ const { projectRoot } = context;
19
17
  const force = flags['force'] === true;
20
18
  const dryRun = flags['dry-run'] === true;
21
19
  const applySQL = flags['apply'] === true;
@@ -26,17 +24,11 @@ export async function pushCommand(context) {
26
24
  const includeViews = config.includeViews ?? false;
27
25
  console.log('');
28
26
  if (!isInitialized(projectRoot)) {
29
- console.log(`${colors.red('fatal:')} not a relq repository`);
30
- console.log('');
31
- console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('to initialize.')}`);
32
- return;
27
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
33
28
  }
34
29
  const localHead = getHead(projectRoot);
35
30
  if (!localHead) {
36
- console.log(`${colors.red('error:')} no commits to push`);
37
- console.log('');
38
- console.log(`${colors.muted('Run')} ${colors.cyan('relq commit -m "message"')} ${colors.muted('to create a commit.')}`);
39
- return;
31
+ fatal('no commits to push', `Run ${colors.cyan('relq commit -m "message"')} to create a commit.`);
40
32
  }
41
33
  const spinner = createSpinner();
42
34
  try {
@@ -60,9 +52,8 @@ export async function pushCommand(context) {
60
52
  spinner.succeed(`Found ${remoteDb.tables.length} tables in remote`);
61
53
  const localSnapshot = loadSnapshot(projectRoot);
62
54
  if (!localSnapshot) {
63
- console.log(`${colors.red('error:')} No local snapshot found`);
64
- console.log(`${colors.muted('Run')} ${colors.cyan('relq pull')} ${colors.muted('or')} ${colors.cyan('relq import')} ${colors.muted('first.')}`);
65
- return;
55
+ spinner.stop();
56
+ fatal('No local snapshot found', `Run ${colors.cyan('relq pull')} or ${colors.cyan('relq import')} first.`);
66
57
  }
67
58
  const ignorePatterns = loadRelqignore(projectRoot);
68
59
  const analysis = analyzeSync(localSnapshot, remoteDb, ignorePatterns, { includeFunctions, includeTriggers, includeViews });
@@ -70,8 +61,7 @@ export async function pushCommand(context) {
70
61
  const hasObjectsToDrop = analysis.objectsToDrop.length > 0;
71
62
  const hasSchemaDrift = analysis.schemaDrift.length > 0;
72
63
  if (hasRemoteAhead && !force) {
73
- console.log('');
74
- console.log(`${colors.red('error:')} Remote has ${remoteMissing.length} commit(s) you don't have locally`);
64
+ spinner.stop();
75
65
  console.log('');
76
66
  for (const commit of remoteMissing.slice(0, 3)) {
77
67
  console.log(` ${colors.yellow(shortHash(commit.hash))} ${commit.message}`);
@@ -79,15 +69,11 @@ export async function pushCommand(context) {
79
69
  if (remoteMissing.length > 3) {
80
70
  console.log(` ${colors.muted(`... and ${remoteMissing.length - 3} more`)}`);
81
71
  }
82
- console.log('');
83
- console.log(`${colors.yellow('hint:')} Run ${colors.cyan('relq pull')} to integrate remote changes.`);
84
- console.log(`${colors.yellow('hint:')} Use ${colors.cyan('relq push --force')} to override (may cause data loss).`);
85
- console.log('');
86
- return;
72
+ fatal(`Remote has ${remoteMissing.length} commit(s) you don't have locally`, `Run ${colors.cyan('relq pull')} to integrate remote changes.\nUse ${colors.cyan('relq push --force')} to override (may cause data loss).`);
87
73
  }
88
74
  if (hasObjectsToDrop) {
89
75
  console.log('');
90
- console.log(`${colors.yellow('⚠')} Remote has ${analysis.objectsToDrop.length} object(s) not in your local schema:`);
76
+ warning(`Remote has ${analysis.objectsToDrop.length} object(s) not in your local schema:`);
91
77
  console.log('');
92
78
  for (const obj of analysis.objectsToDrop.slice(0, 10)) {
93
79
  console.log(` ${colors.red('DROP')} ${obj.type.toLowerCase()}: ${obj.name}`);
@@ -97,39 +83,27 @@ export async function pushCommand(context) {
97
83
  }
98
84
  console.log('');
99
85
  if (!force) {
100
- console.log(`${colors.red('error:')} Cannot push - remote has objects not in your history`);
101
- console.log('');
102
- console.log(`${colors.yellow('hint:')} Run ${colors.cyan('relq pull')} first to sync your local schema.`);
103
- console.log(`${colors.yellow('hint:')} Use ${colors.cyan('relq push --force')} to DROP these objects from remote.`);
104
- console.log('');
105
- return;
86
+ fatal('Cannot push - remote has objects not in your history', `Run ${colors.cyan('relq pull')} first to sync your local schema.\nUse ${colors.cyan('relq push --force')} to DROP these objects from remote.`);
106
87
  }
107
88
  const dependencyErrors = checkDropDependencies(analysis.objectsToDrop, remoteDb, ignorePatterns);
108
89
  if (dependencyErrors.length > 0) {
109
- console.log(`${colors.red('error:')} Cannot drop objects due to dependencies:`);
110
90
  console.log('');
111
91
  for (const err of dependencyErrors) {
112
- console.log(` ${colors.red('✗')} ${err}`);
92
+ console.log(` ${err}`);
113
93
  }
114
- console.log('');
115
- console.log(`${colors.muted('These objects are used by non-ignored objects in your schema.')}`);
116
- console.log(`${colors.muted('Either add them to .relqignore or import them with')} ${colors.cyan('relq pull')}`);
117
- console.log('');
118
- return;
94
+ fatal('Cannot drop objects due to dependencies', `These objects are used by non-ignored objects in your schema.\nEither add them to .relqignore or import them with ${colors.cyan('relq pull')}`);
119
95
  }
120
96
  if (!skipPrompt && !dryRun) {
121
- console.log(`${colors.red('⚠ WARNING: This will DROP data from your database!')}`);
97
+ warning('This will DROP data from your database!');
122
98
  console.log('');
123
- const confirmed = await askConfirmation(`Drop ${analysis.objectsToDrop.length} object(s) from remote? [y/N] `);
99
+ const confirmed = await confirm(`Drop ${analysis.objectsToDrop.length} object(s) from remote?`, false);
124
100
  if (!confirmed) {
125
- console.log(`${colors.muted('Cancelled.')}`);
126
- console.log('');
127
- return;
101
+ fatal('Operation cancelled by user');
128
102
  }
129
103
  }
130
104
  }
131
105
  if (toPush.length === 0 && !hasObjectsToDrop) {
132
- console.log(`${colors.green('✓')} Everything up-to-date`);
106
+ console.log('Everything up-to-date');
133
107
  console.log('');
134
108
  return;
135
109
  }
@@ -219,7 +193,7 @@ export async function pushCommand(context) {
219
193
  console.log(` ${oldHash}..${newHash} ${colors.muted('main -> main')}`);
220
194
  if (hasObjectsToDrop && force && applySQL) {
221
195
  console.log('');
222
- console.log(`${colors.yellow('⚠')} Dropped ${analysis.objectsToDrop.length} object(s) from remote`);
196
+ warning(`Dropped ${analysis.objectsToDrop.length} object(s) from remote`);
223
197
  }
224
198
  if (!applySQL) {
225
199
  console.log('');
@@ -227,10 +201,9 @@ export async function pushCommand(context) {
227
201
  }
228
202
  console.log('');
229
203
  }
230
- catch (error) {
204
+ catch (err) {
231
205
  spinner.fail('Push failed');
232
- console.error(colors.red(`Error: ${error instanceof Error ? error.message : error}`));
233
- process.exit(1);
206
+ fatal(err instanceof Error ? err.message : String(err));
234
207
  }
235
208
  }
236
209
  function analyzeSync(local, remote, patterns, options) {
@@ -329,17 +302,4 @@ function generateDropSQL(obj) {
329
302
  return '';
330
303
  }
331
304
  }
332
- function askConfirmation(question) {
333
- const rl = readline.createInterface({
334
- input: process.stdin,
335
- output: process.stdout,
336
- });
337
- return new Promise((resolve) => {
338
- rl.question(question, (answer) => {
339
- rl.close();
340
- const a = answer.trim().toLowerCase();
341
- resolve(a === 'y' || a === 'yes');
342
- });
343
- });
344
- }
345
305
  export default pushCommand;
@@ -1,6 +1,7 @@
1
+ import { warning } from "../utils/spinner.js";
1
2
  export async function remoteCommand(context) {
2
3
  console.log('');
3
- console.log('⚠️ Remote tracking is coming soon.');
4
+ warning('Remote tracking is coming soon.');
4
5
  console.log('');
5
6
  console.log('This will allow you to:');
6
7
  console.log(' • Track multiple remote databases');
@@ -1,40 +1,26 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import * as readline from 'readline';
4
- import { colors, createSpinner } from "../utils/spinner.js";
4
+ import { colors, createSpinner, fatal, warning } from "../utils/cli-utils.js";
5
5
  import { isInitialized, getHead, setHead, loadCommit, saveSnapshot, shortHash, getAllCommits, } from "../utils/repo-manager.js";
6
6
  export async function resetCommand(context) {
7
- const { flags, args } = context;
8
- const projectRoot = process.cwd();
7
+ const { flags, args, projectRoot } = context;
9
8
  console.log('');
10
9
  if (!isInitialized(projectRoot)) {
11
- console.log(`${colors.red('fatal:')} not a relq repository`);
12
- return;
10
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
13
11
  }
14
12
  const hard = flags['hard'] === true;
15
13
  const soft = flags['soft'] === true;
16
14
  const target = args[0];
17
15
  if (!target) {
18
- console.log(`${colors.red('error:')} Please specify a target`);
19
- console.log('');
20
- console.log('Usage:');
21
- console.log(` ${colors.cyan('relq reset --hard HEAD~1')} Reset to previous commit`);
22
- console.log(` ${colors.cyan('relq reset --hard <hash>')} Reset to specific commit`);
23
- console.log('');
24
- return;
16
+ fatal('Please specify a target', `Usage:\n ${colors.cyan('relq reset --hard HEAD~1')} Reset to previous commit\n ${colors.cyan('relq reset --hard <hash>')} Reset to specific commit`);
25
17
  }
26
18
  if (!hard && !soft) {
27
- console.log(`${colors.red('error:')} Please specify --hard or --soft`);
28
- console.log('');
29
- console.log(` ${colors.cyan('--hard')} Discard all changes (DANGEROUS)`);
30
- console.log(` ${colors.cyan('--soft')} Keep changes unstaged`);
31
- console.log('');
32
- return;
19
+ fatal('Please specify --hard or --soft', ` ${colors.cyan('--hard')} Discard all changes (DANGEROUS)\n ${colors.cyan('--soft')} Keep changes unstaged`);
33
20
  }
34
21
  const currentHead = getHead(projectRoot);
35
22
  if (!currentHead) {
36
- console.log(`${colors.red('error:')} No commits yet`);
37
- return;
23
+ fatal('No commits yet', `Run ${colors.cyan('relq pull')} or ${colors.cyan('relq import')} first.`);
38
24
  }
39
25
  const allCommits = getAllCommits(projectRoot);
40
26
  let targetHash = null;
@@ -55,21 +41,15 @@ export async function resetCommand(context) {
55
41
  }
56
42
  }
57
43
  if (!targetHash) {
58
- console.log(`${colors.red('error:')} Cannot find commit: ${target}`);
59
- console.log('');
60
- console.log('Available commits:');
61
- for (const c of allCommits.slice(0, 5)) {
62
- console.log(` ${colors.yellow(shortHash(c.hash))} ${c.message}`);
63
- }
64
- return;
44
+ const availableList = allCommits.slice(0, 5).map(c => ` ${colors.yellow(shortHash(c.hash))} ${c.message}`).join('\n');
45
+ fatal(`Cannot find commit: ${target}`, `Available commits:\n${availableList}`);
65
46
  }
66
47
  const targetCommit = loadCommit(targetHash, projectRoot);
67
48
  if (!targetCommit) {
68
- console.log(`${colors.red('error:')} Cannot load commit: ${targetHash}`);
69
- return;
49
+ fatal(`Cannot load commit: ${targetHash}`);
70
50
  }
71
51
  if (hard) {
72
- console.log(`${colors.red('⚠ WARNING: This will discard all local changes!')}`);
52
+ warning('This will discard all local changes!');
73
53
  console.log('');
74
54
  console.log(`Reset to: ${colors.yellow(shortHash(targetHash))} ${targetCommit.message}`);
75
55
  console.log('');
@@ -94,14 +74,14 @@ export async function resetCommand(context) {
94
74
  spinner.start(`Resetting to ${shortHash(targetHash)}...`);
95
75
  const commitPath = path.join(projectRoot, '.relq', 'commits', `${targetHash}.json`);
96
76
  if (!fs.existsSync(commitPath)) {
97
- spinner.fail('Cannot find commit data');
98
- return;
77
+ spinner.stop();
78
+ fatal('Cannot find commit data - repository may be corrupt');
99
79
  }
100
80
  const commitData = JSON.parse(fs.readFileSync(commitPath, 'utf-8'));
101
81
  const targetSnapshot = commitData.snapshot;
102
82
  if (!targetSnapshot) {
103
- spinner.fail('Commit has no snapshot data');
104
- return;
83
+ spinner.stop();
84
+ fatal('Commit has no snapshot data - repository may be corrupt');
105
85
  }
106
86
  if (hard) {
107
87
  saveSnapshot(targetSnapshot, projectRoot);
@@ -124,10 +104,9 @@ export async function resetCommand(context) {
124
104
  console.log(`${colors.muted('Changes are unstaged. Use')} ${colors.cyan('relq status')} ${colors.muted('to see.')}`);
125
105
  }
126
106
  }
127
- catch (error) {
107
+ catch (err) {
128
108
  spinner.fail('Reset failed');
129
- console.error(colors.red(`Error: ${error instanceof Error ? error.message : error}`));
130
- process.exit(1);
109
+ fatal(err instanceof Error ? err.message : String(err));
131
110
  }
132
111
  }
133
112
  export default resetCommand;
@@ -1,25 +1,23 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { colors } from "../utils/spinner.js";
3
+ import { colors, fatal, warning } from "../utils/spinner.js";
4
4
  import { isInitialized, loadSnapshot, saveSnapshot, } from "../utils/repo-manager.js";
5
5
  export async function resolveCommand(context) {
6
- const { flags, args } = context;
7
- const projectRoot = process.cwd();
6
+ const { flags, args, projectRoot } = context;
8
7
  console.log('');
9
8
  if (!isInitialized(projectRoot)) {
10
- console.log(`${colors.red('fatal:')} not a relq repository`);
11
- return;
9
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
12
10
  }
13
11
  const mergeStatePath = path.join(projectRoot, '.relq', 'MERGE_STATE');
14
12
  if (!fs.existsSync(mergeStatePath)) {
15
- console.log(`${colors.green('✓')} No conflicts to resolve`);
13
+ console.log('No conflicts to resolve');
16
14
  console.log('');
17
15
  return;
18
16
  }
19
17
  const mergeState = JSON.parse(fs.readFileSync(mergeStatePath, 'utf-8'));
20
18
  if (mergeState.conflicts.length === 0) {
21
19
  fs.unlinkSync(mergeStatePath);
22
- console.log(`${colors.green('✓')} All conflicts resolved`);
20
+ console.log('All conflicts resolved');
23
21
  console.log('');
24
22
  return;
25
23
  }
@@ -33,9 +31,9 @@ export async function resolveCommand(context) {
33
31
  console.log('');
34
32
  saveSnapshot(mergeState.remoteSnapshot, projectRoot);
35
33
  fs.unlinkSync(mergeStatePath);
36
- console.log(`${colors.green('✓')} Applied remote versions for all conflicts`);
34
+ console.log('Applied remote versions for all conflicts');
37
35
  console.log('');
38
- console.log(`${colors.muted('Run')} ${colors.cyan('relq commit -m "Merge remote changes"')} ${colors.muted('to complete.')}`);
36
+ console.log(`hint: run 'relq commit -m "Merge remote changes"' to complete`);
39
37
  console.log('');
40
38
  return;
41
39
  }
@@ -43,9 +41,9 @@ export async function resolveCommand(context) {
43
41
  console.log(`Resolving ${mergeState.conflicts.length} conflict(s) with --all-ours`);
44
42
  console.log('');
45
43
  fs.unlinkSync(mergeStatePath);
46
- console.log(`${colors.green('✓')} Kept local versions for all conflicts`);
44
+ console.log('Kept local versions for all conflicts');
47
45
  console.log('');
48
- console.log(`${colors.muted('Run')} ${colors.cyan('relq commit -m "Keep local changes"')} ${colors.muted('to complete.')}`);
46
+ console.log(`hint: run 'relq commit -m "Keep local changes"' to complete`);
49
47
  console.log('');
50
48
  return;
51
49
  }
@@ -75,17 +73,17 @@ export async function resolveCommand(context) {
75
73
  mergeState.conflicts = mergeState.conflicts.filter(c => c !== conflict);
76
74
  if (mergeState.conflicts.length === 0) {
77
75
  fs.unlinkSync(mergeStatePath);
78
- console.log(`${colors.green('✓')} All conflicts resolved`);
76
+ console.log('All conflicts resolved');
79
77
  }
80
78
  else {
81
79
  fs.writeFileSync(mergeStatePath, JSON.stringify(mergeState, null, 2));
82
- console.log(`${colors.green('✓')} Resolved: ${conflict.objectType} ${objectName}`);
83
- console.log(`${colors.yellow(String(mergeState.conflicts.length))} conflict(s) remaining`);
80
+ console.log(`Resolved: ${conflict.objectType} ${objectName}`);
81
+ console.log(`${mergeState.conflicts.length} conflict(s) remaining`);
84
82
  }
85
83
  console.log('');
86
84
  return;
87
85
  }
88
- console.log(`${colors.yellow('⚠')} You have ${mergeState.conflicts.length} unresolved conflict(s):`);
86
+ warning(`You have ${mergeState.conflicts.length} unresolved conflict(s):`);
89
87
  console.log('');
90
88
  for (const conflict of mergeState.conflicts) {
91
89
  const name = conflict.parentName
@@ -1,17 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import * as readline from 'readline';
4
3
  import { requireValidConfig } from "../utils/config-loader.js";
5
4
  import { getConnectionDescription } from "../utils/env-loader.js";
6
- const colors = {
7
- reset: '\x1b[0m',
8
- bold: '\x1b[1m',
9
- dim: '\x1b[2m',
10
- red: '\x1b[31m',
11
- green: '\x1b[32m',
12
- yellow: '\x1b[33m',
13
- cyan: '\x1b[36m',
14
- };
5
+ import { colors, confirm, fatal, warning } from "../utils/cli-utils.js";
15
6
  function parseMigration(content) {
16
7
  const upMatch = content.match(/--\s*UP\s*\n([\s\S]*?)(?=--\s*DOWN|$)/i);
17
8
  const downMatch = content.match(/--\s*DOWN\s*\n([\s\S]*?)$/i);
@@ -20,32 +11,20 @@ function parseMigration(content) {
20
11
  down: downMatch?.[1]?.trim() || '',
21
12
  };
22
13
  }
23
- function askConfirm(question) {
24
- const rl = readline.createInterface({
25
- input: process.stdin,
26
- output: process.stdout,
27
- });
28
- return new Promise((resolve) => {
29
- rl.question(`${question} [y/N]: `, (answer) => {
30
- rl.close();
31
- resolve(answer.trim().toLowerCase() === 'y');
32
- });
33
- });
34
- }
35
14
  export async function rollbackCommand(context) {
36
15
  const { config, args, flags } = context;
37
16
  if (!config) {
38
- console.error('Error: No configuration found.');
39
- process.exit(1);
17
+ fatal('No configuration found', `run ${colors.cyan('relq init')} to create a configuration file`);
18
+ return;
40
19
  }
41
- requireValidConfig(config);
20
+ await requireValidConfig(config, { calledFrom: 'rollback' });
42
21
  const connection = config.connection;
43
22
  const migrationsDir = config.migrations?.directory || './migrations';
44
23
  const tableName = config.migrations?.tableName || '_relq_migrations';
45
24
  const count = parseInt(args[0]) || 1;
46
25
  const dryRun = flags['dry-run'] === true;
47
26
  const force = flags['force'] === true;
48
- console.log(`${colors.bold}Rolling back ${count} migration(s)...${colors.reset}`);
27
+ console.log(`Rolling back ${count} migration(s)...`);
49
28
  console.log(` Connection: ${getConnectionDescription(connection)}`);
50
29
  console.log('');
51
30
  try {
@@ -65,17 +44,17 @@ export async function rollbackCommand(context) {
65
44
  LIMIT $1;
66
45
  `, [count]);
67
46
  if (result.rows.length === 0) {
68
- console.log(`${colors.yellow}No migrations to rollback.${colors.reset}`);
47
+ console.log('No migrations to rollback.');
69
48
  return;
70
49
  }
71
50
  const toRollback = result.rows.map(r => r.name);
72
- console.log(`${colors.bold}Migrations to rollback:${colors.reset}`);
51
+ console.log('Migrations to rollback:');
73
52
  for (const name of toRollback) {
74
- console.log(` ${colors.red}• ${name}${colors.reset}`);
53
+ console.log(` - ${name}`);
75
54
  }
76
55
  console.log('');
77
56
  if (!force && !dryRun) {
78
- const proceed = await askConfirm(`${colors.red}This will undo ${toRollback.length} migration(s). Continue?${colors.reset}`);
57
+ const proceed = await confirm(`${colors.red('This will undo ' + toRollback.length + ' migration(s). Continue?')}`, false);
79
58
  if (!proceed) {
80
59
  console.log('Cancelled.');
81
60
  return;
@@ -84,18 +63,18 @@ export async function rollbackCommand(context) {
84
63
  for (const name of toRollback) {
85
64
  const filePath = path.join(migrationsDir, name);
86
65
  if (!fs.existsSync(filePath)) {
87
- console.log(`${colors.yellow}⚠️ Migration file not found: ${name}${colors.reset}`);
66
+ warning(`Migration file not found: ${name}`);
88
67
  continue;
89
68
  }
90
69
  const content = fs.readFileSync(filePath, 'utf-8');
91
70
  const { down } = parseMigration(content);
92
71
  if (!down) {
93
- console.log(`${colors.yellow}⚠️ No DOWN section in: ${name}${colors.reset}`);
72
+ warning(`No DOWN section in: ${name}`);
94
73
  continue;
95
74
  }
96
75
  if (dryRun) {
97
- console.log(`${colors.cyan}[dry-run] Would rollback: ${name}${colors.reset}`);
98
- console.log(`${colors.dim}${down}${colors.reset}`);
76
+ console.log(`[dry-run] Would rollback: ${name}`);
77
+ console.log(down);
99
78
  console.log('');
100
79
  }
101
80
  else {
@@ -106,7 +85,7 @@ export async function rollbackCommand(context) {
106
85
  await client.query(down);
107
86
  await client.query(`DELETE FROM "${tableName}" WHERE name = $1`, [name]);
108
87
  await client.query('COMMIT');
109
- console.log(` ${colors.green}✓ Rolled back${colors.reset}`);
88
+ console.log(' Rolled back');
110
89
  }
111
90
  catch (error) {
112
91
  await client.query('ROLLBACK');
@@ -119,7 +98,7 @@ export async function rollbackCommand(context) {
119
98
  }
120
99
  if (!dryRun) {
121
100
  console.log('');
122
- console.log(`${colors.green}✓ Rolled back ${toRollback.length} migration(s).${colors.reset}`);
101
+ console.log(`Rolled back ${toRollback.length} migration(s).`);
123
102
  }
124
103
  }
125
104
  finally {
@@ -127,7 +106,6 @@ export async function rollbackCommand(context) {
127
106
  }
128
107
  }
129
108
  catch (error) {
130
- console.error('Error:', error instanceof Error ? error.message : error);
131
- process.exit(1);
109
+ fatal('Rollback failed', error instanceof Error ? error.message : String(error));
132
110
  }
133
111
  }
@@ -1,15 +1,13 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { colors } from "../utils/spinner.js";
3
+ import { colors, fatal } from "../utils/spinner.js";
4
4
  import { isInitialized, getStagedChanges, getUnstagedChanges, } from "../utils/repo-manager.js";
5
5
  export async function stashCommand(context) {
6
- const { args, flags } = context;
7
- const projectRoot = process.cwd();
6
+ const { args, flags, projectRoot } = context;
8
7
  const subcommand = args[0] || 'push';
9
8
  console.log('');
10
9
  if (!isInitialized(projectRoot)) {
11
- console.log(`${colors.red('fatal:')} not a relq repository`);
12
- return;
10
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
13
11
  }
14
12
  const stashDir = path.join(projectRoot, '.relq', 'stash');
15
13
  switch (subcommand) {
@@ -56,7 +54,7 @@ async function stashPush(projectRoot, stashDir, message) {
56
54
  fs.writeFileSync(stagedPath, '[]');
57
55
  if (fs.existsSync(unstagedPath))
58
56
  fs.writeFileSync(unstagedPath, '[]');
59
- console.log(`${colors.green('✓')} Saved working directory`);
57
+ console.log('Saved working directory');
60
58
  console.log(` stash@{${stashIdx}}: ${message}`);
61
59
  console.log('');
62
60
  }
@@ -81,7 +79,7 @@ async function stashPop(projectRoot, stashDir) {
81
79
  fs.writeFileSync(unstagedPath, JSON.stringify(stash.unstaged, null, 2));
82
80
  }
83
81
  fs.unlinkSync(stashPath);
84
- console.log(`${colors.green('✓')} Applied stash and dropped`);
82
+ console.log('Applied stash and dropped');
85
83
  console.log(` ${stash.message}`);
86
84
  console.log('');
87
85
  }
@@ -112,7 +110,7 @@ async function stashDrop(stashDir) {
112
110
  return;
113
111
  }
114
112
  fs.unlinkSync(path.join(stashDir, stashFiles[0]));
115
- console.log(`${colors.green('✓')} Dropped stash`);
113
+ console.log('Dropped stash');
116
114
  console.log('');
117
115
  }
118
116
  export default stashCommand;
@@ -1,20 +1,15 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { getConnectionDescription } from "../utils/env-loader.js";
4
- import { colors } from "../utils/spinner.js";
4
+ import { colors, fatal } from "../utils/spinner.js";
5
5
  import { loadRelqignore, } from "../utils/relqignore.js";
6
6
  import { isInitialized, getHead, loadCommit, shortHash, getStagedChanges, getUnstagedChanges, } from "../utils/repo-manager.js";
7
7
  export async function statusCommand(context) {
8
- const { config, flags } = context;
9
- const projectRoot = process.cwd();
8
+ const { config, flags, projectRoot } = context;
10
9
  const connection = config?.connection;
11
10
  console.log('');
12
11
  if (!isInitialized(projectRoot)) {
13
- console.log(`${colors.red('fatal:')} not a relq repository`);
14
- console.log('');
15
- console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('to initialize.')}`);
16
- console.log('');
17
- return;
12
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
18
13
  }
19
14
  const head = getHead(projectRoot);
20
15
  const staged = getStagedChanges(projectRoot);
@@ -37,7 +32,7 @@ export async function statusCommand(context) {
37
32
  console.log(`${colors.green('Changes to be committed:')}`);
38
33
  console.log(` ${colors.muted('(use "relq restore --staged <name>..." to unstage)')}`);
39
34
  console.log('');
40
- displayChanges(staged, ' ');
35
+ displayChanges(staged, ' ');
41
36
  console.log('');
42
37
  }
43
38
  if (unstaged.length > 0) {
@@ -45,7 +40,7 @@ export async function statusCommand(context) {
45
40
  console.log(` ${colors.muted('(use "relq add <name>..." to stage)')}`);
46
41
  console.log(` ${colors.muted('(use "relq restore <name>..." to discard)')}`);
47
42
  console.log('');
48
- displayChanges(unstaged, ' ');
43
+ displayChanges(unstaged, ' ');
49
44
  console.log('');
50
45
  }
51
46
  const ignorePatterns = loadRelqignore(projectRoot);
@@ -56,7 +51,7 @@ export async function statusCommand(context) {
56
51
  !p.raw.startsWith('_temp_') &&
57
52
  !p.raw.startsWith('tmp_'));
58
53
  if (userPatterns.length > 0) {
59
- console.log(`${colors.muted(`ℹ ${userPatterns.length} pattern(s) active from .relqignore`)}`);
54
+ console.log(`${colors.muted(`${userPatterns.length} pattern(s) active from .relqignore`)}`);
60
55
  console.log('');
61
56
  }
62
57
  }