dbn-cli 0.2.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.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # dbn
2
+
3
+ A lightweight terminal SQLite browser with an ncdu-style interface.
4
+
5
+ ## Features
6
+
7
+ - **Minimal dependencies** - Uses Node.js 24+ built-in `node:sqlite` module
8
+ - **Full-screen TUI** - ncdu-inspired keyboard navigation
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g dbn-cli
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ dbn <path-to-sqlite-db-file>
20
+ ```
21
+
22
+ ## Keyboard Shortcuts
23
+
24
+ - `j/k` or `↓/↑` - Navigate
25
+ - `Enter` or `l` - Open/view details
26
+ - `h` or `Esc` - Go back
27
+ - `s` - Toggle schema view
28
+ - `r` - Reload data
29
+ - `g/G` - Jump to top/bottom
30
+ - `q` - Quit
31
+
32
+ ## Development
33
+
34
+ ```bash
35
+ npm run dev path/to/database.db # Run in dev mode
36
+ npm test # Run tests
37
+ ```
38
+
39
+ ## License
40
+
41
+ MIT
package/bin/dbn.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
2
+
3
+ import { main } from '../src/index.ts';
4
+
5
+ // Get command line arguments (skip node and script path)
6
+ const args = process.argv.slice(2);
7
+
8
+ // Run the application
9
+ main(args);
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "dbn-cli",
3
+ "version": "0.2.0",
4
+ "description": "A lightweight terminal-based database browser",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "bin": {
8
+ "dbn": "./bin/dbn.ts"
9
+ },
10
+ "scripts": {
11
+ "test": "node --test --experimental-strip-types test/*.test.ts",
12
+ "test:watch": "node --test --watch --experimental-strip-types test/*.test.ts",
13
+ "test:coverage": "node --test --experimental-test-coverage --experimental-strip-types test/*.test.ts",
14
+ "dev": "node bin/dbp.ts"
15
+ },
16
+ "keywords": [
17
+ "database",
18
+ "sqlite",
19
+ "cli",
20
+ "terminal",
21
+ "browser",
22
+ "tui"
23
+ ],
24
+ "author": "Amio <amio.cn@gmail.com>",
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=24"
28
+ },
29
+ "files": [
30
+ "bin/",
31
+ "src/"
32
+ ],
33
+ "devDependencies": {
34
+ "@types/node": "^24.10.9"
35
+ }
36
+ ,
37
+ "dependencies": {
38
+ "string-width": "^5.0.1"
39
+ }
40
+ }
@@ -0,0 +1,46 @@
1
+ import type { TableInfo, ColumnSchema, QueryOptions } from '../types.ts';
2
+
3
+ /**
4
+ * Base class for database adapters
5
+ * Defines the interface that all database adapters must implement
6
+ */
7
+ export abstract class DatabaseAdapter {
8
+ /**
9
+ * Connect to the database
10
+ * @param path - Path to the database file or connection string
11
+ */
12
+ abstract connect(path: string): void;
13
+
14
+ /**
15
+ * Get all tables in the database
16
+ * @returns Array of table information
17
+ */
18
+ abstract getTables(): TableInfo[];
19
+
20
+ /**
21
+ * Get schema information for a specific table
22
+ * @param tableName - Name of the table
23
+ * @returns Array of column schema information
24
+ */
25
+ abstract getTableSchema(tableName: string): ColumnSchema[];
26
+
27
+ /**
28
+ * Get data from a specific table with pagination
29
+ * @param tableName - Name of the table
30
+ * @param options - Query options
31
+ * @returns Array of row objects
32
+ */
33
+ abstract getTableData(tableName: string, options?: QueryOptions): Record<string, any>[];
34
+
35
+ /**
36
+ * Get the total row count for a specific table
37
+ * @param tableName - Name of the table
38
+ * @returns Total number of rows
39
+ */
40
+ abstract getRowCount(tableName: string): number;
41
+
42
+ /**
43
+ * Close the database connection
44
+ */
45
+ abstract close(): void;
46
+ }
@@ -0,0 +1,110 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { DatabaseAdapter } from './base.ts';
3
+ import type { TableInfo, ColumnSchema, QueryOptions } from '../types.ts';
4
+
5
+ /**
6
+ * SQLite adapter using Node.js 22+ built-in node:sqlite
7
+ */
8
+ export class SQLiteAdapter extends DatabaseAdapter {
9
+ private db: DatabaseSync | null = null;
10
+ private path: string | null = null;
11
+
12
+ /**
13
+ * Connect to SQLite database file
14
+ * @param path - Path to the .db or .sqlite file
15
+ */
16
+ connect(path: string): void {
17
+ try {
18
+ this.db = new DatabaseSync(path);
19
+ this.path = path;
20
+ // Enable WAL mode for better performance
21
+ this.db.exec('PRAGMA journal_mode = WAL');
22
+ } catch (error) {
23
+ throw new Error(`Failed to open database: ${(error as Error).message}`);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Get all tables in the database
29
+ * @returns Array of table information
30
+ */
31
+ getTables(): TableInfo[] {
32
+ if (!this.db) {
33
+ throw new Error('Database not connected');
34
+ }
35
+
36
+ const query = `
37
+ SELECT name
38
+ FROM sqlite_master
39
+ WHERE type='table' AND name NOT LIKE 'sqlite_%'
40
+ ORDER BY name
41
+ `;
42
+
43
+ const stmt = this.db.prepare(query);
44
+ const tables = stmt.all() as { name: string }[];
45
+
46
+ // Get row count for each table
47
+ return tables.map(table => {
48
+ const countStmt = this.db!.prepare(`SELECT COUNT(*) as count FROM "${table.name}"`);
49
+ const result = countStmt.get() as { count: number };
50
+ return {
51
+ name: table.name,
52
+ row_count: result.count
53
+ };
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Get schema information for a specific table
59
+ * @param tableName - Name of the table
60
+ * @returns Array of column schema information
61
+ */
62
+ getTableSchema(tableName: string): ColumnSchema[] {
63
+ if (!this.db) {
64
+ throw new Error('Database not connected');
65
+ }
66
+
67
+ const stmt = this.db.prepare(`PRAGMA table_info("${tableName}")`);
68
+ return stmt.all() as unknown as ColumnSchema[];
69
+ }
70
+
71
+ /**
72
+ * Get data from a specific table with pagination
73
+ * @param tableName - Name of the table
74
+ * @param options - Query options
75
+ * @returns Array of row objects
76
+ */
77
+ getTableData(tableName: string, { limit = 50, offset = 0 }: QueryOptions = {}): Record<string, any>[] {
78
+ if (!this.db) {
79
+ throw new Error('Database not connected');
80
+ }
81
+
82
+ const stmt = this.db.prepare(`SELECT * FROM "${tableName}" LIMIT ? OFFSET ?`);
83
+ return stmt.all(limit, offset) as Record<string, any>[];
84
+ }
85
+
86
+ /**
87
+ * Get the total row count for a specific table
88
+ * @param tableName - Name of the table
89
+ * @returns Total number of rows
90
+ */
91
+ getRowCount(tableName: string): number {
92
+ if (!this.db) {
93
+ throw new Error('Database not connected');
94
+ }
95
+
96
+ const stmt = this.db.prepare(`SELECT COUNT(*) as count FROM "${tableName}"`);
97
+ const result = stmt.get() as { count: number };
98
+ return result.count;
99
+ }
100
+
101
+ /**
102
+ * Close the database connection
103
+ */
104
+ close(): void {
105
+ if (this.db) {
106
+ this.db.close();
107
+ this.db = null;
108
+ }
109
+ }
110
+ }
package/src/index.ts ADDED
@@ -0,0 +1,250 @@
1
+ import { stdin, stdout, exit } from 'node:process';
2
+ import { existsSync } from 'node:fs';
3
+ import * as readline from 'node:readline';
4
+ import { SQLiteAdapter } from './adapter/sqlite.ts';
5
+ import { Screen } from './ui/screen.ts';
6
+ import { Renderer } from './ui/renderer.ts';
7
+ import { Navigator } from './ui/navigator.ts';
8
+ import type { KeyPress } from './types.ts';
9
+
10
+ /**
11
+ * Main application class
12
+ */
13
+ export class DBPeek {
14
+ private dbPath: string;
15
+ private adapter: SQLiteAdapter | null = null;
16
+ private screen: Screen | null = null;
17
+ private renderer: Renderer | null = null;
18
+ private navigator: Navigator | null = null;
19
+ private isRunning: boolean = false;
20
+
21
+ constructor(dbPath: string) {
22
+ this.dbPath = dbPath;
23
+ }
24
+
25
+ /**
26
+ * Initialize the application
27
+ */
28
+ init(): void {
29
+ // Validate database file
30
+ if (!existsSync(this.dbPath)) {
31
+ console.error(`Error: Database file not found: ${this.dbPath}`);
32
+ exit(1);
33
+ }
34
+
35
+ // Create adapter and connect
36
+ try {
37
+ this.adapter = new SQLiteAdapter();
38
+ this.adapter.connect(this.dbPath);
39
+ } catch (error) {
40
+ console.error(`Error: ${(error as Error).message}`);
41
+ exit(1);
42
+ }
43
+
44
+ // Initialize UI components
45
+ this.screen = new Screen();
46
+ this.renderer = new Renderer(this.screen);
47
+ this.navigator = new Navigator(this.adapter);
48
+
49
+ // Initialize navigator with tables
50
+ try {
51
+ this.navigator.init();
52
+ } catch (error) {
53
+ console.error(`Error loading tables: ${(error as Error).message}`);
54
+ this.cleanup();
55
+ exit(1);
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Start the application
61
+ */
62
+ start(): void {
63
+ if (!this.screen || !this.navigator) {
64
+ throw new Error('Application not initialized');
65
+ }
66
+
67
+ this.isRunning = true;
68
+
69
+ // Enter full screen mode
70
+ this.screen.enter();
71
+
72
+ // Set up keyboard input
73
+ this.setupInput();
74
+
75
+ // Handle screen resize
76
+ this.screen.on('resize', () => {
77
+ this.render();
78
+ });
79
+
80
+ // Initial render
81
+ this.render();
82
+ }
83
+
84
+ /**
85
+ * Set up keyboard input handling
86
+ */
87
+ private setupInput(): void {
88
+ // Enable raw mode for key-by-key input
89
+ stdin.setRawMode(true);
90
+ stdin.resume();
91
+ stdin.setEncoding('utf8');
92
+
93
+ // Enable keypress events
94
+ readline.emitKeypressEvents(stdin);
95
+
96
+ stdin.on('keypress', (str: string, key: KeyPress) => {
97
+ if (!this.isRunning) return;
98
+
99
+ this.handleKeypress(str, key);
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Handle keypress events
105
+ */
106
+ private handleKeypress(str: string, key: KeyPress): void {
107
+ if (!this.navigator) return;
108
+
109
+ // Handle Ctrl+C
110
+ if (key.ctrl && key.name === 'c') {
111
+ this.quit();
112
+ return;
113
+ }
114
+
115
+ // Handle different keys
116
+ switch (key.name) {
117
+ case 'q':
118
+ this.quit();
119
+ break;
120
+
121
+ case 'j':
122
+ case 'down':
123
+ this.navigator.moveDown();
124
+ this.render();
125
+ break;
126
+
127
+ case 'k':
128
+ case 'up':
129
+ this.navigator.moveUp();
130
+ this.render();
131
+ break;
132
+
133
+ case 'return':
134
+ case 'l':
135
+ case 'right':
136
+ this.navigator.enter();
137
+ this.render();
138
+ break;
139
+
140
+ case 'h':
141
+ case 'left':
142
+ case 'escape':
143
+ this.navigator.back();
144
+ this.render();
145
+ break;
146
+
147
+ case 'g':
148
+ if (key.shift) {
149
+ // G - jump to bottom
150
+ this.navigator.jumpToBottom();
151
+ } else {
152
+ // g - jump to top
153
+ this.navigator.jumpToTop();
154
+ }
155
+ this.render();
156
+ break;
157
+
158
+ case 'r':
159
+ // Reload current view
160
+ this.navigator.reload();
161
+ this.render();
162
+ break;
163
+
164
+ case 's':
165
+ // Toggle schema view in full screen mode
166
+ const currentState = this.navigator.getState();
167
+ if (currentState.type === 'table-detail') {
168
+ this.navigator.viewSchema();
169
+ this.render();
170
+ } else if (currentState.type === 'schema-view') {
171
+ this.navigator.back();
172
+ this.render();
173
+ }
174
+ break;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Render the current state
180
+ */
181
+ private render(): void {
182
+ if (!this.renderer || !this.navigator) return;
183
+
184
+ try {
185
+ const state = this.navigator.getState();
186
+ this.renderer.render(state, this.dbPath);
187
+ } catch (error) {
188
+ // If rendering fails, try to show error and quit gracefully
189
+ this.cleanup();
190
+ console.error(`Render error: ${(error as Error).message}`);
191
+ exit(1);
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Quit the application
197
+ */
198
+ quit(): void {
199
+ this.isRunning = false;
200
+ this.cleanup();
201
+ exit(0);
202
+ }
203
+
204
+ /**
205
+ * Clean up resources
206
+ */
207
+ cleanup(): void {
208
+ // Exit full screen mode
209
+ if (this.screen) {
210
+ this.screen.exit();
211
+ }
212
+
213
+ // Close database connection
214
+ if (this.adapter) {
215
+ this.adapter.close();
216
+ }
217
+
218
+ // Restore terminal
219
+ if (stdin.isTTY) {
220
+ stdin.setRawMode(false);
221
+ stdin.pause();
222
+ }
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Main entry point
228
+ */
229
+ export function main(args: string[]): void {
230
+ const dbPath = args[0];
231
+
232
+ if (!dbPath) {
233
+ console.error('Usage: dbn <path-to-sqlite-db-file>');
234
+ console.error('');
235
+ console.error('Example:');
236
+ console.error(' dbn ./mydatabase.db');
237
+ exit(1);
238
+ }
239
+
240
+ const app = new DBPeek(dbPath);
241
+
242
+ // Handle process termination
243
+ process.on('SIGINT', () => app.quit());
244
+ process.on('SIGTERM', () => app.quit());
245
+ process.on('exit', () => app.cleanup());
246
+
247
+ // Initialize and start
248
+ app.init();
249
+ app.start();
250
+ }
package/src/types.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Type definitions for DBPeek
3
+ */
4
+
5
+ /**
6
+ * Table metadata
7
+ */
8
+ export interface TableInfo {
9
+ name: string;
10
+ row_count: number;
11
+ }
12
+
13
+ /**
14
+ * Column schema information
15
+ */
16
+ export interface ColumnSchema {
17
+ cid: number;
18
+ name: string;
19
+ type: string;
20
+ notnull: number;
21
+ dflt_value: any;
22
+ pk: number;
23
+ }
24
+
25
+ /**
26
+ * Query options for fetching table data
27
+ */
28
+ export interface QueryOptions {
29
+ limit?: number;
30
+ offset?: number;
31
+ }
32
+
33
+ /**
34
+ * Base view state properties
35
+ */
36
+ interface BaseViewState {
37
+ type: string;
38
+ }
39
+
40
+ /**
41
+ * Tables list view state
42
+ */
43
+ export interface TablesViewState extends BaseViewState {
44
+ type: 'tables';
45
+ tables: TableInfo[];
46
+ cursor: number;
47
+ scrollOffset: number;
48
+ }
49
+
50
+ /**
51
+ * Table detail view state
52
+ */
53
+ export interface TableDetailViewState extends BaseViewState {
54
+ type: 'table-detail';
55
+ tableName: string;
56
+ schema: ColumnSchema[];
57
+ data: Record<string, any>[];
58
+ totalRows: number;
59
+ dataOffset: number;
60
+ dataCursor: number;
61
+ visibleRows: number;
62
+ showSchema?: boolean;
63
+ }
64
+
65
+ /**
66
+ * Schema view state (full screen schema display)
67
+ */
68
+ export interface SchemaViewState extends BaseViewState {
69
+ type: 'schema-view';
70
+ tableName: string;
71
+ schema: ColumnSchema[];
72
+ cursor: number;
73
+ scrollOffset: number;
74
+ }
75
+
76
+ /**
77
+ * Row detail view state
78
+ */
79
+ export interface RowDetailViewState extends BaseViewState {
80
+ type: 'row-detail';
81
+ tableName: string;
82
+ row: Record<string, any>;
83
+ rowIndex: number;
84
+ schema: ColumnSchema[];
85
+ }
86
+
87
+ /**
88
+ * Union type for all possible view states
89
+ */
90
+ export type ViewState = TablesViewState | TableDetailViewState | SchemaViewState | RowDetailViewState;
91
+
92
+ /**
93
+ * Screen dimensions
94
+ */
95
+ export interface ScreenDimensions {
96
+ width: number;
97
+ height: number;
98
+ }
99
+
100
+ /**
101
+ * Keypress event from readline
102
+ */
103
+ export interface KeyPress {
104
+ name: string;
105
+ ctrl: boolean;
106
+ shift: boolean;
107
+ meta: boolean;
108
+ sequence: string;
109
+ }