sqlite-hub 0.12.0 → 0.17.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 (64) hide show
  1. package/README.md +118 -23
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +32 -2
  5. package/frontend/js/app.js +989 -13
  6. package/frontend/js/components/modal.js +644 -15
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryEditor.js +9 -1
  9. package/frontend/js/components/queryHistoryPanel.js +126 -131
  10. package/frontend/js/components/queryResults.js +79 -17
  11. package/frontend/js/components/rowEditorPanel.js +204 -10
  12. package/frontend/js/components/sidebar.js +102 -18
  13. package/frontend/js/components/tableDesignerEditor.js +69 -11
  14. package/frontend/js/router.js +8 -0
  15. package/frontend/js/store.js +868 -9
  16. package/frontend/js/utils/copyColumnExport.js +117 -0
  17. package/frontend/js/utils/exportFilenames.js +32 -0
  18. package/frontend/js/utils/filePathPreview.js +315 -0
  19. package/frontend/js/utils/format.js +1 -1
  20. package/frontend/js/utils/markdownDocuments.js +248 -0
  21. package/frontend/js/utils/rowEditorJson.js +65 -0
  22. package/frontend/js/utils/sqlFormatter.js +691 -0
  23. package/frontend/js/utils/tableDesigner.js +178 -6
  24. package/frontend/js/utils/textCellStats.js +20 -0
  25. package/frontend/js/utils/timestampPreview.js +264 -0
  26. package/frontend/js/views/charts.js +3 -1
  27. package/frontend/js/views/data.js +34 -2
  28. package/frontend/js/views/documents.js +300 -0
  29. package/frontend/js/views/editor.js +48 -1
  30. package/frontend/js/views/settings.js +39 -6
  31. package/frontend/js/views/structure.js +154 -212
  32. package/frontend/styles/base.css +6 -0
  33. package/frontend/styles/components.css +476 -2
  34. package/frontend/styles/structure-graph.css +0 -3
  35. package/frontend/styles/tailwind.generated.css +1 -1
  36. package/frontend/styles/tokens.css +1 -1
  37. package/frontend/styles/views.css +422 -0
  38. package/package.json +3 -3
  39. package/server/routes/documents.js +163 -0
  40. package/server/routes/settings.js +22 -6
  41. package/server/server.js +6 -0
  42. package/server/services/sqlite/introspection.js +10 -0
  43. package/server/services/sqlite/sqlExecutor.js +29 -0
  44. package/server/services/sqlite/tableDesigner/changeAnalysis.js +25 -1
  45. package/server/services/sqlite/tableDesigner/schemaMapping.js +105 -10
  46. package/server/services/sqlite/tableDesigner/validation.js +60 -2
  47. package/server/services/sqlite/tableDesignerService.js +1 -1
  48. package/server/services/storage/appStateStore.js +313 -0
  49. package/tests/check-constraint-options.test.js +14 -0
  50. package/tests/cli-args.test.js +100 -0
  51. package/tests/copy-column-modal.test.js +83 -0
  52. package/tests/database-documents.test.js +85 -0
  53. package/tests/export-filenames.test.js +34 -0
  54. package/tests/file-path-preview.test.js +165 -0
  55. package/tests/markdown-documents.test.js +79 -0
  56. package/tests/row-editor-json.test.js +82 -0
  57. package/tests/row-editor-timestamp-preview.test.js +192 -0
  58. package/tests/settings-metadata.test.js +16 -0
  59. package/tests/settings-view.test.js +32 -0
  60. package/tests/sql-formatter.test.js +173 -0
  61. package/tests/sql-highlight.test.js +38 -0
  62. package/tests/table-designer-v2-unique-constraints.test.js +78 -0
  63. package/tests/text-cell-stats.test.js +38 -0
  64. package/fill.js +0 -526
package/bin/sqlite-hub.js CHANGED
@@ -1,26 +1,56 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
3
5
  const { spawn } = require('node:child_process');
4
6
 
5
7
  const DEFAULT_PORT = 4173;
8
+ const EXPORT_FORMATS = new Set(['csv', 'tsv', 'md']);
6
9
 
7
10
  function printHelp() {
8
11
  console.log(`SQLite Hub CLI
9
12
 
10
13
  Usage:
11
14
  sqlite-hub [--port:4173]
15
+ sqlite-hub --database
16
+ sqlite-hub --database:"name" --tables
17
+ sqlite-hub --database:"name" --execute:"Saved Query"
18
+ sqlite-hub --database:"name" --query:"Saved Query"
19
+ sqlite-hub --database:"name" --notes:"Saved Query"
20
+ sqlite-hub --database:"name" --export:"Saved Query" --format:csv
21
+ sqlite-hub --database:"name" --documents
22
+ sqlite-hub --database:"name" --documents:"Document Name"
23
+ sqlite-hub --database:"name" --documents:"Document Name" --export
24
+ sqlite-hub --database:"name" --table:"table_name"
25
+ sqlite-hub --database:"name" --table:"table_name" --export:"primary-key"
12
26
 
13
27
  Options:
14
- --help Show this help text.
15
- --port:PORT Start the server on a custom port.
16
- --version Show the version number.
17
- --database List all imported databases.
18
- --database-path:name Get the file path of a database by name.
19
- --database-size:name Get the size of a database by name.
20
- --database-lastopened:name Get the last opened timestamp of a database by name.
21
- --database-tables:name Get all table names from a database.
22
- --database:name --sqleditor List all saved queries for a database.
23
- --database:name --sqleditor:"query" Execute a saved query by name.
28
+ --help, -h Show this help text.
29
+ --version, -v Show the version number.
30
+ --config Show CLI port, URL, app version, and SQLite version.
31
+ --open Start/open SQLite Hub in the browser.
32
+ --port:PORT Start the server on a custom port.
33
+ --database, -d List all imported databases.
34
+ --database:"name" Select an imported database by name or id.
35
+ --path Print the selected database file path.
36
+ --size Print the selected database file size.
37
+ --lastopened Print the selected database last-opened timestamp.
38
+ --tables List tables in the selected database.
39
+ --queries List saved SQL Editor queries for the selected database.
40
+ --execute:"query" Execute a saved SQL Editor query by name.
41
+ --query:"query" Print a saved SQL Editor query by name.
42
+ --notes:"query" Print notes for a saved SQL Editor query by name.
43
+ --export:"query" Export a saved query when --table is not set.
44
+ --format:csv|tsv|md Export format for query exports. Defaults to csv.
45
+ --documents List Markdown documents for the selected database.
46
+ --documents:"name" Print a document's Markdown content.
47
+ --documents:"name" --export Export a document as a Markdown file.
48
+ --table:"table" Print table metadata.
49
+ --table:"table" --export:"pk" Export one row as JSON by primary key or rowid.
50
+
51
+ Legacy aliases still work:
52
+ --database-path:name, --database-size:name, --database-lastopened:name
53
+ --database-tables:name, --database:name --sqleditor, --database:name --sqleditor:"query"
24
54
  `);
25
55
  }
26
56
 
@@ -34,76 +64,330 @@ function parsePort(rawValue) {
34
64
  return port;
35
65
  }
