@mohamed1_1ibrahim/dcli 1.0.0 → 1.1.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 CHANGED
@@ -15,7 +15,7 @@
15
15
  ## Installation
16
16
 
17
17
  ```bash
18
- npm install -g dcli
18
+ npm install -g @mohamed1_1ibrahim/dcli
19
19
  ```
20
20
 
21
21
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohamed1_1ibrahim/dcli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Database CLI - MongoDB data management tool (export, import, ping, info)",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -0,0 +1,56 @@
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { connect, exportDatabase, extractDbName } from '../utils/mongodb.js';
4
+ import { readCloneConfig } from '../utils/cloneConfig.js';
5
+ import { success, error, info, highlight } from '../utils/logger.js';
6
+
7
+ export async function autoCloneCommand(options) {
8
+ try {
9
+ const config = await readCloneConfig();
10
+ const databases = config.databases;
11
+
12
+ if (databases.length === 0) {
13
+ info('No databases in the clone list. Add some with "dcli clone-add <uri>".');
14
+ process.exit(0);
15
+ }
16
+
17
+ const outputDir = options.output || process.cwd();
18
+ await mkdir(outputDir, { recursive: true });
19
+
20
+ highlight(`── Auto Clone: ${databases.length} database(s) ──`);
21
+
22
+ let cloned = 0;
23
+ let failed = 0;
24
+
25
+ for (const entry of databases) {
26
+ const label = entry.name || entry.uri;
27
+ try {
28
+ info(`Cloning: ${label}`);
29
+ const client = await connect(entry.uri);
30
+ const dbName = extractDbName(entry.uri);
31
+ const data = await exportDatabase(client);
32
+ await client.close();
33
+
34
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
35
+ const fileName = `clone-${timestamp}-${dbName}.json`;
36
+ const filePath = join(outputDir, fileName);
37
+ const exportData = {
38
+ database: dbName,
39
+ clonedAt: new Date().toISOString(),
40
+ data,
41
+ };
42
+ await writeFile(filePath, JSON.stringify(exportData, null, 2), 'utf-8');
43
+ success(`Cloned ${label} → ${fileName} (${Object.keys(data).length} collections)`);
44
+ cloned++;
45
+ } catch (err) {
46
+ error(`Failed to clone ${label}: ${err.message}`);
47
+ failed++;
48
+ }
49
+ }
50
+
51
+ highlight(`── Done: ${cloned} cloned, ${failed} failed ──`);
52
+ } catch (err) {
53
+ error(`Auto-clone failed: ${err.message}`);
54
+ process.exit(1);
55
+ }
56
+ }
@@ -15,20 +15,52 @@ export async function autoPingCommand(options) {
15
15
  }
16
16
 
17
17
  try {
18
- const createCmd = [
19
- `schtasks /create`,
20
- `/tn "${taskName}"`,
21
- `/sc ONLOGON`,
22
- `/delay 0000:05`,
23
- `/tr "cmd /c dcli ping"`,
24
- `/f`,
25
- ].join(' ');
18
+ const schedule = (options.schedule || 'ONLOGON').toUpperCase();
19
+ const at = options.at;
20
+ const every = options.every;
21
+ const delay = options.delay || '5';
22
+
23
+ let parts = [`schtasks /create`, `/tn "${taskName}"`, `/f`];
24
+
25
+ switch (schedule) {
26
+ case 'DAILY': {
27
+ const time = at || '09:00';
28
+ parts.push(`/sc DAILY`, `/st ${time}`);
29
+ break;
30
+ }
31
+ case 'HOURLY': {
32
+ const interval = every || '1';
33
+ parts.push(`/sc HOURLY`, `/mo ${interval}`);
34
+ break;
35
+ }
36
+ case 'ONCE': {
37
+ const time = at || '09:00';
38
+ parts.push(`/sc ONCE`, `/st ${time}`);
39
+ break;
40
+ }
41
+ default: {
42
+ const delayMinutes = Math.max(1, parseInt(delay, 10) || 5);
43
+ parts.push(`/sc ONLOGON`, `/delay ${String(delayMinutes).padStart(4, '0')}:00`);
44
+ break;
45
+ }
46
+ }
47
+
48
+ parts.push(`/tr "cmd /c dcli ping"`);
49
+ const createCmd = parts.join(' ');
26
50
 
27
51
  execSync(createCmd, { stdio: 'pipe' });
52
+
53
+ const messages = {
54
+ DAILY: `It will run "dcli ping" daily at ${at || '09:00'}.`,
55
+ HOURLY: `It will run "dcli ping" every ${every || '1'} hour(s).`,
56
+ ONCE: `It will run "dcli ping" once at ${at || '09:00'}.`,
57
+ ONLOGON: `It will run "dcli ping" ${delay ? `${delay} minute(s) after` : ''} you log on.`,
58
+ };
59
+
28
60
  success(`Task "${taskName}" created successfully.`);
29
- info(`It will run "dcli ping" 5 minutes after you log on.`);
61
+ info(messages[schedule] || messages.ONLOGON);
30
62
  } catch (err) {
31
- error(`Failed to create task: ${err.message.includes('Access is denied') ? 'Please run as Administrator (right-click terminal Run as administrator).' : err.message}`);
63
+ error(`Failed to create task: ${err.message.includes('Access is denied') ? 'Please run as Administrator (right-click terminal \u2192 Run as administrator).' : err.message}`);
32
64
  process.exit(1);
33
65
  }
