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,76 +1,56 @@
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";
4
- import { isInitialized, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, getRepoStatus, } from "../utils/repo-manager.js";
3
+ import { colors, createSpinner, fatal, success } from "../utils/cli-utils.js";
4
+ import { isInitialized, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, } from "../utils/repo-manager.js";
5
5
  import { pullCommand } from "./pull.js";
6
6
  export async function syncCommand(context) {
7
7
  const { config, flags } = context;
8
8
  if (!config) {
9
- console.error('Error: No configuration found.');
10
- process.exit(1);
9
+ fatal('No configuration found', `Run ${colors.cyan('relq init')} to create one.`);
11
10
  }
12
- requireValidConfig(config);
11
+ await requireValidConfig(config, { calledFrom: 'sync' });
13
12
  const connection = config.connection;
14
- const projectRoot = process.cwd();
13
+ const { projectRoot } = context;
15
14
  console.log('');
16
15
  if (!isInitialized(projectRoot)) {
17
- console.log(`${colors.red('fatal:')} not a relq repository`);
18
- console.log('');
19
- console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('to initialize.')}`);
20
- return;
16
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
21
17
  }
22
18
  const spinner = createSpinner();
23
19
  try {
24
- spinner.start('Checking sync status...');
25
- const status = await getRepoStatus(connection, projectRoot);
26
- spinner.stop();
27
- console.log(`On database: ${colors.cyan(getConnectionDescription(connection))}`);
20
+ console.log(`Syncing with ${colors.cyan(getConnectionDescription(connection))}...`);
28
21
  console.log('');
29
- if (status.behindBy > 0) {
30
- console.log(`${colors.yellow('↓')} ${status.behindBy} commit(s) to pull`);
31
- }
32
- if (status.aheadBy > 0) {
33
- console.log(`${colors.green('↑')} ${status.aheadBy} commit(s) to push`);
34
- }
35
- if (status.behindBy === 0 && status.aheadBy === 0) {
36
- console.log(`${colors.green('✓')} Already in sync`);
22
+ await pullCommand(context);
23
+ await ensureRemoteTable(connection);
24
+ const remoteCommits = await fetchRemoteCommits(connection, 100);
25
+ const remoteHashes = new Set(remoteCommits.map(c => c.hash));
26
+ const localCommitsAfter = getAllCommits(projectRoot);
27
+ const toPush = localCommitsAfter.filter(c => !remoteHashes.has(c.hash));
28
+ if (toPush.length === 0) {
29
+ success('Sync complete - up to date');
37
30
  console.log('');
38
31
  return;
39
32
  }
33
+ console.log(`Pushing ${toPush.length} local commit(s)...`);
40
34
  console.log('');
41
- if (status.behindBy > 0) {
42
- console.log(`${colors.cyan('●')} Pulling changes...`);
43
- console.log('');
44
- const pullContext = {
45
- ...context,
46
- flags: { ...flags, force: true },
47
- };
48
- await pullCommand(pullContext);
35
+ spinner.start(`Pushing ${toPush.length} commit(s)...`);
36
+ for (const commit of toPush.reverse()) {
37
+ await pushCommit(connection, commit);
49
38
  }
50
- if (status.aheadBy > 0) {
51
- console.log(`${colors.cyan('')} Pushing commits...`);
52
- console.log('');
53
- await ensureRemoteTable(connection);
54
- const localCommits = getAllCommits(projectRoot);
55
- const remoteCommits = await fetchRemoteCommits(connection, 100);
56
- const remoteHashes = new Set(remoteCommits.map(c => c.hash));
57
- const toPush = localCommits.filter(c => !remoteHashes.has(c.hash));
58
- spinner.start(`Pushing ${toPush.length} commit(s)...`);
59
- for (const commit of toPush.reverse()) {
60
- await pushCommit(connection, commit);
61
- }
62
- spinner.succeed(`Pushed ${toPush.length} commit(s)`);
39
+ spinner.succeed(`Pushed ${toPush.length} commit(s)`);
40
+ console.log('');
41
+ console.log('Pushed commits:');
42
+ for (const commit of toPush.slice(0, 5)) {
43
+ console.log(` ${colors.yellow(shortHash(commit.hash))} ${commit.message}`);
44
+ }
45
+ if (toPush.length > 5) {
46
+ console.log(` ${colors.muted(`... and ${toPush.length - 5} more`)}`);
63
47
  }
64
48
  console.log('');
65
- const pulledText = status.behindBy > 0 ? `${colors.yellow(`↓ ${status.behindBy}`)} pulled` : '';
66
- const pushedText = status.aheadBy > 0 ? `${colors.green(`↑ ${status.aheadBy}`)} pushed` : '';
67
- const separator = pulledText && pushedText ? ' | ' : '';
68
- console.log(`${colors.green('✓')} Sync complete: ${pulledText}${separator}${pushedText}`);
49
+ success(`Sync complete - pushed ${toPush.length} commit(s)`);
69
50
  }
70
- catch (error) {
51
+ catch (err) {
71
52
  spinner.fail('Sync failed');
72
- console.error(colors.red(`Error: ${error instanceof Error ? error.message : error}`));
73
- process.exit(1);
53
+ fatal(err instanceof Error ? err.message : String(err));
74
54
  }
75
55
  console.log('');
76
56
  }
@@ -14,8 +14,7 @@ function saveTags(tags, projectRoot) {
14
14
  fs.writeFileSync(tagPath, JSON.stringify(tags, null, 2));
15
15
  }
16
16
  export async function tagCommand(context) {
17
- const { args, flags } = context;
18
- const projectRoot = process.cwd();
17
+ const { args, flags, projectRoot } = context;
19
18
  console.log('');
20
19
  if (!isInitialized(projectRoot)) {
21
20
  console.log(`${colors.red('fatal:')} not a relq repository`);
@@ -39,7 +38,7 @@ export async function tagCommand(context) {
39
38
  }
40
39
  delete tags[tagName];
41
40
  saveTags(tags, projectRoot);
42
- console.log(`${colors.green('✓')} Deleted tag '${tagName}'`);
41
+ console.log(`Deleted tag '${tagName}'`);
43
42
  console.log('');
44
43
  return;
45
44
  }
@@ -87,7 +86,7 @@ export async function tagCommand(context) {
87
86
  createdAt: new Date().toISOString(),
88
87
  };
89
88
  saveTags(tags, projectRoot);
90
- console.log(`${colors.green('✓')} Tagged ${colors.yellow(shortHash(hash))} as '${colors.cyan(tagName)}'`);
89
+ console.log(`Tagged ${colors.yellow(shortHash(hash))} as '${tagName}'`);
91
90
  console.log(` ${commit.message}`);
92
91
  console.log('');
93
92
  return;
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { initCommand } from "./commands/init.js";
3
2
  import { pullCommand } from "./commands/pull.js";
4
3
  import { pushCommand } from "./commands/push.js";
@@ -22,6 +21,34 @@ import { mergeCommand } from "./commands/merge.js";
22
21
  import { tagCommand } from "./commands/tag.js";
23
22
  import { cherryPickCommand } from "./commands/cherry-pick.js";
24
23
  import { remoteCommand } from "./commands/remote.js";
24
+ import * as fs from 'fs';
25
+ import * as path from 'path';
26
+ function loadEnvFile() {
27
+ let currentDir = process.cwd();
28
+ const root = path.parse(currentDir).root;
29
+ while (currentDir !== root) {
30
+ const envPath = path.join(currentDir, '.env');
31
+ if (fs.existsSync(envPath)) {
32
+ const envContent = fs.readFileSync(envPath, 'utf-8');
33
+ for (const line of envContent.split('\n')) {
34
+ const trimmed = line.trim();
35
+ if (!trimmed || trimmed.startsWith('#'))
36
+ continue;
37
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
38
+ if (match) {
39
+ const key = match[1].trim();
40
+ const value = match[2].trim().replace(/^["']|["']$/g, '');
41
+ if (!process.env[key]) {
42
+ process.env[key] = value;
43
+ }
44
+ }
45
+ }
46
+ return;
47
+ }
48
+ currentDir = path.dirname(currentDir);
49
+ }
50
+ }
51
+ loadEnvFile();
25
52
  const VERSION = '1.1.0';
26
53
  function parseArgs(argv) {
27
54
  const args = [];
@@ -149,20 +176,56 @@ async function main() {
149
176
  process.exit(1);
150
177
  }
151
178
  let config = null;
179
+ let resolvedProjectRoot = process.cwd();
152
180
  if (requiresConfig(command)) {
153
181
  const configPath = flags.config;
154
182
  try {
155
183
  const { loadConfigWithEnv, findConfigFileRecursive } = await import("./utils/config-loader.js");
156
- const foundConfig = configPath ? configPath : findConfigFileRecursive()?.path;
184
+ const { findProjectRoot } = await import("./utils/project-root.js");
185
+ const configResult = configPath ? { path: configPath, directory: path.dirname(path.resolve(configPath)) } : findConfigFileRecursive();
186
+ if (!configResult) {
187
+ const projectRoot = findProjectRoot();
188
+ if (!projectRoot) {
189
+ const colors = {
190
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
191
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
192
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
193
+ };
194
+ console.error('');
195
+ console.error(colors.red('fatal:') + ' not a relq project (or any of the parent directories)');
196
+ console.error('');
197
+ console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} in your project directory to initialize relq.`);
198
+ console.error('');
199
+ process.exit(128);
200
+ }
201
+ }
202
+ resolvedProjectRoot = configResult?.directory || findProjectRoot() || process.cwd();
203
+ const foundConfig = configResult?.path;
157
204
  if (!foundConfig) {
158
- console.error('Error: relq.config.ts not found.');
159
- console.error('Run "relq init" to create one or use --config to specify a path.');
205
+ const colors = {
206
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
207
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
208
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
209
+ };
210
+ console.error('');
211
+ console.error(colors.red('error:') + ' relq.config.ts not found');
212
+ console.error('');
213
+ console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} to create one or use ${colors.cyan('--config')} to specify a path.`);
214
+ console.error('');
160
215
  process.exit(1);
161
216
  }
162
217
  config = await loadConfigWithEnv(configPath);
163
218
  if (requiresDbConnection(command, flags) && !config.connection?.host && !config.connection?.url) {
164
- console.error('Error: No database connection configured.');
165
- console.error('Run "relq init" or set DATABASE_* environment variables.');
219
+ const colors = {
220
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
221
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
222
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
223
+ };
224
+ console.error('');
225
+ console.error(colors.red('error:') + ' No database connection configured');
226
+ console.error('');
227
+ console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} or set DATABASE_* environment variables.`);
228
+ console.error('');
166
229
  process.exit(1);
