flint-orm 0.2.0 → 0.3.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.
Files changed (4) hide show
  1. package/API.md +178 -16
  2. package/README.md +238 -0
  3. package/package.json +5 -1
  4. package/src/cli.ts +46 -38
package/API.md CHANGED
@@ -33,12 +33,12 @@ const user = db.select().from(users).where(eq(users.id, 'u1')).single().execute(
33
33
 
34
34
  ## Schema
35
35
 
36
- ### `table(name, columns)`
36
+ ### `table(name, columns, indexFn?)`
37
37
 
38
38
  Define a table. Columns live as direct properties. SQL metadata is under `._`.
39
39
 
40
40
  ```ts
41
- import { table, text, integer, boolean } from 'flint-orm';
41
+ import { table, text, integer, boolean, index } from 'flint-orm';
42
42
 
43
43
  const users = table('users', {
44
44
  id: text('id').primaryKey(),
@@ -49,7 +49,7 @@ const users = table('users', {
49
49
  });
50
50
  ```
51
51
 
52
- ### `snakeCase.table(name, columns)`
52
+ ### `snakeCase.table(name, columns, indexFn?)`
53
53
 
54
54
  Auto-converts camelCase keys to snake_case SQL names.
55
55
 
@@ -122,6 +122,28 @@ type UserInsert = InsertRow<typeof users>;
122
122
 
123
123
  ---
124
124
 
125
+ ## Indexes
126
+
127
+ ### `index(name).on(columns).unique()`
128
+
129
+ Define indexes via the table callback. Chainable API.
130
+
131
+ ```ts
132
+ const users = table(
133
+ 'users',
134
+ {
135
+ id: text('id').primaryKey(),
136
+ email: text('email'),
137
+ name: text('name'),
138
+ },
139
+ (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_name').on(t.name), index('idx_users_email_name').on(t.email, t.name)],
140
+ );
141
+ ```
142
+
143
+ The callback receives the table definition and returns an array of `IndexBuilder` objects. Indexes are attached to the table automatically and serialized for migrations.
144
+
145
+ ---
146
+
125
147
  ## Query Builder
126
148
 
127
149
  ### `db.select().from(table)`
@@ -333,7 +355,7 @@ const deleted = db.delete(users).where(eq(users.id, 'u1')).returning(['id', 'nam
333
355
 
334
356
  ### `db.leftJoin(parent).on(child, condition?)`
335
357
 
336
- LEFT JOIN. Returns all parent rows, with matching child data or null.
358
+ LEFT JOIN. Returns all parent rows, with matching child data nested under `__children`.
337
359
 
338
360
  ```ts
339
361
  const orders = table('orders', {
@@ -367,7 +389,7 @@ Joins return **nested** results, not flat-merged:
367
389
  {
368
390
  id: 'u1',
369
391
  name: 'Alice',
370
- orders: [
392
+ __children: [
371
393
  { id: 'o1', userId: 'u1', total: 100 },
372
394
  { id: 'o2', userId: 'u1', total: 200 },
373
395
  ],
@@ -381,14 +403,23 @@ Narrow parent columns with `.columns()`:
381
403
 
382
404
  ```ts
383
405
  db.leftJoin(orders).on(users, eq(orders.userId, users.id)).columns(['id', 'name']).execute();
384
- // Returns: { id: string; name: string; orders: OrderRow[] }[]
406
+ // Returns: { id: string; name: string; __children: OrderRow[] }[]
385
407
  ```
386
408
 
387
409
  ### `.single()` on Joins
388
410
 
389
411
  ```ts
390
412
  db.leftJoin(orders).on(users, eq(orders.userId, users.id)).single().execute();
391
- // Returns: { id: string; name: string; orders: OrderRow[] } | null
413
+ // Returns: { id: string; name: string; __children: OrderRow[] } | null
414
+ ```
415
+
416
+ ### Multi-Join
417
+
418
+ Chain multiple joins:
419
+
420
+ ```ts
421
+ db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
422
+ // Returns nested: { ...userFields, __children: [{ ...orderFields, __children: [...items] }] }
392
423
  ```
393
424
 
394
425
  ---
@@ -557,14 +588,21 @@ All queries succeed or all roll back.
557
588
 
558
589
  ## Raw SQL
559
590
 
560
- Access the underlying `bun:sqlite` client directly for raw queries.
591
+ ### `db.$run(sql, ...params)`
592
+
593
+ Execute raw SQL directly against the database.
561
594
 
562
595
  ```ts
563
- // Simple query
564
- const users = db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
596
+ db.$run('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)');
597
+ db.$run('INSERT INTO test VALUES (?, ?)', 1, 'Alice');
598
+ ```
599
+
600
+ ### `db.$client`
565
601
 
566
- // With type annotation
567
- const rows = db.$client.prepare('SELECT count(*) as cnt FROM users').all() as { cnt: number }[];
602
+ Direct access to the underlying `bun:sqlite` client.
603
+
604
+ ```ts
605
+ const rows = db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
568
606
  ```
569
607
 
570
608
  ---
@@ -579,20 +617,142 @@ import { sql } from 'flint-orm';
579
617
  const expr = sql`SELECT * FROM users WHERE name = ${'Alice'} AND age > ${18}`;
580
618
  // { sql: "SELECT * FROM users WHERE name = ? AND age > ?", params: ["Alice", 18] }
581
619
 
582
- // Execute with db.$client
620
+ // Use with db.$client
583
621
  const rows = db.$client.prepare(expr.sql).all(...expr.params);
584
622
  ```
585
623
 
586
624
  ---
587
625
 
588
- ## Escape Hatch
626
+ ## Migration System
627
+
628
+ ### `flint generate` (CLI)
629
+
630
+ Generate a migration from schema changes.
631
+
632
+ ```bash
633
+ # Generate migration
634
+ flint generate --name init_schema
635
+
636
+ # Preview SQL without writing files
637
+ flint generate --name init_schema --preview
638
+ ```
639
+
640
+ ### `flint migrate` (CLI)
641
+
642
+ Apply pending migrations to the database.
643
+
644
+ ```bash
645
+ # Apply all pending migrations
646
+ flint migrate
647
+
648
+ # Show which migrations are pending/applied
649
+ flint migrate --status
650
+ ```
651
+
652
+ ### Config
653
+
654
+ Create `flint.config.ts` in your project root:
655
+
656
+ ```ts
657
+ import { defineConfig } from 'flint-orm/config';
658
+
659
+ export default defineConfig({
660
+ schema: './src/schema', // folder or file with table() definitions
661
+ migrations: './flint', // where migration folders are stored
662
+ database: {
663
+ url: './app.db', // SQLite database path
664
+ },
665
+ });
666
+ ```
667
+
668
+ ### Programmatic API
669
+
670
+ ```ts
671
+ import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';
672
+
673
+ // Serialize table definitions to JSON
674
+ const state = serializeSchema([users, orders]);
675
+
676
+ // Diff two schema states
677
+ const operations = diffSchemas(previousState, currentState);
678
+
679
+ // Generate SQL from operations
680
+ const sql = generateSQL(operations);
681
+
682
+ // Generate a migration folder
683
+ const result = generate([users, orders], './flint', 'init_schema');
684
+
685
+ // Apply pending migrations
686
+ const result = migrate({ url: './app.db' }, './flint');
687
+
688
+ // Check migration status
689
+ const status = getMigrationStatus(client, './flint');
690
+ ```
691
+
692
+ ### Migration Operations
693
+
694
+ Named, pre-vetted operations:
695
+
696
+ ```ts
697
+ import { addTable, dropTable, renameTable, addColumn, dropColumn, renameColumn, createIndex, dropIndex } from 'flint-orm/migration';
698
+ ```
699
+
700
+ | Operation | SQL Generated |
701
+ | -------------- | -------------------------------------- |
702
+ | `addTable` | `CREATE TABLE ...` |
703
+ | `dropTable` | `DROP TABLE ...` |
704
+ | `renameTable` | `ALTER TABLE ... RENAME TO ...` |
705
+ | `addColumn` | `ALTER TABLE ... ADD COLUMN ...` |
706
+ | `dropColumn` | `ALTER TABLE ... DROP COLUMN ...` |
707
+ | `renameColumn` | `ALTER TABLE ... RENAME COLUMN ... TO` |
708
+ | `createIndex` | `CREATE [UNIQUE] INDEX ...` |
709
+ | `dropIndex` | `DROP INDEX ...` |
710
+
711
+ ### Migration File Shape
712
+
713
+ ```ts
714
+ import { defineMigration } from 'flint-orm/migration';
715
+ import { addTable, addColumn } from 'flint-orm/migration';
716
+
717
+ export default defineMigration({
718
+ name: 'init_schema',
719
+ operations: [
720
+ addTable({ name: 'users', columns: [...], indexes: [...] }),
721
+ addColumn('users', { name: 'email', sqlType: 'text', ... }),
722
+ ],
723
+ });
724
+ ```
725
+
726
+ ### Tracking
727
+
728
+ Applied migrations are recorded in `__flint_migrations` (per-database). The table is created on first `migrate()` call — never with `IF NOT EXISTS`.
729
+
730
+ ### FK Ordering
731
+
732
+ Tables are topologically sorted (Kahn's algorithm) before generating `CREATE TABLE` statements. Referenced tables are created first.
733
+
734
+ ---
735
+
736
+ ## SQLite Introspection
589
737
 
590
- Access the underlying `bun:sqlite` client directly.
738
+ ### `introspectSchema(client)`
739
+
740
+ Read the live database schema and return a `SchemaState` that can be diffed against code-defined tables.
591
741
 
592
742
  ```ts
593
- db.$client; // Database instance
743
+ import { introspectSchema } from 'flint-orm/sqlite';
744
+ import { diffSchemas } from 'flint-orm/migration';
745
+
746
+ const client = new Database('./app.db');
747
+ const liveState = introspectSchema(client);
748
+
749
+ // Compare live DB against code schema
750
+ const codeState = serializeSchema([users, orders]);
751
+ const operations = diffSchemas(liveState, codeState);
594
752
  ```
595
753
 
754
+ Normalizes SQLite type aliases (VARCHAR → TEXT, BIGINT → INTEGER, etc.) and parses default values.
755
+
596
756
  ---
597
757
 
598
758
  ## Types
@@ -603,6 +763,8 @@ db.$client; // Database instance
603
763
  | `ColumnDef<T, S>` | Column definition with phantom type `T` and storage class `S` |
604
764
  | `InferRow<T>` | Derives row type from table definition |
605
765
  | `InsertRow<T>` | Derives insert type (defaults are optional) |
766
+ | `IndexDef` | Index definition: `{ name, columns, unique }` |
767
+ | `IndexBuilder` | Chainable index builder: `.on(cols).unique()` |
606
768
  | `Condition` | Condition node for WHERE clauses |
607
769
  | `Executable` | Anything with a `.toSQL()` method (for `batch()`) |
608
770
  | `ConnectionDetails` | `{ url: string }` |
package/README.md CHANGED
@@ -0,0 +1,238 @@
1
+ # flint-orm
2
+
3
+ A minimal, type-safe SQLite ORM. One dialect, one runtime, no abstraction overhead.
4
+
5
+ ## Why
6
+
7
+ Drizzle is great, but its execution layer (`drizzle-kit migrate`) wasn't built for dynamic, multi-tenant, per-request-connection contexts. Flint is narrower by design: SQLite/libSQL only, no multi-dialect abstraction, no pluggable drivers — just a query builder that does exactly what SQLite supports.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install flint-orm
13
+ bun add flint-orm
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```ts
19
+ import { flint, table, text, integer, eq } from 'flint-orm';
20
+
21
+ const users = table('users', {
22
+ id: text('id').primaryKey(),
23
+ name: text('name').notNull(),
24
+ email: text('email').unique(),
25
+ age: integer('age'),
26
+ });
27
+
28
+ const db = flint({ url: 'app.db' });
29
+
30
+ // Insert
31
+ db.insert(users).values({ id: 'u1', name: 'Alice', email: 'alice@example.com' }).execute();
32
+
33
+ // Query
34
+ const user = db.select().from(users).where(eq(users.id, 'u1')).single().execute();
35
+ // { id: "u1", name: "Alice", email: "alice@example.com", age: null }
36
+
37
+ // Update
38
+ db.update(users).set({ name: 'Alice Updated' }).where(eq(users.id, 'u1')).execute();
39
+
40
+ // Delete
41
+ db.delete(users).where(eq(users.id, 'u1')).execute();
42
+ ```
43
+
44
+ ## Features
45
+
46
+ ### Schema
47
+
48
+ ```ts
49
+ import { table, text, integer, boolean, json, date, real, index } from 'flint-orm';
50
+
51
+ const users = table(
52
+ 'users',
53
+ {
54
+ id: text('id').primaryKey(),
55
+ name: text('name').notNull(),
56
+ email: text('email').unique(),
57
+ active: boolean('active').default(true),
58
+ metadata: json('metadata'),
59
+ createdAt: date('createdAt').defaultNow().onUpdate(),
60
+ score: real('score'),
61
+ age: integer('age'),
62
+ orgId: text('orgId').references(orgs.id),
63
+ },
64
+ (t) => [index('idx_users_email').on(t.email).unique(), index('idx_users_org').on(t.orgId)],
65
+ );
66
+ ```
67
+
68
+ | Type | TS Type | SQLite | Modifiers |
69
+ | --------- | --------- | ------- | ------------------------------------------------------------------------- |
70
+ | `text` | `string` | TEXT | `.primaryKey()`, `.notNull()`, `.unique()`, `.default()`, `.references()` |
71
+ | `integer` | `number` | INTEGER | `.autoIncrement()` |
72
+ | `boolean` | `boolean` | INTEGER | Encodes 0/1 |
73
+ | `json<T>` | `T` | TEXT | JSON encode/decode |
74
+ | `date` | `Date` | INTEGER | `.defaultNow()`, `.onUpdate()` |
75
+ | `real` | `number` | REAL | |
76
+
77
+ ### Query Builder
78
+
79
+ Two-phase builders prevent invalid queries at compile time:
80
+
81
+ ```ts
82
+ // SELECT
83
+ db.select().from(users).columns(['id', 'name']).where(eq(users.active, true)).orderBy('name').limit(10).execute();
84
+
85
+ // INSERT
86
+ db.insert(users).values({ id: 'u1', name: 'Alice' }).returning().execute();
87
+
88
+ // UPSERT
89
+ db.insert(users)
90
+ .values({ id: 'u1', name: 'Alice' })
91
+ .onConflictDoUpdate({ target: users.id, set: { name: 'Bob' } })
92
+ .execute();
93
+
94
+ // UPDATE
95
+ db.update(users).set({ name: 'Bob' }).where(eq(users.id, 'u1')).returning().execute();
96
+
97
+ // DELETE
98
+ db.delete(users).where(eq(users.id, 'u1')).returning().execute();
99
+ ```
100
+
101
+ ### Joins
102
+
103
+ ```ts
104
+ const orders = table('orders', {
105
+ id: text('id').primaryKey(),
106
+ userId: text('userId').references(users.id),
107
+ total: integer('total').notNull(),
108
+ });
109
+
110
+ // Left join — nested result shape
111
+ const result = db.leftJoin(orders).on(users).execute();
112
+ // [{ id: "u1", name: "Alice", __children: [{ id: "o1", total: 100 }] }]
113
+
114
+ // Multi-join
115
+ db.leftJoin(orders).on(users).leftJoin(orderItems).on(orders).execute();
116
+ ```
117
+
118
+ ### Aggregates
119
+
120
+ ```ts
121
+ db.count(users);
122
+ db.sum(orders, orders.total);
123
+ db.avg(orders, orders.total, eq(orders.userId, 'u1'));
124
+ ```
125
+
126
+ ### Batch
127
+
128
+ ```ts
129
+ db.batch([db.insert(orders).values({ id: 'o1', userId: 'u1', total: 100 }), db.update(users).set({ totalOrders: 1 }).where(eq(users.id, 'u1'))]);
130
+ ```
131
+
132
+ ### Raw SQL
133
+
134
+ ```ts
135
+ db.$client.prepare('SELECT * FROM users WHERE id = ?').all('u1');
136
+ ```
137
+
138
+ ## Conditions
139
+
140
+ ```ts
141
+ import { eq, neq, gt, gte, lt, lte, and, or, isIn, isNotIn, isNull, isNotNull, like, glob, between } from 'flint-orm';
142
+
143
+ db.select()
144
+ .from(users)
145
+ .where(and(eq(users.active, true), gt(users.age, 18)))
146
+ .execute();
147
+ ```
148
+
149
+ ## Types
150
+
151
+ ```ts
152
+ import type { InferRow, InsertRow } from 'flint-orm';
153
+
154
+ type UserRow = InferRow<typeof users>;
155
+ type UserInsert = InsertRow<typeof users>; // defaults are optional
156
+ ```
157
+
158
+ ## Migration System
159
+
160
+ ### CLI
161
+
162
+ ```bash
163
+ # Generate migration from schema changes
164
+ flint generate --name init_schema
165
+
166
+ # Preview SQL without writing files
167
+ flint generate --preview
168
+
169
+ # Apply pending migrations
170
+ flint migrate
171
+
172
+ # Check status
173
+ flint migrate --status
174
+ ```
175
+
176
+ ### Config
177
+
178
+ ```ts
179
+ // flint.config.ts
180
+ import { defineConfig } from 'flint-orm/config';
181
+
182
+ export default defineConfig({
183
+ schema: './src/schema',
184
+ migrations: './flint',
185
+ database: { url: './app.db' },
186
+ });
187
+ ```
188
+
189
+ ### How It Works
190
+
191
+ 1. `flint generate` serializes your `table()` definitions, diffs against the last snapshot, and writes a migration folder with TypeScript operations
192
+ 2. `flint migrate` reads pending migration folders, executes SQL via `batch()` (atomic), and records applied migrations in `__flint_migrations`
193
+ 3. Tables are topologically sorted by foreign key dependencies (Kahn's algorithm) — referenced tables are created first
194
+
195
+ ### Programmatic
196
+
197
+ ```ts
198
+ import { generate, migrate, getMigrationStatus, serializeSchema, diffSchemas, generateSQL } from 'flint-orm/migration';
199
+ ```
200
+
201
+ ## CLI
202
+
203
+ ```
204
+ flint <command> [options]
205
+
206
+ Commands:
207
+ generate Generate a new migration from schema changes
208
+ migrate Apply pending migrations
209
+ status Show migration status
210
+
211
+ Options:
212
+ --name, -n Migration name
213
+ --preview, -p Show SQL without writing files
214
+ --status Show which migrations are pending/applied
215
+ ```
216
+
217
+ ## Error Handling
218
+
219
+ ```ts
220
+ import { FlintValidationError, FlintQueryError } from 'flint-orm';
221
+ ```
222
+
223
+ | Error | When |
224
+ | ---------------------- | ----------------------------- |
225
+ | `FlintValidationError` | Invalid query construction |
226
+ | `FlintQueryError` | Runtime SQL execution failure |
227
+
228
+ ## Philosophy
229
+
230
+ - **SQLite only** — no multi-dialect abstraction, no driver plugins
231
+ - **Type-safe by construction** — two-phase builders, phantom types, `InferRow<T>`
232
+ - **Immutable** — every chain method returns a new instance
233
+ - **No classes** — functions and plain objects only
234
+ - **Parameterized** — all queries use `?` placeholders, never string interpolation
235
+
236
+ ## License
237
+
238
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flint-orm",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "bin": {
5
5
  "flint": "./src/cli.ts"
6
6
  },
@@ -40,6 +40,10 @@
40
40
  "lint": "oxlint",
41
41
  "format": "oxfmt"
42
42
  },
43
+ "dependencies": {
44
+ "@clack/prompts": "1.7.0",
45
+ "picocolors": "1.1.1"
46
+ },
43
47
  "devDependencies": {
44
48
  "@types/bun": "1.3.14",
45
49
  "oxfmt": "0.57.0",
package/src/cli.ts CHANGED
@@ -3,12 +3,15 @@
3
3
  // flint CLI — migration generation for flint-orm
4
4
  // ---------------------------------------------------------------------------
5
5
 
6
- import { parseArgs } from 'node:util';
6
+ import { parseArgs, styleText } from 'node:util';
7
7
  import { statSync, readdirSync, existsSync, readFileSync } from 'node:fs';
8
8
  import { join, resolve, isAbsolute } from 'node:path';
9
9
  import { pathToFileURL } from 'node:url';
10
10
  import type { AnyTable, TableDef } from './schema/table.js';
11
11
  import type { SchemaState } from './migration/types.js';
12
+ import { intro, outro, log, cancel, isCancel, select, pc, note } from './cli/ui.js';
13
+ import { CancellationError } from './migration/diff.js';
14
+ import type { RenamePrompt } from './migration/diff.js';
12
15
 
13
16
  // ---------------------------------------------------------------------------
14
17
  // Config loading
@@ -93,24 +96,22 @@ async function importTableFolder(folderPath: string): Promise<unknown[]> {
93
96
  async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config: FlintConfig): Promise<void> {
94
97
  const name = typeof args.name === 'string' ? args.name : undefined;
95
98
  const preview = args.preview === true;
99
+ const promptRename: RenamePrompt = (message, options) => select({ message: pc.bold(message), options }) as Promise<string | symbol>;
96
100
 
97
- console.log(`šŸ” Discovering schema from: ${config.schema}`);
101
+ log.info(`Discovering schema from: ${config.schema}`);
98
102
  const tables = await discoverTables(config.schema);
99
103
 
100
104
  if (tables.length === 0) {
101
- console.error('āŒ No table() definitions found in schema path.');
105
+ cancel('No table() definitions found in schema path.');
102
106
  process.exit(1);
103
107
  }
104
108
 
105
- console.log(`šŸ“¦ Found ${tables.length} table(s):`);
106
- for (const t of tables) {
107
- console.log(` - ${(t as Record<string, Record<string, unknown>>)._?.name ?? 'unknown'}`);
108
- }
109
+ log.info(`Found ${tables.length} table(s): ${tables.map((t) => (t as Record<string, Record<string, unknown>>)._?.name ?? 'unknown').join(', ')}`);
109
110
 
110
111
  if (preview) {
111
112
  // Dynamic import of migration functions
112
113
  const { serializeSchema } = await import('./migration/serialize.js');
113
- const { diffSchemas, emptyState } = await import('./migration/diff.js');
114
+ const { diffSchemas, emptyState, resolveRenames } = await import('./migration/diff.js');
114
115
  const { generateSQL } = await import('./migration/sql.js');
115
116
 
116
117
  // Find latest state
@@ -132,20 +133,20 @@ async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config:
132
133
  if (!previousState) previousState = emptyState();
133
134
 
134
135
  const currentState = serializeSchema(tables as AnyTable[]);
135
- const operations = diffSchemas(previousState, currentState);
136
+ const rawOps = diffSchemas(previousState, currentState);
136
137
 
137
- if (operations.length === 0) {
138
- console.log('\nāœ… No changes detected — schema is already up to date.');
138
+ if (rawOps.length === 0) {
139
+ outro('Schema is already up to date.');
139
140
  return;
140
141
  }
141
142
 
143
+ // Resolve renames interactively
144
+ const operations = await resolveRenames(rawOps, { interactive: true, prompt: promptRename });
145
+
142
146
  const sql = generateSQL(operations);
143
- const previewLabel = name ? `${name}` : 'unnamed';
144
- console.log(`\n--- Preview: ${previewLabel} ---`);
145
- console.log(` Operations: ${operations.length}`);
146
- console.log(` Migrations dir: ${config.migrations}`);
147
- console.log(`\n--- SQL ---\n${sql}\n`);
148
- console.log('(dry run, no files written)');
147
+ log.info(`Operations: ${operations.length}`);
148
+ note(sql, 'SQL', { format: (text) => styleText('dim', text) });
149
+ log.info('(dry run, no files written)');
149
150
  return;
150
151
  }
151
152
 
@@ -153,13 +154,18 @@ async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config:
153
154
  const { generate } = await import('./migration/generate.js');
154
155
 
155
156
  try {
156
- const result = generate(tables as TableDef<any>[], resolve(process.cwd(), (config.migrations ?? './flint') as string), name);
157
- console.log(`\nāœ… Migration generated: ${result.folderName}`);
158
- console.log(` Operations: ${result.operations.length}`);
159
- console.log(`\n--- SQL Preview ---\n${result.sql}`);
157
+ const migrationsDir = resolve(process.cwd(), config.migrations ?? './flint');
158
+ const result = await generate(tables as TableDef<any>[], migrationsDir, {
159
+ name,
160
+ interactive: true,
161
+ prompt: promptRename,
162
+ });
163
+ log.info(`Operations: ${result.operations.length}`);
164
+ note(result.sql, 'SQL', { format: (text) => styleText('dim', text) });
165
+ log.success(`Migration generated: ${migrationsDir}/${result.folderName}`);
160
166
  } catch (err: unknown) {
161
167
  if (err instanceof Error && err.message.includes('No changes detected')) {
162
- console.log('\nāœ… No changes detected — schema is already up to date.');
168
+ outro('Schema is already up to date.');
163
169
  } else {
164
170
  throw err;
165
171
  }
@@ -184,25 +190,25 @@ async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config:
184
190
  const status = getMigrationStatus(db, migrationsDir);
185
191
 
186
192
  if (status.applied.length === 0 && status.pending.length === 0) {
187
- console.log('āœ… No migrations found.');
193
+ outro('No migrations found.');
188
194
  return;
189
195
  }
190
196
 
191
197
  if (status.applied.length > 0) {
192
- console.log('\nšŸ“‹ Applied migrations:');
198
+ log.info('Applied migrations:');
193
199
  for (const m of status.applied) {
194
- console.log(` āœ“ ${m.name} (${m.folderName})`);
200
+ log.success(`${m.name} (${m.folderName})`);
195
201
  }
196
202
  }
197
203
 
198
204
  if (status.pending.length > 0) {
199
- console.log('\nā³ Pending migrations:');
205
+ log.warn('Pending migrations:');
200
206
  for (const m of status.pending) {
201
- console.log(` ā—‹ ${m.name} (${m.folderName})`);
207
+ log.message(` ā—‹ ${m.name} (${m.folderName})`);
202
208
  }
203
209
  }
204
210
 
205
- console.log(`\n ${status.applied.length} applied, ${status.pending.length} pending`);
211
+ log.info(`${status.applied.length} applied, ${status.pending.length} pending`);
206
212
  return;
207
213
  }
208
214
 
@@ -212,16 +218,16 @@ async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config:
212
218
  });
213
219
 
214
220
  if (result.applied.length === 0) {
215
- console.log('\nāœ… No pending migrations — database is up to date.');
221
+ outro('No pending migrations — database is up to date.');
216
222
  } else {
217
- console.log(`\nšŸš€ ${dryRun ? 'Would apply' : 'Applied'} ${result.applied.length} migration(s):`);
218
- for (const name of result.applied) {
219
- console.log(` āœ“ ${name}`);
223
+ log.info(`${dryRun ? 'Would apply' : 'Applied'} ${result.applied.length} migration(s):`);
224
+ for (const appliedName of result.applied) {
225
+ log.success(appliedName);
220
226
  }
221
227
  }
222
228
 
223
229
  if (result.skipped.length > 0 && !dryRun) {
224
- console.log(`\nā­ Skipped ${result.skipped.length} already applied migration(s)`);
230
+ log.info(`Skipped ${result.skipped.length} already applied migration(s)`);
225
231
  }
226
232
  } finally {
227
233
  db.close();
@@ -233,9 +239,7 @@ async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config:
233
239
  // ---------------------------------------------------------------------------
234
240
 
235
241
  function printHelp(): void {
236
- console.log(`
237
- flint — migration CLI for flint-orm
238
-
242
+ log.message(`
239
243
  Usage:
240
244
  flint <command> [options]
241
245
 
@@ -294,13 +298,17 @@ async function main(): Promise<void> {
294
298
  await cmdMigrate(values, config);
295
299
  break;
296
300
  default:
297
- console.error(`āŒ Unknown command: ${command}`);
301
+ cancel(`Unknown command: ${command}`);
298
302
  printHelp();
299
303
  process.exit(1);
300
304
  }
301
305
  }
302
306
 
303
307
  main().catch((err) => {
304
- console.error(err);
308
+ if (isCancel(err) || err instanceof CancellationError) {
309
+ cancel('Operation cancelled.');
310
+ } else {
311
+ cancel(err.message ?? 'An error occurred');
312
+ }
305
313
  process.exit(1);
306
314
  });