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,22 +1,376 @@
1
- import { colors } from "../utils/spinner.js";
1
+ import * as fs from 'fs';
2
+ import { colors, fatal, warning } from "../utils/spinner.js";
2
3
  import { loadRelqignore, isIgnored, } from "../utils/relqignore.js";
3
4
  import { loadConfig } from "../../config/config.js";
4
- import { isInitialized, getUnstagedChanges, getStagedChanges, stageChanges, } from "../utils/repo-manager.js";
5
+ import * as path from 'path';
6
+ import { isInitialized, loadSnapshot, getUnstagedChanges, getStagedChanges, stageChanges, detectFileChanges, addUnstagedChanges, clearUnstagedChanges, } from "../utils/repo-manager.js";
5
7
  import { getChangeDisplayName } from "../utils/change-tracker.js";
8
+ import { compareSchemas } from "../utils/schema-comparator.js";
9
+ function parseSchemaFileForComparison(schemaPath) {
10
+ if (!fs.existsSync(schemaPath)) {
11
+ return null;
12
+ }
13
+ const content = fs.readFileSync(schemaPath, 'utf-8');
14
+ const tables = [];
15
+ const tableStartRegex = /defineTable\s*\(\s*['"]([^'"]+)['"],\s*\{/g;
16
+ let tableStartMatch;
17
+ while ((tableStartMatch = tableStartRegex.exec(content)) !== null) {
18
+ const tableName = tableStartMatch[1];
19
+ const startIdx = tableStartMatch.index + tableStartMatch[0].length;
20
+ let braceCount = 1;
21
+ let endIdx = startIdx;
22
+ while (braceCount > 0 && endIdx < content.length) {
23
+ const char = content[endIdx];
24
+ if (char === '{')
25
+ braceCount++;
26
+ else if (char === '}')
27
+ braceCount--;
28
+ endIdx++;
29
+ }
30
+ const columnsBlock = content.substring(startIdx, endIdx - 1);
31
+ let optionsBlock = '';
32
+ const afterColumns = content.substring(endIdx);
33
+ const optionsMatch = afterColumns.match(/^\s*,\s*\{/);
34
+ if (optionsMatch) {
35
+ const optStart = endIdx + optionsMatch[0].length;
36
+ braceCount = 1;
37
+ let optEnd = optStart;
38
+ while (braceCount > 0 && optEnd < content.length) {
39
+ const char = content[optEnd];
40
+ if (char === '{')
41
+ braceCount++;
42
+ else if (char === '}')
43
+ braceCount--;
44
+ optEnd++;
45
+ }
46
+ optionsBlock = content.substring(optStart, optEnd - 1);
47
+ }
48
+ const columns = [];
49
+ const constraints = [];
50
+ const tsToDbNameMap = new Map();
51
+ const lines = columnsBlock.split('\n');
52
+ let currentColDef = '';
53
+ for (const line of lines) {
54
+ const trimmed = line.trim();
55
+ if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
56
+ continue;
57
+ currentColDef += ' ' + trimmed;
58
+ if (trimmed.endsWith(',') || trimmed.endsWith(')')) {
59
+ const colDef = currentColDef.trim();
60
+ currentColDef = '';
61
+ const typePattern = 'varchar|text|uuid|integer|bigint|boolean|timestamp|date|jsonb|json|numeric|serial|bigserial|smallserial|tsvector|smallint|real|doublePrecision|char|inet|cidr|macaddr|macaddr8|interval|time|point|line|lseg|box|path|polygon|circle|bytea|bit|varbit|money|xml|oid';
62
+ const colMatch = colDef.match(new RegExp(`^(\\w+):\\s*(${typePattern})`));
63
+ if (!colMatch)
64
+ continue;
65
+ const tsName = colMatch[1];
66
+ const type = colMatch[2];
67
+ const explicitNameMatch = colDef.match(new RegExp(`${type}\\s*\\(['\"]([^'"]+)['\"]`));
68
+ const dbColName = explicitNameMatch ? explicitNameMatch[1] : tsName;
69
+ tsToDbNameMap.set(tsName, dbColName);
70
+ let defaultValue = null;
71
+ const isJsonbColumn = type === 'jsonb' || type === 'json';
72
+ const isArrayColumn = colDef.includes('.array()');
73
+ const bigintMatch = colDef.match(/\.default\(\s*BigInt\(\s*(-?\d+)\s*\)\s*\)/);
74
+ if (bigintMatch) {
75
+ defaultValue = bigintMatch[1];
76
+ }
77
+ const funcDefaultMatch = !defaultValue && colDef.match(/\.default\(\s*(genRandomUuid|now|currentDate|currentTimestamp|emptyArray|emptyObject)\s*\(\s*\)\s*\)/);
78
+ if (funcDefaultMatch) {
79
+ const funcName = funcDefaultMatch[1];
80
+ if (funcName === 'emptyArray') {
81
+ defaultValue = isJsonbColumn ? "'[]'::jsonb" : "'{}'::text[]";
82
+ }
83
+ else if (funcName === 'emptyObject') {
84
+ defaultValue = "'{}'::jsonb";
85
+ }
86
+ else {
87
+ const funcToSql = {
88
+ 'genRandomUuid': 'gen_random_uuid()',
89
+ 'now': 'now()',
90
+ 'currentDate': 'CURRENT_DATE',
91
+ 'currentTimestamp': 'CURRENT_TIMESTAMP',
92
+ };
93
+ defaultValue = funcToSql[funcName] || `${funcName}()`;
94
+ }
95
+ }
96
+ else {
97
+ const strDefaultMatch = colDef.match(/\.default\(\s*(['"])([^'"]*)\1\s*\)/);
98
+ if (strDefaultMatch) {
99
+ defaultValue = strDefaultMatch[2];
100
+ }
101
+ else {
102
+ const boolDefaultMatch = colDef.match(/\.default\(\s*(true|false)\s*\)/);
103
+ if (boolDefaultMatch) {
104
+ defaultValue = boolDefaultMatch[1];
105
+ }
106
+ else {
107
+ const numDefaultMatch = colDef.match(/\.default\(\s*(-?\d+(?:\.\d+)?)\s*\)/);
108
+ if (numDefaultMatch) {
109
+ defaultValue = numDefaultMatch[1];
110
+ }
111
+ else {
112
+ const jsonObjMatch = colDef.match(/\.default\(\s*(\{[^}]+\})\s*\)/);
113
+ if (jsonObjMatch) {
114
+ defaultValue = `'${jsonObjMatch[1]}'::jsonb`;
115
+ }
116
+ else {
117
+ const arrayLiteralMatch = colDef.match(/\.default\(\s*(\[[^\]]+\])\s*\)/);
118
+ if (arrayLiteralMatch) {
119
+ const arrayStr = arrayLiteralMatch[1];
120
+ defaultValue = `'${arrayStr}'::jsonb`;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ if (colDef.includes('.check(')) {
128
+ const checkValuesMatch = colDef.match(/\.check\(([^)]+)\)/);
129
+ if (checkValuesMatch) {
130
+ const valuesStr = checkValuesMatch[1];
131
+ const values = valuesStr.match(/['"]([^'"]+)['"]/g)?.map(v => v.replace(/['"]/g, '')) || [];
132
+ if (values.length > 0) {
133
+ const constraintName = `${tableName}_${dbColName}_check`;
134
+ constraints.push({
135
+ name: constraintName,
136
+ type: 'CHECK',
137
+ columns: [dbColName],
138
+ definition: '',
139
+ });
140
+ }
141
+ }
142
+ }
143
+ let comment = null;
144
+ const commentMatch = colDef.match(/\.comment\(\s*(['"])([^'"]*)\1\s*\)/);
145
+ if (commentMatch) {
146
+ comment = commentMatch[2];
147
+ }
148
+ if (colDef.includes('.identity()')) {
149
+ defaultValue = defaultValue || 'GENERATED BY DEFAULT AS IDENTITY';
150
+ }
151
+ const isArray = colDef.includes('.array()');
152
+ columns.push({
153
+ name: dbColName,
154
+ dataType: isArray ? `${type}[]` : type,
155
+ isNullable: !colDef.includes('.notNull()') && !colDef.includes('.primaryKey()'),
156
+ defaultValue,
157
+ isPrimaryKey: colDef.includes('.primaryKey()'),
158
+ isUnique: colDef.includes('.unique()'),
159
+ maxLength: null,
160
+ precision: null,
161
+ scale: null,
162
+ references: null,
163
+ comment,
164
+ });
165
+ }
166
+ }
167
+ const indexes = [];
168
+ const indexRegex = /index\s*\(\s*['"]([^'"]+)['"]\s*\)\.on\(([^)]+)\)/g;
169
+ let idxMatch;
170
+ while ((idxMatch = indexRegex.exec(optionsBlock)) !== null) {
171
+ const indexName = idxMatch[1];
172
+ const indexCols = idxMatch[2].split(',').map(c => {
173
+ const tsColName = c.trim().replace(/table\.\s*/, '');
174
+ return tsToDbNameMap.get(tsColName) || tsColName;
175
+ });
176
+ const isUnique = optionsBlock.includes(`index('${indexName}')`) &&
177
+ optionsBlock.substring(optionsBlock.indexOf(`index('${indexName}')`)).split('\n')[0].includes('.unique()');
178
+ indexes.push({
179
+ name: indexName,
180
+ columns: indexCols,
181
+ isUnique: isUnique,
182
+ isPrimary: false,
183
+ type: 'btree',
184
+ definition: '',
185
+ whereClause: null,
186
+ expression: null,
187
+ });
188
+ }
189
+ const checkRegexLegacy = /check\s*\(\s*['"]([^'"]+)['"]\s*,\s*sql`([^`]+)`\s*\)/g;
190
+ let tableCheckMatch;
191
+ while ((tableCheckMatch = checkRegexLegacy.exec(optionsBlock)) !== null) {
192
+ constraints.push({
193
+ name: tableCheckMatch[1],
194
+ type: 'CHECK',
195
+ columns: [],
196
+ definition: tableCheckMatch[2].trim(),
197
+ });
198
+ }
199
+ const checkRegexNew = /check\.constraint\s*\(\s*['"]([^'"]+)['"]/g;
200
+ let newCheckMatch;
201
+ while ((newCheckMatch = checkRegexNew.exec(optionsBlock)) !== null) {
202
+ const constraintName = newCheckMatch[1];
203
+ if (!constraints.some(c => c.name === constraintName)) {
204
+ constraints.push({
205
+ name: constraintName,
206
+ type: 'CHECK',
207
+ columns: [],
208
+ definition: '',
209
+ });
210
+ }
211
+ }
212
+ const checkConstraintsBlockMatch = optionsBlock.match(/checkConstraints:\s*\([^)]+\)\s*=>\s*\[([^\]]+)\]/s);
213
+ if (checkConstraintsBlockMatch) {
214
+ const checkBlock = checkConstraintsBlockMatch[1];
215
+ const constraintNameMatches = checkBlock.matchAll(/check\.constraint\s*\(\s*['"]([^'"]+)['"]/g);
216
+ for (const match of constraintNameMatches) {
217
+ const constraintName = match[1];
218
+ if (!constraints.some(c => c.name === constraintName)) {
219
+ constraints.push({
220
+ name: constraintName,
221
+ type: 'CHECK',
222
+ columns: [],
223
+ definition: '',
224
+ });
225
+ }
226
+ }
227
+ }
228
+ let partitionType;
229
+ let partitionKey;
230
+ const partitionByMatch = optionsBlock.match(/partitionBy:\s*\([^)]+\)\s*=>\s*\w+\.(list|range|hash)\(([^)]+)\)/i);
231
+ if (partitionByMatch) {
232
+ partitionType = partitionByMatch[1].toUpperCase();
233
+ const tsPartitionKey = partitionByMatch[2].replace(/table\./, '').trim();
234
+ const dbPartitionKey = tsToDbNameMap.get(tsPartitionKey) || tsPartitionKey;
235
+ partitionKey = [dbPartitionKey];
236
+ }
237
+ tables.push({
238
+ name: tableName,
239
+ schema: 'public',
240
+ columns,
241
+ indexes,
242
+ constraints,
243
+ rowCount: 0,
244
+ isPartitioned: !!partitionType,
245
+ partitionType,
246
+ partitionKey,
247
+ });
248
+ }
249
+ const enums = [];
250
+ const enumRegex = /defineEnum\s*\(\s*['"]([^'"]+)['"]\s*,\s*\[([^\]]+)\]/g;
251
+ let enumMatch;
252
+ while ((enumMatch = enumRegex.exec(content)) !== null) {
253
+ const enumName = enumMatch[1];
254
+ const valuesStr = enumMatch[2];
255
+ const values = valuesStr.match(/['"]([^'"]+)['"]/g)?.map(v => v.replace(/['"]/g, '')) || [];
256
+ enums.push({ name: enumName, values });
257
+ }
258
+ const extensions = [];
259
+ const singleExtMatch = content.match(/pgExtensions\s*\(\s*['"]([^'"]+)['"]\s*\)/);
260
+ if (singleExtMatch) {
261
+ extensions.push(singleExtMatch[1]);
262
+ }
263
+ else {
264
+ const arrayExtMatch = content.match(/pgExtensions\s*\(\s*\[([^\]]+)\]/);
265
+ if (arrayExtMatch) {
266
+ const extList = arrayExtMatch[1].match(/['"]([^'"]+)['"]/g);
267
+ if (extList) {
268
+ extList.forEach(e => extensions.push(e.replace(/['"]/g, '')));
269
+ }
270
+ }
271
+ }
272
+ return {
273
+ tables,
274
+ enums,
275
+ domains: [],
276
+ compositeTypes: [],
277
+ sequences: [],
278
+ collations: [],
279
+ functions: [],
280
+ triggers: [],
281
+ policies: [],
282
+ partitions: [],
283
+ foreignServers: [],
284
+ foreignTables: [],
285
+ extensions,
286
+ };
287
+ }
288
+ function snapshotToDatabaseSchema(snapshot) {
289
+ const tables = (snapshot.tables || []).map(t => ({
290
+ name: t.name,
291
+ schema: t.schema || 'public',
292
+ columns: (t.columns || []).map(c => ({
293
+ name: c.name,
294
+ dataType: c.type || 'text',
295
+ isNullable: c.nullable !== false,
296
+ defaultValue: c.default || null,
297
+ isPrimaryKey: c.primaryKey || false,
298
+ isUnique: c.unique || false,
299
+ maxLength: null,
300
+ precision: null,
301
+ scale: null,
302
+ references: null,
303
+ comment: c.comment || null,
304
+ })),
305
+ indexes: (t.indexes || []).map(i => ({
306
+ name: i.name,
307
+ columns: Array.isArray(i.columns) ? i.columns : [i.columns],
308
+ isUnique: i.unique || false,
309
+ isPrimary: false,
310
+ type: i.type || 'btree',
311
+ definition: i.definition || '',
312
+ whereClause: i.whereClause || null,
313
+ expression: null,
314
+ })),
315
+ constraints: (t.constraints || []).map(c => ({
316
+ name: c.name,
317
+ type: c.type,
318
+ columns: c.columns || [],
319
+ definition: c.definition || '',
320
+ })),
321
+ rowCount: 0,
322
+ isPartitioned: t.isPartitioned || false,
323
+ partitionType: t.partitionType,
324
+ partitionKey: t.partitionKey ? (Array.isArray(t.partitionKey) ? t.partitionKey : [t.partitionKey]) : undefined,
325
+ }));
326
+ return {
327
+ tables,
328
+ enums: (snapshot.enums || []).map(e => ({ name: e.name, values: e.values })),
329
+ domains: [],
330
+ compositeTypes: [],
331
+ sequences: [],
332
+ collations: [],
333
+ functions: [],
334
+ triggers: [],
335
+ policies: [],
336
+ partitions: [],
337
+ foreignServers: [],
338
+ foreignTables: [],
339
+ extensions: (snapshot.extensions || []).map(e => typeof e === 'string' ? e : e.name),
340
+ };
341
+ }
6
342
  export async function addCommand(context) {
7
- const { args } = context;
8
- const projectRoot = process.cwd();
343
+ const { args, projectRoot } = context;
9
344
  console.log('');
10
345
  if (!isInitialized(projectRoot)) {
11
- console.log(`${colors.red('error:')} relq not initialized`);
12
- console.log('');
13
- console.log(`${colors.muted('Run')} ${colors.cyan('relq init')} ${colors.muted('first.')}`);
14
- return;
346
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
15
347
  }
16
- const allUnstaged = getUnstagedChanges(projectRoot);
17
- const staged = getStagedChanges(projectRoot);
18
348
  const ignorePatterns = loadRelqignore(projectRoot);
19
349
  const config = await loadConfig();
350
+ const schemaPathRaw = typeof config.schema === 'string' ? config.schema : './db/schema.ts';
351
+ const schemaPath = path.resolve(projectRoot, schemaPathRaw);
352
+ const fileChange = detectFileChanges(schemaPath, projectRoot);
353
+ if (fileChange) {
354
+ const currentSchema = parseSchemaFileForComparison(schemaPath);
355
+ const snapshot = loadSnapshot(projectRoot);
356
+ if (currentSchema && snapshot) {
357
+ const snapshotAsDbSchema = snapshotToDatabaseSchema(snapshot);
358
+ const schemaChanges = compareSchemas(snapshotAsDbSchema, currentSchema);
359
+ if (schemaChanges.length > 0) {
360
+ clearUnstagedChanges(projectRoot);
361
+ addUnstagedChanges(schemaChanges, projectRoot);
362
+ }
363
+ else {
364
+ const existingUnstaged = getUnstagedChanges(projectRoot);
365
+ const hasFileChange = existingUnstaged.some(c => c.objectType === 'SCHEMA_FILE');
366
+ if (!hasFileChange) {
367
+ addUnstagedChanges([fileChange], projectRoot);
368
+ }
369
+ }
370
+ }
371
+ }
372
+ const allUnstaged = getUnstagedChanges(projectRoot);
373
+ const staged = getStagedChanges(projectRoot);
20
374
  const unstaged = allUnstaged.filter(change => {
21
375
  const objectType = change.objectType;
22
376
  const result = isIgnored(objectType, change.objectName, change.parentName || null, ignorePatterns);
@@ -40,41 +394,30 @@ export async function addCommand(context) {
40
394
  const filteredCount = allUnstaged.length - unstaged.length;
41
395
  if (unstaged.length === 0) {
42
396
  if (staged.length > 0) {
43
- console.log(`${colors.green('✓')} All changes are already staged`);
397
+ console.log('All changes are already staged');
44
398
  console.log(`${colors.muted(`${staged.length} change(s) ready to commit`)}`);
45
399
  console.log('');
46
- console.log(`${colors.muted('Run')} ${colors.cyan('relq commit -m "message"')} ${colors.muted('to commit.')}`);
400
+ console.log(`hint: run 'relq commit -m "message"' to commit`);
47
401
  }
48
402
  else if (filteredCount > 0) {
49
- console.log(`${colors.green('✓')} No stageable changes`);
403
+ console.log('No stageable changes');
50
404
  console.log(`${colors.muted(`${filteredCount} change(s) filtered by .relqignore or config`)}`);
51
405
  }
52
406
  else {
53
- console.log(`${colors.green('✓')} No changes to stage`);
54
- console.log(`${colors.muted('Run')} ${colors.cyan('relq pull')} ${colors.muted('or')} ${colors.cyan('relq import')} ${colors.muted('to detect changes.')}`);
407
+ console.log('No changes to stage');
408
+ console.log(`hint: run 'relq pull' or 'relq import' to detect changes`);
55
409
  }
56
410
  console.log('');
57
411
  return;
58
412
  }
59
413
  const patterns = args.length > 0 ? args : ['.'];
60
- const addAll = patterns.includes('.') || patterns.includes('*');
61
- console.log(`${colors.cyan('Unstaged changes:')} ${unstaged.length}`);
62
- console.log('');
63
- for (const change of unstaged) {
64
- const display = getChangeDisplayName(change);
65
- const color = change.type === 'CREATE' ? colors.green :
66
- change.type === 'DROP' ? colors.red :
67
- colors.yellow;
68
- console.log(` ${color(display)}`);
69
- }
70
- console.log('');
71
414
  const stagedNow = stageChanges(patterns, projectRoot);
72
415
  if (stagedNow.length === 0) {
73
- console.log(`${colors.yellow('⚠')} No changes matched the pattern(s): ${patterns.join(', ')}`);
416
+ warning(`No changes matched the pattern(s): ${patterns.join(', ')}`);
74
417
  console.log('');
75
418
  return;
76
419
  }
77
- console.log(`${colors.green('✓')} Staged ${stagedNow.length} change(s):`);
420
+ console.log(`Staged ${stagedNow.length} change(s):`);
78
421
  console.log('');
79
422
  for (const change of stagedNow) {
80
423
  const display = getChangeDisplayName(change);
@@ -89,7 +432,7 @@ export async function addCommand(context) {
89
432
  console.log(`${colors.muted(`${remainingUnstaged.length} change(s) still unstaged`)}`);
90
433
  console.log('');
91
434
  }
92
- console.log(`${colors.muted('Run')} ${colors.cyan('relq commit -m "message"')} ${colors.muted('to commit.')}`);
435
+ console.log(`hint: run 'relq commit -m "message"' to commit`);
93
436
  console.log('');
94
437
  }
95
438
  export function getRelatedChanges(tableName, changes) {
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { colors } from "../utils/spinner.js";
3
+ import { colors, fatal } from "../utils/spinner.js";
4
4
  import { isInitialized, getHead } from "../utils/repo-manager.js";
5
5
  function loadBranchState(projectRoot) {
6
6
  const branchPath = path.join(projectRoot, '.relq', 'branches.json');
@@ -18,12 +18,10 @@ function saveBranchState(state, projectRoot) {
18
18
  fs.writeFileSync(branchPath, JSON.stringify(state, null, 2));
19
19
  }
20
20
  export async function branchCommand(context) {
21
- const { args, flags } = context;
22
- const projectRoot = process.cwd();
21
+ const { args, flags, projectRoot } = context;
23
22
  console.log('');
24
23
  if (!isInitialized(projectRoot)) {
25
- console.log(`${colors.red('fatal:')} not a relq repository`);
26
- return;
24
+ fatal('not a relq repository (or any parent directories): .relq', `Run ${colors.cyan('relq init')} to initialize.`);
27
25
  }
28
26
  const state = loadBranchState(projectRoot);
29
27
  const deleteFlag = flags['d'] === true || flags['delete'] === true;
@@ -31,20 +29,17 @@ export async function branchCommand(context) {
31
29
  if (deleteFlag) {
32
30
  const branchName = args[0];
33
31
  if (!branchName) {
34
- console.log(`${colors.red('error:')} Please specify branch name`);
35
- return;
32
+ fatal('Please specify branch name', `Usage: ${colors.cyan('relq branch -d <name>')}`);
36
33
  }
37
34
  if (branchName === state.current) {
38
- console.log(`${colors.red('error:')} Cannot delete current branch`);
39
- return;
35
+ fatal(`Cannot delete the branch '${branchName}' which you are currently on`);
40
36
  }
41
37
  if (!state.branches[branchName]) {
42
- console.log(`${colors.red('error:')} Branch not found: ${branchName}`);
43
- return;
38
+ fatal(`Branch '${branchName}' not found`, `Use ${colors.cyan('relq branch')} to list available branches.`);
44
39
  }
45
40
  delete state.branches[branchName];
46
41
  saveBranchState(state, projectRoot);
47
- console.log(`${colors.green('✓')} Deleted branch '${branchName}'`);
42
+ console.log(`Deleted branch '${branchName}'`);
48
43
  console.log('');
49
44
  return;
50
45
  }
@@ -52,40 +47,35 @@ export async function branchCommand(context) {
52
47
  const oldName = args[0];
53
48
  const newName = args[1];
54
49
  if (!oldName || !newName) {
55
- console.log(`${colors.red('error:')} Usage: relq branch -m <old> <new>`);
56
- return;
50
+ fatal('Missing arguments', `Usage: ${colors.cyan('relq branch -m <old> <new>')}`);
57
51
  }
58
52
  if (!state.branches[oldName]) {
59
- console.log(`${colors.red('error:')} Branch not found: ${oldName}`);
60
- return;
53
+ fatal(`Branch '${oldName}' not found`, `Use ${colors.cyan('relq branch')} to list available branches.`);
61
54
  }
62
55
  if (state.branches[newName]) {
63
- console.log(`${colors.red('error:')} Branch already exists: ${newName}`);
64
- return;
56
+ fatal(`A branch named '${newName}' already exists`);
65
57
  }
66
58
  state.branches[newName] = state.branches[oldName];
67
59
  delete state.branches[oldName];
68
60
  if (state.current === oldName)
69
61
  state.current = newName;
70
62
  saveBranchState(state, projectRoot);
71
- console.log(`${colors.green('✓')} Renamed '${oldName}' to '${newName}'`);
63
+ console.log(`Renamed '${oldName}' to '${newName}'`);
72
64
  console.log('');
73
65
  return;
74
66
  }
75
67
  if (args[0]) {
76
68
  const branchName = args[0];
77
69
  if (state.branches[branchName]) {
78
- console.log(`${colors.red('error:')} Branch already exists: ${branchName}`);
79
- return;
70
+ fatal(`A branch named '${branchName}' already exists`);
80
71
  }
81
72
  const head = getHead(projectRoot);
82
73
  if (!head) {
83
- console.log(`${colors.red('error:')} No commits yet`);
84
- return;
74
+ fatal('No commits yet', `Run ${colors.cyan('relq pull')} or ${colors.cyan('relq import')} first.`);
85
75
  }
86
76
  state.branches[branchName] = head;
87
77
  saveBranchState(state, projectRoot);
88
- console.log(`${colors.green('✓')} Created branch '${branchName}'`);
78
+ console.log(`Created branch '${branchName}'`);
89
79
  console.log('');
90
80
  return;
91
81
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { colors } from "../utils/spinner.js";
3
+ import { fatal, error, hint } from "../utils/cli-utils.js";
4
4
  import { isInitialized, getHead, setHead, loadCommit, saveSnapshot, getStagedChanges, getUnstagedChanges, } from "../utils/repo-manager.js";
5
5
  function loadBranchState(projectRoot) {
6
6
  const branchPath = path.join(projectRoot, '.relq', 'branches.json');
@@ -15,60 +15,48 @@ function saveBranchState(state, projectRoot) {
15
15
  fs.writeFileSync(branchPath, JSON.stringify(state, null, 2));
16
16
  }
17
17
  export async function checkoutCommand(context) {
18
- const { args, flags } = context;
19
- const projectRoot = process.cwd();
18
+ const { 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 'relq init' to initialize");
24
22
  }
25
23
  const createBranch = flags['b'] === true;
26
24
  const branchName = args[0];
27
25
  if (!branchName) {
28
- console.log(`${colors.red('error:')} Please specify a branch`);
29
- console.log('');
30
- console.log(`Usage: ${colors.cyan('relq checkout <branch>')}`);
31
- console.log(` ${colors.cyan('relq checkout -b <new-branch>')}`);
32
- console.log('');
26
+ error('please specify a branch');
27
+ console.log("usage: relq checkout <branch>");
28
+ console.log(" relq checkout -b <new-branch>");
33
29
  return;
34
30
  }
35
31
  const staged = getStagedChanges(projectRoot);
36
32
  const unstaged = getUnstagedChanges(projectRoot);
37
33
  if (staged.length > 0 || unstaged.length > 0) {
38
- console.log(`${colors.red('error:')} You have uncommitted changes`);
39
- console.log('');
40
- console.log('Commit or stash them before switching branches:');
41
- console.log(` ${colors.cyan('relq commit -m "message"')}`);
42
- console.log(` ${colors.cyan('relq stash')}`);
43
- console.log('');
34
+ error('you have uncommitted changes');
35
+ hint("run 'relq commit -m <message>' to commit");
36
+ hint("run 'relq stash' to stash changes");
44
37
  return;
45
38
  }
46
39
  const state = loadBranchState(projectRoot);
47
40
  if (createBranch) {
48
41
  if (state.branches[branchName]) {
49
- console.log(`${colors.red('error:')} Branch already exists: ${branchName}`);
50
- return;
42
+ fatal(`branch '${branchName}' already exists`);
51
43
  }
52
44
  const head = getHead(projectRoot);
53
45
  if (!head) {
54
- console.log(`${colors.red('error:')} No commits yet`);
55
- return;
46
+ fatal('no commits yet', "run 'relq pull' or 'relq import' first");
56
47
  }
57
48
  state.branches[branchName] = head;
58
49
  state.current = branchName;
59
50
  saveBranchState(state, projectRoot);
60
- console.log(`${colors.green('✓')} Switched to new branch '${branchName}'`);
61
- console.log('');
51
+ console.log(`Switched to a new branch '${branchName}'`);
62
52
  return;
63
53
  }
64
54
  if (!state.branches[branchName]) {
65
- console.log(`${colors.red('error:')} Branch not found: ${branchName}`);
66
- console.log('');
55
+ error(`pathspec '${branchName}' did not match any branch known to relq`);
67
56
  console.log('Available branches:');
68
57
  for (const name of Object.keys(state.branches)) {
69
- console.log(` ${name}`);
58
+ console.log(` ${name}`);
70
59
  }
71
- console.log('');
72
60
  return;
73
61
  }
74
62
  if (state.current === branchName) {
@@ -83,8 +71,7 @@ export async function checkoutCommand(context) {
83
71
  const targetHash = state.branches[branchName];
84
72
  const targetCommit = loadCommit(targetHash, projectRoot);
85
73
  if (!targetCommit) {
86
- console.log(`${colors.red('error:')} Cannot find commit for branch`);
87
- return;
74
+ fatal('cannot find commit for branch');
88
75
  }
89
76
  if (targetCommit.schema) {
90
77
  saveSnapshot(targetCommit.schema, projectRoot);
@@ -92,7 +79,7 @@ export async function checkoutCommand(context) {
92
79
  setHead(targetHash, projectRoot);
93
80
  state.current = branchName;
94
81
  saveBranchState(state, projectRoot);
95
- console.log(`${colors.green('✓')} Switched to branch '${branchName}'`);
82
+ console.log(`Switched to branch '${branchName}'`);
96
83
  console.log('');
97
84
  }
98
85
  export default checkoutCommand;
@@ -3,8 +3,7 @@ import * as path from 'path';
3
3
  import { colors, createSpinner } from "../utils/spinner.js";
4
4
  import { isInitialized, loadCommit, loadParentCommit, loadSnapshot, saveSnapshot, createCommit, shortHash, resolveRef, } from "../utils/repo-manager.js";
5
5
  export async function cherryPickCommand(context) {
6
- const { config, args, flags } = context;
7
- const projectRoot = process.cwd();
6
+ const { config, args, flags, projectRoot } = context;
8
7
  console.log('');
9
8
  if (!isInitialized(projectRoot)) {
10
9
  console.log(`${colors.red('fatal:')} not a relq repository`);
@@ -15,10 +14,10 @@ export async function cherryPickCommand(context) {
15
14
  if (abort) {
16
15
  if (fs.existsSync(cherryPickStatePath)) {
17
16
  fs.unlinkSync(cherryPickStatePath);
18
- console.log(`${colors.green('✓')} Cherry-pick aborted`);
17
+ console.log('Cherry-pick aborted');
19
18
  }
20
19
  else {
21
- console.log(`${colors.muted('No cherry-pick in progress.')}`);
20
+ console.log('No cherry-pick in progress.');
22
21
  }
23
22
  console.log('');
24
23
  return;