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,3 +1,15 @@
1
+ function parseOptionsArray(options) {
2
+ if (!options)
3
+ return {};
4
+ const result = {};
5
+ for (const opt of options) {
6
+ const eqIdx = opt.indexOf('=');
7
+ if (eqIdx > 0) {
8
+ result[opt.substring(0, eqIdx)] = opt.substring(eqIdx + 1);
9
+ }
10
+ }
11
+ return result;
12
+ }
1
13
  export async function fastIntrospectDatabase(connection, onProgress, options) {
2
14
  const { includeFunctions = false, includeTriggers = false } = options || {};
3
15
  const { Pool } = await import("../../addon/pg/index.js");
@@ -218,7 +230,7 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
218
230
  const tables = [];
219
231
  for (const row of tablesResult.rows) {
220
232
  const tableName = row.table_name;
221
- if (tableName.startsWith('_relq'))
233
+ if (tableName.startsWith('_relq') || tableName.startsWith('_kuery'))
222
234
  continue;
223
235
  tables.push({
224
236
  name: tableName,
@@ -340,6 +352,99 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
340
352
  isEnabled: t.is_enabled,
341
353
  }));
342
354
  }
355
+ onProgress?.('fetching_collations');
356
+ const collationsResult = await pool.query(`
357
+ SELECT
358
+ c.collname as name,
359
+ n.nspname as schema,
360
+ c.collprovider as provider,
361
+ c.collcollate as lc_collate,
362
+ c.collctype as lc_ctype,
363
+ c.collisdeterministic as deterministic
364
+ FROM pg_collation c
365
+ JOIN pg_namespace n ON c.collnamespace = n.oid
366
+ WHERE n.nspname = 'public'
367
+ ORDER BY c.collname;
368
+ `);
369
+ const collations = collationsResult.rows.map(c => ({
370
+ name: c.name,
371
+ schema: c.schema,
372
+ provider: c.provider === 'i' ? 'icu' : c.provider === 'c' ? 'libc' : 'default',
373
+ lcCollate: c.lc_collate,
374
+ lcCtype: c.lc_ctype,
375
+ deterministic: c.deterministic,
376
+ }));
377
+ onProgress?.('fetching_foreign_servers');
378
+ const foreignServersResult = await pool.query(`
379
+ SELECT
380
+ s.srvname as name,
381
+ f.fdwname as fdw,
382
+ s.srvoptions as options
383
+ FROM pg_foreign_server s
384
+ JOIN pg_foreign_data_wrapper f ON s.srvfdw = f.oid
385
+ ORDER BY s.srvname;
386
+ `);
387
+ const foreignServers = foreignServersResult.rows.map(s => ({
388
+ name: s.name,
389
+ foreignDataWrapper: s.fdw,
390
+ options: parseOptionsArray(s.options),
391
+ }));
392
+ onProgress?.('fetching_foreign_tables');
393
+ const foreignTablesResult = await pool.query(`
394
+ SELECT
395
+ c.relname as name,
396
+ n.nspname as schema,
397
+ s.srvname as server_name,
398
+ ft.ftoptions as options
399
+ FROM pg_foreign_table ft
400
+ JOIN pg_class c ON ft.ftrelid = c.oid
401
+ JOIN pg_namespace n ON c.relnamespace = n.oid
402
+ JOIN pg_foreign_server s ON ft.ftserver = s.oid
403
+ WHERE n.nspname = 'public'
404
+ ORDER BY c.relname;
405
+ `);
406
+ const foreignTableNames = foreignTablesResult.rows.map(t => t.name);
407
+ let foreignTableColumns = new Map();
408
+ if (foreignTableNames.length > 0) {
409
+ const ftColsResult = await pool.query(`
410
+ SELECT
411
+ c.relname as table_name,
412
+ a.attname as column_name,
413
+ pg_catalog.format_type(a.atttypid, a.atttypmod) as data_type
414
+ FROM pg_attribute a
415
+ JOIN pg_class c ON a.attrelid = c.oid
416
+ JOIN pg_namespace n ON c.relnamespace = n.oid
417
+ WHERE n.nspname = 'public'
418
+ AND c.relname = ANY($1)
419
+ AND a.attnum > 0
420
+ AND NOT a.attisdropped
421
+ ORDER BY c.relname, a.attnum;
422
+ `, [foreignTableNames]);
423
+ for (const row of ftColsResult.rows) {
424
+ const cols = foreignTableColumns.get(row.table_name) || [];
425
+ cols.push({ name: row.column_name, type: row.data_type });
426
+ foreignTableColumns.set(row.table_name, cols);
427
+ }
428
+ }
429
+ const foreignTables = foreignTablesResult.rows.map(t => ({
430
+ name: t.name,
431
+ schema: t.schema,
432
+ serverName: t.server_name,
433
+ columns: (foreignTableColumns.get(t.name) || []).map((c, i) => ({
434
+ name: c.name,
435
+ dataType: c.type,
436
+ isNullable: true,
437
+ defaultValue: null,
438
+ isPrimaryKey: false,
439
+ isUnique: false,
440
+ ordinalPosition: i + 1,
441
+ maxLength: null,
442
+ precision: null,
443
+ scale: null,
444
+ references: null,
445
+ })),
446
+ options: parseOptionsArray(t.options),
447
+ }));
343
448
  onProgress?.('complete');