36
66
 
67
+ function splitArgument(argument) {
68
+ const colonIndex = argument.indexOf(':');
69
+ const equalsIndex = argument.indexOf('=');
70
+ const separators = [colonIndex, equalsIndex].filter(index => index > -1);
71
+
72
+ if (separators.length === 0) {
73
+ return {
74
+ flag: normalizeFlagName(argument),
75
+ value: undefined,
76
+ };
77
+ }
78
+
79
+ const separatorIndex = Math.min(...separators);
80
+
81
+ return {
82
+ flag: normalizeFlagName(argument.slice(0, separatorIndex)),
83
+ value: argument.slice(separatorIndex + 1),
84
+ };
85
+ }
86
+
87
+ function normalizeFlagName(flag) {
88
+ if (flag === '---path') {
89
+ return '--path';
90
+ }
91
+
92
+ return flag;
93
+ }
94
+
95
+ function takeFlagValue(flag, inlineValue, argv, index) {
96
+ if (inlineValue !== undefined) {
97
+ return {
98
+ value: inlineValue,
99
+ nextIndex: index,
100
+ };
101
+ }
102
+
103
+ const nextValue = argv[index + 1];
104
+
105
+ if (nextValue === undefined || nextValue.startsWith('--')) {
106
+ throw new Error(`${flag} requires a value.`);
107
+ }
108
+
109
+ return {
110
+ value: nextValue,
111
+ nextIndex: index + 1,
112
+ };
113
+ }
114
+
115
+ function takeOptionalFlagValue(inlineValue, argv, index) {
116
+ if (inlineValue !== undefined) {
117
+ return {
118
+ hasValue: true,
119
+ value: inlineValue,
120
+ nextIndex: index,
121
+ };
122
+ }
123
+
124
+ const nextValue = argv[index + 1];
125
+
126
+ if (nextValue === undefined || nextValue.startsWith('--')) {
127
+ return {
128
+ hasValue: false,
129
+ value: null,
130
+ nextIndex: index,
131
+ };
132
+ }
133
+
134
+ return {
135
+ hasValue: true,
136
+ value: nextValue,
137
+ nextIndex: index + 1,
138
+ };
139
+ }
140
+
141
+ function normalizeExportFormat(format) {
142
+ const normalized = String(format ?? 'csv').toLowerCase();
143
+
144
+ if (!EXPORT_FORMATS.has(normalized)) {
145
+ throw new Error(`Unsupported export format: ${format}. Use csv, tsv, or md.`);
146
+ }
147
+
148
+ return normalized;
149
+ }
150
+
37
151
  function parseCliArguments(argv) {
38
- let port;
39
- let databaseName = null;
152
+ const options = {
153
+ help: false,
154
+ version: false,
155
+ config: false,
156
+ open: false,
157
+ port: undefined,
158
+ databaseList: false,
159
+ databaseName: null,
160
+ pathInfo: false,
161
+ sizeInfo: false,
162
+ lastOpenedInfo: false,
163
+ tables: false,
164
+ queries: false,
165
+ executeQuery: null,
166
+ showQuery: null,
167
+ showNotes: null,
168
+ exportTarget: null,
169
+ exportFormat: 'csv',
170
+ documents: false,
171
+ documentName: null,
172
+ documentExport: false,
173
+ tableName: null,
174
+ };
40
175
 
41
176
  for (let index = 0; index < argv.length; index += 1) {
42
177
  const argument = argv[index];
178
+ const { flag, value } = splitArgument(argument);
43
179
 
44
- if (argument === '--help' || argument === '-h') {
45
- return { help: true };
180
+ if (flag === '--help' || flag === '-h') {
181
+ options.help = true;
182
+ continue;
46
183
  }
47
184
 
48
- if (argument === '--database' || argument === '-d') {
49
- return { database: true };
185
+ if (flag === '--version' || flag === '-v') {
186
+ options.version = true;
187
+ continue;
50
188
  }
51
189
 
52
- if (argument.startsWith('--database:')) {
53
- databaseName = argument.slice('--database:'.length);
190
+ if (flag === '--config') {
191
+ options.config = true;
54
192
  continue;
55
193
  }
56
194
 
57
- if (argument.startsWith('--database-path:')) {
58
- return { databasePath: argument.slice('--database-path:'.length) };
195
+ if (flag === '--open') {
196
+ options.open = true;
197
+ continue;
59
198
  }
60
199
 
61
- if (argument.startsWith('--database-size:')) {
62
- return { databaseSize: argument.slice('--database-size:'.length) };
200
+ if (flag === '--port') {
201
+ const parsed = takeFlagValue(flag, value, argv, index);
202
+ options.port = parsePort(parsed.value);
203
+ index = parsed.nextIndex;
204
+ continue;
63
205
  }
64
206
 
65
- if (argument.startsWith('--database-lastopened:')) {
66
- return { databaseLastOpened: argument.slice('--database-lastopened:'.length) };
207
+ if (flag === '--database' || flag === '-d') {
208
+ const parsed = takeOptionalFlagValue(value, argv, index);
209
+
210
+ if (parsed.hasValue) {
211
+ options.databaseName = parsed.value;
212
+ } else {
213
+ options.databaseList = true;
214
+ }
215
+
216
+ index = parsed.nextIndex;
217
+ continue;
218
+ }
219
+
220
+ if (flag === '--database-path') {
221
+ const parsed = takeFlagValue(flag, value, argv, index);
222
+ options.databaseName = parsed.value;
223
+ options.pathInfo = true;
224
+ index = parsed.nextIndex;
225
+ continue;
67
226
  }
68
227
 
69
- if (argument.startsWith('--database-tables:')) {
70
- return { databaseTables: argument.slice('--database-tables:'.length) };
228
+ if (flag === '--database-size') {
229
+ const parsed = takeFlagValue(flag, value, argv, index);
230
+ options.databaseName = parsed.value;
231
+ options.sizeInfo = true;
232
+ index = parsed.nextIndex;
233
+ continue;
71
234
  }
72
235
 
73
- if (argument === '--sqleditor') {
74
- return { sqlEditor: true, databaseName };
236
+ if (flag === '--database-lastopened') {
237
+ const parsed = takeFlagValue(flag, value, argv, index);
238
+ options.databaseName = parsed.value;
239
+ options.lastOpenedInfo = true;
240
+ index = parsed.nextIndex;
241
+ continue;
75
242
  }
76
243
 
77
- if (argument.startsWith('--sqleditor:')) {
78
- return { sqlEditorQuery: argument.slice('--sqleditor:'.length), databaseName };
244
+ if (flag === '--database-tables') {
245
+ const parsed = takeFlagValue(flag, value, argv, index);
246
+ options.databaseName = parsed.value;
247
+ options.tables = true;
248
+ index = parsed.nextIndex;
249
+ continue;
79
250
  }
80
251
 
81
- if (argument.startsWith('--port:')) {
82
- port = parsePort(argument.slice('--port:'.length));
252
+ if (flag === '--path') {
253
+ options.pathInfo = true;
83
254
  continue;
84
255
  }
85
256
 
86
- if (argument.startsWith('--port=')) {
87
- port = parsePort(argument.slice('--port='.length));
257
+ if (flag === '--size') {
258
+ options.sizeInfo = true;
88
259
  continue;
89
260
  }
90
261
 
91
- if (argument === '--port') {
92
- port = parsePort(argv[index + 1]);
93
- index += 1;
262
+ if (flag === '--lastopened') {
263
+ options.lastOpenedInfo = true;
94
264
  continue;
95
265
  }
96
266
 
97
- if (argument === '--version' || argument === '-v') {
98
- const { version } = require('../package.json');
99
- console.log(`SQLite Hub CLI version ${version}`);
100
- process.exit(0);
267
+ if (flag === '--tables') {
268
+ options.tables = true;
269
+ continue;
270
+ }
271
+
272
+ if (flag === '--queries') {
273
+ options.queries = true;
274
+ continue;
275
+ }
276
+
277
+ if (flag === '--sqleditor') {
278
+ const parsed = takeOptionalFlagValue(value, argv, index);
279
+
280
+ if (parsed.hasValue) {
281
+ options.executeQuery = parsed.value;
282
+ } else {
283
+ options.queries = true;
284
+ }
285
+
286
+ index = parsed.nextIndex;
287
+ continue;
288
+ }
289
+
290
+ if (flag === '--execute') {
291
+ const parsed = takeFlagValue(flag, value, argv, index);
292
+ options.executeQuery = parsed.value;
293
+ index = parsed.nextIndex;
294
+ continue;
295
+ }
296
+
297
+ if (flag === '--query') {
298
+ const parsed = takeFlagValue(flag, value, argv, index);
299
+ options.showQuery = parsed.value;
300
+ index = parsed.nextIndex;
301
+ continue;
302
+ }
303
+
304
+ if (flag === '--notes') {
305
+ const parsed = takeFlagValue(flag, value, argv, index);
306
+ options.showNotes = parsed.value;
307
+ index = parsed.nextIndex;
308
+ continue;
309
+ }
310
+
311
+ if (flag === '--documents') {
312
+ const parsed = takeOptionalFlagValue(value, argv, index);
313
+
314
+ options.documents = true;
315
+ if (parsed.hasValue) {
316
+ const rawDocumentName = String(parsed.value ?? '');
317
+
318
+ if (rawDocumentName.endsWith('--export')) {
319
+ options.documentName = rawDocumentName.slice(0, -'--export'.length);
320
+ options.documentExport = true;
321
+ } else {
322
+ options.documentName = rawDocumentName;
323
+ }
324
+ }
325
+
326
+ index = parsed.nextIndex;
327
+ continue;
328
+ }
329
+
330
+ if (flag === '--documents-export') {
331
+ const parsed = takeFlagValue(flag, value, argv, index);
332
+ options.documents = true;
333
+ options.documentName = parsed.value;
334
+ options.documentExport = true;
335
+ index = parsed.nextIndex;
336
+ continue;
337
+ }
338
+
339
+ if (flag === '--export') {
340
+ if (options.documents && options.documentName && value === undefined) {
341
+ const nextValue = argv[index + 1];
342
+
343
+ if (nextValue === undefined || nextValue.startsWith('--')) {
344
+ options.documentExport = true;
345
+ continue;
346
+ }
347
+ }
348
+
349
+ const parsed = takeFlagValue(flag, value, argv, index);
350
+ options.exportTarget = parsed.value;
351
+ index = parsed.nextIndex;
352
+ continue;
353
+ }
354
+
355
+ if (flag === '--format') {
356
+ const parsed = takeFlagValue(flag, value, argv, index);
357
+ options.exportFormat = normalizeExportFormat(parsed.value);
358
+ index = parsed.nextIndex;
359
+ continue;
360
+ }
361
+
362
+ if (flag === '--table') {
363
+ const parsed = takeFlagValue(flag, value, argv, index);
364
+ options.tableName = parsed.value;
365
+ index = parsed.nextIndex;
366
+ continue;
101
367
  }
102
368
 
103
369
  throw new Error(`Unknown argument: ${argument}`);
104
370
  }
105
371
 
106
- return { help: false, port };
372
+ return options;
373
+ }
374
+
375
+ function hasDatabaseOperation(options) {
376
+ return Boolean(
377
+ options.pathInfo ||
378
+ options.sizeInfo ||
379
+ options.lastOpenedInfo ||
380
+ options.tables ||
381
+ options.queries ||
382
+ options.executeQuery ||
383
+ options.showQuery ||
384
+ options.showNotes ||
385
+ options.documents ||
386
+ options.documentName ||
387
+ options.documentExport ||
388
+ options.exportTarget ||
389
+ options.tableName,
390
+ );
107
391
  }
108
392
 
109
393
  function openInDefaultBrowser(url) {
@@ -138,7 +422,8 @@ function openInDefaultBrowser(url) {
138
422
  }
139
423
 
140
424
  function findDatabaseByName(connections, name) {
141
- const normalizedName = name.toLowerCase();
425
+ const normalizedName = String(name ?? '').toLowerCase();
426
+
142
427
  return connections.find(
143
428
  conn => conn.label.toLowerCase() === normalizedName || conn.id.toLowerCase() === normalizedName,
144
429
  );
@@ -151,243 +436,774 @@ function formatSize(bytes) {
151
436
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
152
437
  }
153
438
 
154
- async function main() {
155
- const {
156
- help,
157
- database,
158
- databasePath,
159
- databaseSize,
160
- databaseLastOpened,
161
- databaseTables,
162
- sqlEditor,
163
- sqlEditorQuery,
164
- databaseName,
165
- port = DEFAULT_PORT,
166
- } = parseCliArguments(process.argv.slice(2));
167
-
168
- if (help) {
169
- printHelp();
170
- return;
439
+ function sanitizeFilenameBase(value, fallback = 'export') {
440
+ const sanitized = String(value ?? '')
441
+ .replace(/[<>:"/\\|?*\u0000-\u001f]/g, ' ')
442
+ .replace(/\s+/g, ' ')
443
+ .trim()
444
+ .replace(/[. ]+$/g, '');
445
+
446
+ if (!sanitized) {
447
+ return fallback;
171
448
  }
172
449
 
450
+ return sanitized.slice(0, 120);
451
+ }
452
+
453
+ function createAppStateStore() {
173
454
  const { resolveAppStatePaths } = require('../server/utils/appPaths');
174
455
  const { AppStateStore } = require('../server/services/storage/appStateStore');
175
- const path = require('path');
456
+ const packageRoot = path.resolve(__dirname, '..');
457
+ const { appStateDbPath } = resolveAppStatePaths(packageRoot);
176
458
 
177
- const PACKAGE_ROOT = path.resolve(__dirname, '..');
178
- const { appStateDbPath: APP_STATE_DB_PATH } = resolveAppStatePaths(PACKAGE_ROOT);
459
+ return new AppStateStore(appStateDbPath);
460
+ }
179
461
 
180
- const appStateStore = new AppStateStore(APP_STATE_DB_PATH);
181
- const connections = appStateStore.getRecentConnections();
462
+ function createReadOnlyRuntime(conn, appStateStore) {
463
+ const { ConnectionManager } = require('../server/services/sqlite/connectionManager');
464
+ const { DataBrowserService } = require('../server/services/sqlite/dataBrowserService');
465
+ const { ExportService } = require('../server/services/sqlite/exportService');
466
+ const { SqlExecutor } = require('../server/services/sqlite/sqlExecutor');
182
467
 
183
- if (databasePath) {
184
- const conn = findDatabaseByName(connections, databasePath);
185
- if (!conn) {
186
- console.error(`Database not found: ${databasePath}`);
187
- process.exit(1);
188
- }
189
- console.log(conn.path);
468
+ const connectionManager = new ConnectionManager({ appStateStore });
469
+
470
+ connectionManager.openConnection({
471
+ filePath: conn.path,
472
+ label: conn.label,
473
+ id: conn.id,
474
+ logoPath: conn.logoPath ?? null,
475
+ makeActive: false,
476
+ readOnly: true,
477
+ });
478
+
479
+ const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
480
+ const exportService = new ExportService({ appStateStore, connectionManager, sqlExecutor });
481
+ const dataBrowserService = new DataBrowserService({ connectionManager });
482
+
483
+ return {
484
+ connectionManager,
485
+ db: connectionManager.getActiveDatabase(),
486
+ dataBrowserService,
487
+ exportService,
488
+ sqlExecutor,
489
+ close() {
490
+ connectionManager.closeCurrent();
491
+ },
492
+ };
493
+ }
494
+
495
+ function printDatabaseList(connections) {
496
+ if (connections.length === 0) {
497
+ console.log('No databases imported yet.');
190
498
  return;
191
499
  }
192
500
 
193
- if (databaseSize) {
194
- const conn = findDatabaseByName(connections, databaseSize);
195
- if (!conn) {
196
- console.error(`Database not found: ${databaseSize}`);
197
- process.exit(1);
198
- }
199
- console.log(formatSize(conn.sizeBytes));
501
+ console.log(`\nImported databases (${connections.length}):`);
502
+ console.log('─'.repeat(60));
503
+
504
+ connections.forEach((conn, index) => {
505
+ const size = formatSize(conn.sizeBytes);
506
+ const readOnly = conn.readOnly ? ' (read-only)' : '';
507
+ console.log(`${index + 1}. ${conn.label}${readOnly}`);
508
+ console.log(` Path: ${conn.path}`);
509
+ console.log(` Size: ${size}`);
510
+ console.log(` Last opened: ${conn.lastOpenedAt}`);
511
+ console.log('');
512
+ });
513
+ }
514
+
515
+ function printSingleDatabaseInfo(conn) {
516
+ const readOnly = conn.readOnly ? 'yes' : 'no';
517
+
518
+ console.log(`Database: ${conn.label}`);
519
+ console.log(`Path: ${conn.path}`);
520
+ console.log(`Size: ${formatSize(conn.sizeBytes)}`);
521
+ console.log(`Last opened: ${conn.lastOpenedAt}`);
522
+ console.log(`Read-only: ${readOnly}`);
523
+ }
524
+
525
+ function printTables(conn, tables) {
526
+ if (tables.length === 0) {
527
+ console.log('No tables found in this database.');
200
528
  return;
201
529
  }
202
530
 
203
- if (databaseLastOpened) {
204
- const conn = findDatabaseByName(connections, databaseLastOpened);
205
- if (!conn) {
206
- console.error(`Database not found: ${databaseLastOpened}`);
207
- process.exit(1);
208
- }
209
- console.log(conn.lastOpenedAt);
531
+ console.log(`\nTables in ${conn.label} (${tables.length}):`);
532
+ console.log('─'.repeat(60));
533
+
534
+ tables.forEach((table, index) => {
535
+ console.log(`${index + 1}. ${table.name ?? table}`);
536
+ });
537
+
538
+ console.log('');
539
+ }
540
+
541
+ function getQueryTitle(item) {
542
+ return item?.title || item?.displayTitle || item?.previewSql || item?.rawSql || '(untitled query)';
543
+ }
544
+
545
+ function findQueryByName(appStateStore, conn, queryName) {
546
+ const normalizedQueryName = String(queryName ?? '').trim().toLowerCase();
547
+
548
+ if (!normalizedQueryName) {
549
+ throw new Error('Query name is required.');
550
+ }
551
+
552
+ const collection = appStateStore.buildQueryHistoryCollection({
553
+ databaseKey: conn.id,
554
+ search: queryName,
555
+ onlySaved: false,
556
+ limit: 100,
557
+ });
558
+
559
+ return (
560
+ collection.items.find(item => {
561
+ const candidates = [item.id, item.title, item.displayTitle].filter(Boolean);
562
+
563
+ return candidates.some(candidate => String(candidate).toLowerCase() === normalizedQueryName);
564
+ }) ||
565
+ collection.items.find(item => String(item.rawSql ?? '').toLowerCase().includes(normalizedQueryName)) ||
566
+ null
567
+ );
568
+ }
569
+
570
+ function getSavedQueries(appStateStore, conn, limit = 100) {
571
+ return appStateStore.buildQueryHistoryCollection({
572
+ databaseKey: conn.id,
573
+ onlySaved: true,
574
+ limit,
575
+ });
576
+ }
577
+
578
+ function printAvailableQueries(appStateStore, conn) {
579
+ const allQueries = getSavedQueries(appStateStore, conn);
580
+
581
+ console.error('\nAvailable saved queries:');
582
+
583
+ if (allQueries.items.length > 0) {
584
+ allQueries.items.forEach(query => {
585
+ console.error(` - ${getQueryTitle(query)}`);
586
+ });
210
587
  return;
211
588
  }
212
589
 
213
- if (databaseTables) {
214
- const conn = findDatabaseByName(connections, databaseTables);
215
- if (!conn) {
216
- console.error(`Database not found: ${databaseTables}`);
217
- process.exit(1);
218
- }
590
+ console.error(' (none)');
591
+ }
219
592
 
220
- const Database = require('better-sqlite3');
221
- const db = new Database(conn.path, { readonly: true });
593
+ function requireMatchingQuery(appStateStore, conn, queryName) {
594
+ const matchingQuery = findQueryByName(appStateStore, conn, queryName);
222
595
 
223
- try {
224
- const tables = db
225
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name")
226
- .all()
227
- .map(row => row.name);
596
+ if (!matchingQuery) {
597
+ console.error(`Saved query not found: ${queryName}`);
598
+ printAvailableQueries(appStateStore, conn);
599
+ process.exit(1);
600
+ }
228
601
 
229
- if (tables.length === 0) {
230
- console.log('No tables found in this database.');
231
- } else {
232
- console.log(`\nTables in ${conn.label} (${tables.length}):`);
233
- console.log('─'.repeat(60));
234
- tables.forEach((table, index) => {
235
- console.log(`${index + 1}. ${table}`);
602
+ return matchingQuery;
603
+ }
604
+
605
+ function listSavedQueries(appStateStore, conn) {
606
+ const savedQueries = getSavedQueries(appStateStore, conn);
607
+
608
+ if (savedQueries.items.length === 0) {
609
+ console.log(`No saved queries found for ${conn.label}.`);
610
+ return;
611
+ }
612
+
613
+ console.log(`\nSaved queries for ${conn.label} (${savedQueries.total}):`);
614
+ console.log('─'.repeat(60));
615
+
616
+ savedQueries.items.forEach((query, index) => {
617
+ console.log(`${index + 1}. ${getQueryTitle(query)}`);
618
+ });
619
+
620
+ console.log('');
621
+ }
622
+
623
+ function formatCellValue(value) {
624
+ if (value === null) {
625
+ return 'NULL';
626
+ }
627
+
628
+ if (value && typeof value === 'object') {
629
+ return JSON.stringify(value);
630
+ }
631
+
632
+ return String(value);
633
+ }
634
+
635
+ function printExecutionResult(result) {
636
+ console.log(`\nStatement count: ${result.statementCount}`);
637
+ console.log(`Timing: ${result.timingMs}ms`);
638
+
639
+ result.statements.forEach((statement, index) => {
640
+ console.log(`\nStatement ${index + 1} (${statement.kind}):`);
641
+
642
+ if (statement.kind === 'resultSet') {
643
+ console.log(`Rows: ${statement.rowCount}`);
644
+ console.log(`Columns: ${statement.columns.join(', ')}`);
645
+
646
+ if (statement.rows.length > 0) {
647
+ console.log('\nResults:');
648
+ statement.rows.forEach((row, rowIndex) => {
649
+ const values = statement.columns.map(column => formatCellValue(row[column]));
650
+ console.log(` [${rowIndex}] ${values.join(' | ')}`);
236
651
  });
237
- console.log('');
238
652
  }
239
- } finally {
240
- db.close();
653
+
654
+ return;
655
+ }
656
+
657
+ if (statement.kind === 'mutation') {
658
+ console.log(`Changes: ${statement.changes}`);
659
+
660
+ if (statement.lastInsertRowid) {
661
+ console.log('Last insert rowid:', statement.lastInsertRowid);
662
+ }
241
663
  }
664
+ });
665
+ }
666
+
667
+ function executeSavedQuery({ appStateStore, conn, sqlExecutor, queryName }) {
668
+ const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
669
+
670
+ console.log(`\nExecuting: ${getQueryTitle(matchingQuery)}`);
671
+ console.log(`SQL: ${matchingQuery.previewSql || matchingQuery.rawSql}`);
672
+ console.log('─'.repeat(60));
673
+
674
+ const result = sqlExecutor.execute(matchingQuery.rawSql, {
675
+ persistHistory: false,
676
+ });
677
+
678
+ printExecutionResult(result);
679
+ }
680
+
681
+ function showSavedQuery({ appStateStore, conn, queryName }) {
682
+ const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
683
+
684
+ console.log(matchingQuery.rawSql);
685
+ }
686
+
687
+ function showSavedQueryNotes({ appStateStore, conn, queryName }) {
688
+ const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
689
+ const notes = String(matchingQuery.notes ?? '').trim();
690
+
691
+ if (notes) {
692
+ console.log(notes);
242
693
  return;
243
694
  }
244
695
 
245
- if (sqlEditor || sqlEditorQuery) {
246
- const dbName = databaseName || databasePath || databaseSize || databaseLastOpened || databaseTables;
247
- if (!dbName) {
248
- console.error('Error: --sqleditor requires a database name.');
249
- console.error('Usage: sqlite-hub --database:name --sqleditor');
250
- console.error(' sqlite-hub --database:name --sqleditor:"query name"');
251
- process.exit(1);
696
+ console.log(`No notes saved for: ${getQueryTitle(matchingQuery)}`);
697
+ }
698
+
699
+ function exportSavedQuery({ appStateStore, conn, exportService, queryName, format }) {
700
+ const matchingQuery = requireMatchingQuery(appStateStore, conn, queryName);
701
+ const result = exportService.exportQuery(matchingQuery.rawSql, { format });
702
+ const outputPath = path.resolve(process.cwd(), result.filename);
703
+
704
+ fs.writeFileSync(outputPath, result.content, 'utf8');
705
+
706
+ console.log(`Exported query: ${getQueryTitle(matchingQuery)}`);
707
+ console.log(`Format: ${result.format}`);
708
+ console.log(`Rows: ${result.rowCount}`);
709
+ console.log(`File: ${outputPath}`);
710
+ }
711
+
712
+ function getDocumentTitle(document) {
713
+ return document?.filename || document?.title || document?.id || '(untitled document)';
714
+ }
715
+
716
+ function printAvailableDocuments(appStateStore, conn) {
717
+ const documents = appStateStore.listDatabaseDocuments(conn.id);
718
+
719
+ console.error('\nAvailable documents:');
720
+
721
+ if (documents.length > 0) {
722
+ documents.forEach(document => {
723
+ console.error(` - ${getDocumentTitle(document)}`);
724
+ });
725
+ return;
726
+ }
727
+
728
+ console.error(' (none)');
729
+ }
730
+
731
+ function findDocumentByName(appStateStore, conn, documentName) {
732
+ const normalizedDocumentName = String(documentName ?? '').trim().toLowerCase();
733
+
734
+ if (!normalizedDocumentName) {
735
+ throw new Error('Document name is required.');
736
+ }
737
+
738
+ const documents = appStateStore.listDatabaseDocuments(conn.id);
739
+ const exactMatch = documents.find(document => {
740
+ const candidates = [document.id, document.filename, document.title].filter(Boolean);
741
+
742
+ return candidates.some(candidate => String(candidate).toLowerCase() === normalizedDocumentName);
743
+ });
744
+
745
+ if (exactMatch) {
746
+ return appStateStore.getDatabaseDocument(conn.id, exactMatch.id);
747
+ }
748
+
749
+ const partialMatch = documents.find(document => {
750
+ const candidates = [document.filename, document.title].filter(Boolean);
751
+
752
+ return candidates.some(candidate => String(candidate).toLowerCase().includes(normalizedDocumentName));
753
+ });
754
+
755
+ return partialMatch ? appStateStore.getDatabaseDocument(conn.id, partialMatch.id) : null;
756
+ }
757
+
758
+ function requireMatchingDocument(appStateStore, conn, documentName) {
759
+ const matchingDocument = findDocumentByName(appStateStore, conn, documentName);
760
+
761
+ if (!matchingDocument) {
762
+ console.error(`Document not found: ${documentName}`);
763
+ printAvailableDocuments(appStateStore, conn);
764
+ process.exit(1);
765
+ }
766
+
767
+ return matchingDocument;
768
+ }
769
+
770
+ function listDocuments(appStateStore, conn) {
771
+ const documents = appStateStore.listDatabaseDocuments(conn.id);
772
+
773
+ if (documents.length === 0) {
774
+ console.log(`No documents found for ${conn.label}.`);
775
+ return;
776
+ }
777
+
778
+ console.log(`\nDocuments for ${conn.label} (${documents.length}):`);
779
+ console.log('─'.repeat(60));
780
+
781
+ documents.forEach((document, index) => {
782
+ console.log(`${index + 1}. ${document.filename}`);
783
+ console.log(` Updated: ${document.updatedAt}`);
784
+ console.log(` Characters: ${document.contentLength}`);
785
+ });
786
+
787
+ console.log('');
788
+ }
789
+
790
+ function showDocumentMarkdown({ appStateStore, conn, documentName }) {
791
+ const matchingDocument = requireMatchingDocument(appStateStore, conn, documentName);
792
+
793
+ console.log(matchingDocument.content ?? '');
794
+ }
795
+
796
+ function normalizeMarkdownExportFilename(filename, fallback = 'document.md') {
797
+ let normalizedFilename = String(filename ?? '')
798
+ .replace(/[\u0000-\u001f\u007f]/g, ' ')
799
+ .replace(/[<>:"/\\|?*]+/g, ' ')
800
+ .replace(/\s+/g, ' ')
801
+ .trim()
802
+ .replace(/^\.+/, '')
803
+ .replace(/[. ]+$/g, '');
804
+
805
+ if (!normalizedFilename) {
806
+ normalizedFilename = fallback;
807
+ }
808
+
809
+ if (!/\.md$/i.test(normalizedFilename)) {
810
+ normalizedFilename = `${normalizedFilename}.md`;
811
+ }
812
+
813
+ if (normalizedFilename.length > 160) {
814
+ normalizedFilename = `${normalizedFilename.slice(0, 157)}.md`;
815
+ }
816
+
817
+ return normalizedFilename;
818
+ }
819
+
820
+ function exportDocumentMarkdown({ appStateStore, conn, documentName }) {
821
+ const matchingDocument = requireMatchingDocument(appStateStore, conn, documentName);
822
+ const filename = normalizeMarkdownExportFilename(matchingDocument.filename, `${matchingDocument.title || 'document'}.md`);
823
+ const outputPath = path.resolve(process.cwd(), filename);
824
+
825
+ fs.writeFileSync(outputPath, matchingDocument.content ?? '', 'utf8');
826
+
827
+ console.log(`Exported document: ${matchingDocument.filename}`);
828
+ console.log(`Characters: ${matchingDocument.contentLength}`);
829
+ console.log(`File: ${outputPath}`);
830
+ }
831
+
832
+ function coerceIdentityValue(column, value) {
833
+ const text = String(value ?? '');
834
+ const affinity = String(column?.affinity ?? '').toUpperCase();
835
+
836
+ if ((affinity === 'INTEGER' || affinity === 'REAL' || affinity === 'NUMERIC') && text.trim() !== '') {
837
+ const numberValue = Number(text);
838
+
839
+ if (Number.isFinite(numberValue)) {
840
+ return numberValue;
252
841
  }
842
+ }
843
+
844
+ return value;
845
+ }
846
+
847
+ function parseCompositePrimaryKeyValue(rawValue) {
848
+ try {
849
+ const parsed = JSON.parse(rawValue);
850
+
851
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
852
+ return parsed;
853
+ }
854
+ } catch (error) {
855
+ // Fall through to the clearer error below.
856
+ }
857
+
858
+ throw new Error('Composite primary key export requires a JSON object, for example --export:\'{"id":1,"locale":"en"}\'.');
859
+ }
860
+
861
+ function buildIdentityFromExportTarget(tableDetail, exportTarget) {
862
+ if (tableDetail.identityStrategy?.type === 'rowid') {
863
+ const numberValue = Number(exportTarget);
864
+ const rowid = Number.isInteger(numberValue) ? numberValue : exportTarget;
865
+
866
+ return {
867
+ kind: 'rowid',
868
+ values: {
869
+ rowid,
870
+ },
871
+ };
872
+ }
873
+
874
+ if (tableDetail.identityStrategy?.type === 'primaryKey') {
875
+ const columns = tableDetail.identityStrategy.columns ?? [];
876
+
877
+ if (columns.length === 1) {
878
+ const columnName = columns[0];
879
+ const column = tableDetail.columns.find(candidate => candidate.name === columnName);
880
+
881
+ return {
882
+ kind: 'primaryKey',
883
+ columns,
884
+ values: {
885
+ [columnName]: coerceIdentityValue(column, exportTarget),
886
+ },
887
+ };
888
+ }
889
+
890
+ const parsed = parseCompositePrimaryKeyValue(exportTarget);
891
+
892
+ return {
893
+ kind: 'primaryKey',
894
+ columns,
895
+ values: Object.fromEntries(
896
+ columns.map(columnName => {
897
+ if (!Object.prototype.hasOwnProperty.call(parsed, columnName)) {
898
+ throw new Error(`Missing primary key value for ${columnName}.`);
899
+ }
900
+
901
+ const column = tableDetail.columns.find(candidate => candidate.name === columnName);
902
+
903
+ return [columnName, coerceIdentityValue(column, parsed[columnName])];
904
+ }),
905
+ ),
906
+ };
907
+ }
908
+
909
+ throw new Error(`Table ${tableDetail.name} has no stable row identity.`);
910
+ }
911
+
912
+ function buildDataRowEditorJsonObject({ row, columns = [] } = {}) {
913
+ const names = columns
914
+ .map(column => String(typeof column === 'object' ? column?.name : column ?? '').trim())
915
+ .filter(name => name && name !== '__identity');
916
+ const sourceNames = names.length
917
+ ? names
918
+ : Object.keys(row ?? {}).filter(name => name !== '__identity');
919
+
920
+ return Object.fromEntries(
921
+ sourceNames
922
+ .map(name => [name, Object.prototype.hasOwnProperty.call(row ?? {}, name) ? row[name] : undefined])
923
+ .filter(([, value]) => value !== undefined),
924
+ );
925
+ }
926
+
927
+ function exportTableRowAsJson({ dataBrowserService, db, tableName, exportTarget }) {
928
+ const { getTableDetail } = require('../server/services/sqlite/introspection');
929
+ const tableDetail = getTableDetail(db, tableName, { includeRowCount: false });
930
+ const identity = buildIdentityFromExportTarget(tableDetail, exportTarget);
931
+ const { row } = dataBrowserService.getTableRow(tableName, { identity });
932
+ const rowObject = buildDataRowEditorJsonObject({
933
+ row,
934
+ columns: tableDetail.columns.filter(column => column.visible),
935
+ });
936
+ const filenameBase = sanitizeFilenameBase(`${tableName}-${exportTarget}`, `${tableName}-row`);
937
+ const outputPath = path.resolve(process.cwd(), `${filenameBase}.json`);
938
+
939
+ fs.writeFileSync(outputPath, `${JSON.stringify(rowObject, null, 2)}\n`, 'utf8');
940
+
941
+ console.log(`Exported row: ${tableName}`);
942
+ console.log(`Key: ${exportTarget}`);
943
+ console.log(`File: ${outputPath}`);
944
+ }
945
+
946
+ function formatColumnFlags(column, foreignKeyColumns) {
947
+ const flags = [];
948
+
949
+ if (column.primaryKeyPosition > 0) {
950
+ flags.push(`PK${column.primaryKeyPosition > 1 ? `:${column.primaryKeyPosition}` : ''}`);
951
+ }
952
+
953
+ if (foreignKeyColumns.has(column.name)) {
954
+ flags.push('FK');
955
+ }
956
+
957
+ if (column.notNull) {
958
+ flags.push('NOT NULL');
959
+ }
960
+
961
+ if (column.generated) {
962
+ flags.push('GENERATED');
963
+ }
964
+
965
+ return flags.length ? ` [${flags.join(', ')}]` : '';
966
+ }
967
+
968
+ function printTableInfo(tableDetail) {
969
+ const foreignKeyColumns = new Set(
970
+ tableDetail.foreignKeys.flatMap(foreignKey => foreignKey.mappings.map(mapping => mapping.from)),
971
+ );
972
+
973
+ console.log(`Table: ${tableDetail.name}`);
974
+ console.log(`Rows: ${tableDetail.rowCount ?? 'N/A'}`);
975
+ console.log(`Identity: ${tableDetail.identityStrategy.type}`);
976
+ console.log(`Columns: ${tableDetail.columns.filter(column => column.visible).length}`);
977
+ console.log('');
978
+
979
+ tableDetail.columns
980
+ .filter(column => column.visible)
981
+ .forEach(column => {
982
+ const type = column.declaredType || column.affinity || 'ANY';
983
+ console.log(` - ${column.name} ${type}${formatColumnFlags(column, foreignKeyColumns)}`);
984
+ });
985
+
986
+ if (tableDetail.foreignKeys.length > 0) {
987
+ console.log('');
988
+ console.log(`Foreign keys: ${tableDetail.foreignKeys.length}`);
989
+ tableDetail.foreignKeys.forEach(foreignKey => {
990
+ const mapping = foreignKey.mappings.map(item => `${item.from} -> ${item.to}`).join(', ');
991
+ console.log(` - ${mapping} (${foreignKey.referencedTable})`);
992
+ });
993
+ }
994
+
995
+ if (tableDetail.indexes.length > 0) {
996
+ console.log('');
997
+ console.log(`Indexes: ${tableDetail.indexes.length}`);
998
+ tableDetail.indexes.forEach(index => {
999
+ const unique = index.unique ? ' UNIQUE' : '';
1000
+ const columns = index.columns.map(column => column.name).filter(Boolean).join(', ') || 'expression';
1001
+ console.log(` - ${index.name}${unique}: ${columns}`);
1002
+ });
1003
+ }
1004
+ }
1005
+
1006
+ function printConfig(port) {
1007
+ const Database = require('better-sqlite3');
1008
+ const { version } = require('../package.json');
1009
+ const db = new Database(':memory:');
1010
+
1011
+ try {
1012
+ const sqliteVersion = db.prepare('SELECT sqlite_version() AS version').get().version;
1013
+ const url = `http://127.0.0.1:${port}`;
1014
+
1015
+ console.log('SQLite Hub config');
1016
+ console.log(`Port: ${port}`);
1017
+ console.log(`URL: ${url}`);
1018
+ console.log(`App version: ${version}`);
1019
+ console.log(`SQLite version: ${sqliteVersion}`);
1020
+ } finally {
1021
+ db.close();
1022
+ }
1023
+ }
1024
+
1025
+ async function startAndOpen(port) {
1026
+ const { startServer } = require('../server/server');
1027
+ const fallbackUrl = `http://127.0.0.1:${port}`;
1028
+
1029
+ try {
1030
+ const { url } = await startServer({ port });
1031
+ openInDefaultBrowser(url);
1032
+ } catch (error) {
1033
+ if (error.code === 'EADDRINUSE') {
1034
+ console.warn(`Server already appears to be running on ${fallbackUrl}`);
1035
+ openInDefaultBrowser(fallbackUrl);
1036
+ return;
1037
+ }
1038
+
1039
+ throw error;
1040
+ }
1041
+ }
1042
+
1043
+ function requireDatabaseName(options) {
1044
+ if (!options.databaseName) {
1045
+ console.error('Error: this command requires --database:"name".');
1046
+ process.exit(1);
1047
+ }
1048
+
1049
+ return options.databaseName;
1050
+ }
1051
+
1052
+ async function main() {
1053
+ const options = parseCliArguments(process.argv.slice(2));
1054
+ const port = options.port ?? DEFAULT_PORT;
1055
+
1056
+ if (options.help) {
1057
+ printHelp();
1058
+ return;
1059
+ }
1060
+
1061
+ if (options.version) {
1062
+ const { version } = require('../package.json');
1063
+ console.log(`SQLite Hub CLI version ${version}`);
1064
+ return;
1065
+ }
1066
+
1067
+ if (options.config) {
1068
+ printConfig(port);
1069
+ return;
1070
+ }
1071
+
1072
+ if (options.open) {
1073
+ await startAndOpen(port);
1074
+ return;
1075
+ }
1076
+
1077
+ const appStateStore = createAppStateStore();
1078
+ const connections = appStateStore.getRecentConnections();
1079
+
1080
+ if (options.databaseList && !options.databaseName && !hasDatabaseOperation(options)) {
1081
+ printDatabaseList(connections);
1082
+ return;
1083
+ }
1084
+
1085
+ if (options.databaseName || hasDatabaseOperation(options)) {
1086
+ const dbName = requireDatabaseName(options);
253
1087
  const conn = findDatabaseByName(connections, dbName);
1088
+
254
1089
  if (!conn) {
255
1090
  console.error(`Database not found: ${dbName}`);
256
1091
  process.exit(1);
257
1092
  }
258
1093
 
259
- const Database = require('better-sqlite3');
260
- const { SqlExecutor } = require('../server/services/sqlite/sqlExecutor');
261
- const { ConnectionManager } = require('../server/services/sqlite/connectionManager');
262
-
263
- const db = new Database(conn.path, { readonly: true });
1094
+ if (options.documents) {
1095
+ if (options.documentName) {
1096
+ if (options.documentExport) {
1097
+ exportDocumentMarkdown({
1098
+ appStateStore,
1099
+ conn,
1100
+ documentName: options.documentName,
1101
+ });
1102
+ } else {
1103
+ showDocumentMarkdown({
1104
+ appStateStore,
1105
+ conn,
1106
+ documentName: options.documentName,
1107
+ });
1108
+ }
1109
+ return;
1110
+ }
264
1111
 
265
- try {
266
- const connectionManager = new ConnectionManager({ appStateStore });
267
- connectionManager.openConnection({
268
- filePath: conn.path,
269
- label: conn.label,
270
- makeActive: true,
271
- readOnly: true,
272
- });
1112
+ listDocuments(appStateStore, conn);
1113
+ return;
1114
+ }
273
1115
 
274
- const sqlExecutor = new SqlExecutor({ connectionManager, appStateStore });
1116
+ if (options.tableName || options.tables || options.queries || options.executeQuery || options.showQuery || options.showNotes || options.exportTarget) {
1117
+ const runtime = createReadOnlyRuntime(conn, appStateStore);
275
1118
 
276
- if (sqlEditorQuery) {
277
- const queryHistory = appStateStore.buildQueryHistoryCollection({
278
- databaseKey: conn.id,
279
- search: sqlEditorQuery,
280
- onlySaved: false,
281
- limit: 100,
282
- });
1119
+ try {
1120
+ if (options.tableName) {
1121
+ const { getTableDetail } = require('../server/services/sqlite/introspection');
283
1122
 
284
- const matchingQuery = queryHistory.items.find(
285
- item =>
286
- item.title?.toLowerCase() === sqlEditorQuery.toLowerCase() ||
287
- item.rawSql.toLowerCase().includes(sqlEditorQuery.toLowerCase()),
288
- );
289
-
290
- if (!matchingQuery) {
291
- console.error(`Saved query not found: ${sqlEditorQuery}`);
292
- console.error('\nAvailable saved queries:');
293
- const allQueries = appStateStore.buildQueryHistoryCollection({
294
- databaseKey: conn.id,
295
- onlySaved: true,
296
- limit: 100,
297
- });
298
- if (allQueries.items.length > 0) {
299
- allQueries.items.forEach(q => {
300
- console.log(` - ${q.title || q.displayTitle}`);
1123
+ if (options.exportTarget) {
1124
+ exportTableRowAsJson({
1125
+ dataBrowserService: runtime.dataBrowserService,
1126
+ db: runtime.db,
1127
+ tableName: options.tableName,
1128
+ exportTarget: options.exportTarget,
301
1129
  });
302
1130
  } else {
303
- console.log(' (none)');
1131
+ printTableInfo(getTableDetail(runtime.db, options.tableName));
304
1132
  }
305
- process.exit(1);
1133
+
1134
+ return;
306
1135
  }
307
1136
 
308
- console.log(`\nExecuting: ${matchingQuery.title || matchingQuery.displayTitle}`);
309
- console.log(`SQL: ${matchingQuery.previewSql}`);
310
- console.log('─'.repeat(60));
1137
+ if (options.exportTarget) {
1138
+ exportSavedQuery({
1139
+ appStateStore,
1140
+ conn,
1141
+ exportService: runtime.exportService,
1142
+ queryName: options.exportTarget,
1143
+ format: options.exportFormat,
1144
+ });
1145
+ return;
1146
+ }
311
1147
 
312
- const result = sqlExecutor.execute(matchingQuery.rawSql, {
313
- persistHistory: false,
314
- });
1148
+ if (options.executeQuery) {
1149
+ executeSavedQuery({
1150
+ appStateStore,
1151
+ conn,
1152
+ sqlExecutor: runtime.sqlExecutor,
1153
+ queryName: options.executeQuery,
1154
+ });
1155
+ return;
1156
+ }
315
1157
 
316
- console.log(`\nStatement count: ${result.statementCount}`);
317
- console.log(`Timing: ${result.timingMs}ms`);
318
-
319
- result.statements.forEach((stmt, index) => {
320
- console.log(`\nStatement ${index + 1} (${stmt.kind}):`);
321
- if (stmt.kind === 'resultSet') {
322
- console.log(`Rows: ${stmt.rowCount}`);
323
- console.log(`Columns: ${stmt.columns.join(', ')}`);
324
- if (stmt.rows.length > 0) {
325
- console.log('\nResults:');
326
- stmt.rows.forEach((row, rowIndex) => {
327
- const values = stmt.columns.map(col => {
328
- const val = row[col];
329
- return val === null ? 'NULL' : String(val);
330
- });
331
- console.log(` [${rowIndex}] ${values.join(' | ')}`);
332
- });
333
- }
334
- } else if (stmt.kind === 'mutation') {
335
- console.log(`Changes: ${stmt.changes}`);
336
- if (stmt.lastInsertRowid) {
337
- console.log('Last insert rowid:', stmt.lastInsertRowid);
338
- }
339
- }
340
- });
341
- } else {
342
- const savedQueries = appStateStore.buildQueryHistoryCollection({
343
- databaseKey: conn.id,
344
- onlySaved: true,
345
- limit: 100,
346
- });
1158
+ if (options.showQuery) {
1159
+ showSavedQuery({ appStateStore, conn, queryName: options.showQuery });
1160
+ return;
1161
+ }
347
1162
 
348
- if (savedQueries.items.length === 0) {
349
- console.log(`No saved queries found for ${conn.label}.`);
350
- } else {
351
- console.log(`\nSaved queries for ${conn.label} (${savedQueries.total}):`);
352
- console.log('─'.repeat(60));
353
- savedQueries.items.forEach((q, index) => {
354
- const title = q.title || q.displayTitle;
355
- console.log(`${index + 1}. ${title}`);
356
- });
357
- console.log('');
1163
+ if (options.showNotes) {
1164
+ showSavedQueryNotes({ appStateStore, conn, queryName: options.showNotes });
1165
+ return;
1166
+ }
1167
+
1168
+ if (options.queries) {
1169
+ listSavedQueries(appStateStore, conn);
1170
+ return;
1171
+ }
1172
+
1173
+ if (options.tables) {
1174
+ printTables(conn, runtime.dataBrowserService.listTables());
1175
+ return;
358
1176
  }
1177
+ } finally {
1178
+ runtime.close();
359
1179
  }
360
- } finally {
361
- db.close();
362
1180
  }
363
- return;
364
- }
365
1181
 
366
- if (database) {
367
- if (connections.length === 0) {
368
- console.log('No databases imported yet.');
1182
+ if (options.pathInfo) {
1183
+ console.log(conn.path);
369
1184
  return;
370
1185
  }
371
1186
 
372
- console.log(`\nImported databases (${connections.length}):`);
373
- console.log('─'.repeat(60));
1187
+ if (options.sizeInfo) {
1188
+ console.log(formatSize(conn.sizeBytes));
1189
+ return;
1190
+ }
374
1191
 
375
- connections.forEach((conn, index) => {
376
- const size = formatSize(conn.sizeBytes);
377
- const readOnly = conn.readOnly ? ' (read-only)' : '';
378
- console.log(`${index + 1}. ${conn.label}${readOnly}`);
379
- console.log(` Path: ${conn.path}`);
380
- console.log(` Size: ${size}`);
381
- console.log(` Last opened: ${conn.lastOpenedAt}`);
382
- console.log('');
383
- });
1192
+ if (options.lastOpenedInfo) {
1193
+ console.log(conn.lastOpenedAt);
1194
+ return;
1195
+ }
384
1196
 
1197
+ printSingleDatabaseInfo(conn);
385
1198
  return;
386
1199
  }
387
1200
 
388
- const { startServer } = require('../server/server');
389
- const { url } = await startServer({ port });
390
- openInDefaultBrowser(url);
1201
+ if (options.databaseList) {
1202
+ printDatabaseList(connections);
1203
+ return;
1204
+ }
1205
+
1206
+ await startAndOpen(port);
391
1207
  }
392
1208
 
393
1209
  if (require.main === module) {
@@ -399,6 +1215,7 @@ if (require.main === module) {
399
1215
 
400
1216
  module.exports = {
401
1217
  main,
1218
+ normalizeExportFormat,
402
1219
  openInDefaultBrowser,
403
1220
  parseCliArguments,
404
1221
  };