myaidev-method 0.2.22 → 0.2.24-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.
Files changed (59) hide show
  1. package/USER_GUIDE.md +453 -48
  2. package/bin/cli.js +236 -38
  3. package/content-rules.example.md +80 -0
  4. package/dist/mcp/mcp-launcher.js +237 -0
  5. package/dist/server/.tsbuildinfo +1 -1
  6. package/dist/server/auth/layers.d.ts +1 -1
  7. package/dist/server/auth/services/AuthService.d.ts +1 -1
  8. package/dist/server/auth/services/TokenService.js.map +1 -1
  9. package/dist/server/auth/services/example.d.ts +5 -5
  10. package/package.json +22 -17
  11. package/src/config/workflows.js +28 -44
  12. package/src/index.js +21 -8
  13. package/src/lib/ascii-banner.js +214 -0
  14. package/src/lib/config-manager.js +470 -0
  15. package/src/lib/content-generator.js +427 -0
  16. package/src/lib/html-conversion-utils.js +843 -0
  17. package/src/lib/seo-optimizer.js +515 -0
  18. package/src/lib/update-manager.js +2 -1
  19. package/src/lib/visual-config-utils.js +321 -295
  20. package/src/lib/visual-generation-utils.js +1000 -811
  21. package/src/lib/wordpress-client.js +633 -0
  22. package/src/lib/workflow-installer.js +3 -3
  23. package/src/scripts/configure-wordpress-mcp.js +8 -3
  24. package/src/scripts/generate-visual-cli.js +365 -235
  25. package/src/scripts/html-conversion-cli.js +526 -0
  26. package/src/scripts/init/configure.js +436 -0
  27. package/src/scripts/init/install.js +460 -0
  28. package/src/scripts/ping.js +250 -0
  29. package/src/scripts/utils/file-utils.js +404 -0
  30. package/src/scripts/utils/logger.js +300 -0
  31. package/src/scripts/utils/write-content.js +293 -0
  32. package/src/scripts/wordpress/publish-to-wordpress.js +165 -0
  33. package/src/server/auth/services/TokenService.ts +1 -1
  34. package/src/templates/claude/agents/content-rules-setup.md +657 -0
  35. package/src/templates/claude/agents/content-writer.md +328 -1
  36. package/src/templates/claude/agents/visual-content-generator.md +311 -8
  37. package/src/templates/claude/commands/myai-configure.md +1 -1
  38. package/src/templates/claude/commands/myai-content-rules-setup.md +204 -0
  39. package/src/templates/claude/commands/myai-convert-html.md +186 -0
  40. package/src/templates/codex/commands/myai-content-rules-setup.md +85 -0
  41. package/src/templates/diagrams/architecture.d2 +52 -0
  42. package/src/templates/diagrams/flowchart.d2 +42 -0
  43. package/src/templates/diagrams/sequence.d2 +47 -0
  44. package/src/templates/docs/content-creation-guide.md +164 -0
  45. package/src/templates/docs/deployment-guide.md +336 -0
  46. package/src/templates/docs/visual-generation-guide.md +248 -0
  47. package/src/templates/docs/wordpress-publishing-guide.md +208 -0
  48. package/src/templates/gemini/commands/myai-content-rules-setup.toml +57 -0
  49. package/src/templates/infographics/comparison-table.html +347 -0
  50. package/src/templates/infographics/data-chart.html +268 -0
  51. package/src/templates/infographics/process-flow.html +365 -0
  52. package/.claude/mcp/sparc-orchestrator-server.js +0 -607
  53. package/.claude/mcp/wordpress-server.js +0 -1277
  54. package/src/agents/content-writer-prompt.md +0 -164
  55. package/src/agents/content-writer.json +0 -70
  56. package/src/templates/claude/mcp_config.json +0 -74
  57. package/src/templates/claude/slash_commands.json +0 -166
  58. package/src/templates/scripts/configure-wordpress-mcp.js +0 -181
  59. /package/src/scripts/{wordpress-health-check.js → wordpress/wordpress-health-check.js} +0 -0
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Install Script
5
+ * Workflow installation orchestrator for MyAIDev Method
6
+ * Handles installation of workflows, agents, commands, and dependencies
7
+ */
8
+
9
+ import WorkflowInstaller from '../../lib/workflow-installer.js';
10
+ import { ConfigManager } from '../../lib/config-manager.js';
11
+ import { logger, ProgressTracker, createSpinner } from '../utils/logger.js';
12
+ import { ensureDir } from '../utils/file-utils.js';
13
+ import inquirer from 'inquirer';
14
+ import path from 'path';
15
+ import fs from 'fs-extra';
16
+
17
+ /**
18
+ * Installation Orchestrator Class
19
+ */
20
+ class InstallationOrchestrator {
21
+ constructor(targetDir = process.cwd()) {
22
+ this.targetDir = targetDir;
23
+ this.configManager = new ConfigManager(targetDir);
24
+ this.installer = new WorkflowInstaller();
25
+ this.installedWorkflows = [];
26
+ }
27
+
28
+ /**
29
+ * Run the installation
30
+ * @param {Object} options - Installation options
31
+ */
32
+ async run(options = {}) {
33
+ const {
34
+ workflows = [],
35
+ cliType = 'claude',
36
+ interactive = false,
37
+ force = false,
38
+ verbose = false
39
+ } = options;
40
+
41
+ logger.header('MyAIDev Method Installation');
42
+ logger.info(`Target directory: ${this.targetDir}`);
43
+ logger.info(`CLI type: ${cliType}`);
44
+ logger.blank();
45
+
46
+ try {
47
+ // Interactive mode - show workflow selection
48
+ if (interactive) {
49
+ const selectedWorkflows = await this.selectWorkflows();
50
+ if (selectedWorkflows.length === 0) {
51
+ logger.info('No workflows selected. Exiting.');
52
+ return;
53
+ }
54
+ workflows.push(...selectedWorkflows);
55
+ }
56
+
57
+ // If no workflows specified, install core
58
+ if (workflows.length === 0) {
59
+ workflows.push('core');
60
+ }
61
+
62
+ // Install each workflow
63
+ const progress = new ProgressTracker(workflows.length, 'Installation');
64
+ progress.start();
65
+
66
+ for (const workflow of workflows) {
67
+ try {
68
+ await this.installWorkflow(workflow, cliType, { force, verbose });
69
+ progress.increment(workflow);
70
+ this.installedWorkflows.push(workflow);
71
+ } catch (error) {
72
+ progress.fail(workflow, error);
73
+ }
74
+ }
75
+
76
+ progress.complete();
77
+
78
+ // Show summary
79
+ await this.showSummary();
80
+
81
+ // Offer to configure
82
+ if (interactive && this.needsConfiguration()) {
83
+ await this.offerConfiguration();
84
+ }
85
+
86
+ } catch (error) {
87
+ logger.error(`Installation failed: ${error.message}`);
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Show workflow selection prompt
94
+ * @returns {Promise<string[]>} Selected workflows
95
+ */
96
+ async selectWorkflows() {
97
+ const available = this.installer.getAvailableWorkflows();
98
+
99
+ const { workflows } = await inquirer.prompt([
100
+ {
101
+ type: 'checkbox',
102
+ name: 'workflows',
103
+ message: 'Select workflows to install:',
104
+ choices: available.map(w => ({
105
+ name: `${w.name} - ${w.description}`,
106
+ value: w.id,
107
+ checked: w.id === 'core'
108
+ })),
109
+ validate: (answer) => {
110
+ if (answer.length === 0) {
111
+ return 'Please select at least one workflow';
112
+ }
113
+ return true;
114
+ }
115
+ }
116
+ ]);
117
+
118
+ return workflows;
119
+ }
120
+
121
+ /**
122
+ * Install a single workflow
123
+ * @param {string} workflow - Workflow ID
124
+ * @param {string} cliType - CLI type
125
+ * @param {Object} options - Installation options
126
+ */
127
+ async installWorkflow(workflow, cliType, options = {}) {
128
+ const { verbose = false } = options;
129
+
130
+ if (verbose) {
131
+ logger.info(`Installing workflow: ${workflow}`);
132
+ }
133
+
134
+ const spinner = createSpinner(`Installing ${workflow}...`);
135
+ spinner.start();
136
+
137
+ try {
138
+ const result = await this.installer.install(workflow, this.targetDir, cliType);
139
+
140
+ if (result.success) {
141
+ spinner.succeed(`${workflow} installed`);
142
+
143
+ if (verbose && result.installed) {
144
+ if (result.installed.agents?.length > 0) {
145
+ logger.info(` Agents: ${result.installed.agents.join(', ')}`);
146
+ }
147
+ if (result.installed.commands?.length > 0) {
148
+ logger.info(` Commands: ${result.installed.commands.join(', ')}`);
149
+ }
150
+ }
151
+ } else {
152
+ spinner.fail(`${workflow} failed`);
153
+ if (result.errors) {
154
+ for (const error of result.errors) {
155
+ logger.error(` ${error}`);
156
+ }
157
+ }
158
+ }
159
+
160
+ return result;
161
+ } catch (error) {
162
+ spinner.fail(`${workflow} error: ${error.message}`);
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Show installation summary
169
+ */
170
+ async showSummary() {
171
+ logger.section('Installation Summary');
172
+
173
+ // Count installed items
174
+ const claudeDir = path.join(this.targetDir, '.claude');
175
+ const agentsDir = path.join(claudeDir, 'agents');
176
+ const commandsDir = path.join(claudeDir, 'commands');
177
+
178
+ let agentCount = 0;
179
+ let commandCount = 0;
180
+
181
+ try {
182
+ if (await fs.pathExists(agentsDir)) {
183
+ const agents = await fs.readdir(agentsDir);
184
+ agentCount = agents.filter(f => f.endsWith('.md')).length;
185
+ }
186
+ if (await fs.pathExists(commandsDir)) {
187
+ const commands = await fs.readdir(commandsDir);
188
+ commandCount = commands.filter(f => f.endsWith('.md')).length;
189
+ }
190
+ } catch (error) {
191
+ // Ignore errors
192
+ }
193
+
194
+ logger.log(`Workflows installed: ${this.installedWorkflows.length}`);
195
+ logger.log(` - ${this.installedWorkflows.join(', ')}`);
196
+ logger.blank();
197
+ logger.log(`Agents: ${agentCount}`);
198
+ logger.log(`Commands: ${commandCount}`);
199
+ logger.blank();
200
+
201
+ // Show next steps
202
+ logger.section('Next Steps');
203
+ logger.log('1. Configure your settings:');
204
+ logger.log(' Run: /myai-configure');
205
+ logger.blank();
206
+ logger.log('2. Available commands:');
207
+
208
+ if (this.installedWorkflows.includes('content') || this.installedWorkflows.includes('core')) {
209
+ logger.log(' /myai-content-writer - Create SEO-optimized content');
210
+ }
211
+ if (this.installedWorkflows.includes('content') || this.installedWorkflows.includes('publish-wordpress')) {
212
+ logger.log(' /myai-wordpress-publish - Publish to WordPress');
213
+ }
214
+ if (this.installedWorkflows.includes('content') || this.installedWorkflows.includes('core')) {
215
+ logger.log(' /myai-coordinate-content - Coordinate content workflow');
216
+ }
217
+
218
+ logger.blank();
219
+ }
220
+
221
+ /**
222
+ * Check if configuration is needed
223
+ * @returns {boolean}
224
+ */
225
+ needsConfiguration() {
226
+ const needsWordPress = this.installedWorkflows.some(w =>
227
+ ['content', 'publish-wordpress'].includes(w)
228
+ );
229
+ const needsOpenStack = this.installedWorkflows.some(w =>
230
+ ['openstack'].includes(w)
231
+ );
232
+
233
+ return needsWordPress || needsOpenStack;
234
+ }
235
+
236
+ /**
237
+ * Offer to run configuration
238
+ */
239
+ async offerConfiguration() {
240
+ const { configure } = await inquirer.prompt([
241
+ {
242
+ type: 'confirm',
243
+ name: 'configure',
244
+ message: 'Would you like to configure your settings now?',
245
+ default: true
246
+ }
247
+ ]);
248
+
249
+ if (configure) {
250
+ // Import and run configure script
251
+ const { default: runConfigure } = await import('./configure.js');
252
+ // Note: This will run the configure wizard
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Uninstall workflows
258
+ * @param {string[]} workflows - Workflows to uninstall
259
+ */
260
+ async uninstall(workflows) {
261
+ logger.header('Uninstalling Workflows');
262
+
263
+ for (const workflow of workflows) {
264
+ const spinner = createSpinner(`Uninstalling ${workflow}...`);
265
+ spinner.start();
266
+
267
+ try {
268
+ // Get workflow info
269
+ const workflowInfo = this.installer.getWorkflow(workflow);
270
+ if (!workflowInfo) {
271
+ spinner.warn(`Workflow not found: ${workflow}`);
272
+ continue;
273
+ }
274
+
275
+ // Remove agents
276
+ if (workflowInfo.agents) {
277
+ for (const agent of workflowInfo.agents) {
278
+ const agentPath = path.join(this.targetDir, '.claude', 'agents', `${agent}.md`);
279
+ await fs.remove(agentPath);
280
+ }
281
+ }
282
+
283
+ // Remove commands
284
+ if (workflowInfo.commands) {
285
+ for (const command of workflowInfo.commands) {
286
+ const commandPath = path.join(this.targetDir, '.claude', 'commands', `${command}.md`);
287
+ await fs.remove(commandPath);
288
+ }
289
+ }
290
+
291
+ spinner.succeed(`${workflow} uninstalled`);
292
+ } catch (error) {
293
+ spinner.fail(`Failed to uninstall ${workflow}: ${error.message}`);
294
+ }
295
+ }
296
+ }
297
+
298
+ /**
299
+ * List installed workflows
300
+ */
301
+ async listInstalled() {
302
+ logger.header('Installed Workflows');
303
+
304
+ const claudeDir = path.join(this.targetDir, '.claude');
305
+
306
+ if (!await fs.pathExists(claudeDir)) {
307
+ logger.info('No workflows installed.');
308
+ return;
309
+ }
310
+
311
+ const agentsDir = path.join(claudeDir, 'agents');
312
+ const commandsDir = path.join(claudeDir, 'commands');
313
+
314
+ logger.section('Agents');
315
+ if (await fs.pathExists(agentsDir)) {
316
+ const agents = await fs.readdir(agentsDir);
317
+ const mdAgents = agents.filter(f => f.endsWith('.md'));
318
+ if (mdAgents.length > 0) {
319
+ for (const agent of mdAgents) {
320
+ logger.log(` • ${agent.replace('.md', '')}`);
321
+ }
322
+ } else {
323
+ logger.info(' No agents installed.');
324
+ }
325
+ }
326
+
327
+ logger.section('Commands');
328
+ if (await fs.pathExists(commandsDir)) {
329
+ const commands = await fs.readdir(commandsDir);
330
+ const mdCommands = commands.filter(f => f.endsWith('.md'));
331
+ if (mdCommands.length > 0) {
332
+ for (const command of mdCommands) {
333
+ logger.log(` • ${command.replace('.md', '')}`);
334
+ }
335
+ } else {
336
+ logger.info(' No commands installed.');
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Parse command line arguments
344
+ * @param {string[]} args - Command line arguments
345
+ * @returns {Object} Parsed options
346
+ */
347
+ function parseArgs(args) {
348
+ const options = {
349
+ workflows: [],
350
+ cliType: 'claude',
351
+ interactive: false,
352
+ force: false,
353
+ verbose: false,
354
+ uninstall: false,
355
+ list: false,
356
+ help: false
357
+ };
358
+
359
+ let i = 0;
360
+ while (i < args.length) {
361
+ const arg = args[i];
362
+
363
+ if (arg === '--help' || arg === '-h') {
364
+ options.help = true;
365
+ } else if (arg === '--interactive' || arg === '-i') {
366
+ options.interactive = true;
367
+ } else if (arg === '--force' || arg === '-f') {
368
+ options.force = true;
369
+ } else if (arg === '--verbose' || arg === '-v') {
370
+ options.verbose = true;
371
+ } else if (arg === '--claude') {
372
+ options.cliType = 'claude';
373
+ } else if (arg === '--gemini') {
374
+ options.cliType = 'gemini';
375
+ } else if (arg === '--codex') {
376
+ options.cliType = 'codex';
377
+ } else if (arg === '--uninstall' || arg === '-u') {
378
+ options.uninstall = true;
379
+ } else if (arg === '--list' || arg === '-l') {
380
+ options.list = true;
381
+ } else if (!arg.startsWith('-')) {
382
+ options.workflows.push(arg);
383
+ }
384
+
385
+ i++;
386
+ }
387
+
388
+ return options;
389
+ }
390
+
391
+ /**
392
+ * Show help message
393
+ */
394
+ function showHelp() {
395
+ console.log(`
396
+ MyAIDev Method - Installation Script
397
+
398
+ Usage: node install.js [workflows...] [options]
399
+
400
+ Workflows:
401
+ core Core workflow with configuration tools
402
+ content Content creation with SEO optimization
403
+ visual Visual content generation
404
+ publish-wordpress WordPress publishing
405
+ development Development tools
406
+ security-pentest Security penetration testing
407
+ security-audit Security auditing
408
+
409
+ Options:
410
+ -h, --help Show this help message
411
+ -i, --interactive Run interactive workflow selection
412
+ -f, --force Force overwrite existing files
413
+ -v, --verbose Show detailed output
414
+ --claude Configure for Claude Code (default)
415
+ --gemini Configure for Gemini CLI
416
+ --codex Configure for Codex CLI
417
+ -l, --list List installed workflows
418
+ -u, --uninstall Uninstall specified workflows
419
+
420
+ Examples:
421
+ node install.js core content --claude
422
+ node install.js --interactive
423
+ node install.js --list
424
+ node install.js content --uninstall
425
+ `);
426
+ }
427
+
428
+ /**
429
+ * Main function
430
+ */
431
+ async function main() {
432
+ const args = process.argv.slice(2);
433
+ const options = parseArgs(args);
434
+
435
+ if (options.help) {
436
+ showHelp();
437
+ process.exit(0);
438
+ }
439
+
440
+ const orchestrator = new InstallationOrchestrator();
441
+
442
+ try {
443
+ if (options.list) {
444
+ await orchestrator.listInstalled();
445
+ } else if (options.uninstall) {
446
+ if (options.workflows.length === 0) {
447
+ logger.error('Please specify workflows to uninstall.');
448
+ process.exit(1);
449
+ }
450
+ await orchestrator.uninstall(options.workflows);
451
+ } else {
452
+ await orchestrator.run(options);
453
+ }
454
+ } catch (error) {
455
+ logger.error(`Error: ${error.message}`);
456
+ process.exit(1);
457
+ }
458
+ }
459
+
460
+ main();
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { exec } from "child_process";
4
+ import { promisify } from "util";
5
+ import os from "os";
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ /**
10
+ * Location to IP mapping
11
+ */
12
+ export const LOCATIONS = {
13
+ "ashburn-va": "8.8.8.8", // Google DNS (East Coast reference)
14
+ "los-angeles": "1.1.1.1", // Cloudflare DNS (West Coast reference)
15
+ chicago: "8.8.4.4", // Google DNS secondary
16
+ europe: "1.0.0.1", // Cloudflare secondary
17
+ asia: "208.67.222.222", // OpenDNS
18
+ amsterdam: "213.165.253.121", // aditya's ramnode vm
19
+ };
20
+
21
+ /**
22
+ * Get device's local IP
23
+ */
24
+ function getLocalIP() {
25
+ const interfaces = os.networkInterfaces();
26
+
27
+ for (const name of Object.keys(interfaces)) {
28
+ for (const iface of interfaces[name]) {
29
+ // Skip internal and non-IPv4
30
+ if (iface.family === "IPv4" && !iface.internal) {
31
+ return iface.address;
32
+ }
33
+ }
34
+ }
35
+
36
+ return "127.0.0.1";
37
+ }
38
+
39
+ /**
40
+ * Get public IP and location info
41
+ */
42
+ async function getPublicIPInfo() {
43
+ try {
44
+ // Try ip-api.com first (free, no key needed)
45
+ const response = await fetch("http://ip-api.com/json/");
46
+ const data = await response.json();
47
+
48
+ // Check if we got valid data
49
+ if (data.status === "success" && data.query) {
50
+ return {
51
+ ip: data.query,
52
+ city: data.city || "Unknown City",
53
+ region: data.regionName || data.region,
54
+ country: data.country || "Unknown Country",
55
+ location: `${data.city}, ${data.country}`,
56
+ };
57
+ }
58
+
59
+ // Fallback if API returns error
60
+ throw new Error("Invalid response from IP API");
61
+ } catch (error) {
62
+ // Use local IP as fallback
63
+ const localIP = getLocalIP();
64
+ return {
65
+ ip: localIP,
66
+ location: `Local Network (${localIP})`,
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Simple ping function
73
+ * @param {string} destination - IP address or hostname to ping
74
+ * @param {string} source - Source IP (defaults to device IP)
75
+ * @returns {Promise<object>} Ping results
76
+ */
77
+ export async function ping(destination, source = null) {
78
+ const sourceIP = source || getLocalIP();
79
+
80
+ try {
81
+ const platform = process.platform;
82
+ const countFlag = platform === "win32" ? "-n" : "-c";
83
+ const command = `ping ${countFlag} 4 ${destination}`;
84
+
85
+ const { stdout } = await execAsync(command);
86
+
87
+ // Parse average latency
88
+ let avgLatency = null;
89
+
90
+ if (platform === "win32") {
91
+ const match = stdout.match(/Average = (\d+)ms/);
92
+ if (match) avgLatency = parseFloat(match[1]);
93
+ } else {
94
+ const match = stdout.match(/min\/avg\/max\/[^\s]+ = [\d.]+\/([\d.]+)\//);
95
+ if (match) avgLatency = parseFloat(match[1]);
96
+ }
97
+
98
+ return {
99
+ source: sourceIP,
100
+ destination,
101
+ latency: avgLatency,
102
+ unit: "ms",
103
+ success: true,
104
+ };
105
+ } catch (error) {
106
+ return {
107
+ source: sourceIP,
108
+ destination,
109
+ latency: null,
110
+ error: error.message,
111
+ success: false,
112
+ };
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Ping multiple locations
118
+ * @param {Array<string>} locations - Array of location keys from LOCATIONS
119
+ * @param {string} source - Source IP (optional)
120
+ * @returns {Promise<Array>} Array of ping results
121
+ */
122
+ export async function pingLocations(locations, source = null) {
123
+ const results = [];
124
+
125
+ for (const loc of locations) {
126
+ const ip = LOCATIONS[loc];
127
+ if (!ip) {
128
+ results.push({
129
+ location: loc,
130
+ error: "Unknown location",
131
+ success: false,
132
+ });
133
+ continue;
134
+ }
135
+
136
+ const result = await ping(ip, source);
137
+ results.push({
138
+ location: loc,
139
+ ...result,
140
+ });
141
+ }
142
+
143
+ return results;
144
+ }
145
+
146
+ /**
147
+ * Format ping results as markdown
148
+ * @param {Array} results - Array of ping results from pingLocations
149
+ * @param {object} options - Formatting options
150
+ * @returns {string} Markdown formatted output
151
+ */
152
+ export async function formatAsMarkdown(results, options = {}) {
153
+ const { includeMetadata = true, title = "Network Latency Test Results" } =
154
+ options;
155
+
156
+ let md = `## ${title}\n\n`;
157
+
158
+ if (includeMetadata && results.length > 0) {
159
+ const timestamp = new Date().toISOString();
160
+ const ipInfo = await getPublicIPInfo();
161
+
162
+ md += `**Test Date**: ${timestamp}\n`;
163
+ md += `**Source**: ${ipInfo.location} (${ipInfo.ip})\n\n`;
164
+ }
165
+
166
+ // Build table
167
+ md += "| Location | Destination | Latency | Status |\n";
168
+ md += "|----------|-------------|---------|--------|\n";
169
+
170
+ for (const result of results) {
171
+ const location = result.location || "N/A";
172
+ const destination = result.destination || "N/A";
173
+ const latency = result.success ? `${result.latency}ms` : "-";
174
+ const status = result.success
175
+ ? "✅ Success"
176
+ : `❌ ${result.error || "Failed"}`;
177
+
178
+ md += `| ${location} | ${destination} | ${latency} | ${status} |\n`;
179
+ }
180
+
181
+ // Add summary statistics
182
+ const successful = results.filter((r) => r.success);
183
+ if (successful.length > 0) {
184
+ md += "\n### Summary\n\n";
185
+
186
+ const latencies = successful.map((r) => r.latency);
187
+ const avgLatency = (
188
+ latencies.reduce((a, b) => a + b, 0) / latencies.length
189
+ ).toFixed(2);
190
+ const minLatency = Math.min(...latencies).toFixed(2);
191
+ const maxLatency = Math.max(...latencies).toFixed(2);
192
+
193
+ md += `- **Average Latency**: ${avgLatency}ms\n`;
194
+ md += `- **Best Latency**: ${minLatency}ms\n`;
195
+ md += `- **Worst Latency**: ${maxLatency}ms\n`;
196
+ md += `- **Success Rate**: ${successful.length}/${results.length} (${((successful.length / results.length) * 100).toFixed(0)}%)\n`;
197
+ }
198
+
199
+ return md;
200
+ }
201
+
202
+ // CLI interface
203
+ if (import.meta.url === `file://${process.argv[1]}`) {
204
+ const args = process.argv.slice(2);
205
+
206
+ // Check for --all flag
207
+ if (args.includes("--all")) {
208
+ console.log("Testing all locations...\n");
209
+ const allLocations = Object.keys(LOCATIONS);
210
+ const results = await pingLocations(allLocations);
211
+ const markdown = await formatAsMarkdown(results);
212
+ console.log(markdown);
213
+ process.exit(0);
214
+ }
215
+
216
+ // Check for multiple locations
217
+ if (args.length > 1) {
218
+ console.log(`Testing ${args.length} locations...\n`);
219
+ const results = await pingLocations(args);
220
+ const markdown = await formatAsMarkdown(results);
221
+ console.log(markdown);
222
+ process.exit(0);
223
+ }
224
+
225
+ const destination = args[0];
226
+
227
+ if (!destination) {
228
+ console.log("Usage:");
229
+ console.log(
230
+ " node ping.js <destination> # Single location (markdown output)",
231
+ );
232
+ console.log(
233
+ " node ping.js <loc1> <loc2> <loc3> # Multiple locations (markdown output)",
234
+ );
235
+ console.log(
236
+ " node ping.js --all # Test all locations (markdown output)",
237
+ );
238
+ console.log("\nAvailable locations:");
239
+ Object.keys(LOCATIONS).forEach((loc) => {
240
+ console.log(` ${loc} -> ${LOCATIONS[loc]}`);
241
+ });
242
+ process.exit(1);
243
+ }
244
+
245
+ // Single location - markdown output (consistent with multiple locations)
246
+ console.log(`Testing ${destination}...\n`);
247
+ const results = await pingLocations([destination]);
248
+ const markdown = await formatAsMarkdown(results);
249
+ console.log(markdown);
250
+ }