@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,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
+ }
@@ -0,0 +1,100 @@
1
+ import { connect, extractDbName } from '../utils/mongodb.js';
2
+ import { resolveName } from '../utils/resolve.js';
3
+ import { renderTable } from '../utils/table.js';
4
+ import { info, error, highlight } from '../utils/logger.js';
5
+
6
+ function formatDoc(doc, fields) {
7
+ const seen = new Set();
8
+ const result = {};
9
+ for (const key of fields) {
10
+ const val = doc[key];
11
+ result[key] = formatValue(val);
12
+ seen.add(key);
13
+ }
14
+ for (const key of Object.keys(doc)) {
15
+ if (!seen.has(key)) {
16
+ result[key] = formatValue(doc[key]);
17
+ seen.add(key);
18
+ }
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function formatValue(val) {
24
+ if (val == null) return 'null';
25
+ if (typeof val === 'object') {
26
+ if (Array.isArray(val)) {
27
+ if (val.length <= 3) return `[${val.map(formatValue).join(', ')}]`;
28
+ return `[${val.length} items]`;
29
+ }
30
+ if (val instanceof Date) return val.toISOString();
31
+ if (Object.keys(val).length > 4) return `{${Object.keys(val).length} keys}`;
32
+ return JSON.stringify(val);
33
+ }
34
+ if (typeof val === 'boolean') return val ? 'true' : 'false';
35
+ return String(val);
36
+ }
37
+
38
+ export async function viewCommand(uri, collection, options) {
39
+ try {
40
+ uri = await resolveName(uri);
41
+ const client = await connect(uri);
42
+ const db = client.db();
43
+ const dbName = extractDbName(uri);
44
+
45
+ if (!collection) {
46
+ const collections = await db.listCollections().toArray();
47
+ highlight(`── ${dbName} ──`);
48
+ if (collections.length === 0) {
49
+ info('No collections found.');
50
+ } else {
51
+ const rows = collections.map((c, i) => ({
52
+ '#': String(i + 1),
53
+ collection: c.name,
54
+ }));
55
+ console.log(renderTable(rows, { maxColWidth: 60 }));
56
+ info(`${collections.length} collection(s)`);
57
+ }
58
+ await client.close();
59
+ return;
60
+ }
61
+
62
+ const limit = options.all ? 0 : Math.max(1, parseInt(options.limit, 10) || 10);
63
+ const fields = options.fields ? options.fields.split(',').map(s => s.trim()).filter(Boolean) : null;
64
+ const sortField = options.sort || null;
65
+
66
+ const cursor = db.collection(collection).find({});
67
+ if (sortField) cursor.sort({ [sortField]: 1 });
68
+ if (limit > 0) cursor.limit(limit);
69
+
70
+ const docs = await cursor.toArray();
71
+ await client.close();
72
+
73
+ if (docs.length === 0) {
74
+ info(`Collection "${collection}" is empty.`);
75
+ return;
76
+ }
77
+
78
+ if (options.json) {
79
+ const output = fields
80
+ ? docs.map(d => {
81
+ const o = {};
82
+ for (const f of fields) o[f] = d[f];
83
+ return o;
84
+ })
85
+ : docs;
86
+ console.log(JSON.stringify(output, null, 2));
87
+ return;
88
+ }
89
+
90
+ const visibleFields = fields || Object.keys(docs[0]);
91
+ const rows = docs.map(d => formatDoc(d, visibleFields));
92
+
93
+ highlight(`── ${dbName}.${collection} ──`);
94
+ console.log(renderTable(rows, { maxColWidth: 60 }));
95
+ info(`${docs.length} document(s)${limit > 0 && docs.length === limit ? ' (limit reached)' : ''}`);
96
+ } catch (err) {
97
+ error(`Failed to view data: ${err.message}`);
98
+ process.exit(1);
99
+ }
100
+ }