sqlite-hub 1.2.0 → 1.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.
Files changed (169) hide show
  1. package/README.md +113 -71
  2. package/bin/sqlite-hub.js +723 -116
  3. package/docs/API.md +152 -0
  4. package/docs/CLI.md +249 -0
  5. package/docs/CLI_API_PARITY.md +61 -0
  6. package/docs/changelog.md +149 -0
  7. package/docs/guidelines/AGENTS.md +32 -0
  8. package/docs/guidelines/DESIGN.md +55 -0
  9. package/docs/shortkeys.md +27 -0
  10. package/docs/todo.md +4 -0
  11. package/examples/api/generate-types.js +52 -0
  12. package/frontend/assets/mockups/backups_1_1920.webp +0 -0
  13. package/frontend/assets/mockups/backups_2_create_backup_modal_1920.webp +0 -0
  14. package/frontend/assets/mockups/backups_3_edit_backup_modal_1920.webp +0 -0
  15. package/frontend/assets/mockups/backups_4_restore_backup_modal_1920.webp +0 -0
  16. package/frontend/assets/mockups/backups_5_delete_backup_modal_1920.webp +0 -0
  17. package/frontend/assets/mockups/charts_1_1920.webp +0 -0
  18. package/frontend/assets/mockups/charts_2_query_detail_1920.webp +0 -0
  19. package/frontend/assets/mockups/charts_3_create_query_chart_modal_1920.webp +0 -0
  20. package/frontend/assets/mockups/charts_4_edit_query_chart_modal_1920.webp +0 -0
  21. package/frontend/assets/mockups/charts_5_delete_query_chart_modal_1920.webp +0 -0
  22. package/frontend/assets/mockups/charts_6_copy_column_modal_1920.webp +0 -0
  23. package/frontend/assets/mockups/connections_1_1920.webp +0 -0
  24. package/frontend/assets/mockups/connections_2_create_connection_modal_1920.webp +0 -0
  25. package/frontend/assets/mockups/connections_3_open_connection_modal_1920.webp +0 -0
  26. package/frontend/assets/mockups/data_1_1920.webp +0 -0
  27. package/frontend/assets/mockups/data_2_roweditor_1920.webp +0 -0
  28. package/frontend/assets/mockups/data_3_data_export_modal_1920.webp +0 -0
  29. package/frontend/assets/mockups/documents_1_1920.webp +0 -0
  30. package/frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp +0 -0
  31. package/frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp +0 -0
  32. package/frontend/assets/mockups/media_tagging_queue_1_1920.webp +0 -0
  33. package/frontend/assets/mockups/media_tagging_setup_1_1920.webp +0 -0
  34. package/frontend/assets/mockups/media_tagging_setup_2_create_media_tagging_tag_table_modal_1920.webp +0 -0
  35. package/frontend/assets/mockups/media_tagging_setup_3_create_media_tagging_mapping_table_modal_1920.webp +0 -0
  36. package/frontend/assets/mockups/overview_1_1920.webp +0 -0
  37. package/frontend/assets/mockups/settings_1_1920.webp +0 -0
  38. package/frontend/assets/mockups/sql_editor_1_1920.webp +0 -0
  39. package/frontend/assets/mockups/sql_editor_2_query_detail_1920.webp +0 -0
  40. package/frontend/assets/mockups/sql_editor_3_query_export_modal_1920.webp +0 -0
  41. package/frontend/assets/mockups/structure_1_1920.webp +0 -0
  42. package/frontend/assets/mockups/structure_2_generate_types_modal_1920.webp +0 -0
  43. package/frontend/assets/mockups/structure_3_generate_types_modal_1920.webp +0 -0
  44. package/frontend/assets/mockups/table_designer_1_1920.webp +0 -0
  45. package/frontend/assets/mockups/table_designer_2_table_designer_constraints_modal_1920.webp +0 -0
  46. package/frontend/js/api.js +63 -0
  47. package/frontend/js/app.js +310 -10
  48. package/frontend/js/components/connectionCard.js +4 -2
  49. package/frontend/js/components/emptyState.js +4 -4
  50. package/frontend/js/components/generateTypesDropdown.js +33 -0
  51. package/frontend/js/components/metricCard.js +1 -1
  52. package/frontend/js/components/modal.js +555 -21
  53. package/frontend/js/components/pageHeader.js +1 -1
  54. package/frontend/js/components/queryHistoryDetail.js +3 -3
  55. package/frontend/js/components/queryHistoryHeader.js +1 -1
  56. package/frontend/js/components/queryHistoryPanel.js +1 -1
  57. package/frontend/js/components/rowEditorPanel.js +13 -3
  58. package/frontend/js/components/sidebar.js +41 -7
  59. package/frontend/js/components/topNav.js +2 -14
  60. package/frontend/js/router.js +38 -6
  61. package/frontend/js/store.js +870 -8
  62. package/frontend/js/utils/inputClear.js +36 -0
  63. package/frontend/js/utils/syntheticData.js +240 -0
  64. package/frontend/js/views/backups.js +581 -37
  65. package/frontend/js/views/charts.js +9 -9
  66. package/frontend/js/views/connections.js +2 -2
  67. package/frontend/js/views/data.js +17 -1
  68. package/frontend/js/views/documents.js +3 -3
  69. package/frontend/js/views/editor.js +6 -6
  70. package/frontend/js/views/logs.js +339 -0
  71. package/frontend/js/views/mediaTagging.js +7 -5
  72. package/frontend/js/views/overview.js +3 -3
  73. package/frontend/js/views/settings.js +80 -30
  74. package/frontend/js/views/structure.js +12 -1
  75. package/frontend/js/views/tableAdvisor.js +385 -0
  76. package/frontend/js/views/tableDesigner.js +6 -0
  77. package/frontend/styles/base.css +1 -40
  78. package/frontend/styles/components.css +258 -232
  79. package/frontend/styles/structure-graph.css +32 -68
  80. package/frontend/styles/tailwind.css +8 -4
  81. package/frontend/styles/tailwind.generated.css +1 -1
  82. package/frontend/styles/tokens.css +28 -21
  83. package/frontend/styles/views.css +102 -261
  84. package/package.json +18 -2
  85. package/server/routes/backups.js +18 -2
  86. package/server/routes/data.js +45 -0
  87. package/server/routes/externalApi.js +321 -2
  88. package/server/routes/logs.js +127 -0
  89. package/server/routes/structure.js +22 -0
  90. package/server/server.js +3 -0
  91. package/server/services/databaseCommandService.js +59 -0
  92. package/server/services/sqlite/backupDiff.js +914 -0
  93. package/server/services/sqlite/backupService.js +77 -1
  94. package/server/services/sqlite/dataBrowserService.js +36 -0
  95. package/server/services/sqlite/introspection.js +226 -22
  96. package/server/services/sqlite/structureService.js +7 -0
  97. package/server/services/sqlite/syntheticDataGenerator.js +728 -0
  98. package/server/services/sqlite/tableAdvisor.js +790 -0
  99. package/server/services/storage/appStateStore.js +773 -169
  100. package/server/services/typeGenerationService.js +663 -0
  101. package/tailwind.config.cjs +0 -1
  102. package/.github/funding.yml +0 -2
  103. package/.github/workflows/ci.yml +0 -36
  104. package/database.sqlite +0 -0
  105. package/frontend/assets/mockups/charts_1_bars_1200.webp +0 -0
  106. package/frontend/assets/mockups/charts_2_pie_1200.webp +0 -0
  107. package/frontend/assets/mockups/charts_3_scatter_plot_1200.webp +0 -0
  108. package/frontend/assets/mockups/connections_1200.webp +0 -0
  109. package/frontend/assets/mockups/data_1_1200.webp +0 -0
  110. package/frontend/assets/mockups/data_2_row_editor_1200.webp +0 -0
  111. package/frontend/assets/mockups/documents_1200.webp +0 -0
  112. package/frontend/assets/mockups/media_tagging_1_setup_1200.webp +0 -0
  113. package/frontend/assets/mockups/media_tagging_2_tagging_queue_1200.webp +0 -0
  114. package/frontend/assets/mockups/media_tagging_3_media_viewer_1200.webp +0 -0
  115. package/frontend/assets/mockups/overview_1200.webp +0 -0
  116. package/frontend/assets/mockups/settings_1200.webp +0 -0
  117. package/frontend/assets/mockups/sql_editor_1_1200.webp +0 -0
  118. package/frontend/assets/mockups/sql_editor_2_query_details_1200.webp +0 -0
  119. package/frontend/assets/mockups/sql_editor_3_export_column_1200.webp +0 -0
  120. package/frontend/assets/mockups/sql_editor_4_export_column_as_markdown_1200.webp +0 -0
  121. package/frontend/assets/mockups/sql_editor_5_export_query_result_1200.webp +0 -0
  122. package/frontend/assets/mockups/structure_1_1200.webp +0 -0
  123. package/frontend/assets/mockups/structure_2_inspector_1200.webp +0 -0
  124. package/frontend/assets/mockups/table_designer_1_1200.webp +0 -0
  125. package/frontend/assets/mockups/table_designer_2_checks_1200.webp +0 -0
  126. package/tests/api-token-auth.test.js +0 -279
  127. package/tests/backup-manager.test.js +0 -140
  128. package/tests/backups-view.test.js +0 -70
  129. package/tests/charts-height-preset-storage.test.js +0 -60
  130. package/tests/charts-route-state.test.js +0 -144
  131. package/tests/check-constraint-options.test.js +0 -90
  132. package/tests/cli-args.test.js +0 -113
  133. package/tests/cli-service-delegation.test.js +0 -140
  134. package/tests/connection-removal.test.js +0 -52
  135. package/tests/connections-file-dialog-route.test.js +0 -89
  136. package/tests/copy-column-modal.test.js +0 -131
  137. package/tests/database-command-service.test.js +0 -174
  138. package/tests/database-documents.test.js +0 -85
  139. package/tests/documents-view.test.js +0 -132
  140. package/tests/dropdown-button.test.js +0 -101
  141. package/tests/email-preview.test.js +0 -89
  142. package/tests/export-blob.test.js +0 -99
  143. package/tests/export-filenames.test.js +0 -38
  144. package/tests/file-path-preview.test.js +0 -165
  145. package/tests/form-controls.test.js +0 -34
  146. package/tests/json-preview.test.js +0 -49
  147. package/tests/local-request-security.test.js +0 -85
  148. package/tests/markdown-documents.test.js +0 -103
  149. package/tests/native-file-dialog.test.js +0 -105
  150. package/tests/query-editor.test.js +0 -28
  151. package/tests/query-history-detail.test.js +0 -37
  152. package/tests/query-history-header.test.js +0 -30
  153. package/tests/query-results-truncation.test.js +0 -20
  154. package/tests/risky-sql.test.js +0 -30
  155. package/tests/row-editor-json.test.js +0 -82
  156. package/tests/row-editor-null-values.test.js +0 -184
  157. package/tests/row-editor-timestamp-preview.test.js +0 -192
  158. package/tests/security-paths.test.js +0 -84
  159. package/tests/settings-api-tokens-route.test.js +0 -97
  160. package/tests/settings-metadata.test.js +0 -114
  161. package/tests/settings-view.test.js +0 -108
  162. package/tests/sql-formatter.test.js +0 -173
  163. package/tests/sql-highlight.test.js +0 -38
  164. package/tests/sql-identifier-safety.test.js +0 -171
  165. package/tests/sql-result-limit.test.js +0 -66
  166. package/tests/structure-view.test.js +0 -60
  167. package/tests/table-designer-v2-unique-constraints.test.js +0 -78
  168. package/tests/table-scroll-state.test.js +0 -70
  169. package/tests/text-cell-stats.test.js +0 -38
