turbine-orm 0.3.1 → 0.5.0

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.
@@ -0,0 +1,421 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm CLI — Migration system
4
+ *
5
+ * SQL-first migrations with UP/DOWN sections, tracked in _turbine_migrations.
6
+ * Migration files are timestamp-prefixed .sql files.
7
+ *
8
+ * File format:
9
+ * -- UP
10
+ * CREATE TABLE users (...);
11
+ *
12
+ * -- DOWN
13
+ * DROP TABLE users;
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.parseMigrationFilename = parseMigrationFilename;
20
+ exports.sanitizeName = sanitizeName;
21
+ exports.formatTimestamp = formatTimestamp;
22
+ exports.getPendingMigrations = getPendingMigrations;
23
+ exports.listMigrationFiles = listMigrationFiles;
24
+ exports.parseMigrationContent = parseMigrationContent;
25
+ exports.parseMigrationSQL = parseMigrationSQL;
26
+ exports.createMigration = createMigration;
27
+ exports.migrateUp = migrateUp;
28
+ exports.migrateDown = migrateDown;
29
+ exports.migrateStatus = migrateStatus;
30
+ const node_fs_1 = require("node:fs");
31
+ const node_path_1 = require("node:path");
32
+ const pg_1 = __importDefault(require("pg"));
33
+ // ---------------------------------------------------------------------------
34
+ // Tracking table management
35
+ // ---------------------------------------------------------------------------
36
+ const TRACKING_TABLE = '_turbine_migrations';
37
+ const CREATE_TRACKING_TABLE = `
38
+ CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE} (
39
+ id SERIAL PRIMARY KEY,
40
+ name TEXT NOT NULL UNIQUE,
41
+ checksum TEXT NOT NULL,
42
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
43
+ );
44
+ `;
45
+ async function ensureTrackingTable(client) {
46
+ await client.query(CREATE_TRACKING_TABLE);
47
+ }
48
+ async function getAppliedMigrations(client) {
49
+ await ensureTrackingTable(client);
50
+ const result = await client.query(`SELECT id, name, applied_at, checksum FROM ${TRACKING_TABLE} ORDER BY id ASC`);
51
+ return result.rows;
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // File operations
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * Parse a migration filename into its components.
58
+ * Expected format: YYYYMMDDHHMMSS_description.sql
59
+ */
60
+ function parseMigrationFilename(filename) {
61
+ const match = filename.match(/^(\d{14})_(.+)\.sql$/);
62
+ if (!match)
63
+ return null;
64
+ return {
65
+ filename,
66
+ path: '', // Set by caller
67
+ name: filename.replace(/\.sql$/, ''),
68
+ timestamp: match[1],
69
+ };
70
+ }
71
+ /**
72
+ * Sanitize a migration name: lowercase, replace non-alnum with _, collapse duplicates, trim.
73
+ */
74
+ function sanitizeName(name) {
75
+ return name
76
+ .toLowerCase()
77
+ .replace(/[^a-z0-9_]/g, '_')
78
+ .replace(/_+/g, '_')
79
+ .replace(/^_|_$/g, '');
80
+ }
81
+ /**
82
+ * Generate a YYYYMMDDHHMMSS timestamp string from a Date.
83
+ */
84
+ function formatTimestamp(date) {
85
+ return [
86
+ date.getFullYear(),
87
+ String(date.getMonth() + 1).padStart(2, '0'),
88
+ String(date.getDate()).padStart(2, '0'),
89
+ String(date.getHours()).padStart(2, '0'),
90
+ String(date.getMinutes()).padStart(2, '0'),
91
+ String(date.getSeconds()).padStart(2, '0'),
92
+ ].join('');
93
+ }
94
+ /**
95
+ * Get pending migration files — those not yet applied.
96
+ * Returns files sorted by timestamp (ascending).
97
+ */
98
+ function getPendingMigrations(migrationsDir, applied) {
99
+ const appliedSet = new Set(applied);
100
+ return listMigrationFiles(migrationsDir).filter((f) => !appliedSet.has(f.name));
101
+ }
102
+ /**
103
+ * List all migration files in the migrations directory, sorted by name.
104
+ */
105
+ function listMigrationFiles(migrationsDir) {
106
+ if (!(0, node_fs_1.existsSync)(migrationsDir))
107
+ return [];
108
+ const entries = (0, node_fs_1.readdirSync)(migrationsDir)
109
+ .filter((f) => f.endsWith('.sql'))
110
+ .sort();
111
+ const files = [];
112
+ for (const entry of entries) {
113
+ const parsed = parseMigrationFilename(entry);
114
+ if (parsed) {
115
+ parsed.path = (0, node_path_1.join)(migrationsDir, entry);
116
+ files.push(parsed);
117
+ }
118
+ }
119
+ return files;
120
+ }
121
+ /**
122
+ * Parse migration content string into UP and DOWN sections.
123
+ * Exported for unit testing.
124
+ */
125
+ function parseMigrationContent(content) {
126
+ const lines = content.split('\n');
127
+ let section = 'none';
128
+ const upLines = [];
129
+ const downLines = [];
130
+ for (const line of lines) {
131
+ const trimmed = line.trim().toUpperCase();
132
+ if (trimmed === '-- UP') {
133
+ section = 'up';
134
+ continue;
135
+ }
136
+ if (trimmed === '-- DOWN') {
137
+ section = 'down';
138
+ continue;
139
+ }
140
+ if (section === 'up')
141
+ upLines.push(line);
142
+ else if (section === 'down')
143
+ downLines.push(line);
144
+ }
145
+ return {
146
+ up: upLines.join('\n').trim(),
147
+ down: downLines.join('\n').trim(),
148
+ };
149
+ }
150
+ /**
151
+ * Parse a migration file into UP and DOWN sections.
152
+ */
153
+ function parseMigrationSQL(filePath) {
154
+ const content = (0, node_fs_1.readFileSync)(filePath, 'utf-8');
155
+ return parseMigrationContent(content);
156
+ }
157
+ /**
158
+ * Simple checksum for a migration file (for drift detection).
159
+ */
160
+ function checksum(content) {
161
+ let hash = 0;
162
+ for (let i = 0; i < content.length; i++) {
163
+ const chr = content.charCodeAt(i);
164
+ hash = ((hash << 5) - hash + chr) | 0;
165
+ }
166
+ return Math.abs(hash).toString(36);
167
+ }
168
+ // ---------------------------------------------------------------------------
169
+ // Commands
170
+ // ---------------------------------------------------------------------------
171
+ /**
172
+ * Create a new migration file.
173
+ */
174
+ function createMigration(migrationsDir, name) {
175
+ (0, node_fs_1.mkdirSync)(migrationsDir, { recursive: true });
176
+ const now = new Date();
177
+ const ts = formatTimestamp(now);
178
+ const safeName = sanitizeName(name);
179
+ const filename = `${ts}_${safeName}.sql`;
180
+ const filePath = (0, node_path_1.join)(migrationsDir, filename);
181
+ const template = `-- Migration: ${name}
182
+ -- Created: ${now.toISOString()}
183
+
184
+ -- UP
185
+ -- Write your migration SQL here
186
+
187
+ -- DOWN
188
+ -- Write your rollback SQL here
189
+ `;
190
+ (0, node_fs_1.writeFileSync)(filePath, template, 'utf-8');
191
+ return {
192
+ filename,
193
+ path: filePath,
194
+ name: filename.replace(/\.sql$/, ''),
195
+ timestamp: ts,
196
+ };
197
+ }
198
+ // ---------------------------------------------------------------------------
199
+ // Advisory lock for concurrent migration safety
200
+ // ---------------------------------------------------------------------------
201
+ /** Fixed lock ID for Turbine migrations — prevents concurrent migrate runs */
202
+ const MIGRATION_LOCK_ID = 8_347_291; // arbitrary but stable
203
+ async function acquireLock(client) {
204
+ const result = await client.query(`SELECT pg_try_advisory_lock($1)`, [MIGRATION_LOCK_ID]);
205
+ return result.rows[0]?.pg_try_advisory_lock ?? false;
206
+ }
207
+ async function releaseLock(client) {
208
+ await client.query(`SELECT pg_advisory_unlock($1)`, [MIGRATION_LOCK_ID]);
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // Checksum validation
212
+ // ---------------------------------------------------------------------------
213
+ /**
214
+ * Validate that applied migration files have not been modified since they were run.
215
+ * Returns an array of mismatched migrations (empty if all are clean).
216
+ */
217
+ async function validateChecksums(client, migrationsDir) {
218
+ const applied = await getAppliedMigrations(client);
219
+ const allFiles = listMigrationFiles(migrationsDir);
220
+ const fileMap = new Map(allFiles.map((f) => [f.name, f]));
221
+ const mismatches = [];
222
+ for (const migration of applied) {
223
+ const file = fileMap.get(migration.name);
224
+ if (!file)
225
+ continue; // file deleted — not a checksum issue
226
+ const content = (0, node_fs_1.readFileSync)(file.path, 'utf-8');
227
+ const currentHash = checksum(content);
228
+ if (currentHash !== migration.checksum) {
229
+ mismatches.push({
230
+ name: migration.name,
231
+ expected: migration.checksum,
232
+ actual: currentHash,
233
+ });
234
+ }
235
+ }
236
+ return mismatches;
237
+ }
238
+ /**
239
+ * Apply all pending migrations (UP).
240
+ *
241
+ * Features:
242
+ * - Idempotent: running twice is safe (already-applied migrations are skipped)
243
+ * - Advisory lock: prevents concurrent migration runs
244
+ * - Checksum validation: detects modified migration files
245
+ * - Each migration runs in its own transaction
246
+ */
247
+ async function migrateUp(connectionString, migrationsDir, options) {
248
+ const client = new pg_1.default.Client({ connectionString });
249
+ await client.connect();
250
+ try {
251
+ // Acquire advisory lock to prevent concurrent migrations
252
+ const gotLock = await acquireLock(client);
253
+ if (!gotLock) {
254
+ return {
255
+ applied: [],
256
+ errors: [{
257
+ file: { filename: '', path: '', name: '', timestamp: '' },
258
+ error: 'Could not acquire migration lock — another migration is already running',
259
+ }],
260
+ };
261
+ }
262
+ try {
263
+ await ensureTrackingTable(client);
264
+ // Validate checksums of already-applied migrations
265
+ const mismatches = await validateChecksums(client, migrationsDir);
266
+ if (mismatches.length > 0) {
267
+ const names = mismatches.map((m) => m.name).join(', ');
268
+ return {
269
+ applied: [],
270
+ errors: [{
271
+ file: { filename: '', path: '', name: '', timestamp: '' },
272
+ error: `Checksum mismatch: migration file(s) modified after application: ${names}. This is dangerous — applied migrations should be immutable. Use --force to skip this check.`,
273
+ }],
274
+ };
275
+ }
276
+ const applied = await getAppliedMigrations(client);
277
+ const appliedNames = new Set(applied.map((m) => m.name));
278
+ const allFiles = listMigrationFiles(migrationsDir);
279
+ let pending = allFiles.filter((f) => !appliedNames.has(f.name));
280
+ if (options?.step != null && options.step > 0) {
281
+ pending = pending.slice(0, options.step);
282
+ }
283
+ const results = [];
284
+ const errors = [];
285
+ for (const file of pending) {
286
+ const { up } = parseMigrationSQL(file.path);
287
+ if (!up) {
288
+ errors.push({ file, error: 'No UP section found in migration file' });
289
+ continue;
290
+ }
291
+ const content = (0, node_fs_1.readFileSync)(file.path, 'utf-8');
292
+ const hash = checksum(content);
293
+ try {
294
+ await client.query('BEGIN');
295
+ await client.query(up);
296
+ await client.query(`INSERT INTO ${TRACKING_TABLE} (name, checksum) VALUES ($1, $2) ON CONFLICT (name) DO NOTHING`, [file.name, hash]);
297
+ await client.query('COMMIT');
298
+ results.push(file);
299
+ }
300
+ catch (err) {
301
+ await client.query('ROLLBACK');
302
+ const msg = err instanceof Error ? err.message : String(err);
303
+ errors.push({ file, error: msg });
304
+ // Stop on first error
305
+ break;
306
+ }
307
+ }
308
+ return { applied: results, errors };
309
+ }
310
+ finally {
311
+ await releaseLock(client);
312
+ }
313
+ }
314
+ finally {
315
+ await client.end();
316
+ }
317
+ }
318
+ /**
319
+ * Rollback the last N migrations (DOWN).
320
+ *
321
+ * Features:
322
+ * - Advisory lock: prevents concurrent rollback runs
323
+ * - Each rollback runs in its own transaction
324
+ * - Properly reverses changes in reverse application order
325
+ */
326
+ async function migrateDown(connectionString, migrationsDir, options) {
327
+ const client = new pg_1.default.Client({ connectionString });
328
+ await client.connect();
329
+ try {
330
+ const gotLock = await acquireLock(client);
331
+ if (!gotLock) {
332
+ return {
333
+ rolledBack: [],
334
+ errors: [{
335
+ file: { filename: '', path: '', name: '', timestamp: '' },
336
+ error: 'Could not acquire migration lock — another migration is already running',
337
+ }],
338
+ };
339
+ }
340
+ try {
341
+ await ensureTrackingTable(client);
342
+ const applied = await getAppliedMigrations(client);
343
+ if (applied.length === 0) {
344
+ return { rolledBack: [], errors: [] };
345
+ }
346
+ const allFiles = listMigrationFiles(migrationsDir);
347
+ const fileMap = new Map(allFiles.map((f) => [f.name, f]));
348
+ // Reverse order — rollback most recent first
349
+ const toRollback = applied.reverse().slice(0, options?.step ?? 1);
350
+ const results = [];
351
+ const errors = [];
352
+ for (const migration of toRollback) {
353
+ const file = fileMap.get(migration.name);
354
+ if (!file) {
355
+ errors.push({
356
+ file: { filename: migration.name + '.sql', path: '', name: migration.name, timestamp: '' },
357
+ error: `Migration file not found for "${migration.name}"`,
358
+ });
359
+ continue;
360
+ }
361
+ const { down } = parseMigrationSQL(file.path);
362
+ if (!down) {
363
+ errors.push({ file, error: 'No DOWN section found in migration file' });
364
+ continue;
365
+ }
366
+ try {
367
+ await client.query('BEGIN');
368
+ await client.query(down);
369
+ await client.query(`DELETE FROM ${TRACKING_TABLE} WHERE name = $1`, [migration.name]);
370
+ await client.query('COMMIT');
371
+ results.push(file);
372
+ }
373
+ catch (err) {
374
+ await client.query('ROLLBACK');
375
+ const msg = err instanceof Error ? err.message : String(err);
376
+ errors.push({ file, error: msg });
377
+ break;
378
+ }
379
+ }
380
+ return { rolledBack: results, errors };
381
+ }
382
+ finally {
383
+ await releaseLock(client);
384
+ }
385
+ }
386
+ finally {
387
+ await client.end();
388
+ }
389
+ }
390
+ /**
391
+ * Get the status of all migrations (applied vs pending).
392
+ * Includes checksum validation for applied migrations.
393
+ */
394
+ async function migrateStatus(connectionString, migrationsDir) {
395
+ const client = new pg_1.default.Client({ connectionString });
396
+ await client.connect();
397
+ try {
398
+ await ensureTrackingTable(client);
399
+ const applied = await getAppliedMigrations(client);
400
+ const appliedMap = new Map(applied.map((m) => [m.name, m]));
401
+ const allFiles = listMigrationFiles(migrationsDir);
402
+ return allFiles.map((file) => {
403
+ const record = appliedMap.get(file.name);
404
+ let checksumValid;
405
+ if (record) {
406
+ const content = (0, node_fs_1.readFileSync)(file.path, 'utf-8');
407
+ const currentHash = checksum(content);
408
+ checksumValid = currentHash === record.checksum;
409
+ }
410
+ return {
411
+ file,
412
+ applied: !!record,
413
+ appliedAt: record?.applied_at,
414
+ checksumValid,
415
+ };
416
+ });
417
+ }
418
+ finally {
419
+ await client.end();
420
+ }
421
+ }
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm CLI — UI utilities
4
+ *
5
+ * ANSI colors, spinners, box-drawing, and formatting helpers.
6
+ * Zero dependencies — raw escape codes only.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Spinner = exports.symbols = exports.bgCyan = exports.bgYellow = exports.bgRed = exports.bgGreen = exports.redBright = exports.yellowBright = exports.cyanBright = exports.greenBright = exports.gray = exports.white = exports.cyan = exports.magenta = exports.blue = exports.yellow = exports.green = exports.red = exports.underline = exports.italic = exports.dim = exports.bold = void 0;
10
+ exports.box = box;
11
+ exports.table = table;
12
+ exports.header = header;
13
+ exports.success = success;
14
+ exports.error = error;
15
+ exports.warn = warn;
16
+ exports.info = info;
17
+ exports.label = label;
18
+ exports.newline = newline;
19
+ exports.divider = divider;
20
+ exports.banner = banner;
21
+ exports.elapsed = elapsed;
22
+ exports.stripAnsi = stripAnsi;
23
+ exports.redactUrl = redactUrl;
24
+ // ---------------------------------------------------------------------------
25
+ // ANSI escape codes
26
+ // ---------------------------------------------------------------------------
27
+ const isColorSupported = process.env['NO_COLOR'] == null &&
28
+ process.env['TERM'] !== 'dumb' &&
29
+ (process.stdout.isTTY ?? false);
30
+ function code(open, close) {
31
+ if (!isColorSupported)
32
+ return (s) => s;
33
+ return (s) => `\x1b[${open}m${s}\x1b[${close}m`;
34
+ }
35
+ exports.bold = code('1', '22');
36
+ exports.dim = code('2', '22');
37
+ exports.italic = code('3', '23');
38
+ exports.underline = code('4', '24');
39
+ exports.red = code('31', '39');
40
+ exports.green = code('32', '39');
41
+ exports.yellow = code('33', '39');
42
+ exports.blue = code('34', '39');
43
+ exports.magenta = code('35', '39');
44
+ exports.cyan = code('36', '39');
45
+ exports.white = code('37', '39');
46
+ exports.gray = code('90', '39');
47
+ // Bright variants
48
+ exports.greenBright = code('92', '39');
49
+ exports.cyanBright = code('96', '39');
50
+ exports.yellowBright = code('93', '39');
51
+ exports.redBright = code('91', '39');
52
+ // Background
53
+ exports.bgGreen = code('42', '49');
54
+ exports.bgRed = code('41', '49');
55
+ exports.bgYellow = code('43', '49');
56
+ exports.bgCyan = code('46', '49');
57
+ // ---------------------------------------------------------------------------
58
+ // Symbols
59
+ // ---------------------------------------------------------------------------
60
+ exports.symbols = {
61
+ check: isColorSupported ? '\u2713' : 'v',
62
+ cross: isColorSupported ? '\u2717' : 'x',
63
+ bullet: isColorSupported ? '\u2022' : '*',
64
+ arrow: isColorSupported ? '\u2192' : '->',
65
+ arrowRight: isColorSupported ? '\u25B8' : '>',
66
+ info: isColorSupported ? '\u2139' : 'i',
67
+ warning: isColorSupported ? '\u26A0' : '!',
68
+ dot: isColorSupported ? '\u2219' : '.',
69
+ line: isColorSupported ? '\u2500' : '-',
70
+ vertLine: isColorSupported ? '\u2502' : '|',
71
+ topLeft: isColorSupported ? '\u256D' : '+',
72
+ topRight: isColorSupported ? '\u256E' : '+',
73
+ bottomLeft: isColorSupported ? '\u2570' : '+',
74
+ bottomRight: isColorSupported ? '\u256F' : '+',
75
+ tee: isColorSupported ? '\u251C' : '|',
76
+ teeEnd: isColorSupported ? '\u2514' : '\\',
77
+ };
78
+ // ---------------------------------------------------------------------------
79
+ // Box drawing
80
+ // ---------------------------------------------------------------------------
81
+ function box(content, options) {
82
+ const padding = options?.padding ?? 1;
83
+ const lines = content.split('\n');
84
+ const pad = ' '.repeat(padding);
85
+ // Calculate max width (strip ANSI for measurement)
86
+ const maxWidth = Math.max(...lines.map((l) => stripAnsi(l).length), options?.title ? stripAnsi(options.title).length + 2 : 0);
87
+ const innerWidth = maxWidth + padding * 2;
88
+ const top = options?.title
89
+ ? `${exports.symbols.topLeft}${exports.symbols.line} ${options.title} ${exports.symbols.line.repeat(Math.max(0, innerWidth - stripAnsi(options.title).length - 3))}${exports.symbols.topRight}`
90
+ : `${exports.symbols.topLeft}${exports.symbols.line.repeat(innerWidth)}${exports.symbols.topRight}`;
91
+ const bottom = `${exports.symbols.bottomLeft}${exports.symbols.line.repeat(innerWidth)}${exports.symbols.bottomRight}`;
92
+ const body = lines.map((line) => {
93
+ const stripped = stripAnsi(line);
94
+ const rightPad = ' '.repeat(Math.max(0, maxWidth - stripped.length));
95
+ return `${exports.symbols.vertLine}${pad}${line}${rightPad}${pad}${exports.symbols.vertLine}`;
96
+ });
97
+ return [top, ...body, bottom].join('\n');
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // Table formatting
101
+ // ---------------------------------------------------------------------------
102
+ function table(headers, rows) {
103
+ const colWidths = headers.map((h, i) => {
104
+ const dataMax = rows.reduce((max, row) => {
105
+ const cell = row[i] ?? '';
106
+ return Math.max(max, stripAnsi(cell).length);
107
+ }, 0);
108
+ return Math.max(stripAnsi(h).length, dataMax);
109
+ });
110
+ const headerLine = headers
111
+ .map((h, i) => {
112
+ const w = colWidths[i];
113
+ return ` ${(0, exports.bold)(h)}${' '.repeat(Math.max(0, w - stripAnsi(h).length))} `;
114
+ })
115
+ .join((0, exports.dim)(exports.symbols.vertLine));
116
+ const separator = colWidths
117
+ .map((w) => exports.symbols.line.repeat(w + 2))
118
+ .join((0, exports.dim)(exports.symbols.line));
119
+ const bodyLines = rows.map((row) => row
120
+ .map((cell, i) => {
121
+ const w = colWidths[i];
122
+ return ` ${cell}${' '.repeat(Math.max(0, w - stripAnsi(cell).length))} `;
123
+ })
124
+ .join((0, exports.dim)(exports.symbols.vertLine)));
125
+ return [headerLine, (0, exports.dim)(separator), ...bodyLines].join('\n');
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // Spinner (simple dots animation)
129
+ // ---------------------------------------------------------------------------
130
+ class Spinner {
131
+ frames = [' ', '. ', '.. ', '...', ' ..', ' .'];
132
+ frameIndex = 0;
133
+ interval = null;
134
+ message;
135
+ constructor(message) {
136
+ this.message = message;
137
+ }
138
+ start() {
139
+ if (!isColorSupported || !process.stdout.isTTY) {
140
+ process.stdout.write(` ${this.message}...\n`);
141
+ return this;
142
+ }
143
+ this.interval = setInterval(() => {
144
+ const frame = this.frames[this.frameIndex % this.frames.length];
145
+ process.stdout.write(`\r ${(0, exports.cyan)(frame)} ${this.message}`);
146
+ this.frameIndex++;
147
+ }, 120);
148
+ return this;
149
+ }
150
+ succeed(msg) {
151
+ this.stop();
152
+ const text = msg ?? this.message;
153
+ process.stdout.write(`\r ${(0, exports.green)(exports.symbols.check)} ${text}\n`);
154
+ }
155
+ fail(msg) {
156
+ this.stop();
157
+ const text = msg ?? this.message;
158
+ process.stdout.write(`\r ${(0, exports.red)(exports.symbols.cross)} ${text}\n`);
159
+ }
160
+ info(msg) {
161
+ this.stop();
162
+ const text = msg ?? this.message;
163
+ process.stdout.write(`\r ${(0, exports.blue)(exports.symbols.info)} ${text}\n`);
164
+ }
165
+ stop() {
166
+ if (this.interval) {
167
+ clearInterval(this.interval);
168
+ this.interval = null;
169
+ // Clear the line
170
+ if (isColorSupported && process.stdout.isTTY) {
171
+ process.stdout.write('\r\x1b[K');
172
+ }
173
+ }
174
+ }
175
+ }
176
+ exports.Spinner = Spinner;
177
+ // ---------------------------------------------------------------------------
178
+ // Logging helpers
179
+ // ---------------------------------------------------------------------------
180
+ function header(text) {
181
+ console.log('');
182
+ console.log(` ${(0, exports.bold)((0, exports.cyan)(text))}`);
183
+ console.log('');
184
+ }
185
+ function success(msg) {
186
+ console.log(` ${(0, exports.green)(exports.symbols.check)} ${msg}`);
187
+ }
188
+ function error(msg) {
189
+ console.log(` ${(0, exports.red)(exports.symbols.cross)} ${msg}`);
190
+ }
191
+ function warn(msg) {
192
+ console.log(` ${(0, exports.yellow)(exports.symbols.warning)} ${msg}`);
193
+ }
194
+ function info(msg) {
195
+ console.log(` ${(0, exports.blue)(exports.symbols.info)} ${msg}`);
196
+ }
197
+ function label(key, value) {
198
+ console.log(` ${(0, exports.dim)(key + ':')} ${value}`);
199
+ }
200
+ function newline() {
201
+ console.log('');
202
+ }
203
+ function divider() {
204
+ const width = Math.min(process.stdout.columns ?? 60, 60);
205
+ console.log(` ${(0, exports.dim)(exports.symbols.line.repeat(width - 4))}`);
206
+ }
207
+ // ---------------------------------------------------------------------------
208
+ // Banner
209
+ // ---------------------------------------------------------------------------
210
+ function banner() {
211
+ console.log('');
212
+ console.log(` ${(0, exports.bold)((0, exports.cyan)('turbine'))} ${(0, exports.dim)('by')} ${(0, exports.bold)('BataData')}`);
213
+ console.log(` ${(0, exports.dim)('TypeScript ORM with json_agg nested queries')}`);
214
+ console.log('');
215
+ }
216
+ // ---------------------------------------------------------------------------
217
+ // Elapsed time formatting
218
+ // ---------------------------------------------------------------------------
219
+ function elapsed(startMs) {
220
+ const ms = performance.now() - startMs;
221
+ if (ms < 1000)
222
+ return `${Math.round(ms)}ms`;
223
+ return `${(ms / 1000).toFixed(2)}s`;
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // Strip ANSI codes (for width calculation)
227
+ // ---------------------------------------------------------------------------
228
+ function stripAnsi(s) {
229
+ // eslint-disable-next-line no-control-regex
230
+ return s.replace(/\x1b\[[0-9;]*m/g, '');
231
+ }
232
+ // ---------------------------------------------------------------------------
233
+ // Redact password from connection URL
234
+ // ---------------------------------------------------------------------------
235
+ function redactUrl(url) {
236
+ return url.replace(/:([^@/:]+)@/, ':***@');
237
+ }