34
66
  }
@@ -0,0 +1,45 @@
1
+ import { writeFile, mkdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { connect, exportDatabase, extractDbName } from '../utils/mongodb.js';
4
+ import { readCloneConfig } from '../utils/cloneConfig.js';
5
+ import { readConfig } from '../utils/config.js';
6
+ import { success, error, info, highlight } from '../utils/logger.js';
7
+
8
+ export async function cloneCommand(name, options) {
9
+ try {
10
+ const config = await readCloneConfig();
11
+ const entry = config.databases.find(e => (e.name && e.name === name) || e.uri === name);
12
+
13
+ if (!entry) {
14
+ error(`Database "${name}" not found in clone list.`);
15
+ info('Use "dcli clone-add <uri> -n <name>" to add it first.');
16
+ process.exit(1);
17
+ }
18
+
19
+ const uri = typeof entry === 'string' ? entry : entry.uri;
20
+ const label = entry.name || uri;
21
+
22
+ const outputDir = options.output || process.cwd();
23
+ await mkdir(outputDir, { recursive: true });
24
+
25
+ highlight(`Cloning: ${label}`);
26
+ const client = await connect(uri);
27
+ const dbName = extractDbName(uri);
28
+ const data = await exportDatabase(client);
29
+ await client.close();
30
+
31
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
32
+ const fileName = `clone-${timestamp}-${dbName}.json`;
33
+ const filePath = join(outputDir, fileName);
34
+ const exportData = {
35
+ database: dbName,
36
+ clonedAt: new Date().toISOString(),
37
+ data,
38
+ };
39
+ await writeFile(filePath, JSON.stringify(exportData, null, 2), 'utf-8');
40
+ success(`Cloned ${label} → ${fileName} (${Object.keys(data).length} collections)`);
41
+ } catch (err) {
42
+ error(`Clone failed: ${err.message}`);
43
+ process.exit(1);
44
+ }
45
+ }
@@ -0,0 +1,20 @@
1
+ import { addCloneDatabase } from '../utils/cloneConfig.js';
2
+ import { success, warn, error } from '../utils/logger.js';
3
+
4
+ export async function cloneAddCommand(uri, options) {
5
+ try {
6
+ const name = options.name;
7
+ const result = await addCloneDatabase(uri, name);
8
+ const label = name ? `${name} (${uri})` : uri;
9
+ if (result === 'added') {
10
+ success(`Database added to clone list: ${label}`);
11
+ } else if (result === 'updated') {
12
+ success(`Name updated to "${name}" for ${uri}`);
13
+ } else {
14
+ warn(`Database is already in the clone list.`);
15
+ }
16
+ } catch (err) {
17
+ error(`Failed to add database: ${err.message}`);
18
+ process.exit(1);
19
+ }
20
+ }
@@ -0,0 +1,16 @@
1
+ import { removeCloneDatabase } from '../utils/cloneConfig.js';
2
+ import { success, warn, error } from '../utils/logger.js';
3
+
4
+ export async function cloneRemoveCommand(target) {
5
+ try {
6
+ const removed = await removeCloneDatabase(target);
7
+ if (removed) {
8
+ success(`Database removed from clone list.`);
9
+ } else {
10
+ warn(`No database found matching "${target}" in the clone list.`);
11
+ }
12
+ } catch (err) {
13
+ error(`Failed to remove database: ${err.message}`);
14
+ process.exit(1);
15
+ }
16
+ }
@@ -2,6 +2,7 @@ import { writeFile, mkdir, access } from 'node:fs/promises';
2
2
  import { join, extname, dirname, basename } from 'node:path';
3
3
  import { constants } from 'node:fs';
4
4
  import { connect, exportDatabase, extractDbName } from '../utils/mongodb.js';
5
+ import { resolveName } from '../utils/resolve.js';
5
6
  import { success, error, info, highlight } from '../utils/logger.js';
6
7
 
7
8
  function ensureJsonExt(name) {
@@ -50,6 +51,7 @@ async function getUniquePath(filePath) {
50
51
  }
51
52
 
52
53
  export async function exportCommand(uri, options) {
54
+ uri = await resolveName(uri);
53
55
  const dbName = extractDbName(uri);
54
56
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
55
57
  const outputName = options.output || `data-${timestamp}-${dbName}`;
@@ -0,0 +1,350 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { writeFile, readFile, mkdir, readdir } from 'node:fs/promises';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join, extname, parse } from 'node:path';
6
+ import { execSync } from 'node:child_process';
7
+ import { connect, exportDatabase, importDatabase, dropDatabase, extractDbName, pingDatabase } from '../utils/mongodb.js';
8
+ import { readConfig, addDatabase, removeDatabase } from '../utils/config.js';
9
+ import { readCloneConfig, addCloneDatabase, removeCloneDatabase } from '../utils/cloneConfig.js';
10
+ import { info, success, error } from '../utils/logger.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ const GUI_DIR = join(__dirname, '..', 'gui');
15
+ const HTML_PATH = join(GUI_DIR, 'index.html');
16
+
17
+ function sendJson(res, data, status = 200) {
18
+ res.writeHead(status, { 'Content-Type': 'application/json' });
19
+ res.end(JSON.stringify(data));
20
+ }
21
+
22
+ function parseBody(req) {
23
+ return new Promise((resolve) => {
24
+ let body = '';
25
+ req.on('data', chunk => body += chunk);
26
+ req.on('end', () => {
27
+ try { resolve(JSON.parse(body)); }
28
+ catch { resolve({}); }
29
+ });
30
+ });
31
+ }
32
+
33
+ async function handleApi(req, res) {
34
+ const method = req.method;
35
+ const path = req.url.split('?')[0];
36
+ const body = method === 'POST' || method === 'DELETE' ? await parseBody(req) : {};
37
+
38
+ try {
39
+ // Ping list
40
+ if (method === 'GET' && path === '/api/ping-list') {
41
+ const config = await readConfig();
42
+ return sendJson(res, { databases: config.databases });
43
+ }
44
+ if (method === 'POST' && path === '/api/ping-add') {
45
+ const result = await addDatabase(body.uri, body.name);
46
+ const msg = result === 'added' ? `Added to ping list: ${body.name || body.uri}` : result === 'updated' ? `Name updated for ${body.uri}` : `Already in ping list`;
47
+ return sendJson(res, { success: true, message: msg });
48
+ }
49
+ if (method === 'POST' && path === '/api/ping-remove') {
50
+ const removed = await removeDatabase(body.uri);
51
+ return sendJson(res, { success: removed, message: removed ? 'Removed from ping list' : 'Not found in ping list' });
52
+ }
53
+
54
+ // Clone list
55
+ if (method === 'GET' && path === '/api/clone-list') {
56
+ const config = await readCloneConfig();
57
+ return sendJson(res, { databases: config.databases });
58
+ }
59
+ if (method === 'POST' && path === '/api/clone-add') {
60
+ const result = await addCloneDatabase(body.uri, body.name);
61
+ const msg = result === 'added' ? `Added to clone list: ${body.name || body.uri}` : result === 'updated' ? `Name updated for ${body.uri}` : `Already in clone list`;
62
+ return sendJson(res, { success: true, message: msg });
63
+ }
64
+ if (method === 'POST' && path === '/api/clone-remove') {
65
+ const removed = await removeCloneDatabase(body.uri);
66
+ return sendJson(res, { success: removed, message: removed ? 'Removed from clone list' : 'Not found in clone list' });
67
+ }
68
+
69
+ // Action: Export
70
+ if (method === 'POST' && path === '/api/action/export') {
71
+ const { uri, output, format, compact, include, exclude, dryRun } = body;
72
+ const client = await connect(uri);
73
+ const dbName = extractDbName(uri);
74
+ const rawData = await exportDatabase(client);
75
+ await client.close();
76
+
77
+ let data = rawData;
78
+ if (include && include.length > 0) {
79
+ const set = new Set(include);
80
+ data = Object.fromEntries(Object.entries(data).filter(([k]) => set.has(k)));
81
+ }
82
+ if (exclude && exclude.length > 0) {
83
+ const set = new Set(exclude);
84
+ data = Object.fromEntries(Object.entries(data).filter(([k]) => !set.has(k)));
85
+ }
86
+
87
+ if (Object.keys(data).length === 0) {
88
+ return sendJson(res, { success: true, message: 'No collections matched the filters' });
89
+ }
90
+
91
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
92
+ const outputName = output || `data-${timestamp}-${dbName}`;
93
+ const fmt = format || 'file';
94
+ const indent = compact ? 0 : 2;
95
+ const files = [];
96
+
97
+ if (fmt === 'file' || fmt === 'all') {
98
+ const fileName = `${outputName}.json`;
99
+ const exportData = { database: dbName, exportedAt: new Date().toISOString(), data };
100
+ await writeFile(fileName, JSON.stringify(exportData, null, indent), 'utf-8');
101
+ files.push(fileName);
102
+ }
103
+ if (fmt === 'split' || fmt === 'all') {
104
+ const dirName = extname(outputName) ? outputName.replace(/\.[^.]+$/, '') : outputName;
105
+ await mkdir(dirName, { recursive: true });
106
+ for (const [name, docs] of Object.entries(data)) {
107
+ const fp = join(dirName, `${name}.json`);
108
+ await writeFile(fp, JSON.stringify(docs, null, indent), 'utf-8');
109
+ files.push(fp);
110
+ }
111
+ }
112
+
113
+ return sendJson(res, { success: true, message: `Exported ${Object.keys(data).length} collection(s) to ${files.join(', ')}`, files });
114
+ }
115
+
116
+ // Action: Import
117
+ if (method === 'POST' && path === '/api/action/import') {
118
+ const { uri, file, confirm } = body;
119
+
120
+ if (!file) return sendJson(res, { success: false, error: 'File path is required' }, 400);
121
+
122
+ const isDir = statSync(file).isDirectory();
123
+
124
+ if (isDir) {
125
+ const entries = await readdir(file);
126
+ const jsonFiles = entries.filter(e => e.endsWith('.json')).sort();
127
+ const dataToRestore = {};
128
+ for (const f of jsonFiles) {
129
+ const content = await readFile(join(file, f), 'utf-8');
130
+ const parsed = JSON.parse(content);
131
+ const name = parse(f).name;
132
+ const docs = Array.isArray(parsed) ? parsed : [];
133
+ dataToRestore[name] = docs;
134
+ }
135
+ const client = await connect(uri);
136
+ await importDatabase(client, dataToRestore);
137
+ await client.close();
138
+ return sendJson(res, { success: true, message: `Restored ${Object.keys(dataToRestore).length} collection(s) from ${file}` });
139
+ }
140
+
141
+ const content = await readFile(file, 'utf-8');
142
+ const parsed = JSON.parse(content);
143
+ const isFull = parsed && typeof parsed === 'object' && 'database' in parsed && 'data' in parsed;
144
+
145
+ if (isFull) {
146
+ if (!confirm) return sendJson(res, { success: false, confirm: true, message: 'This will delete current data. Check the confirmation box.' });
147
+
148
+ const client = await connect(uri);
149
+ const currentData = await exportDatabase(client);
150
+ const backupName = `backup-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
151
+ await writeFile(backupName, JSON.stringify({ database: extractDbName(uri), exportedAt: new Date().toISOString(), data: currentData }, null, 2), 'utf-8');
152
+ await dropDatabase(client);
153
+ await importDatabase(client, parsed.data);
154
+ await client.close();
155
+ return sendJson(res, { success: true, message: `Database restored from ${file}. Backup saved to ${backupName}` });
156
+ }
157
+
158
+ const name = parse(file).name;
159
+ const docs = Array.isArray(parsed) ? parsed : [];
160
+ const client = await connect(uri);
161
+ await importDatabase(client, { [name]: docs });
162
+ await client.close();
163
+ return sendJson(res, { success: true, message: `Restored collection "${name}" (${docs.length} documents) from ${file}` });
164
+ }
165
+
166
+ // Action: Ping
167
+ if (method === 'POST' && path === '/api/action/ping') {
168
+ const { uri } = body;
169
+ let entries;
170
+
171
+ if (uri) {
172
+ const config = await readConfig();
173
+ const named = config.databases.find(e => e.name === uri);
174
+ entries = named ? [named] : [{ uri }];
175
+ } else {
176
+ const config = await readConfig();
177
+ entries = config.databases;
178
+ }
179
+
180
+ if (entries.length === 0) {
181
+ return sendJson(res, { success: true, results: [], message: 'No databases to ping' });
182
+ }
183
+
184
+ const results = [];
185
+ for (const entry of entries) {
186
+ const label = entry.name || extractDbName(entry.uri) || entry.uri;
187
+ try {
188
+ const client = await connect(entry.uri);
189
+ await pingDatabase(client);
190
+ await client.close();
191
+ results.push({ uri: entry.uri, ok: true, message: `Pinged ${label} successfully` });
192
+ } catch {
193
+ results.push({ uri: entry.uri, ok: false, message: `Failed to ping ${label}` });
194
+ }
195
+ }
196
+ return sendJson(res, { success: true, results });
197
+ }
198
+
199
+ // Action: Info
200
+ if (method === 'POST' && path === '/api/action/info') {
201
+ const { uri } = body;
202
+ const client = await connect(uri);
203
+ const db = client.db();
204
+ const dbName = extractDbName(uri);
205
+ const collections = await db.listCollections().toArray();
206
+ const collInfo = [];
207
+ let totalDocs = 0;
208
+ for (const coll of collections) {
209
+ const count = await db.collection(coll.name).countDocuments();
210
+ totalDocs += count;
211
+ collInfo.push({ name: coll.name, count });
212
+ }
213
+ await client.close();
214
+ return sendJson(res, { success: true, message: `── ${dbName} ── Collections: ${collections.length}`, dbName, collections: collInfo, totalDocs });
215
+ }
216
+
217
+ // Auto-ping
218
+ if (method === 'GET' && path === '/api/auto-ping') {
219
+ try {
220
+ const out = execSync(`schtasks /query /tn "DCLI-AutoPing" /v /fo csv`, { stdio: 'pipe', encoding: 'utf-8' });
221
+ const lines = out.split('\n').filter(Boolean);
222
+ const header = lines[0]?.split(',') || [];
223
+ const values = lines[1]?.split(',') || [];
224
+ const idx = (name) => header.findIndex(h => h.replace(/"/g, '').trim() === name);
225
+ const get = (name) => (values[idx(name)] || '').replace(/"/g, '').trim();
226
+ return sendJson(res, {
227
+ scheduled: true,
228
+ scheduleType: get('Schedule Type') || get('Task To Run'),
229
+ startTime: get('Start Time'),
230
+ repeatInterval: get('Repeat: Every'),
231
+ });
232
+ } catch {
233
+ return sendJson(res, { scheduled: false });
234
+ }
235
+ }
236
+ if (method === 'POST' && path === '/api/auto-ping') {
237
+ const schedule = (body.schedule || 'ONLOGON').toUpperCase();
238
+ const at = body.at;
239
+ const every = body.every;
240
+ const delay = body.delay || '5';
241
+
242
+ let parts = ['schtasks /create', '/tn "DCLI-AutoPing"', '/f'];
243
+
244
+ switch (schedule) {
245
+ case 'DAILY': {
246
+ const time = at || '09:00';
247
+ parts.push(`/sc DAILY`, `/st ${time}`);
248
+ break;
249
+ }
250
+ case 'HOURLY': {
251
+ const interval = every || '1';
252
+ parts.push(`/sc HOURLY`, `/mo ${interval}`);
253
+ break;
254
+ }
255
+ case 'ONCE': {
256
+ const time = at || '09:00';
257
+ parts.push(`/sc ONCE`, `/st ${time}`);
258
+ break;
259
+ }
260
+ default: {
261
+ const delayMinutes = Math.max(1, parseInt(delay, 10) || 5);
262
+ parts.push(`/sc ONLOGON`, `/delay ${String(delayMinutes).padStart(4, '0')}:00`);
263
+ break;
264
+ }
265
+ }
266
+
267
+ parts.push(`/tr "cmd /c dcli ping"`);
268
+ execSync(parts.join(' '), { stdio: 'pipe' });
269
+
270
+ const messages = {
271
+ DAILY: `Auto-ping task created. It will run "dcli ping" daily at ${at || '09:00'}.`,
272
+ HOURLY: `Auto-ping task created. It will run "dcli ping" every ${every || '1'} hour(s).`,
273
+ ONCE: `Auto-ping task created. It will run "dcli ping" once at ${at || '09:00'}.`,
274
+ ONLOGON: `Auto-ping task created. It will run "dcli ping" ${delay ? `${delay} minute(s) after` : ''} you log on.`,
275
+ };
276
+ return sendJson(res, { success: true, message: messages[schedule] || messages.ONLOGON });
277
+ }
278
+ if (method === 'DELETE' && path === '/api/auto-ping') {
279
+ execSync('schtasks /delete /tn "DCLI-AutoPing" /f', { stdio: 'pipe' });
280
+ return sendJson(res, { success: true, message: 'Auto-ping task removed.' });
281
+ }
282
+
283
+ // Action: Auto-clone
284
+ if (method === 'POST' && path === '/api/action/auto-clone') {
285
+ const config = await readCloneConfig();
286
+ const databases = config.databases;
287
+ if (databases.length === 0) {
288
+ return sendJson(res, { success: true, results: [], cloned: 0, failed: 0, message: 'No databases in clone list' });
289
+ }
290
+ const outputDir = body.output || process.cwd();
291
+ await mkdir(outputDir, { recursive: true });
292
+ const results = [];
293
+ let cloned = 0;
294
+ let failed = 0;
295
+ for (const entry of databases) {
296
+ const label = entry.name || entry.uri;
297
+ try {
298
+ const client = await connect(entry.uri);
299
+ const dbName = extractDbName(entry.uri);
300
+ const data = await exportDatabase(client);
301
+ await client.close();
302
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
303
+ const fileName = `clone-${timestamp}-${dbName}.json`;
304
+ const filePath = join(outputDir, fileName);
305
+ await writeFile(filePath, JSON.stringify({ database: dbName, clonedAt: new Date().toISOString(), data }, null, 2), 'utf-8');
306
+ results.push({ uri: entry.uri, ok: true, message: `Cloned ${label} -> ${fileName} (${Object.keys(data).length} collections)` });
307
+ cloned++;
308
+ } catch (err) {
309
+ results.push({ uri: entry.uri, ok: false, message: `Failed to clone ${label}: ${err.message}` });
310
+ failed++;
311
+ }
312
+ }
313
+ return sendJson(res, { success: true, results, cloned, failed });
314
+ }
315
+
316
+ sendJson(res, { error: 'Not found' }, 404);
317
+ } catch (err) {
318
+ sendJson(res, { error: err.message }, 500);
319
+ }
320
+ }
321
+
322
+ export async function guiCommand(options) {
323
+ const port = options.port || 3456;
324
+
325
+ try {
326
+ statSync(HTML_PATH);
327
+ } catch {
328
+ error(`GUI files not found at ${GUI_DIR}`);
329
+ process.exit(1);
330
+ }
331
+
332
+ const html = readFileSync(HTML_PATH, 'utf-8');
333
+
334
+ const server = createServer((req, res) => {
335
+ if (req.url.startsWith('/api/')) {
336
+ return handleApi(req, res);
337
+ }
338
+ res.writeHead(200, { 'Content-Type': 'text/html' });
339
+ res.end(html);
340
+ });
341
+
342
+ server.listen(port, () => {
343
+ success(`GUI opened at http://localhost:${port}`);
344
+ try {
345
+ execSync(`start http://localhost:${port}`, { stdio: 'ignore' });
346
+ } catch {
347
+ info(`Open http://localhost:${port} in your browser`);
348
+ }
349
+ });
350
+ }
@@ -4,6 +4,7 @@ import { join, parse } from 'node:path';
4
4
  import { createInterface } from 'node:readline';
5
5
  import { stdin as input, stdout as output } from 'node:process';
6
6
  import { connect, exportDatabase, dropDatabase, importDatabase, extractDbName } from '../utils/mongodb.js';
7
+ import { resolveName } from '../utils/resolve.js';
7
8
  import { info, warn, success, error } from '../utils/logger.js';
8
9
 
9
10
  function askQuestion(query) {
@@ -39,6 +40,7 @@ async function loadDirectory(dirPath) {
39
40
  }
40
41
 
41
42
  export async function importCommand(uri, options) {
43
+ uri = await resolveName(uri);
42
44
  const filePath = options.file;
43
45
 
44
46
  try {
@@ -1,7 +1,9 @@
1
1
  import { connect, extractDbName } from '../utils/mongodb.js';
2
+ import { resolveName } from '../utils/resolve.js';
2
3
  import { info, error, highlight } from '../utils/logger.js';
3
4
 
4
5
  export async function infoCommand(uri) {
6
+ uri = await resolveName(uri);
5
7
  try {
6
8
  const client = await connect(uri);
7
9
  const db = client.db();
@@ -8,7 +8,8 @@ async function pingUri(uri) {
8
8
  await pingDatabase(client);
9
9
  await client.close();
10
10
  return true;
11
- } catch {
11
+ } catch (err) {
12
+ error(`Ping error: ${err.message}`);
12
13
  return false;
13
14
  }
14
15
  }
@@ -0,0 +1,30 @@
1
+ import { readConfig } from '../utils/config.js';
2
+ import { readCloneConfig } from '../utils/cloneConfig.js';
3
+ import { info, warn, error, highlight } from '../utils/logger.js';
4
+
5
+ export async function showCommand(options) {
6
+ try {
7
+ const isClone = options.clone;
8
+ const config = isClone ? await readCloneConfig() : await readConfig();
9
+ const label = isClone ? 'Clone list' : 'Ping list';
10
+ const path = isClone ? '~/.dcli/auto-clone.json' : '~/.dcli/refresh.json';
11
+ const dbs = config.databases || [];
12
+
13
+ highlight(`${label} (${path}):`);
14
+ info(`Database entries: ${dbs.length}`);
15
+
16
+ if (dbs.length === 0) {
17
+ warn(`No databases in the ${label.toLowerCase()}.`);
18
+ return;
19
+ }
20
+
21
+ for (const [i, entry] of dbs.entries()) {
22
+ const num = String(i + 1).padStart(2, ' ');
23
+ const { uri, name } = typeof entry === 'string' ? { uri: entry } : entry;
24
+ console.log(` ${num}. ${name ? `${name} ` : ''}${uri}`);
25
+ }
26
+ } catch (err) {
27
+ error(`Failed to show ${label.toLowerCase()}: ${err.message}`);
28
+ process.exit(1);
29
+ }
30
+ }