@semics-tech/mongolite 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +14 -1
  2. package/dist/bin/mongolite-debug.d.ts +6 -1
  3. package/dist/bin/mongolite-debug.js +6 -113
  4. package/dist/bin/mongolite-debug.js.map +1 -1
  5. package/dist/bin/mongolite.d.ts +13 -0
  6. package/dist/bin/mongolite.js +172 -0
  7. package/dist/bin/mongolite.js.map +1 -0
  8. package/dist/cjs/bin/mongolite-debug.js +6 -149
  9. package/dist/cjs/bin/mongolite.js +207 -0
  10. package/dist/cjs/cli/discover.js +156 -0
  11. package/dist/cjs/cli/explorer.js +929 -0
  12. package/dist/cjs/cli/filter-builder.js +276 -0
  13. package/dist/cjs/cli/format.js +161 -0
  14. package/dist/cjs/cli/schema.js +211 -0
  15. package/dist/cjs/cli/ui.js +431 -0
  16. package/dist/cjs/cursors/findCursor.js +20 -8
  17. package/dist/cli/discover.d.ts +40 -0
  18. package/dist/cli/discover.js +146 -0
  19. package/dist/cli/discover.js.map +1 -0
  20. package/dist/cli/explorer.d.ts +59 -0
  21. package/dist/cli/explorer.js +923 -0
  22. package/dist/cli/explorer.js.map +1 -0
  23. package/dist/cli/filter-builder.d.ts +52 -0
  24. package/dist/cli/filter-builder.js +268 -0
  25. package/dist/cli/filter-builder.js.map +1 -0
  26. package/dist/cli/format.d.ts +29 -0
  27. package/dist/cli/format.js +153 -0
  28. package/dist/cli/format.js.map +1 -0
  29. package/dist/cli/schema.d.ts +58 -0
  30. package/dist/cli/schema.js +205 -0
  31. package/dist/cli/schema.js.map +1 -0
  32. package/dist/cli/ui.d.ts +71 -0
  33. package/dist/cli/ui.js +379 -0
  34. package/dist/cli/ui.js.map +1 -0
  35. package/dist/cursors/findCursor.d.ts +12 -0
  36. package/dist/cursors/findCursor.js +20 -8
  37. package/dist/cursors/findCursor.js.map +1 -1
  38. package/package.json +2 -1