167
230
  }
168
231
  }
@@ -171,7 +234,7 @@ async function main() {
171
234
  process.exit(1);
172
235
  }
173
236
  }
174
- const context = { config, args, flags };
237
+ const context = { config, args, flags, projectRoot: resolvedProjectRoot };
175
238
  try {
176
239
  switch (command) {
177
240
  case 'init':
@@ -261,7 +324,7 @@ async function main() {
261
324
  theirs: Boolean(flags.theirs),
262
325
  ours: Boolean(flags.ours),
263
326
  abort: Boolean(flags.abort),
264
- });
327
+ }, resolvedProjectRoot);
265
328
  break;
266
329
  case 'export':
267
330
  await exportCommand(context);
@@ -281,4 +344,4 @@ async function main() {
281
344
  process.exit(1);
282
345
  }
283
346
  }
284
- main().catch(console.error);
347
+ export { main };
@@ -31,6 +31,12 @@ export function generateChangeSQL(change) {
31
31
  return generateConstraintSQL(change);
32
32
  case 'PARTITION':
33
33
  return generatePartitionSQL(change);
34
+ case 'PARTITION_CHILD':
35
+ return generatePartitionChildSQL(change);
36
+ case 'TABLE_COMMENT':
37
+ return generateTableCommentSQL(change);
38
+ case 'COLUMN_COMMENT':
39
+ return generateColumnCommentSQL(change);
34
40
  case 'VIEW':
35
41
  return generateViewSQL(change);
36
42
  case 'MATERIALIZED_VIEW':
@@ -44,6 +50,8 @@ export function generateChangeSQL(change) {
44
50
  return generateForeignServerSQL(change);
45
51
  case 'FOREIGN_TABLE':
46
52
  return generateForeignTableSQL(change);
53
+ case 'COLLATION':
54
+ return generateCollationSQL(change);
47
55
  default:
48
56
  return `-- Unsupported object type: ${change.objectType}`;
49
57
  }
@@ -144,8 +152,11 @@ function generateTableSQL(change) {
144
152
  }
145
153
  const allDefs = [...colDefs, ...constraintDefs].join(',\n');
146
154
  let sql = `CREATE TABLE "${data.name}" (\n${allDefs}\n)`;
147
- if (data.isPartitioned && data.partitionType && data.partitionKey?.length) {
148
- sql += ` PARTITION BY ${data.partitionType} (${data.partitionKey.join(', ')})`;
155
+ if (data.isPartitioned && data.partitionType && data.partitionKey) {
156
+ const keyArr = Array.isArray(data.partitionKey) ? data.partitionKey : [data.partitionKey];
157
+ if (keyArr.length) {
158
+ sql += ` PARTITION BY ${data.partitionType} (${keyArr.join(', ')})`;
159
+ }
149
160
  }
150
161
  return sql + ';';
151
162
  }
@@ -226,6 +237,9 @@ function generateConstraintSQL(change) {
226
237
  return '';
227
238
  }
228
239
  function generatePartitionSQL(change) {
240
+ return '';
241
+ }
242
+ function generatePartitionChildSQL(change) {
229
243
  const data = change.after;
230
244
  if (change.type === 'CREATE' && data) {
231
245
  return `CREATE TABLE "${data.name}" PARTITION OF "${data.parentTable}" ${data.bound};`;
@@ -235,6 +249,33 @@ function generatePartitionSQL(change) {
235
249
  }
236
250
  return '';
237
251
  }
252
+ function generateTableCommentSQL(change) {
253
+ const data = change.after;
254
+ if ((change.type === 'CREATE' || change.type === 'ALTER') && data) {
255
+ const escaped = data.comment.replace(/'/g, "''");
256
+ return `COMMENT ON TABLE "${data.tableName}" IS '${escaped}';`;
257
+ }
258
+ else if (change.type === 'DROP') {
259
+ const beforeData = change.before;
260
+ const tableName = beforeData?.tableName || change.objectName;
261
+ return `COMMENT ON TABLE "${tableName}" IS NULL;`;
262
+ }
263
+ return '';
264
+ }
265
+ function generateColumnCommentSQL(change) {
266
+ const data = change.after;
267
+ if ((change.type === 'CREATE' || change.type === 'ALTER') && data) {
268
+ const escaped = data.comment.replace(/'/g, "''");
269
+ return `COMMENT ON COLUMN "${data.tableName}"."${data.columnName}" IS '${escaped}';`;
270
+ }
271
+ else if (change.type === 'DROP') {
272
+ const beforeData = change.before;
273
+ const tableName = beforeData?.tableName || change.parentName || '';
274
+ const columnName = beforeData?.columnName || change.objectName;
275
+ return `COMMENT ON COLUMN "${tableName}"."${columnName}" IS NULL;`;
276
+ }
277
+ return '';
278
+ }
238
279
  function generateViewSQL(change) {
239
280
  const data = change.after;
240
281
  if (change.type === 'CREATE' && data) {
@@ -320,6 +361,28 @@ function generateForeignTableSQL(change) {
320
361
  }
321
362
  return '';
322
363
  }
364
+ function generateCollationSQL(change) {
365
+ const data = change.after;
366
+ if (change.type === 'CREATE' && data) {
367
+ const options = [];
368
+ if (data.locale)
369
+ options.push(`LOCALE = '${data.locale}'`);
370
+ if (data.lcCollate)
371
+ options.push(`LC_COLLATE = '${data.lcCollate}'`);
372
+ if (data.lcCtype)
373
+ options.push(`LC_CTYPE = '${data.lcCtype}'`);
374
+ if (data.provider)
375
+ options.push(`PROVIDER = ${data.provider}`);
376
+ if (data.deterministic !== undefined) {
377
+ options.push(`DETERMINISTIC = ${data.deterministic ? 'TRUE' : 'FALSE'}`);
378
+ }
379
+ return `CREATE COLLATION IF NOT EXISTS "${data.name}" (${options.join(', ')});`;
380
+ }
381
+ else if (change.type === 'DROP') {
382
+ return `DROP COLLATION IF EXISTS "${change.objectName}";`;
383
+ }
384
+ return '';
385
+ }
323
386
  export function createChange(type, objectType, objectName, before, after, parentName) {
324
387
  const id = generateChangeId(type, objectType, objectName, parentName);
325
388
  const change = {
@@ -341,6 +404,42 @@ export function getChangeDisplayName(change) {
341
404
  change.type === 'DROP' ? '-' :
342
405
  change.type === 'ALTER' ? '~' :
343
406
  '>';
407
+ if (change.objectType === 'INDEX') {
408
+ const data = change.after;
409
+ const tableName = data?.tableName || change.parentName;
410
+ if (tableName) {
411
+ return `${prefix} ${change.objectType} ${change.objectName} on ${tableName}`;
412
+ }
413
+ }
414
+ if (change.objectType === 'PARTITION') {
415
+ const data = change.after;
416
+ if (data?.tableName && data?.type && data?.key) {
417
+ let keyStr = Array.isArray(data.key) ? data.key.join(', ') : data.key;
418
+ keyStr = keyStr.replace(/[{}]/g, '');
419
+ return `${prefix} PARTITIONED ${data.tableName} by ${data.type.toUpperCase()}(${keyStr})`;
420
+ }
421
+ }
422
+ if (change.objectType === 'PARTITION_CHILD') {
423
+ const data = change.after;
424
+ if (data?.name && data?.parentTable) {
425
+ const bound = data.bound || '';
426
+ return `${prefix} PARTITION ${data.name} of ${data.parentTable} ${bound}`;
427
+ }
428
+ }
429
+ if (change.objectType === 'CHECK') {
430
+ const actionWord = change.type === 'CREATE' ? 'ADD' : change.type === 'DROP' ? 'DROP' : 'ALTER';
431
+ if (change.parentName) {
432
+ return `${prefix} ${actionWord} CHECK ${change.objectName} on ${change.parentName}`;
433
+ }
434
+ return `${prefix} ${actionWord} CHECK ${change.objectName}`;
435
+ }
436
+ if (change.objectType === 'PRIMARY_KEY' || change.objectType === 'FOREIGN_KEY' || change.objectType === 'EXCLUSION') {
437
+ const actionWord = change.type === 'CREATE' ? 'ADD' : change.type === 'DROP' ? 'DROP' : 'ALTER';
438
+ if (change.parentName) {
439
+ return `${prefix} ${actionWord} ${change.objectType.replace('_', ' ')} ${change.objectName} on ${change.parentName}`;
440
+ }
441
+ return `${prefix} ${actionWord} ${change.objectType.replace('_', ' ')} ${change.objectName}`;
442
+ }
344
443
  if (change.parentName) {
345
444
  return `${prefix} ${change.objectType} ${change.parentName}.${change.objectName}`;
346
445
  }
@@ -350,14 +449,18 @@ export function sortChangesByDependency(changes) {
350
449
  const order = {
351
450
  'EXTENSION': 1,
352
451
  'SCHEMA': 2,
452
+ 'SCHEMA_FILE': 0,
353
453
  'ENUM': 3,
354
454
  'DOMAIN': 4,
355
455
  'COMPOSITE_TYPE': 5,
356
456
  'SEQUENCE': 6,
357
457
  'FOREIGN_SERVER': 7,
358
458
  'TABLE': 10,
459
+ 'TABLE_COMMENT': 10,
359
460
  'PARTITION': 11,
360
- 'COLUMN': 12,
461
+ 'PARTITION_CHILD': 12,
462
+ 'COLUMN': 13,
463
+ 'COLUMN_COMMENT': 12,
361
464
  'INDEX': 13,
362
465
  'CONSTRAINT': 14,
363
466
  'PRIMARY_KEY': 14,
@@ -370,6 +473,7 @@ export function sortChangesByDependency(changes) {
370
473
  'PROCEDURE': 31,
371
474
  'TRIGGER': 40,
372
475
  'ENUM_VALUE': 3,
476
+ 'COLLATION': 4,
373
477
  'FOREIGN_TABLE': 8,
374
478
  };
375
479
  return [...changes].sort((a, b) => {
@@ -0,0 +1,169 @@
1
+ import * as readline from 'readline';
2
+ const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR;
3
+ export const colors = {
4
+ reset: '\x1b[0m',
5
+ red: (s) => isColorSupported ? `\x1b[31m${s}\x1b[0m` : s,
6
+ green: (s) => isColorSupported ? `\x1b[32m${s}\x1b[0m` : s,
7
+ yellow: (s) => isColorSupported ? `\x1b[33m${s}\x1b[0m` : s,
8
+ blue: (s) => isColorSupported ? `\x1b[34m${s}\x1b[0m` : s,
9
+ magenta: (s) => isColorSupported ? `\x1b[35m${s}\x1b[0m` : s,
10
+ cyan: (s) => isColorSupported ? `\x1b[36m${s}\x1b[0m` : s,
11
+ white: (s) => isColorSupported ? `\x1b[37m${s}\x1b[0m` : s,
12
+ gray: (s) => isColorSupported ? `\x1b[90m${s}\x1b[0m` : s,
13
+ bold: (s) => isColorSupported ? `\x1b[1m${s}\x1b[0m` : s,
14
+ dim: (s) => isColorSupported ? `\x1b[2m${s}\x1b[0m` : s,
15
+ muted: (s) => isColorSupported ? `\x1b[90m${s}\x1b[0m` : s,
16
+ success: (s) => isColorSupported ? `\x1b[32m${s}\x1b[0m` : s,
17
+ error: (s) => isColorSupported ? `\x1b[31m${s}\x1b[0m` : s,
18
+ warning: (s) => isColorSupported ? `\x1b[33m${s}\x1b[0m` : s,
19
+ info: (s) => isColorSupported ? `\x1b[34m${s}\x1b[0m` : s,
20
+ };
21
+ export function createSpinner() {
22
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
23
+ let frameIndex = 0;
24
+ let interval = null;
25
+ let currentMessage = '';
26
+ let isSpinning = false;
27
+ const isTTY = process.stdout.isTTY;
28
+ const clearLine = () => {
29
+ if (isTTY)
30
+ process.stdout.write('\r\x1b[K');
31
+ };
32
+ const render = () => {
33
+ if (!isSpinning)
34
+ return;
35
+ clearLine();
36
+ process.stdout.write(`${colors.cyan(frames[frameIndex])} ${currentMessage}`);
37
+ frameIndex = (frameIndex + 1) % frames.length;
38
+ };
39
+ return {
40
+ start(message) {
41
+ if (isSpinning)
42
+ this.stop();
43
+ currentMessage = message;
44
+ isSpinning = true;
45
+ frameIndex = 0;
46
+ if (isTTY) {
47
+ interval = setInterval(render, 80);
48
+ render();
49
+ }
50
+ else {
51
+ console.log(`${message}...`);
52
+ }
53
+ },
54
+ update(message) {
55
+ currentMessage = message;
56
+ },
57
+ succeed(message) {
58
+ this.stop();
59
+ console.log(message || currentMessage);
60
+ },
61
+ fail(message) {
62
+ this.stop();
63
+ },
64
+ info(message) {
65
+ this.stop();
66
+ console.log(message || currentMessage);
67
+ },
68
+ warn(message) {
69
+ this.stop();
70
+ warning(message || currentMessage);
71
+ },
72
+ stop() {
73
+ if (interval) {
74
+ clearInterval(interval);
75
+ interval = null;
76
+ }
77
+ if (isSpinning && isTTY) {
78
+ clearLine();
79
+ }
80
+ isSpinning = false;
81
+ }
82
+ };
83
+ }
84
+ export function fatal(message, hintMessage) {
85
+ console.error(`${colors.red('fatal:')} ${message}`);
86
+ if (hintMessage) {
87
+ hint(hintMessage);
88
+ }
89
+ process.exit(1);
90
+ }
91
+ export function error(message, hintMessage) {
92
+ console.error(`${colors.red('error:')} ${message}`);
93
+ if (hintMessage) {
94
+ hint(hintMessage);
95
+ }
96
+ }
97
+ export function warning(message) {
98
+ console.error(`${colors.yellow('warning:')} ${message}`);
99
+ }
100
+ export function hint(message) {
101
+ console.error(`${colors.yellow('hint:')} ${message}`);
102
+ }
103
+ export function success(message) {
104
+ console.log(colors.green(message));
105
+ }
106
+ export function confirm(question, defaultYes = true) {
107
+ const rl = readline.createInterface({
108
+ input: process.stdin,
109
+ output: process.stdout,
110
+ });
111
+ const suffix = defaultYes ? '[Y/n]' : '[y/N]';
112
+ return new Promise((resolve) => {
113
+ rl.question(`${question} ${suffix} `, (answer) => {
114
+ rl.close();
115
+ const a = answer.trim().toLowerCase();
116
+ if (!a)
117
+ resolve(defaultYes);
118
+ else
119
+ resolve(a === 'y' || a === 'yes');
120
+ });
121
+ });
122
+ }
123
+ export function select(question, options) {
124
+ const rl = readline.createInterface({
125
+ input: process.stdin,
126
+ output: process.stdout,
127
+ });
128
+ console.log(question);
129
+ options.forEach((opt, i) => {
130
+ console.log(` ${i + 1}) ${opt}`);
131
+ });
132
+ return new Promise((resolve) => {
133
+ rl.question(`Select [1-${options.length}]: `, (answer) => {
134
+ rl.close();
135
+ const num = parseInt(answer.trim(), 10);
136
+ if (num >= 1 && num <= options.length) {
137
+ resolve(num - 1);
138
+ }
139
+ else {
140
+ resolve(0);
141
+ }
142
+ });
143
+ });
144
+ }
145
+ export function formatBytes(bytes) {
146
+ if (bytes < 1024)
147
+ return `${bytes} B`;
148
+ if (bytes < 1024 * 1024)
149
+ return `${(bytes / 1024).toFixed(1)} KB`;
150
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
151
+ }
152
+ export function formatDuration(ms) {
153
+ if (ms < 1000)
154
+ return `${ms}ms`;
155
+ if (ms < 60000)
156
+ return `${(ms / 1000).toFixed(1)}s`;
157
+ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
158
+ }
159
+ export function progressBar(current, total, width = 30) {
160
+ const percentage = Math.min(100, Math.round((current / total) * 100));
161
+ const filled = Math.round((percentage / 100) * width);
162
+ const empty = width - filled;
163
+ return `${'#'.repeat(filled)}${'.'.repeat(empty)} ${percentage}%`;
164
+ }
165
+ export function requireInit(isInitialized, projectRoot) {
166
+ if (!isInitialized) {
167
+ fatal('not a relq repository (or any of the parent directories): .relq', "run 'relq init' to initialize a repository");
168
+ }
169
+ }
@@ -64,7 +64,7 @@ export function validateConfig(config) {
64
64
  if (!config.connection?.host && !config.connection?.url) {
65
65
  errors.push('No database connection configured. Set connection in relq.config.ts or use DATABASE_* env vars.');
66
66
  }
67
- const hasSchemaPath = typeof config.schema === 'string';
67
+ const hasSchemaPath = typeof config.schema === 'string' && config.schema.length > 0;
68
68
  const hasSchemaDir = typeof config.schema === 'object' && config.schema?.directory;
69
69
  const hasTypeGenOutput = config.typeGeneration?.output;
70
70
  const hasGenerateOutDir = config.generate?.outDir;
@@ -73,14 +73,40 @@ export function validateConfig(config) {
73
73
  }
74
74
  return errors;
75
75
  }
76
- export function requireValidConfig(config) {
76
+ export async function requireValidConfig(config, options) {
77
77
  const errors = validateConfig(config);
78
- if (errors.length > 0) {
79
- console.error('Configuration errors:');
80
- for (const error of errors) {
81
- console.error(` • ${error}`);
78
+ if (errors.length === 0)
79
+ return;
80
+ const colors = {
81
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
82
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
83
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
84
+ bold: (s) => `\x1b[1m${s}\x1b[0m`,
85
+ };
86
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
87
+ if (options?.autoComplete !== false && isInteractive) {
88
+ try {
89
+ const { initCommand } = await import("../commands/init.js");
90
+ const { findProjectRoot } = await import("./project-root.js");
91
+ const projectRoot = findProjectRoot() || process.cwd();
92
+ const flags = {};
93
+ if (options?.calledFrom) {
94
+ flags['called-from'] = options.calledFrom;
95
+ }
96
+ await initCommand({ args: [], flags, config, projectRoot });
97
+ return;
82
98
  }
83
- console.error('\nRun "relq init" to create a configuration file.');
84
- process.exit(1);
99
+ catch (e) {
100
+ process.exit(1);
101
+ }
102
+ }
103
+ console.error('');
104
+ console.error(colors.red('error:') + ' Configuration errors:');
105
+ for (const error of errors) {
106
+ console.error(` ${colors.yellow('•')} ${error}`);
85
107
  }
108
+ console.error('');
109
+ console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} to create a configuration file or check your config settings.`);
110
+ console.error('');
111
+ process.exit(1);
86
112
  }