@hasna/microservices 0.0.4 → 0.0.5

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/microservices/microservice-ads/src/cli/index.ts +198 -0
  2. package/microservices/microservice-ads/src/db/campaigns.ts +304 -0
  3. package/microservices/microservice-ads/src/mcp/index.ts +160 -0
  4. package/microservices/microservice-contracts/src/cli/index.ts +410 -23
  5. package/microservices/microservice-contracts/src/db/contracts.ts +430 -1
  6. package/microservices/microservice-contracts/src/db/migrations.ts +83 -0
  7. package/microservices/microservice-contracts/src/mcp/index.ts +312 -3
  8. package/microservices/microservice-domains/src/cli/index.ts +253 -0
  9. package/microservices/microservice-domains/src/db/domains.ts +613 -0
  10. package/microservices/microservice-domains/src/index.ts +21 -0
  11. package/microservices/microservice-domains/src/mcp/index.ts +168 -0
  12. package/microservices/microservice-hiring/src/cli/index.ts +318 -8
  13. package/microservices/microservice-hiring/src/db/hiring.ts +503 -0
  14. package/microservices/microservice-hiring/src/db/migrations.ts +21 -0
  15. package/microservices/microservice-hiring/src/index.ts +29 -0
  16. package/microservices/microservice-hiring/src/lib/scoring.ts +206 -0
  17. package/microservices/microservice-hiring/src/mcp/index.ts +245 -0
  18. package/microservices/microservice-payments/src/cli/index.ts +255 -3
  19. package/microservices/microservice-payments/src/db/migrations.ts +18 -0
  20. package/microservices/microservice-payments/src/db/payments.ts +552 -0
  21. package/microservices/microservice-payments/src/mcp/index.ts +223 -0
  22. package/microservices/microservice-payroll/src/cli/index.ts +269 -0
  23. package/microservices/microservice-payroll/src/db/migrations.ts +26 -0
  24. package/microservices/microservice-payroll/src/db/payroll.ts +636 -0
  25. package/microservices/microservice-payroll/src/mcp/index.ts +246 -0
  26. package/microservices/microservice-shipping/src/cli/index.ts +211 -3
  27. package/microservices/microservice-shipping/src/db/migrations.ts +8 -0
  28. package/microservices/microservice-shipping/src/db/shipping.ts +453 -3
  29. package/microservices/microservice-shipping/src/mcp/index.ts +149 -1
  30. package/microservices/microservice-social/src/cli/index.ts +244 -2
  31. package/microservices/microservice-social/src/db/migrations.ts +33 -0
  32. package/microservices/microservice-social/src/db/social.ts +378 -4
  33. package/microservices/microservice-social/src/mcp/index.ts +221 -1
  34. package/microservices/microservice-subscriptions/src/cli/index.ts +315 -0
  35. package/microservices/microservice-subscriptions/src/db/migrations.ts +68 -0
  36. package/microservices/microservice-subscriptions/src/db/subscriptions.ts +567 -3
  37. package/microservices/microservice-subscriptions/src/mcp/index.ts +267 -1
  38. package/package.json +1 -1
@@ -13,16 +13,33 @@ import {
13
13
  listExpiring,
14
14
  renewContract,
15
15
  getContractStats,
16
+ submitForReview,
17
+ approveContract,
18
+ getContractHistory,
19
+ recordSignature,
20
+ listSignatures,
21
+ compareContracts,
22
+ exportContract,
16
23
  } from "../db/contracts.js";
17
24
  import {
18
25
  createClause,
19
26
  listClauses,
20
27
  deleteClause,
28
+ addClauseFromTemplate,
29
+ saveClauseTemplate,
30
+ listClauseTemplates,
21
31
  } from "../db/contracts.js";