package/bin/sqlite-hub.js CHANGED
@@ -5,6 +5,7 @@ const path = require('node:path');
5
5
  const { spawn } = require('node:child_process');
6
6
  const { DatabaseCommandService, getQueryTitle } = require('../server/services/databaseCommandService');
7
7
  const { buildAppInfo } = require('../server/services/appInfoService');
8
+ const { FILE_EXTENSIONS } = require('../server/services/typeGenerationService');
8
9
 
9
10
  const DEFAULT_PORT = 4173;
10
11
  const EXPORT_FORMATS = new Set(['csv', 'tsv', 'md', 'json']);
@@ -25,8 +26,12 @@ Usage:
25
26
  sqlite-hub --database:"name" --documents
26
27
  sqlite-hub --database:"name" --documents:"Document Name"
27
28
  sqlite-hub --database:"name" --documents:"Document Name" --export
29
+ sqlite-hub --database:"name" --backups
30
+ sqlite-hub --database:"name" --backup
31
+ sqlite-hub --database:"name" --backup:"Before migration"
28
32
  sqlite-hub --database:"name" --table:"table_name"
29
33
  sqlite-hub --database:"name" --table:"table_name" --export:"primary-key"
34
+ sqlite-hub --database:"name" --table:"table_name" --types:typescript
30
35
 
