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
@@ -6,27 +6,23 @@ import { saveSnapshot, loadSnapshot, isInitialized, initRepository, stageChanges
6
6
  import { compareSchemas } from "../utils/schema-comparator.js";
7
7
  import { getChangeDisplayName } from "../utils/change-tracker.js";
8
8
  import { loadRelqignore, validateIgnoreDependencies, isTableIgnored, isColumnIgnored, isIndexIgnored, isConstraintIgnored, isEnumIgnored, isDomainIgnored, isSequenceIgnored, isCompositeTypeIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
9
- import { colors, error, fatal, warning, hint, getWorkingTreeStatus, printDirtyWorkingTreeError, printMergeStrategyHelp, readSQLFile, createSpinner, formatBytes, } from "../utils/git-utils.js";
10
- export async function importCommand(sqlFilePath, options = {}) {
9
+ import { colors, fatal, warning, hint, getWorkingTreeStatus, printDirtyWorkingTreeError, printMergeStrategyHelp, readSQLFile, createSpinner, formatBytes, } from "../utils/git-utils.js";
10
+ export async function importCommand(sqlFilePath, options = {}, projectRoot = process.cwd()) {
11
11
  const { includeFunctions = false, includeTriggers = false, force = false, dryRun = false } = options;
12
- const projectRoot = process.cwd();
13
12
  const spinner = createSpinner();
14
13
  console.log('');
15
14
  if (!sqlFilePath) {
16
- error('No SQL file specified.');
17
- console.log('');
18
- console.log('usage: relq import <sql-file> [options]');
19
- console.log('');
20
- console.log('Options:');
21
- console.log(' --output <path> Output schema file path');
22
- console.log(' --force Force import, overwrite local changes');
23
- console.log(' --dry-run Preview changes without applying');
24
- console.log(' --theirs Accept all incoming changes');
25
- console.log(' --ours Keep all local changes (reject incoming)');
26
- console.log(' --abort Abort the import operation');
27
- console.log(' --include-functions Include functions in import');
28
- console.log(' --include-triggers Include triggers in import');
29
- process.exit(1);
15
+ fatal('No SQL file specified', 'usage: relq import <sql-file> [options]\n\n' +
16
+ 'Options:\n' +
17
+ ' --output <path> Output schema file path\n' +
18
+ ' --force Force import, overwrite local changes\n' +
19
+ ' --dry-run Preview changes without applying\n' +
20
+ ' --theirs Accept all incoming changes\n' +
21
+ ' --ours Keep all local changes (reject incoming)\n' +
22
+ ' --abort Abort the import operation\n' +
23
+ ' --include-functions Include functions in import\n' +
24
+ ' --include-triggers Include triggers in import');
25
+ return;
30
26
  }
31
27
  if (options.abort) {
32
28
  console.log('Aborting import...');
@@ -49,11 +45,8 @@ export async function importCommand(sqlFilePath, options = {}) {
49
45
  warning(warn);
50
46
  }
51
47
  if (!validation.valid) {
52
- error('Invalid PostgreSQL SQL file:');
53
- for (const err of validation.errors) {
54
- console.log(` - ${err}`);
55
- }
56
- process.exit(1);
48
+ fatal('Invalid PostgreSQL SQL file', validation.errors.join('\n - '));
49
+ return;
57
50
  }
58
51
  console.log(`Importing ${colors.cyan(path.basename(sqlFilePath))} ${colors.gray(`(${formatBytes(sqlContent.length)})`)}`);
59
52
  console.log('');
@@ -68,7 +61,8 @@ export async function importCommand(sqlFilePath, options = {}) {
68
61
  printDirtyWorkingTreeError(status, 'import');
69
62
  console.log('');
70
63
  printMergeStrategyHelp();
71
- process.exit(1);
64
+ fatal('Working tree is not clean', 'Commit or stash your changes before importing.');
65
+ return;
72
66
  }
73
67
  }
74
68
  spinner.start('Parsing SQL schema');
@@ -196,7 +190,7 @@ export async function importCommand(sqlFilePath, options = {}) {
196
190
  partitions: parsedSchema.partitions,
197
191
  };
198
192
  if (ignoredCount > 0) {
199
- console.log(`${colors.gray(`ℹ ${ignoredCount} object(s) ignored by .relqignore`)}`);
193
+ console.log(`${ignoredCount} object(s) ignored by .relqignore`);
200
194
  }
201
195
  const dependencyErrors = validateIgnoreDependencies({
202
196
  tables: filteredTables.map(t => ({
@@ -214,14 +208,9 @@ export async function importCommand(sqlFilePath, options = {}) {
214
208
  }, ignorePatterns);
215
209
  if (dependencyErrors.length > 0) {
216
210
  spinner.stop();
217
- error('Dependency validation failed:');
218
- console.log('');
219
- for (const depError of dependencyErrors) {
220
- console.log(` ${colors.red('✗')} ${depError.message}`);
221
- }
222
- console.log('');
223
- hint('Either un-ignore the type or add the column to .relqignore');
224
- process.exit(1);
211
+ const errorMessages = dependencyErrors.map(e => e.message).join('\n ');
212
+ fatal('Dependency validation failed', `${errorMessages}\n\nEither un-ignore the type or add the column to .relqignore`);
213
+ return;
225
214
  }
226
215
  spinner.start('Generating TypeScript schema');
227
216
  const dbSchema = convertToDbSchema(filteredSchema, filteredFunctions, triggers, comments);
@@ -277,7 +266,7 @@ export async function importCommand(sqlFilePath, options = {}) {
277
266
  }
278
267
  else {
279
268
  console.log('');
280
- console.log(`${colors.gray(`ℹ ${drops.length} object(s) only in existing schema (preserved)`)}`);
269
+ console.log(`${drops.length} object(s) only in existing schema (preserved)`);
281
270
  }
282
271
  }
283
272
  console.log('');
@@ -304,8 +293,10 @@ export async function importCommand(sqlFilePath, options = {}) {
304
293
  console.log(`${colors.yellow('Dry run mode')} - no files written`);
305
294
  console.log('');
306
295
  console.log('Would write:');
307
- console.log(` ${colors.cyan(options.output || './db/schema.ts')} ${colors.gray(`(${formatBytes(finalTypescriptContent.length)})`)}`);
308
- console.log(` ${colors.cyan('.relq/snapshot.json')}`);
296
+ const dryRunOutputPath = options.output || './db/schema.ts';
297
+ const dryRunAbsPath = path.resolve(projectRoot, dryRunOutputPath);
298
+ console.log(` ${colors.cyan(dryRunAbsPath)} ${colors.gray(`(${formatBytes(finalTypescriptContent.length)})`)}`);
299
+ console.log(` ${colors.cyan(path.join(projectRoot, '.relq/snapshot.json'))}`);
309
300
  if (changes.length > 0) {
310
301
  console.log(` Stage ${changes.length} change(s)`);
311
302
  }
@@ -313,29 +304,29 @@ export async function importCommand(sqlFilePath, options = {}) {
313
304
  return;
314
305
  }
315
306
  const outputPath = options.output || './db/schema.ts';
316
- const absoluteOutputPath = path.resolve(outputPath);
307
+ const absoluteOutputPath = path.resolve(projectRoot, outputPath);
317
308
  const outputDir = path.dirname(absoluteOutputPath);
318
309
  if (!fs.existsSync(outputDir)) {
319
310
  fs.mkdirSync(outputDir, { recursive: true });
320
311
  }
321
312
  fs.writeFileSync(absoluteOutputPath, finalTypescriptContent, 'utf-8');
322
- console.log(`Written ${colors.cyan(outputPath)} ${colors.gray(`(${formatBytes(finalTypescriptContent.length)})`)}`);
313
+ console.log(`Written ${colors.cyan(absoluteOutputPath)} ${colors.gray(`(${formatBytes(finalTypescriptContent.length)})`)}`);
323
314
  saveSnapshot(mergedSchema, projectRoot);
324
315
  if (changes.length > 0) {
325
316
  addUnstagedChanges(changes, projectRoot);
326
317
  stageChanges(['.'], projectRoot);
327
318
  console.log('');
328
- console.log(`${colors.green('✓')} ${changes.length} change(s) staged for commit`);
319
+ console.log(`${changes.length} change(s) staged for commit`);
329
320
  }
330
321
  console.log('');
331
- console.log(`${colors.green('Import successful.')}`);
322
+ console.log('Import successful.');
332
323
  console.log('');
333
324
  if (changes.length > 0) {
334
- hint('Run "relq status" to see staged changes');
335
- hint('Run "relq commit -m <message>" to commit');
325
+ hint("run 'relq status' to see staged changes");
326
+ hint("run 'relq commit -m <message>' to commit");
336
327
  }
337
328
  else {
338
- hint('Run "relq status" to see current state');
329
+ hint("run 'relq status' to see current state");
339
330
  }
340
331
  console.log('');
341
332
  }
@@ -432,6 +423,7 @@ function convertToDbSchema(parsed, functions = [], triggers = [], comments = [])
432
423
  cycle: s.cycle,
433
424
  ownedBy: s.ownedBy,
434
425
  })) || [],
426
+ collations: [],
435
427
  extensions: parsed.extensions,
436
428
  partitions: parsed.partitions,
437
429
  functions: functions.map(f => ({
@@ -700,6 +692,7 @@ function snapshotToDbSchemaForGeneration(snapshot) {
700
692
  cycle: s.cycle,
701
693
  ownedBy: s.ownedBy ?? undefined,
702
694
  })) || [],
695
+ collations: [],
703
696
  extensions: (snapshot.extensions || []).map(e => typeof e === 'string' ? e : e.name),
704
697
  partitions: [],
705
698
  functions: snapshot.functions?.map(f => ({
@@ -4,7 +4,7 @@ import * as readline from 'readline';
4
4
  import { loadEnvConfig } from "../utils/env-loader.js";
5
5
  import { initRepository, ensureRemoteTable, isInitialized } from "../utils/repo-manager.js";
6
6
  import { createDefaultRelqignore } from "../utils/relqignore.js";
7
- import { colors, createSpinner } from "../utils/spinner.js";
7
+ import { colors, createSpinner, fatal, warning, hint } from "../utils/cli-utils.js";
8
8
  function ask(rl, question, defaultValue) {
9
9
  const suffix = defaultValue ? ` ${colors.muted(`[${defaultValue}]`)}` : '';
10
10
  return new Promise((resolve) => {
@@ -120,11 +120,9 @@ export async function initCommand(context) {
120
120
  const cwd = process.cwd();
121
121
  const spinner = createSpinner();
122
122
  console.log('');
123
- console.log(`${colors.bold('Relq')} - Git-like schema versioning for PostgreSQL`);
124
- console.log('');
125
123
  const { findConfigFileRecursive } = await import("../utils/config-loader.js");
124
+ const { findProjectRoot } = await import("../utils/project-root.js");
126
125
  const existingConfig = findConfigFileRecursive(cwd);
127
- const localRelqExists = isInitialized(cwd);
128
126
  let projectRoot = cwd;
129
127
  let existingConfigValues = null;
130
128
  if (existingConfig) {
@@ -136,28 +134,35 @@ export async function initCommand(context) {
136
134
  catch {
137
135
  }
138
136
  if (existingConfig.directory !== cwd) {
139
- console.log(`${colors.cyan('ℹ')} Found ${colors.cyan('relq.config.ts')} in parent directory:`);
140
- console.log(` ${colors.muted(existingConfig.path)}`);
137
+ hint(`Found relq.config.ts in: ${colors.cyan(existingConfig.directory)}`);
138
+ console.log(` Initializing at project root...`);
141
139
  console.log('');
142
- console.log(`${colors.muted('Run commands from project root:')} ${colors.cyan(existingConfig.directory)}`);
140
+ }
141
+ }
142
+ else {
143
+ const packageRoot = findProjectRoot(cwd);
144
+ if (packageRoot && packageRoot !== cwd) {
145
+ projectRoot = packageRoot;
146
+ hint(`Found package.json in: ${colors.cyan(packageRoot)}`);
147
+ console.log(` Initializing at project root...`);
143
148
  console.log('');
144
- return;
145
149
  }
150
+ }
151
+ if (existingConfig && existingConfig.directory === projectRoot) {
146
152
  const relqFolderExists = isInitialized(projectRoot);
147
153
  if (relqFolderExists) {
148
- console.log(`${colors.green('✓')} Relq is already initialized.`);
154
+ console.log(`${colors.green('Relq is already initialized.')}`);
149
155
  console.log('');
150
- console.log(` ${colors.green('')} ${colors.muted('relq.config.ts')}`);
151
- console.log(` ${colors.green('')} ${colors.muted('.relq/')} folder`);
156
+ console.log(` ${colors.dim('-')} relq.config.ts`);
157
+ console.log(` ${colors.dim('-')} .relq/ folder`);
152
158
  console.log('');
153
- console.log(`${colors.muted('Use')} ${colors.cyan('relq status')} ${colors.muted('to see current state.')}`);
154
- console.log(`${colors.muted('Use')} ${colors.cyan('relq pull')} ${colors.muted('to sync with database.')}`);
159
+ hint(`Use ${colors.cyan('relq status')} to see current state`);
160
+ hint(`Use ${colors.cyan('relq pull')} to sync with database`);
155
161
  return;
156
162
  }
157
163
  else {
158
- console.log(`${colors.yellow('⚠')} Found ${colors.cyan('relq.config.ts')} but ${colors.cyan('.relq/')} folder is missing.`);
159
- console.log('');
160
- console.log(`${colors.muted('This could mean the setup was incomplete.')}`);
164
+ warning('Found relq.config.ts but .relq/ folder is missing');
165
+ hint('This could mean the setup was incomplete');
161
166
  console.log('');
162
167
  const rl = readline.createInterface({
163
168
  input: process.stdin,
@@ -181,12 +186,12 @@ export async function initCommand(context) {
181
186
  }
182
187
  if (missingFields.length > 0) {
183
188
  console.log('');
184
- console.log(`${colors.yellow('⚠')} Your config is missing required fields:`);
189
+ warning('Your config is missing required fields:');
185
190
  for (const field of missingFields) {
186
- console.log(` ${colors.red('')} ${field}`);
191
+ console.log(` ${colors.dim('-')} ${field}`);
187
192
  }
188
193
  console.log('');
189
- console.log(`${colors.cyan('●')} Let's fill in the missing fields:`);
194
+ console.log(`${colors.dim("Let's fill in the missing fields:")}`);
190
195
  console.log('');
191
196
  let authorValue = existingConfigValues?.author;
192
197
  let schemaValue = existingConfigValues?.schema;
@@ -198,9 +203,9 @@ export async function initCommand(context) {
198
203
  }
199
204
  if (missingFields.includes('connection')) {
200
205
  console.log('');
201
- console.log(`${colors.red('●')} Connection not configured.`);
202
- console.log(` ${colors.muted('Set DATABASE_* environment variables in .env')}`);
203
- console.log(` ${colors.muted('or update relq.config.ts with connection details.')}`);
206
+ console.log('Connection not configured.');
207
+ console.log(' Set DATABASE_* environment variables in .env');
208
+ console.log(' or update relq.config.ts with connection details.');
204
209
  rl.close();
205
210
  return;
206
211
  }
@@ -226,18 +231,18 @@ export async function initCommand(context) {
226
231
  }
227
232
  rl.close();
228
233
  console.log('');
229
- console.log(`${colors.cyan('●')} Completing setup...`);
234
+ console.log('Completing setup...');
230
235
  console.log('');
231
236
  initRepository(projectRoot);
232
- console.log(` ${colors.green('✓')} Created ${colors.cyan('.relq/')} folder`);
237
+ console.log(' Created .relq/ folder');
233
238
  createDefaultRelqignore(projectRoot);
234
- console.log(` ${colors.green('✓')} Created ${colors.cyan('.relqignore')}`);
239
+ console.log(' Created .relqignore');
235
240
  const gitignorePath = path.join(projectRoot, '.gitignore');
236
241
  if (fs.existsSync(gitignorePath)) {
237
242
  const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
238
243
  if (!gitignore.includes('.relq')) {
239
244
  fs.appendFileSync(gitignorePath, '\n# Relq local state\n.relq/\n');
240
- console.log(` ${colors.green('✓')} Added ${colors.cyan('.relq/')} to .gitignore`);
245
+ console.log(' Added .relq/ to .gitignore');
241
246
  }
242
247
  }
243
248
  if (existingConfigValues?.connection) {
@@ -251,9 +256,15 @@ export async function initCommand(context) {
251
256
  }
252
257
  }
253
258
  console.log('');
254
- console.log(`${colors.green('✓')} Setup complete!`);
259
+ console.log('Setup complete!');
255
260
  console.log('');
256
- console.log(`${colors.muted('Run')} ${colors.cyan('relq pull')} ${colors.muted('to sync with database.')}`);
261
+ const calledFrom = context.flags['called-from'];
262
+ if (calledFrom) {
263
+ console.log(`Continuing with ${calledFrom}...`);
264
+ }
265
+ else {
266
+ console.log('Run "relq pull" to sync with database.');
267
+ }
257
268
  return;
258
269
  }
259
270
  rl.close();
@@ -272,7 +283,7 @@ export async function initCommand(context) {
272
283
  output: process.stdout,
273
284
  });
274
285
  try {
275
- console.log(`${colors.cyan('●')} Checking for database connection...`);
286
+ console.log('Checking for database connection...');
276
287
  console.log('');
277
288
  const envCheck = checkEnvVars();
278
289
  let useEnv = false;
@@ -282,28 +293,28 @@ export async function initCommand(context) {
282
293
  let user = 'postgres';
283
294
  let password = '';
284
295
  if (envCheck.found) {
285
- console.log(`${colors.green('✓')} Found database environment variables:`);
296
+ console.log('Found database environment variables:');
286
297
  if (envCheck.vars.RELQ_PG_CONN_URL) {
287
- console.log(` ${colors.green('✓')} RELQ_PG_CONN_URL`);
298
+ console.log(' RELQ_PG_CONN_URL');
288
299
  }
289
300
  else {
290
301
  if (envCheck.vars.DATABASE_HOST)
291
- console.log(` ${colors.green('✓')} DATABASE_HOST: ${envCheck.vars.DATABASE_HOST}`);
302
+ console.log(` DATABASE_HOST: ${envCheck.vars.DATABASE_HOST}`);
292
303
  if (envCheck.vars.DATABASE_PORT)
293
- console.log(` ${colors.green('✓')} DATABASE_PORT: ${envCheck.vars.DATABASE_PORT}`);
304
+ console.log(` DATABASE_PORT: ${envCheck.vars.DATABASE_PORT}`);
294
305
  if (envCheck.vars.DATABASE_NAME)
295
- console.log(` ${colors.green('✓')} DATABASE_NAME: ${envCheck.vars.DATABASE_NAME}`);
306
+ console.log(` DATABASE_NAME: ${envCheck.vars.DATABASE_NAME}`);
296
307
  if (envCheck.vars.DATABASE_USER)
297
- console.log(` ${colors.green('✓')} DATABASE_USER: ${envCheck.vars.DATABASE_USER}`);
308
+ console.log(` DATABASE_USER: ${envCheck.vars.DATABASE_USER}`);
298
309
  if (envCheck.vars.DATABASE_PASSWORD)
299
- console.log(` ${colors.green('✓')} DATABASE_PASSWORD: ***`);
310
+ console.log(' DATABASE_PASSWORD: ***');
300
311
  }
301
312
  console.log('');
302
313
  useEnv = await askYesNo(rl, 'Use these environment variables?', true);
303
314
  console.log('');
304
315
  }
305
316
  if (!useEnv) {
306
- console.log(`${colors.cyan('●')} Database Connection`);
317
+ console.log('Database Connection');
307
318
  console.log('');
308
319
  host = await ask(rl, ' Host', 'localhost');
309
320
  port = await ask(rl, ' Port', '5432');
@@ -313,21 +324,21 @@ export async function initCommand(context) {
313
324
  console.log('');
314
325
  }
315
326
  const detectedPath = detectSchemaPath();
316
- console.log(`${colors.cyan('●')} Schema Configuration`);
327
+ console.log('Schema Configuration');
317
328
  console.log('');
318
329
  const schemaPath = await ask(rl, ' Schema file path', detectedPath);
319
330
  console.log('');
320
- console.log(`${colors.cyan('●')} Author (for commit history)`);
331
+ console.log('Author (for commit history)');
321
332
  console.log('');
322
333
  const author = await ask(rl, ' Name <email>', 'Developer <dev@example.com>');
323
334
  console.log('');
324
- console.log(`${colors.cyan('●')} Features to track`);
335
+ console.log('Features to track');
325
336
  console.log('');
326
337
  const includeFunctions = await askYesNo(rl, ' Include functions?', false);
327
338
  const includeTriggers = await askYesNo(rl, ' Include triggers?', false);
328
339
  console.log('');
329
340
  rl.close();
330
- console.log(`${colors.cyan('●')} Creating files...`);
341
+ console.log('Creating files...');
331
342
  console.log('');
332
343
  const configPath = path.join(cwd, 'relq.config.ts');
333
344
  if (!fs.existsSync(configPath)) {
@@ -344,26 +355,26 @@ export async function initCommand(context) {
344
355
  includeTriggers,
345
356
  });
346
357
  fs.writeFileSync(configPath, configContent, 'utf-8');
347
- console.log(` ${colors.green('✓')} Created ${colors.cyan('relq.config.ts')}`);
358
+ console.log(' Created relq.config.ts');
348
359
  }
349
360
  else {
350
- console.log(` ${colors.yellow('○')} ${colors.cyan('relq.config.ts')} already exists`);
361
+ console.log(' relq.config.ts already exists');
351
362
  }
352
363
  initRepository(cwd);
353
- console.log(` ${colors.green('✓')} Created ${colors.cyan('.relq/')} folder`);
364
+ console.log(' Created .relq/ folder');
354
365
  createDefaultRelqignore(cwd);
355
- console.log(` ${colors.green('✓')} Created ${colors.cyan('.relqignore')}`);
366
+ console.log(' Created .relqignore');
356
367
  const schemaDir = path.dirname(schemaPath);
357
368
  if (!fs.existsSync(schemaDir)) {
358
369
  fs.mkdirSync(schemaDir, { recursive: true });
359
- console.log(` ${colors.green('✓')} Created ${colors.cyan(schemaDir + '/')}`);
370
+ console.log(` Created ${schemaDir}/`);
360
371
  }
361
372
  const gitignorePath = path.join(cwd, '.gitignore');
362
373
  if (fs.existsSync(gitignorePath)) {
363
374
  const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
364
375
  if (!gitignore.includes('.relq')) {
365
376
  fs.appendFileSync(gitignorePath, '\n# Relq local state\n.relq/\n');
366
- console.log(` ${colors.green('✓')} Added ${colors.cyan('.relq/')} to ${colors.cyan('.gitignore')}`);
377
+ console.log(' Added .relq/ to .gitignore');
367
378
  }
368
379
  }
369
380
  console.log('');
@@ -382,20 +393,19 @@ export async function initCommand(context) {
382
393
  }
383
394
  catch (error) {
384
395
  spinner.fail('Could not connect to database');
385
- console.log(` ${colors.yellow('⚠')} ${colors.muted('Run relq pull after fixing connection')}`);
396
+ hint("run 'relq pull' after fixing connection");
386
397
  }
387
398
  console.log('');
388
- console.log(`${colors.green('✓')} ${colors.bold('Relq initialized successfully!')}`);
399
+ console.log('Relq initialized successfully!');
389
400
  console.log('');
390
- console.log(`${colors.bold('Next steps:')}`);
391
- console.log(` 1. Review ${colors.cyan('relq.config.ts')}`);
392
- console.log(` 2. Run ${colors.cyan('relq pull')} to sync with database`);
393
- console.log(` 3. Run ${colors.cyan('relq status')} to see current state`);
401
+ console.log('Next steps:');
402
+ console.log(" 1. Review 'relq.config.ts'");
403
+ console.log(" 2. Run 'relq pull' to sync with database");
404
+ console.log(" 3. Run 'relq status' to see current state");
394
405
  console.log('');
395
406
  }
396
407
  catch (error) {
397
408
  rl.close();
398
- console.error(colors.red(`Error: ${error instanceof Error ? error.message : error}`));
399
- process.exit(1);
409
+ fatal('Initialization failed', error instanceof Error ? error.message : String(error));
400
410
  }
401
411
  }
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as readline from 'readline';
3
3
  import { parseSqlToDefineTable } from "../../introspect/index.js";
4
+ import { fatal } from "../utils/cli-utils.js";
4
5
  function readStdin() {
5
6
  return new Promise((resolve) => {
6
7
  let data = '';
@@ -43,8 +44,7 @@ export async function introspectCommand(context) {
43
44
  let sql;
44
45
  if (filePath) {
45
46
  if (!fs.existsSync(filePath)) {
46
- console.error(`Error: File not found: ${filePath}`);
47
- process.exit(1);
47
+ fatal(`File not found: ${filePath}`, 'Check the file path and try again.');
48
48
  }
49
49
  sql = fs.readFileSync(filePath, 'utf-8');
50
50
  console.log(`📄 Reading from: ${filePath}\n`);
@@ -56,10 +56,7 @@ export async function introspectCommand(context) {
56
56
  sql = await readInteractive();
57
57
  }
58
58
  if (!sql.trim()) {
59
- console.error('Error: No SQL input provided.');
60
- console.error('Usage: relq introspect --file schema.sql');
61
- console.error(' or: cat schema.sql | relq introspect');
62
- process.exit(1);
59
+ fatal('No SQL input provided', 'Usage: relq introspect --file schema.sql\n or: cat schema.sql | relq introspect');
63
60
  }
64
61
  console.log('🔍 Parsing SQL...\n');
65
62
  try {
@@ -80,8 +77,7 @@ export async function introspectCommand(context) {
80
77
  console.log(output.join('\n'));
81
78
  }
82
79
  catch (error) {
83
- console.error('Error parsing SQL:', error instanceof Error ? error.message : error);
84
- process.exit(1);
80
+ fatal('Error parsing SQL', error instanceof Error ? error.message : String(error));
85
81
  }
86
82
  }
87
83
  function generateDefineTableCode(table) {
@@ -1,4 +1,4 @@
1
- import { colors } from "../utils/spinner.js";
1
+ import { colors, fatal, hint } from "../utils/cli-utils.js";
2
2
  import { isInitialized, getHead, getCommitHistory, shortHash, } from "../utils/repo-manager.js";
3
3
  function formatDate(timestamp) {
4
4
  const date = new Date(timestamp);
@@ -12,23 +12,18 @@ function formatDate(timestamp) {
12
12
  return `${dayName} ${monthName} ${day} ${time} ${year}`;
13
13
  }
14
14
  export async function logCommand(context) {
15
- const { flags } = context;
16
- const projectRoot = process.cwd();
15
+ const { flags, projectRoot } = context;
17
16
  const limit = parseInt(flags['n']) || 10;
18
17
  const oneline = flags['oneline'] === true;
19
18
  console.log('');
20
19
  if (!isInitialized(projectRoot)) {
21
- console.log(`${colors.red('fatal:')} not a relq repository`);
22
- console.log('');
23
- console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('to initialize.')}`);
24
- return;
20
+ fatal('not a relq repository (or any parent directories): .relq', "run 'relq init' to initialize");
25
21
  }
26
22
  const head = getHead(projectRoot);
27
23
  const commits = getCommitHistory(limit, projectRoot);
28
24
  if (commits.length === 0) {
29
- console.log(`${colors.muted('No commits yet.')}`);
30
- console.log('');
31
- console.log(`${colors.muted('Run')} ${colors.cyan('relq commit -m "message"')} ${colors.muted('to create first commit.')}`);
25
+ console.log('No commits yet.');
26
+ hint("run 'relq commit -m <message>' to create first commit");
32
27
  console.log('');
33
28
  return;
34
29
  }
@@ -57,8 +52,7 @@ export async function logCommand(context) {
57
52
  }
58
53
  }
59
54
  export async function showCommand(context) {
60
- const { args } = context;
61
- const projectRoot = process.cwd();
55
+ const { args, projectRoot } = context;
62
56
  const target = args[0];
63
57
  console.log('');
64
58
  if (!isInitialized(projectRoot)) {
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { colors, createSpinner } from "../utils/spinner.js";
3
+ import { colors, createSpinner, fatal } from "../utils/cli-utils.js";
4
4
  import { isInitialized, getHead, loadCommit, loadSnapshot, saveSnapshot, createCommit, shortHash, } from "../utils/repo-manager.js";
5
5
  function loadBranchState(projectRoot) {
6
6
  const branchPath = path.join(projectRoot, '.relq', 'branches.json');
@@ -15,12 +15,10 @@ function saveBranchState(state, projectRoot) {
15
15
  fs.writeFileSync(branchPath, JSON.stringify(state, null, 2));
16
16
  }
17
17
  export async function mergeCommand(context) {
18
- const { config, args, flags } = context;
19
- const projectRoot = process.cwd();
18
+ const { config, args, flags, projectRoot } = context;
20
19
  console.log('');
21
20
  if (!isInitialized(projectRoot)) {
22
- console.log(`${colors.red('fatal:')} not a relq repository`);
23
- return;
21
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
24
22
  }
25
23
  const branchName = args[0];
26
24
  const abort = flags['abort'] === true;
@@ -28,38 +26,27 @@ export async function mergeCommand(context) {
28
26
  if (abort) {
29
27
  if (fs.existsSync(mergeStatePath)) {
30
28
  fs.unlinkSync(mergeStatePath);
31
- console.log(`${colors.green('✓')} Merge aborted`);
29
+ console.log('Merge aborted');
30
+ console.log('');
31
+ return;
32
32
  }
33
33
  else {
34
- console.log(`${colors.muted('No merge in progress.')}`);
34
+ fatal('There is no merge to abort (MERGE_STATE missing)');
35
35
  }
36
- console.log('');
37
- return;
38
36
  }
39
37
  if (fs.existsSync(mergeStatePath)) {
40
38
  const mergeState = JSON.parse(fs.readFileSync(mergeStatePath, 'utf-8'));
41
- console.log(`${colors.red('error:')} Merge in progress from '${mergeState.fromBranch}'`);
42
- console.log('');
43
- console.log(`${colors.muted('Use')} ${colors.cyan('relq resolve')} ${colors.muted('to resolve conflicts')}`);
44
- console.log(`${colors.muted('Or')} ${colors.cyan('relq merge --abort')} ${colors.muted('to cancel')}`);
45
- console.log('');
46
- return;
39
+ fatal(`Merge in progress from '${mergeState.fromBranch}'`, `Use ${colors.cyan('relq resolve')} to resolve conflicts\nOr ${colors.cyan('relq merge --abort')} to cancel`);
47
40
  }
48
41
  if (!branchName) {
49
- console.log(`${colors.red('error:')} Please specify a branch to merge`);
50
- console.log('');
51
- console.log(`Usage: ${colors.cyan('relq merge <branch>')}`);
52
- console.log('');
53
- return;
42
+ fatal('Please specify a branch to merge', `Usage: ${colors.cyan('relq merge <branch>')}`);
54
43
  }
55
44
  const state = loadBranchState(projectRoot);
56
45
  if (!state.branches[branchName]) {
57
- console.log(`${colors.red('error:')} Branch not found: ${branchName}`);
58
- return;
46
+ fatal(`Branch not found: ${branchName}`, `Use ${colors.cyan('relq branch')} to list available branches.`);
59
47
  }
60
48
  if (branchName === state.current) {
61
- console.log(`${colors.red('error:')} Cannot merge branch into itself`);
62
- return;
49
+ fatal('Cannot merge branch into itself');
63
50
  }
64
51
  const spinner = createSpinner();
65
52
  spinner.start(`Merging '${branchName}' into '${state.current}'...`);
@@ -67,8 +54,8 @@ export async function mergeCommand(context) {
67
54
  const currentHash = getHead(projectRoot);
68
55
  const incomingHash = state.branches[branchName];
69
56
  if (!currentHash || !incomingHash) {
70
- spinner.fail('No commits to merge');
71
- return;
57
+ spinner.stop();
58
+ fatal('No commits to merge', `Run ${colors.cyan('relq pull')} first.`);
72
59
  }
73
60
  if (currentHash === incomingHash) {
74
61
  spinner.succeed('Already up to date');
@@ -78,14 +65,14 @@ export async function mergeCommand(context) {
78
65
  const currentCommit = loadCommit(currentHash, projectRoot);
79
66
  const incomingCommit = loadCommit(incomingHash, projectRoot);
80
67
  if (!currentCommit || !incomingCommit) {
81
- spinner.fail('Cannot load commit data');
82
- return;
68
+ spinner.stop();
69
+ fatal('Cannot load commit data - repository may be corrupt');
83
70
  }
84
71
  const currentSnapshot = loadSnapshot(projectRoot) || currentCommit.schema;
85
72
  const incomingSnapshot = incomingCommit.schema;
86
73
  if (!currentSnapshot || !incomingSnapshot) {
87
- spinner.fail('No snapshot data');
88
- return;
74
+ spinner.stop();
75
+ fatal('No snapshot data - repository may be corrupt');
89
76
  }
90
77
  const conflicts = detectMergeConflicts(currentSnapshot, incomingSnapshot);
91
78
  if (conflicts.length > 0) {
@@ -106,11 +93,7 @@ export async function mergeCommand(context) {
106
93
  if (conflicts.length > 5) {
107
94
  console.log(` ${colors.muted(`... and ${conflicts.length - 5} more`)}`);
108
95
  }
109
- console.log('');
110
- console.log(`${colors.muted('Use')} ${colors.cyan('relq resolve --all-theirs')} ${colors.muted('to accept incoming')}`);
111
- console.log(`${colors.muted('Or')} ${colors.cyan('relq merge --abort')} ${colors.muted('to cancel')}`);
112
- console.log('');
113
- return;
96
+ fatal(`Automatic merge failed; fix conflicts and then commit`, `Use ${colors.cyan('relq resolve --all-theirs')} to accept incoming\nOr ${colors.cyan('relq merge --abort')} to cancel`);
114
97
  }
115
98
  const mergedSnapshot = mergeSnapshots(currentSnapshot, incomingSnapshot);
116
99
  saveSnapshot(mergedSnapshot, projectRoot);
@@ -121,9 +104,9 @@ export async function mergeCommand(context) {
121
104
  console.log(` ${colors.yellow(shortHash(commit.hash))} ${message}`);
122
105
  console.log('');
123
106
  }
124
- catch (error) {
107
+ catch (err) {
125
108
  spinner.fail('Merge failed');
126
- console.error(colors.red(`Error: ${error instanceof Error ? error.message : error}`));
109
+ fatal(err instanceof Error ? err.message : String(err));
127
110
  }
128
111
  }
129
112
  function detectMergeConflicts(current, incoming) {