@mohamed1_1ibrahim/dcli 1.0.0 → 1.1.1

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.
@@ -0,0 +1,406 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>dcli GUI</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; height: 100vh; display: flex; flex-direction: column; }
10
+ #app { display: flex; flex-direction: column; height: 100vh; }
11
+ header { background: #161b22; border-bottom: 1px solid #30363d; padding: 12px 20px; display: flex; align-items: center; gap: 12px; }
12
+ header h1 { font-size: 18px; font-weight: 600; }
13
+ header h1 span { color: #58a6ff; }
14
+ #port-badge { background: #21262d; color: #8b949e; font-size: 12px; padding: 2px 8px; border-radius: 10px; }
15
+ nav { display: flex; background: #161b22; border-bottom: 1px solid #30363d; padding: 0 12px; gap: 4px; }
16
+ .tab-btn { background: none; border: none; color: #8b949e; padding: 10px 16px; cursor: pointer; font-size: 13px; border-bottom: 2px solid transparent; transition: all .2s; }
17
+ .tab-btn:hover { color: #c9d1d9; background: #21262d; }
18
+ .tab-btn.active { color: #f0f6fc; border-bottom-color: #58a6ff; }
19
+ main { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 16px; }
20
+ .tab-content { display: none; flex-direction: column; gap: 16px; }
21
+ .tab-content.active { display: flex; }
22
+ .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 16px; }
23
+ .card h3 { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: .5px; color: #8b949e; margin-bottom: 12px; }
24
+ .card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
25
+ .list-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #21262d; font-size: 13px; }
26
+ .list-item:last-child { border-bottom: none; }
27
+ .list-item .label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
28
+ .list-item .label .name { color: #f0f6fc; font-weight: 500; }
29
+ .list-item .label .uri { color: #8b949e; font-size: 11px; }
30
+ .list-item .remove-btn { background: none; border: 1px solid #30363d; color: #f85149; padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; }
31
+ .list-item .remove-btn:hover { background: #f8514911; }
32
+ .empty-state { color: #484f58; font-size: 13px; font-style: italic; padding: 12px 0; }
33
+ .form-row { display: flex; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
34
+ .form-row:last-child { margin-bottom: 0; }
35
+ input, select, textarea { background: #0d1117; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 10px; border-radius: 4px; font-size: 13px; font-family: inherit; }
36
+ input:focus, select:focus, textarea:focus { outline: none; border-color: #58a6ff; }
37
+ input[type="text"], input[type="url"] { flex: 1; min-width: 150px; }
38
+ input[type="checkbox"] { accent-color: #58a6ff; }
39
+ .check-label { display: flex; align-items: center; gap: 6px; font-size: 13px; color: #8b949e; cursor: pointer; }
40
+ .btn { background: #21262d; border: 1px solid #30363d; color: #c9d1d9; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 13px; transition: all .2s; }
41
+ .btn:hover { background: #30363d; }
42
+ .btn-primary { background: #1f6feb; border-color: #1f6feb; color: #fff; }
43
+ .btn-primary:hover { background: #388bfd; }
44
+ .btn-danger { background: #da3633; border-color: #da3633; color: #fff; }
45
+ .btn-danger:hover { background: #f85149; }
46
+ .btn-success { background: #238636; border-color: #238636; color: #fff; }
47
+ .btn-success:hover { background: #2ea043; }
48
+ .actions-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
49
+ @media (max-width: 800px) { .card-grid, .actions-grid { grid-template-columns: 1fr; } }
50
+ #output { background: #0d1117; border-top: 1px solid #30363d; max-height: 200px; min-height: 120px; overflow-y: auto; padding: 8px 16px; font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace; font-size: 12px; }
51
+ #output h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .5px; color: #484f58; margin-bottom: 4px; }
52
+ .log-entry { padding: 2px 0; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
53
+ .log-info { color: #8b949e; }
54
+ .log-info::before { content: "\2139\FE0F "; }
55
+ .log-success { color: #3fb950; }
56
+ .log-success::before { content: "\2714\FE0F "; }
57
+ .log-warn { color: #d29922; }
58
+ .log-warn::before { content: "\26A0\FE0F "; }
59
+ .log-error { color: #f85149; }
60
+ .log-error::before { content: "\2716\FE0F "; }
61
+ .log-highlight { color: #58a6ff; font-weight: 600; }
62
+ .status-badge { display: inline-block; padding: 2px 10px; border-radius: 10px; font-size: 12px; font-weight: 500; }
63
+ .status-active { background: #23863622; color: #3fb950; border: 1px solid #238636; }
64
+ .status-inactive { background: #21262d; color: #8b949e; border: 1px solid #30363d; }
65
+ .status-group { display: flex; align-items: center; gap: 12px; margin-top: 8px; }
66
+ </style>
67
+ </head>
68
+ <body>
69
+ <div id="app">
70
+ <header>
71
+ <h1><span>dcli</span> GUI</h1>
72
+ <span id="port-badge"></span>
73
+ </header>
74
+ <nav>
75
+ <button class="tab-btn active" data-tab="databases">Databases</button>
76
+ <button class="tab-btn" data-tab="actions">Actions</button>
77
+ <button class="tab-btn" data-tab="automation">Automation</button>
78
+ </nav>
79
+ <main>
80
+ <section id="tab-databases" class="tab-content active">
81
+ <div class="card-grid">
82
+ <div class="card">
83
+ <h3>Ping List</h3>
84
+ <div id="ping-list-items"></div>
85
+ </div>
86
+ <div class="card">
87
+ <h3>Clone List</h3>
88
+ <div id="clone-list-items"></div>
89
+ </div>
90
+ </div>
91
+ <div class="card">
92
+ <h3>Add Database</h3>
93
+ <div class="form-row">
94
+ <input type="url" id="add-uri" placeholder="mongodb://localhost:27017/mydb" autocomplete="off">
95
+ <input type="text" id="add-name" placeholder="Friendly name (optional)" autocomplete="off">
96
+ <select id="add-list">
97
+ <option value="ping">Ping List</option>
98
+ <option value="clone">Clone List</option>
99
+ </select>
100
+ <button class="btn btn-primary" id="add-btn">Add</button>
101
+ </div>
102
+ </div>
103
+ </section>
104
+ <section id="tab-actions" class="tab-content">
105
+ <div class="actions-grid">
106
+ <div class="card">
107
+ <h3>Export</h3>
108
+ <div class="form-row"><input type="url" id="exp-uri" placeholder="mongodb://..." autocomplete="off"></div>
109
+ <div class="form-row">
110
+ <input type="text" id="exp-output" placeholder="Output name (optional)" style="flex:1">
111
+ <select id="exp-format">
112
+ <option value="file">File</option>
113
+ <option value="split">Split</option>
114
+ <option value="all">All</option>
115
+ </select>
116
+ </div>
117
+ <div class="form-row">
118
+ <input type="text" id="exp-include" placeholder="Include collections (space separated)" style="flex:2">
119
+ <input type="text" id="exp-exclude" placeholder="Exclude collections" style="flex:2">
120
+ </div>
121
+ <div class="form-row" style="align-items:center">
122
+ <label class="check-label"><input type="checkbox" id="exp-compact"> Compact</label>
123
+ <label class="check-label"><input type="checkbox" id="exp-dryrun"> Dry-run</label>
124
+ <button class="btn btn-primary" id="exp-btn">Export</button>
125
+ </div>
126
+ </div>
127
+ <div class="card">
128
+ <h3>Import</h3>
129
+ <div class="form-row"><input type="url" id="imp-uri" placeholder="mongodb://..." autocomplete="off"></div>
130
+ <div class="form-row"><input type="text" id="imp-file" placeholder="File path (.json) or directory" style="flex:1"></div>
131
+ <div class="form-row" style="align-items:center">
132
+ <label class="check-label"><input type="checkbox" id="imp-confirm"> <span style="color:#f85149">Confirm: this may delete current data</span></label>
133
+ <button class="btn btn-primary" id="imp-btn">Import</button>
134
+ </div>
135
+ </div>
136
+ <div class="card">
137
+ <h3>Ping</h3>
138
+ <div class="form-row">
139
+ <input type="url" id="ping-uri" placeholder="URI (leave empty for all in ping list)" autocomplete="off" style="flex:1">
140
+ <button class="btn btn-primary" id="ping-btn">Ping</button>
141
+ <button class="btn" id="ping-all-btn">Ping All</button>
142
+ </div>
143
+ </div>
144
+ <div class="card">
145
+ <h3>Info</h3>
146
+ <div class="form-row">
147
+ <input type="url" id="info-uri" placeholder="mongodb://..." autocomplete="off" style="flex:1">
148
+ <button class="btn btn-primary" id="info-btn">Get Info</button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </section>
153
+ <section id="tab-automation" class="tab-content">
154
+ <div class="card-grid">
155
+ <div class="card">
156
+ <h3>Auto-Ping</h3>
157
+ <div id="auto-ping-status"></div>
158
+ <div class="form-row" style="margin-top:8px">
159
+ <select id="ap-schedule" style="flex:1">
160
+ <option value="ONLOGON">On Logon</option>
161
+ <option value="DAILY">Daily</option>
162
+ <option value="HOURLY">Hourly</option>
163
+ <option value="ONCE">Once</option>
164
+ </select>
165
+ <input type="text" id="ap-at" placeholder="Time (e.g. 09:00)" style="flex:1;display:none">
166
+ <input type="number" id="ap-every" placeholder="Hours (e.g. 2)" min="1" style="flex:1;display:none">
167
+ <input type="number" id="ap-delay" placeholder="Delay min (e.g. 10)" min="1" value="5" style="flex:1">
168
+ </div>
169
+ <div class="status-group">
170
+ <button class="btn btn-success" id="auto-ping-on">Schedule</button>
171
+ <button class="btn btn-danger" id="auto-ping-off">Remove</button>
172
+ </div>
173
+ </div>
174
+ <div class="card">
175
+ <h3>Auto-Clone</h3>
176
+ <div class="form-row">
177
+ <input type="text" id="aclone-output" placeholder="Output directory (default: current)" style="flex:1">
178
+ <button class="btn btn-primary" id="aclone-btn">Run Auto-Clone</button>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </section>
183
+ </main>
184
+ <div id="output">
185
+ <h3>Output</h3>
186
+ <div id="output-log"></div>
187
+ </div>
188
+ </div>
189
+ <script>
190
+ const $ = s => document.querySelector(s);
191
+ const $$ = s => document.querySelectorAll(s);
192
+
193
+ function log(msg, type = 'info') { const d = document.createElement('div'); d.className = 'log-entry log-' + type; d.textContent = msg; $('#output-log').appendChild(d); d.scrollIntoView({ behavior: 'smooth', block: 'end' }); }
194
+ function clearLog() { $('#output-log').innerHTML = ''; }
195
+
196
+ async function api(method, path, body) {
197
+ const opts = { method, headers: { 'Content-Type': 'application/json' } };
198
+ if (body) opts.body = JSON.stringify(body);
199
+ const res = await fetch(path, opts);
200
+ const data = await res.json();
201
+ if (!res.ok) throw new Error(data.error || res.statusText);
202
+ return data;
203
+ }
204
+
205
+ // Tabs
206
+ $$('.tab-btn').forEach(btn => {
207
+ btn.addEventListener('click', () => {
208
+ $$('.tab-btn').forEach(b => b.classList.remove('active'));
209
+ $$('.tab-content').forEach(t => t.classList.remove('active'));
210
+ btn.classList.add('active');
211
+ document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
212
+ });
213
+ });
214
+
215
+ // Load lists
216
+ async function loadPingList() {
217
+ try {
218
+ const { databases } = await api('GET', '/api/ping-list');
219
+ const el = $('#ping-list-items');
220
+ if (!databases || databases.length === 0) { el.innerHTML = '<div class="empty-state">No databases in ping list</div>'; return; }
221
+ el.innerHTML = databases.map(d => {
222
+ const label = d.name ? `<span class="name">${esc(d.name)}</span> <span class="uri">${esc(d.uri)}</span>` : `<span class="uri">${esc(d.uri)}</span>`;
223
+ return `<div class="list-item"><div class="label">${label}</div><button class="remove-btn" data-uri="${esc(d.uri)}" data-list="ping">Remove</button></div>`;
224
+ }).join('');
225
+ el.querySelectorAll('.remove-btn').forEach(b => b.addEventListener('click', () => removeDb(b.dataset.uri, b.dataset.list)));
226
+ } catch (e) { log('Failed to load ping list: ' + e.message, 'error'); }
227
+ }
228
+
229
+ async function loadCloneList() {
230
+ try {
231
+ const { databases } = await api('GET', '/api/clone-list');
232
+ const el = $('#clone-list-items');
233
+ if (!databases || databases.length === 0) { el.innerHTML = '<div class="empty-state">No databases in clone list</div>'; return; }
234
+ el.innerHTML = databases.map(d => {
235
+ const label = d.name ? `<span class="name">${esc(d.name)}</span> <span class="uri">${esc(d.uri)}</span>` : `<span class="uri">${esc(d.uri)}</span>`;
236
+ return `<div class="list-item"><div class="label">${label}</div><button class="remove-btn" data-uri="${esc(d.uri)}" data-list="clone">Remove</button></div>`;
237
+ }).join('');
238
+ el.querySelectorAll('.remove-btn').forEach(b => b.addEventListener('click', () => removeDb(b.dataset.uri, b.dataset.list)));
239
+ } catch (e) { log('Failed to load clone list: ' + e.message, 'error'); }
240
+ }
241
+
242
+ function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
243
+
244
+ // Add database
245
+ $('#add-btn').addEventListener('click', async () => {
246
+ const uri = $('#add-uri').value.trim();
247
+ const name = $('#add-name').value.trim();
248
+ const list = $('#add-list').value;
249
+ if (!uri) { log('Please enter a URI', 'warn'); return; }
250
+ try {
251
+ const ep = list === 'ping' ? '/api/ping-add' : '/api/clone-add';
252
+ const res = await api('POST', ep, { uri, name: name || undefined });
253
+ log(res.message, 'success');
254
+ $('#add-uri').value = ''; $('#add-name').value = '';
255
+ loadPingList(); loadCloneList();
256
+ } catch (e) { log('Failed: ' + e.message, 'error'); }
257
+ });
258
+
259
+ async function removeDb(uri, list) {
260
+ try {
261
+ const ep = list === 'ping' ? '/api/ping-remove' : '/api/clone-remove';
262
+ const res = await api('POST', ep, { uri });
263
+ log(res.message, 'success');
264
+ loadPingList(); loadCloneList();
265
+ } catch (e) { log('Failed: ' + e.message, 'error'); }
266
+ }
267
+
268
+ // Export
269
+ $('#exp-btn').addEventListener('click', async () => {
270
+ const uri = $('#exp-uri').value.trim();
271
+ if (!uri) { log('Please enter a URI', 'warn'); return; }
272
+ const body = { uri };
273
+ const out = $('#exp-output').value.trim(); if (out) body.output = out;
274
+ body.format = $('#exp-format').value;
275
+ const inc = $('#exp-include').value.trim(); if (inc) body.include = inc.split(/\s+/);
276
+ const exc = $('#exp-exclude').value.trim(); if (exc) body.exclude = exc.split(/\s+/);
277
+ if ($('#exp-compact').checked) body.compact = true;
278
+ if ($('#exp-dryrun').checked) body.dryRun = true;
279
+ $('#exp-btn').disabled = true; $('#exp-btn').textContent = 'Exporting...';
280
+ try {
281
+ const res = await api('POST', '/api/action/export', body);
282
+ log(res.message, 'success');
283
+ } catch (e) { log('Export failed: ' + e.message, 'error'); }
284
+ finally { $('#exp-btn').disabled = false; $('#exp-btn').textContent = 'Export'; }
285
+ });
286
+
287
+ // Import
288
+ $('#imp-btn').addEventListener('click', async () => {
289
+ const uri = $('#imp-uri').value.trim();
290
+ const file = $('#imp-file').value.trim();
291
+ const confirm = $('#imp-confirm').checked;
292
+ if (!uri) { log('Please enter a URI', 'warn'); return; }
293
+ if (!file) { log('Please enter a file path', 'warn'); return; }
294
+ $('#imp-btn').disabled = true; $('#imp-btn').textContent = 'Importing...';
295
+ try {
296
+ const res = await api('POST', '/api/action/import', { uri, file, confirm });
297
+ if (res.confirm) { log('Import requires confirmation. Check the confirmation box.', 'warn'); return; }
298
+ log(res.message, 'success');
299
+ } catch (e) { log('Import failed: ' + e.message, 'error'); }
300
+ finally { $('#imp-btn').disabled = false; $('#imp-btn').textContent = 'Import'; }
301
+ });
302
+
303
+ // Ping
304
+ $('#ping-btn').addEventListener('click', async () => {
305
+ const uri = $('#ping-uri').value.trim();
306
+ if (!uri) { log('Please enter a URI or use Ping All', 'warn'); return; }
307
+ $('#ping-btn').disabled = true; $('#ping-btn').textContent = 'Pinging...';
308
+ try {
309
+ const res = await api('POST', '/api/action/ping', { uri });
310
+ (res.results || []).forEach(r => log(r.message, r.ok ? 'success' : 'error'));
311
+ if (res.message) log(res.message, 'info');
312
+ } catch (e) { log('Ping failed: ' + e.message, 'error'); }
313
+ finally { $('#ping-btn').disabled = false; $('#ping-btn').textContent = 'Ping'; }
314
+ });
315
+
316
+ $('#ping-all-btn').addEventListener('click', async () => {
317
+ $('#ping-all-btn').disabled = true; $('#ping-all-btn').textContent = 'Pinging...';
318
+ try {
319
+ const res = await api('POST', '/api/action/ping', {});
320
+ (res.results || []).forEach(r => log(r.message, r.ok ? 'success' : 'error'));
321
+ if (res.message) log(res.message, 'info');
322
+ } catch (e) { log('Ping all failed: ' + e.message, 'error'); }
323
+ finally { $('#ping-all-btn').disabled = false; $('#ping-all-btn').textContent = 'Ping All'; }
324
+ });
325
+
326
+ // Info
327
+ $('#info-btn').addEventListener('click', async () => {
328
+ const uri = $('#info-uri').value.trim();
329
+ if (!uri) { log('Please enter a URI', 'warn'); return; }
330
+ $('#info-btn').disabled = true; $('#info-btn').textContent = 'Loading...';
331
+ try {
332
+ const res = await api('POST', '/api/action/info', { uri });
333
+ log(res.message, 'highlight');
334
+ (res.collections || []).forEach(c => log(` ${c.name}: ${c.count} document(s)`, 'info'));
335
+ log(`Total documents: ${res.totalDocs}`, 'info');
336
+ } catch (e) { log('Info failed: ' + e.message, 'error'); }
337
+ finally { $('#info-btn').disabled = false; $('#info-btn').textContent = 'Get Info'; }
338
+ });
339
+
340
+ // Auto-ping schedule UI
341
+ const apSchedule = $('#ap-schedule');
342
+ const apAt = $('#ap-at');
343
+ const apEvery = $('#ap-every');
344
+ const apDelay = $('#ap-delay');
345
+
346
+ function toggleApFields() {
347
+ const val = apSchedule.value;
348
+ apAt.style.display = (val === 'DAILY' || val === 'ONCE') ? 'block' : 'none';
349
+ apEvery.style.display = val === 'HOURLY' ? 'block' : 'none';
350
+ apDelay.style.display = val === 'ONLOGON' ? 'block' : 'none';
351
+ }
352
+ apSchedule.addEventListener('change', toggleApFields);
353
+ toggleApFields();
354
+
355
+ async function loadAutoPingStatus() {
356
+ try {
357
+ const res = await api('GET', '/api/auto-ping');
358
+ $('#auto-ping-status').innerHTML = res.scheduled
359
+ ? '<span class="status-badge status-active">Scheduled</span>'
360
+ : '<span class="status-badge status-inactive">Not Scheduled</span>';
361
+ } catch (e) { log('Failed to check auto-ping status: ' + e.message, 'error'); }
362
+ }
363
+
364
+ $('#auto-ping-on').addEventListener('click', async () => {
365
+ const body = { schedule: apSchedule.value };
366
+ if (apAt.value.trim()) body.at = apAt.value.trim();
367
+ if (apEvery.value) body.every = apEvery.value;
368
+ if (apDelay.value) body.delay = apDelay.value;
369
+ $('#auto-ping-on').disabled = true;
370
+ try {
371
+ const res = await api('POST', '/api/auto-ping', body);
372
+ log(res.message, 'success');
373
+ loadAutoPingStatus();
374
+ } catch (e) { log('Failed to schedule auto-ping: ' + e.message, 'error'); }
375
+ finally { $('#auto-ping-on').disabled = false; }
376
+ });
377
+
378
+ $('#auto-ping-off').addEventListener('click', async () => {
379
+ $('#auto-ping-off').disabled = true;
380
+ try {
381
+ const res = await api('DELETE', '/api/auto-ping');
382
+ log(res.message, 'success');
383
+ loadAutoPingStatus();
384
+ } catch (e) { log('Failed to remove auto-ping: ' + e.message, 'error'); }
385
+ finally { $('#auto-ping-off').disabled = false; }
386
+ });
387
+
388
+ // Auto-clone
389
+ $('#aclone-btn').addEventListener('click', async () => {
390
+ const output = $('#aclone-output').value.trim();
391
+ $('#aclone-btn').disabled = true; $('#aclone-btn').textContent = 'Cloning...';
392
+ try {
393
+ const res = await api('POST', '/api/action/auto-clone', { output: output || undefined });
394
+ (res.results || []).forEach(r => log(r.message, r.ok ? 'success' : 'error'));
395
+ log(`Done: ${res.cloned} cloned, ${res.failed} failed`, 'highlight');
396
+ } catch (e) { log('Auto-clone failed: ' + e.message, 'error'); }
397
+ finally { $('#aclone-btn').disabled = false; $('#aclone-btn').textContent = 'Run Auto-Clone'; }
398
+ });
399
+
400
+ // Init
401
+ loadPingList(); loadCloneList(); loadAutoPingStatus();
402
+ log('dcli GUI ready', 'success');
403
+ log('Select a tab to get started', 'info');
404
+ </script>
405
+ </body>
406
+ </html>
package/src/main.js CHANGED
@@ -6,6 +6,13 @@ import { infoCommand } from './commands/info.js';
6
6
  import { addCommand } from './commands/add.js';
7
7
  import { removeCommand } from './commands/remove.js';
8
8
  import { autoPingCommand } from './commands/autoPing.js';
9
+ import { cloneAddCommand } from './commands/cloneAdd.js';
10
+ import { cloneRemoveCommand } from './commands/cloneRemove.js';
11
+ import { autoCloneCommand } from './commands/autoClone.js';
12
+ import { cloneCommand } from './commands/clone.js';
13
+ import { viewCommand } from './commands/view.js';
14
+ import { showCommand } from './commands/show.js';
15
+ import { guiCommand } from './commands/gui.js';
9
16
  import { generateHelp, colorizeDefaultHelp } from './utils/help.js';
10
17
  import { readFileSync } from 'node:fs';
11
18
  import { fileURLToPath } from 'node:url';
@@ -27,7 +34,7 @@ program
27
34
  program
28
35
  .command('export')
29
36
  .description('Export a MongoDB database to JSON file(s)')
30
- .argument('<uri>', 'MongoDB connection URI')
37
+ .argument('<uri>', 'MongoDB connection URI or friendly name from config')
31
38
  .option('-o, --output <name>', 'Output name (default: data-<timestamp>-<db>)')
32
39
  .option('--format <type>', 'Output format: file, split, all (default: file)', 'file')
33
40
  .option('--compact', 'Minify JSON output (default is prettified)')
@@ -39,7 +46,7 @@ program
39
46
  program
40
47
  .command('import')
41
48
  .description('Import a database or collection(s) from JSON file(s)')
42
- .argument('<uri>', 'MongoDB connection URI')
49
+ .argument('<uri>', 'MongoDB connection URI or friendly name from config')
43
50
  .requiredOption('-f, --file <path>', 'File (.json), collection file, or directory of .json files')
44
51
  .action(importCommand);
45
52
 
@@ -53,7 +60,7 @@ program
53
60
  program
54
61
  .command('info')
55
62
  .description('Show database collections and document counts')
56
- .argument('<uri>', 'MongoDB connection URI')
63
+ .argument('<uri>', 'MongoDB connection URI or friendly name from config')
57
64
  .action(infoCommand);
58
65
 
59
66
  program
@@ -71,10 +78,64 @@ program
71
78
 
72
79
  program
73
80
  .command('auto-ping')
74
- .description('Schedule "dcli ping" to run automatically on startup')
81
+ .description('Schedule "dcli ping" to run automatically (customizable)')
75
82
  .option('--remove', 'Remove the scheduled task')
83
+ .option('--schedule <type>', 'Schedule type: ONLOGON, DAILY, HOURLY, ONCE (default: ONLOGON)')
84
+ .option('--at <time>', 'Time for DAILY/ONCE schedules (24h format, e.g. 09:00)')
85
+ .option('--every <n>', 'Interval in hours for HOURLY schedule (default: 1)')
86
+ .option('--delay <n>', 'Delay in minutes for ONLOGON schedule (default: 5)')
76
87
  .action(autoPingCommand);
77
88
 
89
+ program
90
+ .command('clone-add')
91
+ .description('Add a database URI to the auto-clone list')
92
+ .argument('<uri>', 'MongoDB connection URI')
93
+ .option('-n, --name <name>', 'Friendly name for this database')
94
+ .action(cloneAddCommand);
95
+
96
+ program
97
+ .command('clone-remove')
98
+ .description('Remove a database by URI or friendly name from the clone list')
99
+ .argument('<uri>', 'URI or friendly name of the database')
100
+ .action(cloneRemoveCommand);
101
+
102
+ program
103
+ .command('clone')
104
+ .description('Clone a database by its friendly name from the clone list')
105
+ .argument('<name>', 'Friendly name or URI of the database in the clone list')
106
+ .option('-o, --output <dir>', 'Output directory (default: current directory)')
107
+ .action(cloneCommand);
108
+
109
+ program
110
+ .command('auto-clone')
111
+ .description('Clone all databases in the clone list to JSON files')
112
+ .option('-o, --output <dir>', 'Output directory (default: current directory)')
113
+ .action(autoCloneCommand);
114
+
115
+ program
116
+ .command('view')
117
+ .description('Browse collections and documents in a styled table')
118
+ .argument('<uri>', 'MongoDB connection URI or friendly name from config')
119
+ .argument('[collection]', 'Collection to query (lists collections if omitted)')
120
+ .option('--limit <n>', 'Maximum documents to show (default: 10)', '10')
121
+ .option('--fields <fields>', 'Comma-separated list of fields to display')
122
+ .option('--sort <field>', 'Sort by field (ascending)')
123
+ .option('--all', 'Show all documents (no limit)')
124
+ .option('--json', 'Output raw JSON instead of a table')
125
+ .action(viewCommand);
126
+
127
+ program
128
+ .command('show')
129
+ .description('Show the auto-ping or auto-clone database list')
130
+ .option('--clone', 'Show the auto-clone list instead of the ping list')
131
+ .action(showCommand);
132
+
133
+ program
134
+ .command('gui')
135
+ .description('Open the dcli graphical interface in your browser')
136
+ .option('-p, --port <number>', 'Port to run the GUI on (default: 3456)', '3456')
137
+ .action(guiCommand);
138
+
78
139
  program.helpInformation = generateHelp;
79
140
 
80
141
  const origHelpInfo = Command.prototype.helpInformation;
@@ -0,0 +1,56 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ const CONFIG_DIR = join(homedir(), '.dcli');
6
+ const CONFIG_PATH = join(CONFIG_DIR, 'auto-clone.json');
7
+
8
+ async function ensureConfigDir() {
9
+ await mkdir(CONFIG_DIR, { recursive: true });
10
+ }
11
+
12
+ export function normalizeEntry(entry) {
13
+ if (typeof entry === 'string') return { uri: entry };
14
+ return entry;
15
+ }
16
+
17
+ export async function readCloneConfig() {
18
+ try {
19
+ const data = await readFile(CONFIG_PATH, 'utf-8');
20
+ const config = JSON.parse(data);
21
+ config.databases = (config.databases || []).map(normalizeEntry);
22
+ return config;
23
+ } catch {
24
+ return { databases: [] };
25
+ }
26
+ }
27
+
28
+ export async function writeCloneConfig(config) {
29
+ await ensureConfigDir();
30
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
31
+ }
32
+
33
+ export async function addCloneDatabase(uri, name) {
34
+ const config = await readCloneConfig();
35
+ const existing = config.databases.find(e => e.uri === uri);
36
+ if (existing) {
37
+ if (name && existing.name !== name) {
38
+ existing.name = name;
39
+ await writeCloneConfig(config);
40
+ return 'updated';
41
+ }
42
+ return 'exists';
43
+ }
44
+ config.databases.push(name ? { uri, name } : { uri });
45
+ await writeCloneConfig(config);
46
+ return 'added';
47
+ }
48
+
49
+ export async function removeCloneDatabase(uri) {
50
+ const config = await readCloneConfig();
51
+ const index = config.databases.findIndex(e => e.uri === uri || e.name === uri);
52
+ if (index === -1) return false;
53
+ config.databases.splice(index, 1);
54
+ await writeCloneConfig(config);
55
+ return true;
56
+ }