31
36
  Options:
32
37
  --help, -h Show this help text.
@@ -51,8 +56,27 @@ Options:
51
56
  --documents List Markdown documents for the selected database.
52
57
  --documents:"name" Print a document's Markdown content.
53
58
  --documents:"name" --export Export a document as a Markdown file.
59
+ --backups List managed backups for the selected database.
60
+ --backup Create and verify a managed backup.
61
+ --backup:"name" Create a backup with a custom name.
62
+ --backup-notes:"text" Add notes to a created backup.
54
63
  --table:"table" Print table metadata.
55
64
  --table:"table" --export:"pk" Export one row as JSON by primary key or rowid.
65
+ --types:typescript|ts|rust|rs|kotlin|kt|swift
66
+ Generate application types for --table.
67
+ --type-name:"name" Override generated type name.
68
+ --naming:preserve|camel|pascal|snake
69
+ Select generated property naming.
70
+ --nullable:native|optional Select nullable handling. Optional is TypeScript only.
71
+ --comments Include schema comments.
72
+ --defaults-as-comments Include default values in comments.
73
+ --json-type:unknown|record|json-value
74
+ Select TypeScript JSON mapping.
75
+ --include-generated Include generated columns.
76
+ --include-hidden Include hidden columns.
77
+ --output:"file" Write generated types to a file.
78
+ --json Print generated type result as JSON.
79
+ --force Overwrite --output file if it exists.
56
80
  `);
57
81
  }
58
82
 
@@ -174,7 +198,23 @@ function parseCliArguments(argv) {
174
198
  documents: false,
175
199
  documentName: null,
176
200
  documentExport: false,
201
+ backups: false,
202
+ backup: false,
203
+ backupName: null,
204
+ backupNotes: null,
177
205
  tableName: null,
206
+ typesTarget: null,
207
+ typeName: null,
208
+ naming: null,
209
+ nullableMode: null,
210
+ includeComments: false,
211
+ includeDefaultsAsComments: false,
212
+ includeGeneratedColumns: undefined,
213
+ includeHiddenColumns: false,
214
+ jsonType: null,
215
+ outputPath: null,
216
+ jsonOutput: false,
217
+ force: false,
178
218
  };
179
219
 
180
220
  for (let index = 0; index < argv.length; index += 1) {
@@ -354,6 +394,39 @@ function parseCliArguments(argv) {
354
394
  continue;
355
395
  }
356
396
 
397
+ if (flag === '--backups') {
398
+ options.backups = true;
399
+ continue;
400
+ }
401
+
402
+ if (flag === '--backup') {
403
+ const parsed = takeOptionalFlagValue(value, argv, index);
404
+
405
+ options.backup = true;
406
+ if (parsed.hasValue) {
407
+ options.backupName = parsed.value;
408
+ }
409
+
410
+ index = parsed.nextIndex;
411
+ continue;
412
+ }
413
+
414
+ if (flag === '--backup-name') {
415
+ const parsed = takeFlagValue(flag, value, argv, index);
416
+ options.backup = true;
417
+ options.backupName = parsed.value;
418
+ index = parsed.nextIndex;
419
+ continue;
420
+ }
421
+
422
+ if (flag === '--backup-notes') {
423
+ const parsed = takeFlagValue(flag, value, argv, index);
424
+ options.backup = true;
425
+ options.backupNotes = parsed.value;
426
+ index = parsed.nextIndex;
427
+ continue;
428
+ }
429
+
357
430
  if (flag === '--export') {
358
431
  if (options.documents && options.documentName && value === undefined) {
359
432
  const nextValue = argv[index + 1];
@@ -384,6 +457,78 @@ function parseCliArguments(argv) {
384
457
  continue;
385
458
  }
386
459
 
460
+ if (flag === '--types') {
461
+ const parsed = takeFlagValue(flag, value, argv, index);
462
+ options.typesTarget = parsed.value;
463
+ index = parsed.nextIndex;
464
+ continue;
465
+ }
466
+
467
+ if (flag === '--type-name') {
468
+ const parsed = takeFlagValue(flag, value, argv, index);
469
+ options.typeName = parsed.value;
470
+ index = parsed.nextIndex;
471
+ continue;
472
+ }
473
+
474
+ if (flag === '--naming') {
475
+ const parsed = takeFlagValue(flag, value, argv, index);
476
+ options.naming = parsed.value;
477
+ index = parsed.nextIndex;
478
+ continue;
479
+ }
480
+
481
+ if (flag === '--nullable') {
482
+ const parsed = takeFlagValue(flag, value, argv, index);
483
+ options.nullableMode = parsed.value;
484
+ index = parsed.nextIndex;
485
+ continue;
486
+ }
487
+
488
+ if (flag === '--comments') {
489
+ options.includeComments = true;
490
+ continue;
491
+ }
492
+
493
+ if (flag === '--defaults-as-comments') {
494
+ options.includeDefaultsAsComments = true;
495
+ continue;
496
+ }
497
+
498
+ if (flag === '--include-generated') {
499
+ options.includeGeneratedColumns = true;
500
+ continue;
501
+ }
502
+
503
+ if (flag === '--include-hidden') {
504
+ options.includeHiddenColumns = true;
505
+ continue;
506
+ }
507
+
508
+ if (flag === '--json-type') {
509
+ const parsed = takeFlagValue(flag, value, argv, index);
510
+ options.jsonType = parsed.value;
511
+ index = parsed.nextIndex;
512
+ continue;
513
+ }
514
+
515
+ if (flag === '--output') {
516
+ const parsed = takeFlagValue(flag, value, argv, index);
517
+ options.outputPath = parsed.value;
518
+ index = parsed.nextIndex;
519
+ continue;
520
+ }
521
+
522
+ if (flag === '--json') {
523
+ options.jsonOutput = true;
524
+ continue;
525
+ }
526
+
527
+ if (flag === '--force') {
528
+ options.force = true;
529
+ continue;
530
+ }
531
+
387
532
  throw new Error(`Unknown argument: ${argument}`);
388
533
  }
389
534
 
@@ -405,8 +550,11 @@ function hasDatabaseOperation(options) {
405
550
  options.documents ||
406
551
  options.documentName ||
407
552
  options.documentExport ||
553
+ options.backups ||
554
+ options.backup ||
408
555
  options.exportTarget ||
409
- options.tableName,
556
+ options.tableName ||
557
+ options.typesTarget,
410
558
  );
411
559
  }
412
560
 
@@ -626,6 +774,67 @@ function exportSavedQuery({ databaseService, conn, queryName, format }) {
626
774
  console.log(`File: ${outputPath}`);
627
775
  }
628
776
 
777
+ function buildTypeOptions(options) {
778
+ const typeOptions = {};
779
+
780
+ if (options.typeName) typeOptions.typeName = options.typeName;
781
+ if (options.naming) typeOptions.propertyNaming = options.naming;
782
+ if (options.nullableMode) typeOptions.nullableMode = options.nullableMode;
783
+ if (options.includeComments) typeOptions.includeComments = true;
784
+ if (options.includeDefaultsAsComments) typeOptions.includeDefaultsAsComments = true;
785
+ if (options.includeGeneratedColumns !== undefined) {
786
+ typeOptions.includeGeneratedColumns = options.includeGeneratedColumns;
787
+ }
788
+ if (options.includeHiddenColumns) typeOptions.includeHiddenColumns = true;
789
+ if (options.jsonType) typeOptions.jsonType = options.jsonType;
790
+
791
+ return typeOptions;
792
+ }
793
+
794
+ function generateTypes({ databaseService, conn, tableName, options }) {
795
+ if (!tableName) {
796
+ throw new Error('--types requires --database and --table.');
797
+ }
798
+
799
+ if (options.jsonOutput && options.outputPath) {
800
+ throw new Error('--json cannot be combined with --output.');
801
+ }
802
+
803
+ const result = databaseService.generateTableTypes(
804
+ conn.id,
805
+ tableName,
806
+ options.typesTarget,
807
+ buildTypeOptions(options)
808
+ );
809
+
810
+ if (options.jsonOutput) {
811
+ console.log(JSON.stringify(result, null, 2));
812
+ return;
813
+ }
814
+
815
+ if (options.outputPath) {
816
+ const outputPath = path.resolve(process.cwd(), options.outputPath);
817
+ const expectedExtension = FILE_EXTENSIONS[result.target];
818
+
819
+ if (path.extname(outputPath) !== expectedExtension) {
820
+ throw new Error(`Output file for ${result.target} must use ${expectedExtension}.`);
821
+ }
822
+
823
+ if (!options.force && fs.existsSync(outputPath)) {
824
+ throw new Error(`Output file already exists: ${outputPath}`);
825
+ }
826
+
827
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
828
+ fs.writeFileSync(outputPath, result.code, 'utf8');
829
+ result.warnings.forEach(warning => console.error(`Warning: ${warning}`));
830
+ console.error(`Generated ${result.target} types: ${outputPath}`);
831
+ return;
832
+ }
833
+
834
+ result.warnings.forEach(warning => console.error(`Warning: ${warning}`));
835
+ process.stdout.write(`${result.code}\n`);
836
+ }
837
+
629
838
  function listDocuments(databaseService, conn) {
630
839
  const documents = databaseService.listDocuments(conn.id);
631
840
 
@@ -662,6 +871,58 @@ function exportDocumentMarkdown({ databaseService, conn, documentName }) {
662
871
  console.log(`Characters: ${result.document.contentLength}`);
663
872
  console.log(`File: ${outputPath}`);
664
873
  }
874
+
875
+ function listManagedBackups({ databaseService, conn, options }) {
876
+ const backups = databaseService.listBackups(conn.id);
877
+
878
+ if (options.jsonOutput) {
879
+ console.log(JSON.stringify({ items: backups, total: backups.length }, null, 2));
880
+ return;
881
+ }
882
+
883
+ if (backups.length === 0) {
884
+ console.log(`No backups found for ${conn.label}.`);
885
+ return;
886
+ }
887
+
888
+ console.log(`\nBackups for ${conn.label} (${backups.length}):`);
889
+ console.log('─'.repeat(60));
890
+
891
+ backups.forEach((backup, index) => {
892
+ const fileState = backup.fileExists ? 'available' : 'missing';
893
+ console.log(`${index + 1}. ${backup.name}`);
894
+ console.log(` ID: ${backup.id}`);
895
+ console.log(` Status: ${backup.status} (${fileState})`);
896
+ console.log(` Size: ${formatSize(backup.sizeBytes)}`);
897
+ console.log(` Created: ${backup.createdAt}`);
898
+ console.log(` File: ${backup.path}`);
899
+ if (backup.notes) {
900
+ console.log(` Notes: ${backup.notes}`);
901
+ }
902
+ console.log('');
903
+ });
904
+ }
905
+
906
+ async function createManagedBackup({ databaseService, conn, options }) {
907
+ const backup = await databaseService.createBackup(conn.id, {
908
+ name: options.backupName,
909
+ notes: options.backupNotes,
910
+ context: 'cli',
911
+ });
912
+
913
+ if (options.jsonOutput) {
914
+ console.log(JSON.stringify(backup, null, 2));
915
+ return;
916
+ }
917
+
918
+ console.log(`Backup created: ${backup.name}`);
919
+ console.log(`Status: ${backup.status}`);
920
+ console.log(`Database: ${conn.label}`);
921
+ console.log(`Size: ${formatSize(backup.sizeBytes)}`);
922
+ console.log(`File: ${backup.path}`);
923
+ console.log(`ID: ${backup.id}`);
924
+ }
925
+
665
926
  function exportTableRowAsJson({ databaseService, conn, tableName, exportTarget }) {
666
927
  const result = databaseService.getTableRow(conn.id, tableName, exportTarget);
667
928
  const outputPath = path.resolve(process.cwd(), result.filename);
@@ -795,180 +1056,526 @@ async function startAndOpen(port) {
795
1056
 
796
1057
  function requireDatabaseName(options) {
797
1058
  if (!options.databaseName) {
798
- console.error('Error: this command requires --database:"name".');
799
- process.exit(1);
1059
+ throw new Error('this command requires --database:"name".');
800
1060
  }
801
1061
 
802
1062
  return options.databaseName;
803
1063
  }
804
1064
 
805
- async function main(argv = process.argv.slice(2), dependencies = {}) {
806
- const options = parseCliArguments(argv);
807
- const port = options.port ?? DEFAULT_PORT;
1065
+ function readCliFlags(argv = []) {
1066
+ return argv
1067
+ .filter(argument => String(argument ?? '').startsWith('-'))
1068
+ .map(argument => splitArgument(argument).flag)
1069
+ .filter(Boolean);
1070
+ }
1071
+
1072
+ function describeCliAccess(options, argv = []) {
1073
+ const metadata = {
1074
+ flags: readCliFlags(argv),
1075
+ };
1076
+ const entry = {
1077
+ source: 'cli',
1078
+ action: 'cli.open',
1079
+ targetType: 'app',
1080
+ targetName: 'server',
1081
+ metadata,
1082
+ };
1083
+
1084
+ if (!options) {
1085
+ return {
1086
+ ...entry,
1087
+ action: 'cli.parse',
1088
+ targetType: 'command',
1089
+ targetName: 'arguments',
1090
+ };
1091
+ }
1092
+
1093
+ if (options.exportFormat) metadata.exportFormat = options.exportFormat;
1094
+ if (options.typesTarget) metadata.typesTarget = options.typesTarget;
1095
+ if (options.outputPath) metadata.hasOutputPath = true;
1096
+ if (options.jsonOutput) metadata.jsonOutput = true;
1097
+ if (options.force) metadata.force = true;
808
1098
 
809
1099
  if (options.help) {
810
- printHelp();
811
- return;
1100
+ return {
1101
+ ...entry,
1102
+ action: 'cli.help',
1103
+ targetType: 'app',
1104
+ targetName: 'help',
1105
+ };
812
1106
  }
813
1107
 
814
1108
  if (options.version) {
815
- const { version } = require('../package.json');
816
- console.log(`SQLite Hub CLI version ${version}`);
817
- return;
1109
+ return {
1110
+ ...entry,
1111
+ action: 'cli.version',
1112
+ targetType: 'app',
1113
+ targetName: 'version',
1114
+ };
818
1115
  }
819
1116
 
820
1117
  if (options.info) {
821
- await printInfo(port, dependencies);
822
- return;
1118
+ return {
1119
+ ...entry,
1120
+ action: 'cli.info',
1121
+ targetType: 'app',
1122
+ targetName: 'info',
1123
+ };
823
1124
  }
824
1125
 
825
1126
  if (options.open) {
826
- await startAndOpen(port);
827
- return;
1127
+ return entry;
1128
+ }
1129
+
1130
+ if (options.documents) {
1131
+ if (options.documentExport) {
1132
+ return {
1133
+ ...entry,
1134
+ action: 'cli.document.export',
1135
+ targetType: 'document',
1136
+ targetName: options.documentName || 'document',
1137
+ };
1138
+ }
1139
+
1140
+ if (options.documentName) {
1141
+ return {
1142
+ ...entry,
1143
+ action: 'cli.document.get',
1144
+ targetType: 'document',
1145
+ targetName: options.documentName,
1146
+ };
1147
+ }
1148
+
1149
+ return {
1150
+ ...entry,
1151
+ action: 'cli.documents.list',
1152
+ targetType: 'database',
1153
+ targetName: options.databaseName,
1154
+ };
828
1155
  }
829
1156
 
830
- if (options.storeName && !options.rawQuery) {
831
- throw new Error('--store requires --query:"sql".');
1157
+ if (options.backups) {
1158
+ return {
1159
+ ...entry,
1160
+ action: 'cli.backups.list',
1161
+ targetType: 'database',
1162
+ targetName: options.databaseName,
1163
+ };
832
1164
  }
833
1165
 
834
- const databaseService =
835
- dependencies.databaseService ??
836
- new DatabaseCommandService({
837
- appStateStore: dependencies.appStateStore ?? createAppStateStore(),
838
- });
839
- const connections = databaseService.listDatabases();
1166
+ if (options.backup) {
1167
+ return {
1168
+ ...entry,
1169
+ action: 'cli.backup.create',
1170
+ targetType: 'database',
1171
+ targetName: options.databaseName,
1172
+ metadata: {
1173
+ ...metadata,
1174
+ hasBackupName: Boolean(options.backupName),
1175
+ hasBackupNotes: Boolean(options.backupNotes),
1176
+ },
1177
+ };
1178
+ }
1179
+
1180
+ if (options.tableName) {
1181
+ if (options.typesTarget) {
1182
+ return {
1183
+ ...entry,
1184
+ action: 'cli.table.types.generate',
1185
+ targetType: 'table',
1186
+ targetName: options.tableName,
1187
+ };
1188
+ }
1189
+
1190
+ if (options.exportTarget) {
1191
+ return {
1192
+ ...entry,
1193
+ action: 'cli.table.row.export',
1194
+ targetType: 'table',
1195
+ targetName: options.tableName,
1196
+ };
1197
+ }
1198
+
1199
+ return {
1200
+ ...entry,
1201
+ action: 'cli.table.get',
1202
+ targetType: 'table',
1203
+ targetName: options.tableName,
1204
+ };
1205
+ }
1206
+
1207
+ if (options.exportTarget) {
1208
+ return {
1209
+ ...entry,
1210
+ action: 'cli.query.export',
1211
+ targetType: 'query',
1212
+ targetName: options.exportTarget,
1213
+ };
1214
+ }
1215
+
1216
+ if (options.executeQuery) {
1217
+ return {
1218
+ ...entry,
1219
+ action: 'cli.query.execute.saved',
1220
+ targetType: 'query',
1221
+ targetName: options.executeQuery,
1222
+ };
1223
+ }
1224
+
1225
+ if (options.rawQuery) {
1226
+ return {
1227
+ ...entry,
1228
+ action: 'cli.query.execute',
1229
+ targetType: 'query',
1230
+ targetName: options.storeName || 'raw query',
1231
+ metadata: {
1232
+ ...metadata,
1233
+ hasStoreName: Boolean(options.storeName),
1234
+ },
1235
+ };
1236
+ }
1237
+
1238
+ if (options.showQuery) {
1239
+ return {
1240
+ ...entry,
1241
+ action: 'cli.query.get',
1242
+ targetType: 'query',
1243
+ targetName: options.showQuery,
1244
+ };
1245
+ }
1246
+
1247
+ if (options.showNotes) {
1248
+ return {
1249
+ ...entry,
1250
+ action: 'cli.query.notes.get',
1251
+ targetType: 'query',
1252
+ targetName: options.showNotes,
1253
+ };
1254
+ }
1255
+
1256
+ if (options.queries) {
1257
+ return {
1258
+ ...entry,
1259
+ action: 'cli.queries.list',
1260
+ targetType: 'database',
1261
+ targetName: options.databaseName,
1262
+ };
1263
+ }
1264
+
1265
+ if (options.tables) {
1266
+ return {
1267
+ ...entry,
1268
+ action: 'cli.tables.list',
1269
+ targetType: 'database',
1270
+ targetName: options.databaseName,
1271
+ };
1272
+ }
840
1273
 
841
- if (options.databaseList && !options.databaseName && !hasDatabaseOperation(options)) {
842
- printDatabaseList(connections);
1274
+ if (options.pathInfo) {
1275
+ return {
1276
+ ...entry,
1277
+ action: 'cli.database.path',
1278
+ targetType: 'database',
1279
+ targetName: options.databaseName,
1280
+ };
1281
+ }
1282
+
1283
+ if (options.sizeInfo) {
1284
+ return {
1285
+ ...entry,
1286
+ action: 'cli.database.size',
1287
+ targetType: 'database',
1288
+ targetName: options.databaseName,
1289
+ };
1290
+ }
1291
+
1292
+ if (options.lastOpenedInfo) {
1293
+ return {
1294
+ ...entry,
1295
+ action: 'cli.database.lastopened',
1296
+ targetType: 'database',
1297
+ targetName: options.databaseName,
1298
+ };
1299
+ }
1300
+
1301
+ if (options.databaseList && !options.databaseName) {
1302
+ return {
1303
+ ...entry,
1304
+ action: 'cli.databases.list',
1305
+ targetType: 'app',
1306
+ targetName: 'databases',
1307
+ };
1308
+ }
1309
+
1310
+ if (options.databaseName) {
1311
+ return {
1312
+ ...entry,
1313
+ action: 'cli.database.get',
1314
+ targetType: 'database',
1315
+ targetName: options.databaseName,
1316
+ };
1317
+ }
1318
+
1319
+ return entry;
1320
+ }
1321
+
1322
+ function recordCliAccess({ appStateStore, entry, startedAtMs, error }) {
1323
+ if (!appStateStore?.recordAccessLog || !entry) {
843
1324
  return;
844
1325
  }
845
1326
 
846
- if (options.databaseName || hasDatabaseOperation(options)) {
847
- const dbName = requireDatabaseName(options);
848
- const conn = databaseService.getDatabase(dbName);
1327
+ try {
1328
+ appStateStore.recordAccessLog({
1329
+ ...entry,
1330
+ status: error ? 'error' : 'success',
1331
+ startedAt: new Date(startedAtMs).toISOString(),
1332
+ durationMs: Date.now() - startedAtMs,
1333
+ errorMessage: error ? error.message : null,
1334
+ });
1335
+ } catch {
1336
+ // Access logging must not change CLI behavior or command output.
1337
+ }
1338
+ }
1339
+
1340
+ async function main(argv = process.argv.slice(2), dependencies = {}) {
1341
+ const startedAtMs = Date.now();
1342
+ let options = null;
1343
+ let accessEntry = describeCliAccess(null, argv);
1344
+ let accessLogStore = dependencies.appStateStore ?? null;
1345
+ let databaseService = dependencies.databaseService ?? null;
1346
+ let commandError = null;
1347
+
1348
+ function getAccessLogStore() {
1349
+ if (dependencies.disableAccessLog) {
1350
+ return null;
1351
+ }
849
1352
 
850
- if (options.documents) {
851
- if (options.documentName) {
852
- if (options.documentExport) {
853
- exportDocumentMarkdown({
1353
+ if (accessLogStore) {
1354
+ return accessLogStore;
1355
+ }
1356
+
1357
+ if (databaseService?.appStateStore) {
1358
+ accessLogStore = databaseService.appStateStore;
1359
+ return accessLogStore;
1360
+ }
1361
+
1362
+ if (!dependencies.databaseService) {
1363
+ accessLogStore = createAppStateStore();
1364
+ return accessLogStore;
1365
+ }
1366
+
1367
+ return null;
1368
+ }
1369
+
1370
+ try {
1371
+ options = parseCliArguments(argv);
1372
+ accessEntry = describeCliAccess(options, argv);
1373
+ const port = options.port ?? DEFAULT_PORT;
1374
+
1375
+ if (options.help) {
1376
+ printHelp();
1377
+ return;
1378
+ }
1379
+
1380
+ if (options.version) {
1381
+ const { version } = require('../package.json');
1382
+ console.log(`SQLite Hub CLI version ${version}`);
1383
+ return;
1384
+ }
1385
+
1386
+ if (options.info) {
1387
+ await printInfo(port, dependencies);
1388
+ return;
1389
+ }
1390
+
1391
+ if (options.open) {
1392
+ await startAndOpen(port);
1393
+ return;
1394
+ }
1395
+
1396
+ if (options.storeName && !options.rawQuery) {
1397
+ throw new Error('--store requires --query:"sql".');
1398
+ }
1399
+
1400
+ if (options.backup && options.backups) {
1401
+ throw new Error('--backup and --backups cannot be combined.');
1402
+ }
1403
+
1404
+ databaseService =
1405
+ databaseService ??
1406
+ new DatabaseCommandService({
1407
+ appStateStore: getAccessLogStore(),
1408
+ });
1409
+ const connections = databaseService.listDatabases();
1410
+
1411
+ if (options.databaseList && !options.databaseName && !hasDatabaseOperation(options)) {
1412
+ printDatabaseList(connections);
1413
+ return;
1414
+ }
1415
+
1416
+ if (options.databaseName || hasDatabaseOperation(options)) {
1417
+ const dbName = requireDatabaseName(options);
1418
+ const conn = databaseService.getDatabase(dbName);
1419
+ accessEntry.databaseKey = conn.id;
1420
+ accessEntry.metadata = {
1421
+ ...(accessEntry.metadata ?? {}),
1422
+ databaseLabel: conn.label ?? null,
1423
+ };
1424
+
1425
+ if (options.backups) {
1426
+ listManagedBackups({ databaseService, conn, options });
1427
+ return;
1428
+ }
1429
+
1430
+ if (options.backup) {
1431
+ await createManagedBackup({ databaseService, conn, options });
1432
+ return;
1433
+ }
1434
+
1435
+ if (options.documents) {
1436
+ if (options.documentName) {
1437
+ if (options.documentExport) {
1438
+ exportDocumentMarkdown({
1439
+ databaseService,
1440
+ conn,
1441
+ documentName: options.documentName,
1442
+ });
1443
+ } else {
1444
+ showDocumentMarkdown({
1445
+ databaseService,
1446
+ conn,
1447
+ documentName: options.documentName,
1448
+ });
1449
+ }
1450
+ return;
1451
+ }
1452
+
1453
+ listDocuments(databaseService, conn);
1454
+ return;
1455
+ }
1456
+
1457
+ if (
1458
+ options.tableName ||
1459
+ options.tables ||
1460
+ options.queries ||
1461
+ options.executeQuery ||
1462
+ options.rawQuery ||
1463
+ options.showQuery ||
1464
+ options.showNotes ||
1465
+ options.exportTarget ||
1466
+ options.typesTarget
1467
+ ) {
1468
+ if (options.tableName) {
1469
+ if (options.typesTarget) {
1470
+ generateTypes({
1471
+ databaseService,
1472
+ conn,
1473
+ tableName: options.tableName,
1474
+ options,
1475
+ });
1476
+ return;
1477
+ }
1478
+
1479
+ if (options.exportTarget) {
1480
+ exportTableRowAsJson({
1481
+ databaseService,
1482
+ conn,
1483
+ tableName: options.tableName,
1484
+ exportTarget: options.exportTarget,
1485
+ });
1486
+ } else {
1487
+ printTableInfo(databaseService.getTable(conn.id, options.tableName));
1488
+ }
1489
+
1490
+ return;
1491
+ }
1492
+
1493
+ if (options.exportTarget) {
1494
+ exportSavedQuery({
854
1495
  databaseService,
855
1496
  conn,
856
- documentName: options.documentName,
1497
+ queryName: options.exportTarget,
1498
+ format: options.exportFormat,
857
1499
  });
858
- } else {
859
- showDocumentMarkdown({
1500
+ return;
1501
+ }
1502
+
1503
+ if (options.executeQuery) {
1504
+ executeSavedQuery({
860
1505
  databaseService,
861
1506
  conn,
862
- documentName: options.documentName,
1507
+ queryName: options.executeQuery,
863
1508
  });
1509
+ return;
864
1510
  }
865
- return;
866
- }
867
-
868
- listDocuments(databaseService, conn);
869
- return;
870
- }
871
1511
 
872
- if (
873
- options.tableName ||
874
- options.tables ||
875
- options.queries ||
876
- options.executeQuery ||
877
- options.rawQuery ||
878
- options.showQuery ||
879
- options.showNotes ||
880
- options.exportTarget
881
- ) {
882
- if (options.tableName) {
883
- if (options.exportTarget) {
884
- exportTableRowAsJson({
1512
+ if (options.rawQuery) {
1513
+ executeRawQuery({
885
1514
  databaseService,
886
1515
  conn,
887
- tableName: options.tableName,
888
- exportTarget: options.exportTarget,
1516
+ sql: options.rawQuery,
1517
+ storeName: options.storeName,
889
1518
  });
890
- } else {
891
- printTableInfo(databaseService.getTable(conn.id, options.tableName));
1519
+ return;
892
1520
  }
893
1521
 
894
- return;
895
- }
896
-
897
- if (options.exportTarget) {
898
- exportSavedQuery({
899
- databaseService,
900
- conn,
901
- queryName: options.exportTarget,
902
- format: options.exportFormat,
903
- });
904
- return;
905
- }
1522
+ if (options.showQuery) {
1523
+ showSavedQuery({ databaseService, conn, queryName: options.showQuery });
1524
+ return;
1525
+ }
906
1526
 
907
- if (options.executeQuery) {
908
- executeSavedQuery({
909
- databaseService,
910
- conn,
911
- queryName: options.executeQuery,
912
- });
913
- return;
914
- }
1527
+ if (options.showNotes) {
1528
+ showSavedQueryNotes({ databaseService, conn, queryName: options.showNotes });
1529
+ return;
1530
+ }
915
1531
 
916
- if (options.rawQuery) {
917
- executeRawQuery({
918
- databaseService,
919
- conn,
920
- sql: options.rawQuery,
921
- storeName: options.storeName,
922
- });
923
- return;
924
- }
1532
+ if (options.queries) {
1533
+ listSavedQueries(databaseService, conn);
1534
+ return;
1535
+ }
925
1536
 
926
- if (options.showQuery) {
927
- showSavedQuery({ databaseService, conn, queryName: options.showQuery });
928
- return;
1537
+ if (options.tables) {
1538
+ printTables(conn, databaseService.listTables(conn.id));
1539
+ return;
1540
+ }
929
1541
  }
930
1542
 
931
- if (options.showNotes) {
932
- showSavedQueryNotes({ databaseService, conn, queryName: options.showNotes });
1543
+ if (options.pathInfo) {
1544
+ console.log(conn.path);
933
1545
  return;
934
1546
  }
935
1547
 
936
- if (options.queries) {
937
- listSavedQueries(databaseService, conn);
1548
+ if (options.sizeInfo) {
1549
+ console.log(formatSize(conn.sizeBytes));
938
1550
  return;
939
1551
  }
940
1552
 
941
- if (options.tables) {
942
- printTables(conn, databaseService.listTables(conn.id));
1553
+ if (options.lastOpenedInfo) {
1554
+ console.log(conn.lastOpenedAt);
943
1555
  return;
944
1556
  }
945
- }
946
-
947
- if (options.pathInfo) {
948
- console.log(conn.path);
949
- return;
950
- }
951
1557
 
952
- if (options.sizeInfo) {
953
- console.log(formatSize(conn.sizeBytes));
1558
+ printSingleDatabaseInfo(conn);
954
1559
  return;
955
1560
  }
956
1561
 
957
- if (options.lastOpenedInfo) {
958
- console.log(conn.lastOpenedAt);
1562
+ if (options.databaseList) {
1563
+ printDatabaseList(connections);
959
1564
  return;
960
1565
  }
961
1566
 
962
- printSingleDatabaseInfo(conn);
963
- return;
964
- }
965
-
966
- if (options.databaseList) {
967
- printDatabaseList(connections);
968
- return;
1567
+ await startAndOpen(port);
1568
+ } catch (error) {
1569
+ commandError = error;
1570
+ throw error;
1571
+ } finally {
1572
+ recordCliAccess({
1573
+ appStateStore: getAccessLogStore(),
1574
+ entry: accessEntry,
1575
+ startedAtMs,
1576
+ error: commandError,
1577
+ });
969
1578
  }
970
-
971
- await startAndOpen(port);
972
1579
  }
973
1580
 
974
1581
  if (require.main === module) {