344
449
  return {
345
450
  tables,
@@ -347,12 +452,13 @@ export async function fastIntrospectDatabase(connection, onProgress, options) {
347
452
  domains: [],
348
453
  compositeTypes: [],
349
454
  sequences: [],
455
+ collations,
350
456
  functions,
351
457
  triggers,
352
458
  policies: [],
353
459
  partitions,
354
- foreignServers: [],
355
- foreignTables: [],
460
+ foreignServers,
461
+ foreignTables,
356
462
  extensions,
357
463
  };
358
464
  }
@@ -1,35 +1,8 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { isInitialized, getStagedChanges, getUnstagedChanges, getHead, loadCommit, } from "./repo-manager.js";
4
- const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR;
5
- export const colors = {
6
- red: (s) => isColorSupported ? `\x1b[31m${s}\x1b[0m` : s,
7
- green: (s) => isColorSupported ? `\x1b[32m${s}\x1b[0m` : s,
8
- yellow: (s) => isColorSupported ? `\x1b[33m${s}\x1b[0m` : s,
9
- blue: (s) => isColorSupported ? `\x1b[34m${s}\x1b[0m` : s,
10
- magenta: (s) => isColorSupported ? `\x1b[35m${s}\x1b[0m` : s,
11
- cyan: (s) => isColorSupported ? `\x1b[36m${s}\x1b[0m` : s,
12
- white: (s) => isColorSupported ? `\x1b[37m${s}\x1b[0m` : s,
13
- gray: (s) => isColorSupported ? `\x1b[90m${s}\x1b[0m` : s,
14
- bold: (s) => isColorSupported ? `\x1b[1m${s}\x1b[0m` : s,
15
- dim: (s) => isColorSupported ? `\x1b[2m${s}\x1b[0m` : s,
16
- };
17
- export function error(message) {
18
- console.error(`${colors.red('error:')} ${message}`);
19
- }
20
- export function fatal(message) {
21
- console.error(`${colors.red('fatal:')} ${message}`);
22
- process.exit(128);
23
- }
24
- export function warning(message) {
25
- console.error(`${colors.yellow('warning:')} ${message}`);
26
- }
27
- export function hint(message) {
28
- console.log(`${colors.yellow('hint:')} ${message}`);
29
- }
30
- export function success(message) {
31
- console.log(`${colors.green('✓')} ${message}`);
32
- }
4
+ export { colors, createSpinner, fatal, error, warning, hint, success, confirm, select, formatBytes, formatDuration, progressBar, requireInit, } from "./cli-utils.js";
5
+ import { colors, error, hint, fatal } from "./cli-utils.js";
33
6
  export function info(message) {
34
7
  console.log(message);
35
8
  }
@@ -250,98 +223,3 @@ export function readSQLFile(filePath) {
250
223
  const validation = validatePostgresSQL(content);
251
224
  return { content, validation };
252
225
  }