@@ -0,0 +1,431 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PromptCancelledError = exports.style = exports.isInteractive = void 0;
37
+ exports.displayWidth = displayWidth;
38
+ exports.truncate = truncate;
39
+ exports.terminalWidth = terminalWidth;
40
+ exports.select = select;
41
+ exports.closePrompts = closePrompts;
42
+ exports.question = question;
43
+ exports.text = text;
44
+ exports.multilineText = multilineText;
45
+ exports.confirm = confirm;
46
+ exports.pause = pause;
47
+ exports.heading = heading;
48
+ exports.info = info;
49
+ exports.warn = warn;
50
+ exports.error = error;
51
+ exports.success = success;
52
+ /**
53
+ * Small, dependency-free terminal UI helpers used by the guided MongoLite CLI.
54
+ *
55
+ * Everything degrades gracefully:
56
+ * - When stdin/stdout is a TTY the prompts are interactive (arrow keys, filtering).
57
+ * - When it is not (pipes, CI, dumb terminals) the same prompts fall back to a
58
+ * numbered list read line-by-line, so the CLI stays scriptable.
59
+ */
60
+ const readline = __importStar(require("node:readline"));
61
+ const NO_COLOR = process.env.NO_COLOR !== undefined || process.env.TERM === 'dumb';
62
+ const isInteractive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.MONGOLITE_NON_INTERACTIVE);
63
+ exports.isInteractive = isInteractive;
64
+ const wrap = (open, close) => (text) => NO_COLOR || !process.stdout.isTTY ? text : `\u001b[${open}m${text}\u001b[${close}m`;
65
+ exports.style = {
66
+ bold: wrap('1', '22'),
67
+ dim: wrap('2', '22'),
68
+ italic: wrap('3', '23'),
69
+ underline: wrap('4', '24'),
70
+ red: wrap('31', '39'),
71
+ green: wrap('32', '39'),
72
+ yellow: wrap('33', '39'),
73
+ blue: wrap('34', '39'),
74
+ magenta: wrap('35', '39'),
75
+ cyan: wrap('36', '39'),
76
+ grey: wrap('90', '39'),
77
+ };
78
+ /** Visible width of a string, ignoring ANSI escape sequences. */
79
+ function displayWidth(text) {
80
+ return text.replace(/\u001b\[[0-9;]*m/g, '').length;
81
+ }
82
+ /** Truncate to `max` visible characters, appending an ellipsis when cut. */
83
+ function truncate(text, max) {
84
+ if (max <= 0)
85
+ return '';
86
+ if (text.length <= max)
87
+ return text;
88
+ if (max <= 1)
89
+ return text.slice(0, max);
90
+ return `${text.slice(0, max - 1)}…`;
91
+ }
92
+ function terminalWidth() {
93
+ return Math.max(40, process.stdout.columns || 100);
94
+ }
95
+ /** Thrown when the user aborts a prompt with Esc or Ctrl+C. */
96
+ class PromptCancelledError extends Error {
97
+ constructor() {
98
+ super('Prompt cancelled');
99
+ this.name = 'PromptCancelledError';
100
+ }
101
+ }
102
+ exports.PromptCancelledError = PromptCancelledError;
103
+ function selectableIndexes(choices) {
104
+ return choices.map((c, i) => (c.separator ? -1 : i)).filter((i) => i >= 0);
105
+ }
106
+ /**
107
+ * Ask the user to pick one of `choices`.
108
+ * Interactive terminals get an arrow-key menu with type-to-filter; everything
109
+ * else gets a numbered list.
110
+ */
111
+ async function select(options) {
112
+ if (options.choices.length === 0) {
113
+ throw new Error(`Nothing to choose from for: ${options.message}`);
114
+ }
115
+ const selectable = selectableIndexes(options.choices);
116
+ if (selectable.length === 1 && options.choices.length === 1) {
117
+ // Nothing to decide - tell the user what was picked for them and move on.
118
+ const only = options.choices[selectable[0]];
119
+ process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(options.message)} ${exports.style.cyan(only.label)} ${exports.style.grey('(only option)')}\n`);
120
+ return only.value;
121
+ }
122
+ return (0, exports.isInteractive)() ? interactiveSelect(options) : fallbackSelect(options);
123
+ }
124
+ async function interactiveSelect(options) {
125
+ const { message, choices } = options;
126
+ const pageSize = Math.min(options.pageSize ?? 12, Math.max(3, (process.stdout.rows || 24) - 6));
127
+ let filter = '';
128
+ let cursor = 0;
129
+ let renderedLines = 0;
130
+ const visible = () => {
131
+ const lowered = filter.toLowerCase();
132
+ return choices
133
+ .map((choice, index) => ({ choice, index }))
134
+ .filter(({ choice }) => {
135
+ if (!filter)
136
+ return true;
137
+ if (choice.separator)
138
+ return false;
139
+ return `${choice.label} ${choice.hint ?? ''}`.toLowerCase().includes(lowered);
140
+ });
141
+ };
142
+ const selectablePositions = (list) => list.map((entry, pos) => (entry.choice.separator ? -1 : pos)).filter((pos) => pos >= 0);
143
+ const initial = options.initialIndex ?? 0;
144
+ {
145
+ const list = visible();
146
+ const positions = selectablePositions(list);
147
+ const wanted = list.findIndex((entry) => entry.index === initial);
148
+ cursor = positions.includes(wanted) ? wanted : (positions[0] ?? 0);
149
+ }
150
+ const clear = () => {
151
+ if (renderedLines > 0) {
152
+ process.stdout.write(`\u001b[${renderedLines}A`);
153
+ process.stdout.write('\u001b[0J');
154
+ }
155
+ renderedLines = 0;
156
+ };
157
+ const render = () => {
158
+ clear();
159
+ const width = terminalWidth();
160
+ const list = visible();
161
+ const positions = selectablePositions(list);
162
+ if (positions.length && !positions.includes(cursor)) {
163
+ cursor = positions[0];
164
+ }
165
+ const lines = [];
166
+ const filterSuffix = filter ? ` ${exports.style.yellow(`/${filter}`)}` : '';
167
+ lines.push(`${exports.style.green('?')} ${exports.style.bold(message)}${filterSuffix}`);
168
+ lines.push(exports.style.grey(options.hint ?? ' ↑/↓ move · type to filter · enter to select · esc to go back'));
169
+ // Scroll window so the cursor stays visible.
170
+ let start = 0;
171
+ if (list.length > pageSize) {
172
+ start = Math.min(Math.max(0, cursor - Math.floor(pageSize / 2)), list.length - pageSize);
173
+ }
174
+ const window = list.slice(start, start + pageSize);
175
+ if (window.length === 0) {
176
+ lines.push(exports.style.grey(' no matches'));
177
+ }
178
+ window.forEach((entry, offset) => {
179
+ const pos = start + offset;
180
+ const { choice } = entry;
181
+ if (choice.separator) {
182
+ lines.push(exports.style.grey(` ${choice.label}`));
183
+ return;
184
+ }
185
+ const active = pos === cursor;
186
+ const pointer = active ? exports.style.cyan('❯') : ' ';
187
+ const label = active ? exports.style.cyan(choice.label) : choice.label;
188
+ const hint = choice.hint ? ` ${exports.style.grey(choice.hint)}` : '';
189
+ const line = `${pointer} ${label}${hint}`;
190
+ lines.push(displayWidth(line) > width ? truncate(line, width) : line);
191
+ });
192
+ if (list.length > pageSize) {
193
+ lines.push(exports.style.grey(` … ${start + window.length}/${list.length}`));
194
+ }
195
+ process.stdout.write(`${lines.join('\n')}\n`);
196
+ renderedLines = lines.length;
197
+ };
198
+ return new Promise((resolve, reject) => {
199
+ readline.emitKeypressEvents(process.stdin);
200
+ const wasRaw = process.stdin.isRaw;
201
+ process.stdin.setRawMode(true);
202
+ process.stdin.resume();
203
+ const finish = (fn) => {
204
+ process.stdin.removeListener('keypress', onKeypress);
205
+ process.stdin.setRawMode(Boolean(wasRaw));
206
+ process.stdin.pause();
207
+ fn();
208
+ };
209
+ const onKeypress = (str, key) => {
210
+ const list = visible();
211
+ const positions = selectablePositions(list);
212
+ if (key.ctrl && key.name === 'c') {
213
+ clear();
214
+ finish(() => {
215
+ process.stdout.write(exports.style.grey('Cancelled.\n'));
216
+ process.exit(0);
217
+ });
218
+ return;
219
+ }
220
+ switch (key.name) {
221
+ case 'escape':
222
+ clear();
223
+ finish(() => reject(new PromptCancelledError()));
224
+ return;
225
+ case 'up': {
226
+ const at = positions.indexOf(cursor);
227
+ cursor = positions[(at - 1 + positions.length) % positions.length] ?? cursor;
228
+ render();
229
+ return;
230
+ }
231
+ case 'down': {
232
+ const at = positions.indexOf(cursor);
233
+ cursor = positions[(at + 1) % positions.length] ?? cursor;
234
+ render();
235
+ return;
236
+ }
237
+ case 'pageup':
238
+ cursor = positions[0] ?? cursor;
239
+ render();
240
+ return;
241
+ case 'pagedown':
242
+ cursor = positions[positions.length - 1] ?? cursor;
243
+ render();
244
+ return;
245
+ case 'return':
246
+ case 'enter': {
247
+ const chosen = list[cursor];
248
+ if (!chosen || chosen.choice.separator)
249
+ return;
250
+ clear();
251
+ process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(message)} ${exports.style.cyan(chosen.choice.label)}\n`);
252
+ finish(() => resolve(chosen.choice.value));
253
+ return;
254
+ }
255
+ case 'backspace':
256
+ filter = filter.slice(0, -1);
257
+ render();
258
+ return;
259
+ default:
260
+ break;
261
+ }
262
+ if (str && !key.ctrl && !key.meta && str >= ' ' && str <= '~') {
263
+ filter += str;
264
+ render();
265
+ }
266
+ };
267
+ process.stdin.on('keypress', onKeypress);
268
+ render();
269
+ });
270
+ }
271
+ async function fallbackSelect(options) {
272
+ const { message, choices } = options;
273
+ const numbered = choices
274
+ .map((choice, index) => ({ choice, index }))
275
+ .filter((e) => !e.choice.separator);
276
+ process.stdout.write(`\n${exports.style.bold(message)}\n`);
277
+ numbered.forEach((entry, position) => {
278
+ const hint = entry.choice.hint ? ` ${exports.style.grey(entry.choice.hint)}` : '';
279
+ process.stdout.write(` ${position + 1}) ${entry.choice.label}${hint}\n`);
280
+ });
281
+ for (;;) {
282
+ const answer = (await question(`Choose 1-${numbered.length}: `)).trim();
283
+ if (answer === '')
284
+ continue;
285
+ const position = Number.parseInt(answer, 10);
286
+ if (Number.isInteger(position) && position >= 1 && position <= numbered.length) {
287
+ return numbered[position - 1].choice.value;
288
+ }
289
+ // Also accept an exact (case-insensitive) label so scripted input stays readable.
290
+ const byLabel = numbered.find((entry) => entry.choice.label.toLowerCase() === answer.toLowerCase());
291
+ if (byLabel)
292
+ return byLabel.choice.value;
293
+ process.stdout.write(exports.style.red(' Not a valid choice, try again.\n'));
294
+ }
295
+ }
296
+ /**
297
+ * A single readline interface is reused for line input when stdin is not a
298
+ * TTY. Creating one per prompt would work for a human typing, but a *piped*
299
+ * script has all its answers buffered at once: closing the interface after the
300
+ * first question discards every remaining line. Interactive sessions still get
301
+ * a throwaway interface per prompt so they can coexist with the raw-mode
302
+ * keypress handling in `select`.
303
+ */
304
+ let sharedInterface = null;
305
+ let stdinEnded = false;
306
+ /** Lines that arrived before anything asked for them. */
307
+ const bufferedLines = [];
308
+ /** Callbacks waiting for the next line; `null` means stdin ended first. */
309
+ const lineWaiters = [];
310
+ function getSharedInterface() {
311
+ if (!sharedInterface) {
312
+ sharedInterface = readline.createInterface({
313
+ input: process.stdin,
314
+ output: process.stdout,
315
+ });
316
+ // Queue every line as it arrives. Piped input delivers all of its lines
317
+ // immediately, long before the later prompts ask for them.
318
+ sharedInterface.on('line', (line) => {
319
+ const waiter = lineWaiters.shift();
320
+ if (waiter)
321
+ waiter(line);
322
+ else
323
+ bufferedLines.push(line);
324
+ });
325
+ sharedInterface.on('close', () => {
326
+ stdinEnded = true;
327
+ while (lineWaiters.length > 0)
328
+ lineWaiters.shift()?.(null);
329
+ });
330
+ }
331
+ return sharedInterface;
332
+ }
333
+ /** Release the shared interface so the process can exit. */
334
+ function closePrompts() {
335
+ sharedInterface?.close();
336
+ sharedInterface = null;
337
+ }
338
+ /** Read a single line from stdin. */
339
+ function question(prompt) {
340
+ if ((0, exports.isInteractive)()) {
341
+ return new Promise((resolve) => {
342
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
343
+ rl.question(prompt, (answer) => {
344
+ rl.close();
345
+ resolve(answer);
346
+ });
347
+ });
348
+ }
349
+ getSharedInterface();
350
+ process.stdout.write(prompt);
351
+ const buffered = bufferedLines.shift();
352
+ if (buffered !== undefined) {
353
+ process.stdout.write(`${buffered}\n`);
354
+ return Promise.resolve(buffered);
355
+ }
356
+ if (stdinEnded) {
357
+ // Input is exhausted - carrying on would spin forever re-asking.
358
+ process.stdout.write(`\n${exports.style.grey('Input ended.')}\n`);
359
+ process.exit(0);
360
+ }
361
+ return new Promise((resolve) => {
362
+ lineWaiters.push((line) => {
363
+ if (line === null) {
364
+ process.stdout.write(`\n${exports.style.grey('Input ended.')}\n`);
365
+ process.exit(0);
366
+ }
367
+ resolve(line);
368
+ });
369
+ });
370
+ }
371
+ /** Ask for free-text input, re-prompting until `validate` is happy. */
372
+ async function text(options) {
373
+ const suffix = options.defaultValue ? exports.style.grey(` (${options.defaultValue})`) : '';
374
+ if (options.hint)
375
+ process.stdout.write(`${exports.style.grey(options.hint)}\n`);
376
+ for (;;) {
377
+ const raw = await question(`${exports.style.green('?')} ${exports.style.bold(options.message)}${suffix} `);
378
+ const value = raw.trim() === '' ? (options.defaultValue ?? '') : raw.trim();
379
+ if (value === '' && !options.allowEmpty && !options.defaultValue) {
380
+ process.stdout.write(exports.style.red(' A value is required.\n'));
381
+ continue;
382
+ }
383
+ const error = options.validate?.(value);
384
+ if (error) {
385
+ process.stdout.write(`${exports.style.red(` ${error}`)}\n`);
386
+ continue;
387
+ }
388
+ return value;
389
+ }
390
+ }
391
+ /** Ask for multi-line input; a blank line ends the entry. */
392
+ async function multilineText(message) {
393
+ process.stdout.write(`${exports.style.green('?')} ${exports.style.bold(message)}\n`);
394
+ process.stdout.write(exports.style.grey(' Finish with an empty line.\n'));
395
+ const lines = [];
396
+ for (;;) {
397
+ const line = await question(' ');
398
+ if (line.trim() === '')
399
+ break;
400
+ lines.push(line);
401
+ }
402
+ return lines.join('\n');
403
+ }
404
+ async function confirm(message, defaultValue = false) {
405
+ const hint = defaultValue ? 'Y/n' : 'y/N';
406
+ const answer = (await question(`${exports.style.green('?')} ${exports.style.bold(message)} ${exports.style.grey(`(${hint})`)} `))
407
+ .trim()
408
+ .toLowerCase();
409
+ if (answer === '')
410
+ return defaultValue;
411
+ return answer === 'y' || answer === 'yes';
412
+ }
413
+ /** Wait for the user before scrolling more output past them. */
414
+ async function pause(message = 'Press enter to continue') {
415
+ await question(exports.style.grey(`${message} `));
416
+ }
417
+ function heading(text) {
418
+ process.stdout.write(`\n${exports.style.bold(exports.style.underline(text))}\n`);
419
+ }
420
+ function info(text) {
421
+ process.stdout.write(`${text}\n`);
422
+ }
423
+ function warn(text) {
424
+ process.stdout.write(`${exports.style.yellow('!')} ${text}\n`);
425
+ }
426
+ function error(text) {
427
+ process.stdout.write(`${exports.style.red('✖')} ${text}\n`);
428
+ }
429
+ function success(text) {
430
+ process.stdout.write(`${exports.style.green('✔')} ${text}\n`);
431
+ }
@@ -779,6 +779,25 @@ class FindCursor {
779
779
  * @returns A promise that resolves to an array of documents.
780
780
  */
781
781
  async toArray() {
782
+ const { sql: finalSql, params: finalParams } = this.toSQL();
783
+ // Handle malformed json in the database
784
+ const rows = await this.db.all(finalSql, finalParams);
785
+ return rows.map((row) => {
786
+ const parsedData = safeJsonParse(row.data, `findCursor toArray for document ${row._id}`);
787
+ const restoredData = restoreDates(parsedData);
788
+ const doc = { _id: row._id, ...restoredData };
789
+ return this.applyProjection(doc);
790
+ });
791
+ }
792
+ /**
793
+ * Returns the SQL this cursor will execute, along with its bound parameters.
794
+ *
795
+ * Useful for debugging and for tools that want to show how a MongoDB-style
796
+ * filter was translated. Reflects any `sort`, `limit` and `skip` applied so
797
+ * far; `project` is applied in JavaScript after the rows come back and so
798
+ * does not appear here.
799
+ */
800
+ toSQL() {
782
801
  let finalSql = this.queryParts.sql;
783
802
  const finalParams = [...this.queryParts.params];
784
803
  if (this.sortCriteria) {
@@ -805,14 +824,7 @@ class FindCursor {
805
824
  finalSql += ` OFFSET ?`;
806
825
  finalParams.push(this.skipCount);
807
826
  }
808
- // Handle malformed json in the database
809
- const rows = await this.db.all(finalSql, finalParams);
810
- return rows.map((row) => {
811
- const parsedData = safeJsonParse(row.data, `findCursor toArray for document ${row._id}`);
812
- const restoredData = restoreDates(parsedData);
813
- const doc = { _id: row._id, ...restoredData };
814
- return this.applyProjection(doc);
815
- });
827
+ return { sql: finalSql, params: finalParams };
816
828
  }
817
829
  /**
818
830
  * Executes the query and returns the first matching document.
@@ -0,0 +1,40 @@
1
+ import type { IDatabaseAdapter } from '../db.js';
2
+ export interface DatabaseCandidate {
3
+ /** Absolute path to the file. */
4
+ path: string;
5
+ /** Path relative to the search root, for display. */
6
+ relativePath: string;
7
+ sizeBytes: number;
8
+ modified: Date;
9
+ }
10
+ export interface FindDatabasesOptions {
11
+ /** How many directory levels below the root to search. Default 2. */
12
+ depth?: number;
13
+ /** Stop after this many files. Default 50. */
14
+ limit?: number;
15
+ }
16
+ /**
17
+ * Walk `root` looking for SQLite-looking files, skipping the usual noisy
18
+ * directories. Results are newest-first, since the file you just wrote is
19
+ * almost always the one you want.
20
+ */
21
+ export declare function findDatabaseFiles(root?: string, options?: FindDatabasesOptions): DatabaseCandidate[];
22
+ export declare function formatBytes(bytes: number): string;
23
+ export declare function formatRelativeTime(date: Date, now?: Date): string;
24
+ export interface CollectionInfo {
25
+ name: string;
26
+ /** Number of documents (rows) in the collection. */
27
+ count: number;
28
+ /** False for tables that are not shaped like a MongoLite collection. */
29
+ isMongoLite: boolean;
30
+ }
31
+ /**
32
+ * List the tables in the database, flagging which ones look like MongoLite
33
+ * collections (an `_id` column plus a `data` column). Foreign tables are still
34
+ * reported so a user pointed at the wrong file gets a useful message rather
35
+ * than an empty list.
36
+ */
37
+ export declare function listCollectionInfo(db: IDatabaseAdapter): Promise<CollectionInfo[]>;
38
+ /** Resolve a user-supplied database path, allowing `:memory:` and `~` shorthand. */
39
+ export declare function resolveDatabasePath(input: string): string;
40
+ export declare function databaseExists(dbPath: string): boolean;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Finding things for the user so they do not have to type paths or remember
3
+ * collection names: SQLite files on disk, and MongoLite-shaped tables inside
4
+ * a database.
5
+ */
6
+ import { readdirSync, statSync, existsSync } from 'node:fs';
7
+ import path from 'node:path';
8
+ const DB_EXTENSIONS = ['.db', '.sqlite', '.sqlite3', '.db3'];
9
+ const SKIP_DIRS = new Set([
10
+ 'node_modules',
11
+ '.git',
12
+ 'dist',
13
+ 'build',
14
+ 'coverage',
15
+ '.next',
16
+ '.cache',
17
+ 'vendor',
18
+ '.venv',
19
+ 'venv',
20
+ '__pycache__',
21
+ ]);
22
+ /**
23
+ * Walk `root` looking for SQLite-looking files, skipping the usual noisy
24
+ * directories. Results are newest-first, since the file you just wrote is
25
+ * almost always the one you want.
26
+ */
27
+ export function findDatabaseFiles(root = process.cwd(), options = {}) {
28
+ const maxDepth = options.depth ?? 2;
29
+ const limit = options.limit ?? 50;
30
+ const found = [];
31
+ const walk = (dir, depth) => {
32
+ if (found.length >= limit || depth > maxDepth)
33
+ return;
34
+ let entries;
35
+ try {
36
+ entries = readdirSync(dir, { withFileTypes: true });
37
+ }
38
+ catch {
39
+ return; // Unreadable directory - nothing useful to say about it.
40
+ }
41
+ for (const entry of entries) {
42
+ if (found.length >= limit)
43
+ return;
44
+ const full = path.join(dir, entry.name);
45
+ if (entry.isDirectory()) {
46
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.'))
47
+ continue;
48
+ walk(full, depth + 1);
49
+ continue;
50
+ }
51
+ if (!entry.isFile())
52
+ continue;
53
+ if (!DB_EXTENSIONS.includes(path.extname(entry.name).toLowerCase()))
54
+ continue;
55
+ // WAL/SHM siblings are not databases you can open directly.
56
+ if (/-(wal|shm)$/.test(entry.name))
57
+ continue;
58
+ try {
59
+ const stats = statSync(full);
60
+ found.push({
61
+ path: full,
62
+ relativePath: path.relative(root, full) || entry.name,
63
+ sizeBytes: stats.size,
64
+ modified: stats.mtime,
65
+ });
66
+ }
67
+ catch {
68
+ // File vanished between readdir and stat - ignore.
69
+ }
70
+ }
71
+ };
72
+ walk(root, 0);
73
+ found.sort((a, b) => b.modified.getTime() - a.modified.getTime());
74
+ return found;
75
+ }
76
+ export function formatBytes(bytes) {
77
+ if (bytes < 1024)
78
+ return `${bytes} B`;
79
+ const units = ['KB', 'MB', 'GB', 'TB'];
80
+ let value = bytes / 1024;
81
+ let unit = 0;
82
+ while (value >= 1024 && unit < units.length - 1) {
83
+ value /= 1024;
84
+ unit += 1;
85
+ }
86
+ return `${value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
87
+ }
88
+ export function formatRelativeTime(date, now = new Date()) {
89
+ const seconds = Math.max(0, Math.round((now.getTime() - date.getTime()) / 1000));
90
+ if (seconds < 60)
91
+ return 'just now';
92
+ const minutes = Math.round(seconds / 60);
93
+ if (minutes < 60)
94
+ return `${minutes}m ago`;
95
+ const hours = Math.round(minutes / 60);
96
+ if (hours < 24)
97
+ return `${hours}h ago`;
98
+ const days = Math.round(hours / 24);
99
+ if (days < 30)
100
+ return `${days}d ago`;
101
+ return date.toISOString().slice(0, 10);
102
+ }
103
+ /**
104
+ * List the tables in the database, flagging which ones look like MongoLite
105
+ * collections (an `_id` column plus a `data` column). Foreign tables are still
106
+ * reported so a user pointed at the wrong file gets a useful message rather
107
+ * than an empty list.
108
+ */
109
+ export async function listCollectionInfo(db) {
110
+ const tables = await db.all(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
111
+ const infos = [];
112
+ for (const { name } of tables) {
113
+ let isMongoLite = false;
114
+ try {
115
+ const columns = await db.all(`PRAGMA table_info("${name}")`);
116
+ const columnNames = new Set(columns.map((column) => column.name));
117
+ isMongoLite = columnNames.has('_id') && columnNames.has('data');
118
+ }
119
+ catch {
120
+ isMongoLite = false;
121
+ }
122
+ let count = 0;
123
+ try {
124
+ const row = await db.get(`SELECT COUNT(*) as count FROM "${name}"`);
125
+ count = row?.count ?? 0;
126
+ }
127
+ catch {
128
+ count = 0;
129
+ }
130
+ infos.push({ name, count, isMongoLite });
131
+ }
132
+ return infos;
133
+ }
134
+ /** Resolve a user-supplied database path, allowing `:memory:` and `~` shorthand. */
135
+ export function resolveDatabasePath(input) {
136
+ if (input === ':memory:')
137
+ return input;
138
+ const expanded = input.startsWith('~')
139
+ ? path.join(process.env.HOME ?? '', input.slice(1))
140
+ : input;
141
+ return path.resolve(expanded);
142
+ }
143
+ export function databaseExists(dbPath) {
144
+ return dbPath === ':memory:' || existsSync(dbPath);
145
+ }
146
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../../src/cli/discover.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,IAAI,MAAM,WAAW,CAAC;AAG7B,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC;AAC7D,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,cAAc;IACd,MAAM;IACN,MAAM;IACN,OAAO;IACP,UAAU;IACV,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,OAAO;IACP,MAAM;IACN,aAAa;CACd,CAAC,CAAC;AAkBH;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAe,OAAO,CAAC,GAAG,EAAE,EAC5B,UAAgC,EAAE;IAElC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC;IACpC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;IAClC,MAAM,KAAK,GAAwB,EAAE,CAAC;IAEtC,MAAM,IAAI,GAAG,CAAC,GAAW,EAAE,KAAa,EAAQ,EAAE;QAChD,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,IAAI,KAAK,GAAG,QAAQ;YAAE,OAAO;QAEtD,IAAI,OAAmC,CAAC;QACxC,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,yDAAyD;QACnE,CAAC;QAED,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK;gBAAE,OAAO;YAClC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAExC,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;oBAAE,SAAS;gBACtE,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACtB,SAAS;YACX,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE;gBAAE,SAAS;YAC9B,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;gBAAE,SAAS;YAC9E,4DAA4D;YAC5D,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YAE7C,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC7B,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI,EAAE,IAAI;oBACV,YAAY,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI;oBACrD,SAAS,EAAE,KAAK,CAAC,IAAI;oBACrB,QAAQ,EAAE,KAAK,CAAC,KAAK;iBACtB,CAAC,CAAC;YACL,CAAC;YAAC,MAAM,CAAC;gBACP,mDAAmD;YACrD,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACd,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IAClE,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,GAAG,KAAK,IAAI,CAAC;IACtC,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;IACzB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,OAAO,KAAK,IAAI,IAAI,IAAI,IAAI,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChD,KAAK,IAAI,IAAI,CAAC;QACd,IAAI,IAAI,CAAC,CAAC;IACZ,CAAC;IACD,OAAO,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAU,EAAE,MAAY,IAAI,IAAI,EAAE;IACnE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IACjF,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,UAAU,CAAC;IACpC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACzC,IAAI,OAAO,GAAG,EAAE;QAAE,OAAO,GAAG,OAAO,OAAO,CAAC;IAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC,CAAC;IACvC,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,GAAG,KAAK,OAAO,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IACpC,IAAI,IAAI,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,OAAO,CAAC;IACrC,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACzC,CAAC;AAUD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EAAoB;IAC3D,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CACzB,8FAA8F,CAC/F,CAAC;IAEF,MAAM,KAAK,GAAqB,EAAE,CAAC;IACnC,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,CAAC;QAC9B,IAAI,WAAW,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,GAAG,CAAmB,sBAAsB,IAAI,IAAI,CAAC,CAAC;YAC/E,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YAClE,WAAW,GAAG,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAClE,CAAC;QAAC,MAAM,CAAC;YACP,WAAW,GAAG,KAAK,CAAC;QACtB,CAAC;QAED,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,GAAG,CAAoB,kCAAkC,IAAI,GAAG,CAAC,CAAC;YACvF,KAAK,GAAG,GAAG,EAAE,KAAK,IAAI,CAAC,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,KAAK,GAAG,CAAC,CAAC;QACZ,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,IAAI,KAAK,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACvC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC;QACpC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC,CAAC,KAAK,CAAC;IACV,OAAO,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,OAAO,MAAM,KAAK,UAAU,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC;AACrD,CAAC"}