22
32
  import {
23
33
  createReminder,
24
34
  listReminders,
25
35
  deleteReminder,
36
+ setMultiReminders,
37
+ } from "../db/contracts.js";
38
+ import {
39
+ createObligation,
40
+ listObligations,
41
+ completeObligation,
42
+ listOverdueObligations,
26
43
  } from "../db/contracts.js";
27
44
 
28
45
  const server = new McpServer({
@@ -40,7 +57,7 @@ server.registerTool(
40
57
  inputSchema: {
41
58
  title: z.string(),
42
59
  type: z.enum(["nda", "service", "employment", "license", "other"]).optional(),
43
- status: z.enum(["draft", "pending_signature", "active", "expired", "terminated"]).optional(),
60
+ status: z.enum(["draft", "pending_review", "pending_signature", "active", "expired", "terminated"]).optional(),
44
61
  counterparty: z.string().optional(),
45
62
  counterparty_email: z.string().optional(),
46
63
  start_date: z.string().optional(),
@@ -82,7 +99,7 @@ server.registerTool(
82
99
  inputSchema: {
83
100
  search: z.string().optional(),
84
101
  type: z.enum(["nda", "service", "employment", "license", "other"]).optional(),
85
- status: z.enum(["draft", "pending_signature", "active", "expired", "terminated"]).optional(),
102
+ status: z.enum(["draft", "pending_review", "pending_signature", "active", "expired", "terminated"]).optional(),
86
103
  counterparty: z.string().optional(),
87
104
  limit: z.number().optional(),
88
105
  },
@@ -109,7 +126,7 @@ server.registerTool(
109
126
  id: z.string(),
110
127
  title: z.string().optional(),
111
128
  type: z.enum(["nda", "service", "employment", "license", "other"]).optional(),
112
- status: z.enum(["draft", "pending_signature", "active", "expired", "terminated"]).optional(),
129
+ status: z.enum(["draft", "pending_review", "pending_signature", "active", "expired", "terminated"]).optional(),
113
130
  counterparty: z.string().optional(),
114
131
  counterparty_email: z.string().optional(),
115
132
  start_date: z.string().optional(),
@@ -209,6 +226,148 @@ server.registerTool(
209
226
  }
210
227
  );
211
228
 
229
+ // --- Approval workflow ---
230
+
231
+ server.registerTool(
232
+ "submit_for_review",
233
+ {
234
+ title: "Submit for Review",
235
+ description: "Submit a draft contract for review (draft -> pending_review).",
236
+ inputSchema: { id: z.string() },
237
+ },
238
+ async ({ id }) => {
239
+ try {
240
+ const contract = submitForReview(id);
241
+ if (!contract) {
242
+ return { content: [{ type: "text", text: `Contract '${id}' not found.` }], isError: true };
243
+ }
244
+ return { content: [{ type: "text", text: JSON.stringify(contract, null, 2) }] };
245
+ } catch (err) {
246
+ return { content: [{ type: "text", text: (err as Error).message }], isError: true };
247
+ }
248
+ }
249
+ );
250
+
251
+ server.registerTool(
252
+ "approve_contract",
253
+ {
254
+ title: "Approve Contract",
255
+ description: "Approve a contract, advancing it through the approval workflow (pending_review -> pending_signature -> active).",
256
+ inputSchema: { id: z.string() },
257
+ },
258
+ async ({ id }) => {
259
+ try {
260
+ const contract = approveContract(id);
261
+ if (!contract) {
262
+ return { content: [{ type: "text", text: `Contract '${id}' not found.` }], isError: true };
263
+ }
264
+ return { content: [{ type: "text", text: JSON.stringify(contract, null, 2) }] };
265
+ } catch (err) {
266
+ return { content: [{ type: "text", text: (err as Error).message }], isError: true };
267
+ }
268
+ }
269
+ );
270
+
271
+ // --- Version history ---
272
+
273
+ server.registerTool(
274
+ "contract_history",
275
+ {
276
+ title: "Contract History",
277
+ description: "Get version history for a contract, showing previous states before each update.",
278
+ inputSchema: { contract_id: z.string() },
279
+ },
280
+ async ({ contract_id }) => {
281
+ const history = getContractHistory(contract_id);
282
+ return {
283
+ content: [
284
+ { type: "text", text: JSON.stringify({ history, count: history.length }, null, 2) },
285
+ ],
286
+ };
287
+ }
288
+ );
289
+
290
+ // --- Signature logging ---
291
+
292
+ server.registerTool(
293
+ "record_signature",
294
+ {
295
+ title: "Record Signature",
296
+ description: "Record a signature on a contract.",
297
+ inputSchema: {
298
+ contract_id: z.string(),
299
+ signer_name: z.string(),
300
+ signer_email: z.string().optional(),
301
+ method: z.enum(["digital", "wet", "docusign"]).optional(),
302
+ },
303
+ },
304
+ async (params) => {
305
+ const sig = recordSignature(params);
306
+ return { content: [{ type: "text", text: JSON.stringify(sig, null, 2) }] };
307
+ }
308
+ );
309
+
310
+ server.registerTool(
311
+ "list_signatures",
312
+ {
313
+ title: "List Signatures",
314
+ description: "List all signatures for a contract.",
315
+ inputSchema: { contract_id: z.string() },
316
+ },
317
+ async ({ contract_id }) => {
318
+ const sigs = listSignatures(contract_id);
319
+ return {
320
+ content: [
321
+ { type: "text", text: JSON.stringify({ signatures: sigs, count: sigs.length }, null, 2) },
322
+ ],
323
+ };
324
+ }
325
+ );
326
+
327
+ // --- Contract comparison ---
328
+
329
+ server.registerTool(
330
+ "compare_contracts",
331
+ {
332
+ title: "Compare Contracts",
333
+ description: "Compare two contracts, showing field and clause differences.",
334
+ inputSchema: {
335
+ id1: z.string(),
336
+ id2: z.string(),
337
+ },
338
+ },
339
+ async ({ id1, id2 }) => {
340
+ try {
341
+ const diff = compareContracts(id1, id2);
342
+ return { content: [{ type: "text", text: JSON.stringify(diff, null, 2) }] };
343
+ } catch (err) {
344
+ return { content: [{ type: "text", text: (err as Error).message }], isError: true };
345
+ }
346
+ }
347
+ );
348
+
349
+ // --- Markdown export ---
350
+
351
+ server.registerTool(
352
+ "export_contract",
353
+ {
354
+ title: "Export Contract",
355
+ description: "Export a contract as formatted markdown or JSON.",
356
+ inputSchema: {
357
+ id: z.string(),
358
+ format: z.enum(["md", "json"]).optional(),
359
+ },
360
+ },
361
+ async ({ id, format }) => {
362
+ try {
363
+ const output = exportContract(id, format || "md");
364
+ return { content: [{ type: "text", text: output }] };
365
+ } catch (err) {
366
+ return { content: [{ type: "text", text: (err as Error).message }], isError: true };
367
+ }
368
+ }
369
+ );
370
+
212
371
  // --- Clauses ---
213
372
 
214
373
  server.registerTool(
@@ -259,6 +418,132 @@ server.registerTool(
259
418
  }
260
419
  );
261
420
 
421
+ // --- Clause templates ---
422
+
423
+ server.registerTool(
424
+ "save_clause_template",
425
+ {
426
+ title: "Save Clause Template",
427
+ description: "Save a clause as a reusable template in the clause library.",
428
+ inputSchema: {
429
+ name: z.string(),
430
+ text: z.string(),
431
+ type: z.enum(["standard", "custom", "negotiated"]).optional(),
432
+ },
433
+ },
434
+ async (params) => {
435
+ const template = saveClauseTemplate(params);
436
+ return { content: [{ type: "text", text: JSON.stringify(template, null, 2) }] };
437
+ }
438
+ );
439
+
440
+ server.registerTool(
441
+ "list_clause_templates",
442
+ {
443
+ title: "List Clause Templates",
444
+ description: "List all clause templates in the clause library.",
445
+ inputSchema: {},
446
+ },
447
+ async () => {
448
+ const templates = listClauseTemplates();
449
+ return {
450
+ content: [
451
+ { type: "text", text: JSON.stringify({ templates, count: templates.length }, null, 2) },
452
+ ],
453
+ };
454
+ }
455
+ );
456
+
457
+ server.registerTool(
458
+ "add_clause_from_template",
459
+ {
460
+ title: "Add Clause from Template",
461
+ description: "Add a clause to a contract using a clause template by name.",
462
+ inputSchema: {
463
+ contract_id: z.string(),
464
+ template_name: z.string(),
465
+ },
466
+ },
467
+ async ({ contract_id, template_name }) => {
468
+ try {
469
+ const clause = addClauseFromTemplate(contract_id, template_name);
470
+ return { content: [{ type: "text", text: JSON.stringify(clause, null, 2) }] };
471
+ } catch (err) {
472
+ return { content: [{ type: "text", text: (err as Error).message }], isError: true };
473
+ }
474
+ }
475
+ );
476
+
477
+ // --- Obligations ---
478
+
479
+ server.registerTool(
480
+ "add_obligation",
481
+ {
482
+ title: "Add Obligation",
483
+ description: "Add an obligation to a clause for tracking.",
484
+ inputSchema: {
485
+ clause_id: z.string(),
486
+ description: z.string(),
487
+ due_date: z.string().optional(),
488
+ assigned_to: z.string().optional(),
489
+ },
490
+ },
491
+ async (params) => {
492
+ const obligation = createObligation(params);
493
+ return { content: [{ type: "text", text: JSON.stringify(obligation, null, 2) }] };
494
+ }
495
+ );
496
+
497
+ server.registerTool(
498
+ "list_obligations",
499
+ {
500
+ title: "List Obligations",
501
+ description: "List all obligations for a clause.",
502
+ inputSchema: { clause_id: z.string() },
503
+ },
504
+ async ({ clause_id }) => {
505
+ const obligations = listObligations(clause_id);
506
+ return {
507
+ content: [
508
+ { type: "text", text: JSON.stringify({ obligations, count: obligations.length }, null, 2) },
509
+ ],
510
+ };
511
+ }
512
+ );
513
+
514
+ server.registerTool(
515
+ "complete_obligation",
516
+ {
517
+ title: "Complete Obligation",
518
+ description: "Mark an obligation as completed.",
519
+ inputSchema: { id: z.string() },
520
+ },
521
+ async ({ id }) => {
522
+ const obligation = completeObligation(id);
523
+ if (!obligation) {
524
+ return { content: [{ type: "text", text: `Obligation '${id}' not found.` }], isError: true };
525
+ }
526
+ return { content: [{ type: "text", text: JSON.stringify(obligation, null, 2) }] };
527
+ }
528
+ );
529
+
530
+ server.registerTool(
531
+ "list_overdue_obligations",
532
+ {
533
+ title: "List Overdue Obligations",
534
+ description: "List all overdue obligations across all contracts.",
535
+ inputSchema: {},
536
+ },
537
+ async () => {
538
+ const obligations = listOverdueObligations();
539
+ return {
540
+ content: [
541
+ { type: "text", text: JSON.stringify({ obligations, count: obligations.length }, null, 2) },
542
+ ],
543
+ };
544
+ }
545
+ );
546
+
262
547
  // --- Reminders ---
263
548
 
264
549
  server.registerTool(
@@ -295,6 +580,30 @@ server.registerTool(
295
580
  }
296
581
  );
297
582
 
583
+ server.registerTool(
584
+ "set_multi_reminders",
585
+ {
586
+ title: "Set Multi-Stage Reminders",
587
+ description: "Set multiple reminders at once based on days before contract end date.",
588
+ inputSchema: {
589
+ contract_id: z.string(),
590
+ days_before: z.array(z.number()),
591
+ },
592
+ },
593
+ async ({ contract_id, days_before }) => {
594
+ try {
595
+ const reminders = setMultiReminders(contract_id, days_before);
596
+ return {
597
+ content: [
598
+ { type: "text", text: JSON.stringify({ reminders, count: reminders.length }, null, 2) },
599
+ ],
600
+ };
601
+ } catch (err) {
602
+ return { content: [{ type: "text", text: (err as Error).message }], isError: true };
603
+ }
604
+ }
605
+ );
606
+
298
607
  // --- Start ---
299
608
  async function main() {
300
609
  const transport = new StdioServerTransport();
@@ -19,7 +19,18 @@ import {
19
19
  createAlert,
20
20
  listAlerts,
21
21
  deleteAlert,
22
+ whoisLookup,
23
+ checkDnsPropagation,
24
+ checkSsl,
25
+ exportZoneFile,
26
+ importZoneFile,
27
+ discoverSubdomains,
28
+ validateDns,
29
+ exportPortfolio,
30
+ checkAllDomains,
31
+ getDomainByName,
22
32
  } from "../db/domains.js";
33
+ import { readFileSync, writeFileSync } from "node:fs";
23
34
 
24
35
  const program = new Command();
25
36
 
@@ -269,6 +280,109 @@ program
269
280
  }
270
281
  });
271
282
 
283
+ // --- WHOIS Lookup ---
284
+
285
+ program
286
+ .command("whois")
287
+ .description("Run WHOIS lookup for a domain and update DB record")
288
+ .argument("<name>", "Domain name (e.g. example.com)")
289
+ .option("--json", "Output as JSON", false)
290
+ .action((name, opts) => {
291
+ try {
292
+ const result = whoisLookup(name);
293
+ if (opts.json) {
294
+ console.log(JSON.stringify(result, null, 2));
295
+ } else {
296
+ console.log(`WHOIS for ${result.domain}:`);
297
+ console.log(` Registrar: ${result.registrar || "unknown"}`);
298
+ console.log(` Expires: ${result.expires_at || "unknown"}`);
299
+ if (result.nameservers.length > 0) {
300
+ console.log(` Nameservers: ${result.nameservers.join(", ")}`);
301
+ }
302
+ }
303
+ } catch (error: unknown) {
304
+ console.error(`WHOIS lookup failed: ${error instanceof Error ? error.message : String(error)}`);
305
+ process.exit(1);
306
+ }
307
+ });
308
+
309
+ // --- SSL Check ---
310
+
311
+ program
312
+ .command("ssl-check")
313
+ .description("Check SSL certificate for a domain and update DB record")
314
+ .argument("<name>", "Domain name (e.g. example.com)")
315
+ .option("--json", "Output as JSON", false)
316
+ .action((name, opts) => {
317
+ const result = checkSsl(name);
318
+ if (opts.json) {
319
+ console.log(JSON.stringify(result, null, 2));
320
+ } else {
321
+ if (result.error) {
322
+ console.error(`SSL check failed: ${result.error}`);
323
+ process.exit(1);
324
+ }
325
+ console.log(`SSL Certificate for ${result.domain}:`);
326
+ console.log(` Issuer: ${result.issuer || "unknown"}`);
327
+ console.log(` Expires: ${result.expires_at || "unknown"}`);
328
+ if (result.subject) console.log(` Subject: ${result.subject}`);
329
+ }
330
+ });
331
+
332
+ // --- Portfolio Export ---
333
+
334
+ program
335
+ .command("export")
336
+ .description("Export all domains as CSV or JSON")
337
+ .option("--format <format>", "Export format (csv or json)", "json")
338
+ .option("--output <file>", "Write to file instead of stdout")
339
+ .action((opts) => {
340
+ const format = opts.format === "csv" ? "csv" : "json";
341
+ const output = exportPortfolio(format as "csv" | "json");
342
+ if (opts.output) {
343
+ writeFileSync(opts.output, output, "utf-8");
344
+ console.log(`Exported to ${opts.output}`);
345
+ } else {
346
+ console.log(output);
347
+ }
348
+ });
349
+
350
+ // --- Bulk Domain Check ---
351
+
352
+ program
353
+ .command("check-all")
354
+ .description("Run WHOIS + SSL + DNS validation on all domains")
355
+ .option("--json", "Output as JSON", false)
356
+ .action((opts) => {
357
+ const results = checkAllDomains();
358
+ if (opts.json) {
359
+ console.log(JSON.stringify(results, null, 2));
360
+ } else {
361
+ if (results.length === 0) {
362
+ console.log("No domains to check.");
363
+ return;
364
+ }
365
+ for (const r of results) {
366
+ console.log(`\n${r.domain}:`);
367
+ if (r.whois) {
368
+ console.log(` WHOIS: registrar=${r.whois.registrar || "?"}, expires=${r.whois.expires_at || "?"}`);
369
+ if (r.whois.error) console.log(` Error: ${r.whois.error}`);
370
+ }
371
+ if (r.ssl) {
372
+ console.log(` SSL: issuer=${r.ssl.issuer || "?"}, expires=${r.ssl.expires_at || "?"}`);
373
+ if (r.ssl.error) console.log(` Error: ${r.ssl.error}`);
374
+ }
375
+ if (r.dns_validation) {
376
+ console.log(` DNS: valid=${r.dns_validation.valid}, issues=${r.dns_validation.issue_count}`);
377
+ for (const e of r.dns_validation.errors) {
378
+ console.log(` ${e}`);
379
+ }
380
+ }
381
+ }
382
+ console.log(`\nChecked ${results.length} domain(s)`);
383
+ }
384
+ });
385
+
272
386
  // --- DNS Records ---
273
387
 
274
388
  const dnsCmd = program
@@ -370,6 +484,145 @@ dnsCmd
370
484
  }
371
485
  });