253
- export function createSpinner() {
254
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
255
- let frameIndex = 0;
256
- let interval = null;
257
- let currentMessage = '';
258
- return {
259
- start(message) {
260
- currentMessage = message;
261
- if (process.stdout.isTTY) {
262
- interval = setInterval(() => {
263
- process.stdout.write(`\r${frames[frameIndex]} ${currentMessage}`);
264
- frameIndex = (frameIndex + 1) % frames.length;
265
- }, 80);
266
- }
267
- else {
268
- console.log(` ${message}...`);
269
- }
270
- },
271
- stop() {
272
- if (interval) {
273
- clearInterval(interval);
274
- interval = null;
275
- if (process.stdout.isTTY) {
276
- process.stdout.write('\r' + ' '.repeat(currentMessage.length + 4) + '\r');
277
- }
278
- }
279
- },
280
- succeed(message) {
281
- this.stop();
282
- console.log(`${colors.green('✓')} ${message}`);
283
- },
284
- fail(message) {
285
- this.stop();
286
- console.log(`${colors.red('✗')} ${message}`);
287
- },
288
- info(message) {
289
- this.stop();
290
- console.log(`${colors.blue('ℹ')} ${message}`);
291
- },
292
- };
293
- }
294
- import * as readline from 'readline';
295
- export function confirm(question, defaultYes = true) {
296
- const rl = readline.createInterface({
297
- input: process.stdin,
298
- output: process.stdout,
299
- });
300
- const suffix = defaultYes ? '[Y/n]' : '[y/N]';
301
- return new Promise((resolve) => {
302
- rl.question(`${question} ${colors.gray(suffix)} `, (answer) => {
303
- rl.close();
304
- const a = answer.trim().toLowerCase();
305
- if (!a)
306
- resolve(defaultYes);
307
- else
308
- resolve(a === 'y' || a === 'yes');
309
- });
310
- });
311
- }
312
- export function select(question, options) {
313
- const rl = readline.createInterface({
314
- input: process.stdin,
315
- output: process.stdout,
316
- });
317
- console.log(question);
318
- options.forEach((opt, i) => {
319
- console.log(` ${i + 1}) ${opt}`);
320
- });
321
- return new Promise((resolve) => {
322
- rl.question(`${colors.gray('Select [1-' + options.length + ']:')} `, (answer) => {
323
- rl.close();
324
- const num = parseInt(answer.trim(), 10);
325
- if (num >= 1 && num <= options.length) {
326
- resolve(num - 1);
327
- }
328
- else {
329
- resolve(0);
330
- }
331
- });
332
- });
333
- }
334
- export function formatBytes(bytes) {
335
- if (bytes < 1024)
336
- return `${bytes} B`;
337
- if (bytes < 1024 * 1024)
338
- return `${(bytes / 1024).toFixed(1)} KB`;
339
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
340
- }
341
- export function formatDuration(ms) {
342
- if (ms < 1000)
343
- return `${ms}ms`;
344
- if (ms < 60000)
345
- return `${(ms / 1000).toFixed(1)}s`;
346
- return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
347
- }
@@ -0,0 +1,114 @@
1
+ const pools = new Map();
2
+ const IDLE_TIMEOUT = 30000;
3
+ let cleanupInterval = null;
4
+ function getPoolKey(config) {
5
+ if (config.url) {
6
+ return config.url;
7
+ }
8
+ return `${config.host || 'localhost'}:${config.port || 5432}/${config.database}@${config.user}`;
9
+ }
10
+ function startCleanupInterval() {
11
+ if (cleanupInterval)
12
+ return;
13
+ cleanupInterval = setInterval(() => {
14
+ const now = Date.now();
15
+ for (const [key, entry] of pools.entries()) {
16
+ if (entry.refCount === 0 && now - entry.lastUsed > IDLE_TIMEOUT) {
17
+ entry.pool.end().catch(() => { });
18
+ pools.delete(key);
19
+ }
20
+ }
21
+ if (pools.size === 0 && cleanupInterval) {
22
+ clearInterval(cleanupInterval);
23
+ cleanupInterval = null;
24
+ }
25
+ }, IDLE_TIMEOUT);
26
+ cleanupInterval.unref();
27
+ }
28
+ export async function getPool(config) {
29
+ const key = getPoolKey(config);
30
+ let entry = pools.get(key);
31
+ if (entry) {
32
+ entry.refCount++;
33
+ entry.lastUsed = Date.now();
34
+ return entry.pool;
35
+ }
36
+ const { Pool } = await import("../../addon/pg/index.js");
37
+ const pool = new Pool({
38
+ host: config.host,
39
+ port: config.port || 5432,
40
+ database: config.database,
41
+ user: config.user,
42
+ password: config.password,
43
+ connectionString: config.url,
44
+ ssl: config.ssl,
45
+ max: config.max || 10,
46
+ idleTimeoutMillis: config.idleTimeoutMillis || 10000,
47
+ connectionTimeoutMillis: config.connectionTimeoutMillis || 5000,
48
+ });
49
+ entry = {
50
+ pool,
51
+ refCount: 1,
52
+ lastUsed: Date.now(),
53
+ };
54
+ pools.set(key, entry);
55
+ startCleanupInterval();
56
+ return pool;
57
+ }
58
+ export function releasePool(config) {
59
+ const key = getPoolKey(config);
60
+ const entry = pools.get(key);
61
+ if (entry && entry.refCount > 0) {
62
+ entry.refCount--;
63
+ entry.lastUsed = Date.now();
64
+ }
65
+ }
66
+ export async function withPool(config, fn) {
67
+ const pool = await getPool(config);
68
+ try {
69
+ return await fn(pool);
70
+ }
71
+ finally {
72
+ releasePool(config);
73
+ }
74
+ }
75
+ export async function withClient(config, fn) {
76
+ const pool = await getPool(config);
77
+ const client = await pool.connect();
78
+ try {
79
+ return await fn(client);
80
+ }
81
+ finally {
82
+ client.release();
83
+ releasePool(config);
84
+ }
85
+ }
86
+ export async function withTransaction(config, fn) {
87
+ return withClient(config, async (client) => {
88
+ await client.query('BEGIN');
89
+ try {
90
+ const result = await fn(client);
91
+ await client.query('COMMIT');
92
+ return result;
93
+ }
94
+ catch (error) {
95
+ await client.query('ROLLBACK');
96
+ throw error;
97
+ }
98
+ });
99
+ }
100
+ export async function closeAllPools() {
101
+ if (cleanupInterval) {
102
+ clearInterval(cleanupInterval);
103
+ cleanupInterval = null;
104
+ }
105
+ const closePromises = [];
106
+ for (const [key, entry] of pools.entries()) {
107
+ closePromises.push(entry.pool.end());
108
+ pools.delete(key);
109
+ }
110
+ await Promise.all(closePromises);
111
+ }
112
+ export function getActivePoolCount() {
113
+ return pools.size;
114
+ }
@@ -1,19 +1,69 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
+ import * as os from 'os';
4
+ const CONFIG_FILENAMES = [
5
+ 'relq.config.ts',
6
+ 'relq.config.js',
7
+ 'relq.config.mjs',
8
+ ];
9
+ function hasProjectMarker(dir) {
10
+ for (const filename of CONFIG_FILENAMES) {
11
+ if (fs.existsSync(path.join(dir, filename))) {
12
+ return true;
13
+ }
14
+ }
15
+ if (fs.existsSync(path.join(dir, 'package.json'))) {
16
+ return true;
17
+ }
18
+ return false;
19
+ }
3
20
  export function findProjectRoot(startDir = process.cwd()) {
4
21
  let currentDir = path.resolve(startDir);
5
22
  const root = path.parse(currentDir).root;
23
+ const homeDir = os.homedir();
6
24
  while (currentDir !== root) {
7
- const packageJsonPath = path.join(currentDir, 'package.json');
8
- if (fs.existsSync(packageJsonPath)) {
25
+ if (hasProjectMarker(currentDir)) {
9
26
  return currentDir;
10
27
  }
28
+ if (currentDir === homeDir) {
29
+ return null;
30
+ }
11
31
  currentDir = path.dirname(currentDir);
12
32
  }
13
- return process.cwd();
33
+ return null;
14
34
  }
15
35
  export function getRelqDir(startDir = process.cwd()) {
16
- const projectRoot = findProjectRoot(startDir) || process.cwd();
36
+ const projectRoot = findProjectRoot(startDir);
37
+ if (!projectRoot) {
38
+ const colors = {
39
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
40
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
41
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
42
+ };
43
+ console.error('');
44
+ console.error(colors.red('fatal:') + ' not a relq project (or any of the parent directories)');
45
+ console.error('');
46
+ console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} in your project directory to initialize relq.`);
47
+ console.error('');
48
+ process.exit(128);
49
+ }
17
50
  return path.join(projectRoot, '.relq');
18
51
  }
19
- export default { findProjectRoot, getRelqDir };
52
+ export function getProjectRoot(startDir = process.cwd()) {
53
+ const projectRoot = findProjectRoot(startDir);
54
+ if (!projectRoot) {
55
+ const colors = {
56
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
57
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
58
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
59
+ };
60
+ console.error('');
61
+ console.error(colors.red('fatal:') + ' not a relq project (or any of the parent directories)');
62
+ console.error('');
63
+ console.error(colors.yellow('hint:') + ` Run ${colors.cyan('relq init')} in your project directory to initialize relq.`);
64
+ console.error('');
65
+ process.exit(128);
66
+ }
67
+ return projectRoot;
68
+ }
69
+ export default { findProjectRoot, getRelqDir, getProjectRoot };
@@ -6,6 +6,7 @@ const REQUIRES_PARENT = [
6
6
  ];
7
7
  const DEFAULT_PATTERNS = [
8
8
  'TABLE:_relq_*',
9
+ 'TABLE:_kuery_*',
9
10
  'TABLE:pg_*',
10
11
  'TABLE:_temp_*',
11
12
  'TABLE:tmp_*',
@@ -8,6 +8,7 @@ const STAGED_FILE = 'staged.json';
8
8
  const WORKING_FILE = 'working.json';
9
9
  const SNAPSHOT_FILE = 'snapshot.json';
10
10
  const COMMITS_DIR = 'commits';
11
+ const FILE_HASH_FILE = 'file_hash';
11
12
  export function isInitialized(projectRoot = process.cwd()) {
12
13
  const relqPath = path.join(projectRoot, RELQ_DIR);
13
14
  const headPath = path.join(relqPath, HEAD_FILE);
@@ -172,6 +173,41 @@ export function saveSnapshot(schema, projectRoot = process.cwd()) {
172
173
  const snapshotPath = path.join(projectRoot, RELQ_DIR, SNAPSHOT_FILE);
173
174
  fs.writeFileSync(snapshotPath, JSON.stringify(schema, null, 2), 'utf-8');
174
175
  }
176
+ export function hashFileContent(content) {
177
+ return crypto.createHash('sha1').update(content, 'utf8').digest('hex');
178
+ }
179
+ export function getSavedFileHash(projectRoot = process.cwd()) {
180
+ const hashPath = path.join(projectRoot, RELQ_DIR, FILE_HASH_FILE);
181
+ if (!fs.existsSync(hashPath)) {
182
+ return null;
183
+ }
184
+ return fs.readFileSync(hashPath, 'utf-8').trim() || null;
185
+ }
186
+ export function saveFileHash(hash, projectRoot = process.cwd()) {
187
+ const hashPath = path.join(projectRoot, RELQ_DIR, FILE_HASH_FILE);
188
+ fs.writeFileSync(hashPath, hash, 'utf-8');
189
+ }
190
+ export function detectFileChanges(schemaPath, projectRoot = process.cwd()) {
191
+ if (!fs.existsSync(schemaPath)) {
192
+ return null;
193
+ }
194
+ const currentContent = fs.readFileSync(schemaPath, 'utf-8');
195
+ const currentHash = hashFileContent(currentContent);
196
+ const savedHash = getSavedFileHash(projectRoot);
197
+ if (!savedHash || currentHash === savedHash) {
198
+ return null;
199
+ }
200
+ return {
201
+ id: `file_${currentHash.substring(0, 8)}`,
202
+ type: 'ALTER',
203
+ objectType: 'SCHEMA_FILE',
204
+ objectName: path.basename(schemaPath),
205
+ before: { hash: savedHash },
206
+ after: { hash: currentHash },
207
+ sql: '-- Schema file modified (comments, formatting, or manual edits)',
208
+ detectedAt: new Date().toISOString(),
209
+ };
210
+ }
175
211
  export function loadWorkingState(projectRoot = process.cwd()) {
176
212
  const workingPath = path.join(projectRoot, RELQ_DIR, WORKING_FILE);
177
213
  if (!fs.existsSync(workingPath)) {
@@ -206,6 +242,12 @@ export function addUnstagedChanges(changes, projectRoot = process.cwd()) {
206
242
  state.timestamp = new Date().toISOString();
207
243
  saveWorkingState(state, projectRoot);
208
244
  }
245
+ export function clearUnstagedChanges(projectRoot = process.cwd()) {
246
+ const state = getOrCreateWorkingState(projectRoot);
247
+ state.unstaged = [];
248
+ state.timestamp = new Date().toISOString();
249
+ saveWorkingState(state, projectRoot);
250
+ }
209
251
  export function stageChanges(patterns, projectRoot = process.cwd()) {
210
252
  const state = getOrCreateWorkingState(projectRoot);
211
253
  const staged = [];