372
486
 
487
+ // --- DNS Propagation Check ---
488
+
489
+ dnsCmd
490
+ .command("check-propagation")
491
+ .description("Check DNS propagation across multiple servers")
492
+ .argument("<domain>", "Domain name to check")
493
+ .option("--record <type>", "Record type (A/AAAA/CNAME/MX/TXT/NS)", "A")
494
+ .option("--json", "Output as JSON", false)
495
+ .action((domain, opts) => {
496
+ const result = checkDnsPropagation(domain, opts.record);
497
+ if (opts.json) {
498
+ console.log(JSON.stringify(result, null, 2));
499
+ } else {
500
+ console.log(`DNS Propagation for ${result.domain} (${result.record_type}):`);
501
+ console.log(` Consistent: ${result.consistent ? "yes" : "NO"}`);
502
+ for (const s of result.servers) {
503
+ const values = s.values.length > 0 ? s.values.join(", ") : "(empty)";
504
+ const status = s.status === "error" ? ` [ERROR: ${s.error}]` : "";
505
+ console.log(` ${s.name} (${s.server}): ${values}${status}`);
506
+ }
507
+ }
508
+ });
509
+
510
+ // --- Zone File Export ---
511
+
512
+ dnsCmd
513
+ .command("export")
514
+ .description("Export DNS records as BIND zone file")
515
+ .argument("<domain-id>", "Domain ID")
516
+ .option("--format <format>", "Export format (zone)", "zone")
517
+ .option("--output <file>", "Write to file instead of stdout")
518
+ .action((domainId, opts) => {
519
+ const zone = exportZoneFile(domainId);
520
+ if (!zone) {
521
+ console.error(`Domain '${domainId}' not found.`);
522
+ process.exit(1);
523
+ }
524
+ if (opts.output) {
525
+ writeFileSync(opts.output, zone, "utf-8");
526
+ console.log(`Exported zone file to ${opts.output}`);
527
+ } else {
528
+ console.log(zone);
529
+ }
530
+ });
531
+
532
+ // --- Zone File Import ---
533
+
534
+ dnsCmd
535
+ .command("import")
536
+ .description("Import DNS records from a BIND zone file")
537
+ .argument("<domain-id>", "Domain ID")
538
+ .requiredOption("--file <path>", "Path to zone file")
539
+ .option("--json", "Output as JSON", false)
540
+ .action((domainId, opts) => {
541
+ let content: string;
542
+ try {
543
+ content = readFileSync(opts.file, "utf-8");
544
+ } catch {
545
+ console.error(`Could not read file: ${opts.file}`);
546
+ process.exit(1);
547
+ }
548
+
549
+ const result = importZoneFile(domainId, content);
550
+ if (!result) {
551
+ console.error(`Domain '${domainId}' not found.`);
552
+ process.exit(1);
553
+ }
554
+
555
+ if (opts.json) {
556
+ console.log(JSON.stringify(result, null, 2));
557
+ } else {
558
+ console.log(`Imported ${result.imported} record(s), skipped ${result.skipped}`);
559
+ if (result.errors.length > 0) {
560
+ console.log("Errors:");
561
+ for (const e of result.errors) {
562
+ console.log(` - ${e}`);
563
+ }
564
+ }
565
+ }
566
+ });
567
+
568
+ // --- Subdomain Discovery ---
569
+
570
+ dnsCmd
571
+ .command("discover-subdomains")
572
+ .description("Discover subdomains via certificate transparency logs (crt.sh)")
573
+ .argument("<domain>", "Domain name")
574
+ .option("--json", "Output as JSON", false)
575
+ .action(async (domain, opts) => {
576
+ const result = await discoverSubdomains(domain);
577
+ if (opts.json) {
578
+ console.log(JSON.stringify(result, null, 2));
579
+ } else {
580
+ if (result.error) {
581
+ console.error(`Discovery failed: ${result.error}`);
582
+ process.exit(1);
583
+ }
584
+ if (result.subdomains.length === 0) {
585
+ console.log(`No subdomains found for ${domain}.`);
586
+ return;
587
+ }
588
+ console.log(`Subdomains for ${domain} (source: ${result.source}):`);
589
+ for (const s of result.subdomains) {
590
+ console.log(` ${s}`);
591
+ }
592
+ console.log(`\n${result.subdomains.length} subdomain(s) found`);
593
+ }
594
+ });
595
+
596
+ // --- DNS Validation ---
597
+
598
+ dnsCmd
599
+ .command("validate")
600
+ .description("Validate DNS records for common issues")
601
+ .argument("<domain-id>", "Domain ID")
602
+ .option("--json", "Output as JSON", false)
603
+ .action((domainId, opts) => {
604
+ const result = validateDns(domainId);
605
+ if (!result) {
606
+ console.error(`Domain '${domainId}' not found.`);
607
+ process.exit(1);
608
+ }
609
+
610
+ if (opts.json) {
611
+ console.log(JSON.stringify(result, null, 2));
612
+ } else {
613
+ console.log(`DNS Validation for ${result.domain_name}:`);
614
+ console.log(` Valid: ${result.valid ? "yes" : "NO"}`);
615
+ if (result.issues.length === 0) {
616
+ console.log(" No issues found.");
617
+ } else {
618
+ for (const issue of result.issues) {
619
+ const prefix = issue.type === "error" ? "ERROR" : "WARN";
620
+ console.log(` [${prefix}] ${issue.message}`);
621
+ }
622
+ }
623
+ }
624
+ });
625
+
373
626
  // --- Alerts ---
374
627
 
375
628
  const alertCmd = program