recker 1.0.43 → 1.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/dist/bin/recker-linux-x64 +0 -0
- package/dist/bin/recker-macos-x64 +0 -0
- package/dist/bin/recker-win-x64.exe +0 -0
- package/dist/bin/rek.cjs +85152 -100207
- package/dist/browser/ai/adaptive-timeout.d.ts +50 -0
- package/dist/browser/ai/adaptive-timeout.js +208 -0
- package/dist/browser/ai/client.d.ts +22 -0
- package/dist/browser/ai/client.js +294 -0
- package/dist/browser/ai/index.d.ts +14 -0
- package/dist/browser/ai/index.js +11 -0
- package/dist/browser/ai/providers/anthropic.d.ts +63 -0
- package/dist/browser/ai/providers/anthropic.js +370 -0
- package/dist/browser/ai/providers/base.d.ts +48 -0
- package/dist/browser/ai/providers/base.js +150 -0
- package/dist/browser/ai/providers/google.d.ts +59 -0
- package/dist/browser/ai/providers/google.js +305 -0
- package/dist/browser/ai/providers/ollama.d.ts +44 -0
- package/dist/browser/ai/providers/ollama.js +240 -0
- package/dist/browser/ai/providers/openai.d.ts +64 -0
- package/dist/browser/ai/providers/openai.js +298 -0
- package/dist/browser/ai/rate-limiter.d.ts +43 -0
- package/dist/browser/ai/rate-limiter.js +215 -0
- package/dist/browser/ai/vector/index.d.ts +2 -0
- package/dist/browser/ai/vector/index.js +2 -0
- package/dist/browser/ai/vector/similarity.d.ts +2 -0
- package/dist/browser/ai/vector/similarity.js +27 -0
- package/dist/browser/ai/vector/store.d.ts +27 -0
- package/dist/browser/ai/vector/store.js +82 -0
- package/dist/browser/browser/cache.d.ts +2 -40
- package/dist/browser/browser/cache.js +2 -199
- package/dist/browser/browser/index.d.ts +8 -0
- package/dist/browser/browser/index.js +8 -0
- package/dist/browser/browser/recker.d.ts +8 -1
- package/dist/browser/browser/recker.js +8 -2
- package/dist/browser/cache/indexed-db.d.ts +10 -0
- package/dist/browser/cache/indexed-db.js +88 -0
- package/dist/browser/cache/service-worker-cache.d.ts +18 -0
- package/dist/browser/cache/service-worker-cache.js +103 -0
- package/dist/browser/cache.d.ts +2 -40
- package/dist/browser/cache.js +2 -199
- package/dist/browser/constants/user-agents.d.ts +7 -0
- package/dist/browser/constants/user-agents.js +7 -0
- package/dist/browser/core/client.d.ts +2 -0
- package/dist/browser/core/client.js +19 -1
- package/dist/browser/index.d.ts +8 -0
- package/dist/browser/index.js +8 -0
- package/dist/browser/plugins/har-recorder.d.ts +40 -0
- package/dist/browser/plugins/har-recorder.js +120 -0
- package/dist/browser/plugins/network-simulation.d.ts +7 -0
- package/dist/browser/plugins/network-simulation.js +13 -0
- package/dist/browser/presets/android.d.ts +2 -0
- package/dist/browser/presets/android.js +16 -0
- package/dist/browser/presets/anthropic.d.ts +8 -0
- package/dist/browser/presets/anthropic.js +27 -0
- package/dist/browser/presets/aws.d.ts +19 -0
- package/dist/browser/presets/aws.js +68 -0
- package/dist/browser/presets/azure-openai.d.ts +10 -0
- package/dist/browser/presets/azure-openai.js +35 -0
- package/dist/browser/presets/azure.d.ts +41 -0
- package/dist/browser/presets/azure.js +104 -0
- package/dist/browser/presets/chaturbate.d.ts +2 -0
- package/dist/browser/presets/chaturbate.js +17 -0
- package/dist/browser/presets/cloudflare.d.ts +12 -0
- package/dist/browser/presets/cloudflare.js +39 -0
- package/dist/browser/presets/cohere.d.ts +7 -0
- package/dist/browser/presets/cohere.js +22 -0
- package/dist/browser/presets/deepseek.d.ts +7 -0
- package/dist/browser/presets/deepseek.js +22 -0
- package/dist/browser/presets/digitalocean.d.ts +5 -0
- package/dist/browser/presets/digitalocean.js +16 -0
- package/dist/browser/presets/discord.d.ts +6 -0
- package/dist/browser/presets/discord.js +17 -0
- package/dist/browser/presets/elevenlabs.d.ts +6 -0
- package/dist/browser/presets/elevenlabs.js +20 -0
- package/dist/browser/presets/enhancers.d.ts +20 -0
- package/dist/browser/presets/enhancers.js +85 -0
- package/dist/browser/presets/fireworks.d.ts +7 -0
- package/dist/browser/presets/fireworks.js +22 -0
- package/dist/browser/presets/gcp.d.ts +34 -0
- package/dist/browser/presets/gcp.js +91 -0
- package/dist/browser/presets/gemini.d.ts +7 -0
- package/dist/browser/presets/gemini.js +23 -0
- package/dist/browser/presets/github.d.ts +6 -0
- package/dist/browser/presets/github.js +17 -0
- package/dist/browser/presets/gitlab.d.ts +6 -0
- package/dist/browser/presets/gitlab.js +16 -0
- package/dist/browser/presets/groq.d.ts +7 -0
- package/dist/browser/presets/groq.js +22 -0
- package/dist/browser/presets/hubspot.d.ts +9 -0
- package/dist/browser/presets/hubspot.js +28 -0
- package/dist/browser/presets/huggingface.d.ts +7 -0
- package/dist/browser/presets/huggingface.js +23 -0
- package/dist/browser/presets/index.d.ts +47 -0
- package/dist/browser/presets/index.js +47 -0
- package/dist/browser/presets/ios.d.ts +2 -0
- package/dist/browser/presets/ios.js +13 -0
- package/dist/browser/presets/linear.d.ts +5 -0
- package/dist/browser/presets/linear.js +16 -0
- package/dist/browser/presets/mailgun.d.ts +7 -0
- package/dist/browser/presets/mailgun.js +20 -0
- package/dist/browser/presets/meta.d.ts +10 -0
- package/dist/browser/presets/meta.js +33 -0
- package/dist/browser/presets/mistral.d.ts +7 -0
- package/dist/browser/presets/mistral.js +22 -0
- package/dist/browser/presets/notion.d.ts +6 -0
- package/dist/browser/presets/notion.js +17 -0
- package/dist/browser/presets/openai.d.ts +9 -0
- package/dist/browser/presets/openai.js +30 -0
- package/dist/browser/presets/oracle.d.ts +19 -0
- package/dist/browser/presets/oracle.js +117 -0
- package/dist/browser/presets/perplexity.d.ts +7 -0
- package/dist/browser/presets/perplexity.js +22 -0
- package/dist/browser/presets/pinecone.d.ts +8 -0
- package/dist/browser/presets/pinecone.js +42 -0
- package/dist/browser/presets/registry.d.ts +23 -0
- package/dist/browser/presets/registry.js +519 -0
- package/dist/browser/presets/replicate.d.ts +7 -0
- package/dist/browser/presets/replicate.js +23 -0
- package/dist/browser/presets/sendgrid.d.ts +6 -0
- package/dist/browser/presets/sendgrid.js +20 -0
- package/dist/browser/presets/sentry.d.ts +11 -0
- package/dist/browser/presets/sentry.js +48 -0
- package/dist/browser/presets/sinch.d.ts +9 -0
- package/dist/browser/presets/sinch.js +39 -0
- package/dist/browser/presets/slack.d.ts +5 -0
- package/dist/browser/presets/slack.js +16 -0
- package/dist/browser/presets/square.d.ts +10 -0
- package/dist/browser/presets/square.js +33 -0
- package/dist/browser/presets/stripe.d.ts +7 -0
- package/dist/browser/presets/stripe.js +23 -0
- package/dist/browser/presets/supabase.d.ts +6 -0
- package/dist/browser/presets/supabase.js +18 -0
- package/dist/browser/presets/tiktok.d.ts +10 -0
- package/dist/browser/presets/tiktok.js +38 -0
- package/dist/browser/presets/together.d.ts +7 -0
- package/dist/browser/presets/together.js +22 -0
- package/dist/browser/presets/twilio.d.ts +6 -0
- package/dist/browser/presets/twilio.js +17 -0
- package/dist/browser/presets/vercel.d.ts +6 -0
- package/dist/browser/presets/vercel.js +23 -0
- package/dist/browser/presets/vultr.d.ts +5 -0
- package/dist/browser/presets/vultr.js +16 -0
- package/dist/browser/presets/xai.d.ts +8 -0
- package/dist/browser/presets/xai.js +23 -0
- package/dist/browser/presets/youtube.d.ts +5 -0
- package/dist/browser/presets/youtube.js +20 -0
- package/dist/browser/recker.d.ts +8 -1
- package/dist/browser/recker.js +8 -2
- package/dist/browser/scrape/document.d.ts +5 -4
- package/dist/browser/scrape/document.js +89 -76
- package/dist/browser/scrape/element.d.ts +10 -8
- package/dist/browser/scrape/element.js +295 -81
- package/dist/browser/scrape/extractors.d.ts +11 -11
- package/dist/browser/scrape/extractors.js +145 -113
- package/dist/browser/scrape/parser/back.d.ts +1 -0
- package/dist/browser/scrape/parser/back.js +3 -0
- package/dist/browser/scrape/parser/index.d.ts +20 -0
- package/dist/browser/scrape/parser/index.js +19 -0
- package/dist/browser/scrape/parser/matcher.d.ts +30 -0
- package/dist/browser/scrape/parser/matcher.js +99 -0
- package/dist/browser/scrape/parser/nodes/comment.d.ts +12 -0
- package/dist/browser/scrape/parser/nodes/comment.js +21 -0
- package/dist/browser/scrape/parser/nodes/html.d.ts +110 -0
- package/dist/browser/scrape/parser/nodes/html.js +978 -0
- package/dist/browser/scrape/parser/nodes/node.d.ts +18 -0
- package/dist/browser/scrape/parser/nodes/node.js +31 -0
- package/dist/browser/scrape/parser/nodes/text.d.ts +14 -0
- package/dist/browser/scrape/parser/nodes/text.js +30 -0
- package/dist/browser/scrape/parser/nodes/type.d.ts +6 -0
- package/dist/browser/scrape/parser/nodes/type.js +7 -0
- package/dist/browser/scrape/parser/parse.d.ts +1 -0
- package/dist/browser/scrape/parser/parse.js +1 -0
- package/dist/browser/scrape/parser/valid.d.ts +2 -0
- package/dist/browser/scrape/parser/valid.js +5 -0
- package/dist/browser/scrape/parser/void-tag.d.ts +7 -0
- package/dist/browser/scrape/parser/void-tag.js +43 -0
- package/dist/browser/scrape/types.d.ts +7 -0
- package/dist/browser/seo/analyzer.d.ts +59 -0
- package/dist/browser/seo/analyzer.js +1399 -0
- package/dist/browser/seo/keywords.d.ts +16 -0
- package/dist/browser/seo/keywords.js +55 -0
- package/dist/browser/seo/rules/accessibility.d.ts +2 -0
- package/dist/browser/seo/rules/accessibility.js +733 -0
- package/dist/browser/seo/rules/ai-search.d.ts +2 -0
- package/dist/browser/seo/rules/ai-search.js +436 -0
- package/dist/browser/seo/rules/analytics.d.ts +2 -0
- package/dist/browser/seo/rules/analytics.js +306 -0
- package/dist/browser/seo/rules/best-practices.d.ts +2 -0
- package/dist/browser/seo/rules/best-practices.js +195 -0
- package/dist/browser/seo/rules/canonical.d.ts +12 -0
- package/dist/browser/seo/rules/canonical.js +270 -0
- package/dist/browser/seo/rules/content.d.ts +2 -0
- package/dist/browser/seo/rules/content.js +522 -0
- package/dist/browser/seo/rules/crawl.d.ts +2 -0
- package/dist/browser/seo/rules/crawl.js +435 -0
- package/dist/browser/seo/rules/cwv.d.ts +2 -0
- package/dist/browser/seo/rules/cwv.js +248 -0
- package/dist/browser/seo/rules/ecommerce.d.ts +2 -0
- package/dist/browser/seo/rules/ecommerce.js +312 -0
- package/dist/browser/seo/rules/i18n.d.ts +2 -0
- package/dist/browser/seo/rules/i18n.js +288 -0
- package/dist/browser/seo/rules/images.d.ts +2 -0
- package/dist/browser/seo/rules/images.js +255 -0
- package/dist/browser/seo/rules/index.d.ts +52 -0
- package/dist/browser/seo/rules/index.js +159 -0
- package/dist/browser/seo/rules/internal-linking.d.ts +2 -0
- package/dist/browser/seo/rules/internal-linking.js +394 -0
- package/dist/browser/seo/rules/links.d.ts +2 -0
- package/dist/browser/seo/rules/links.js +498 -0
- package/dist/browser/seo/rules/local.d.ts +2 -0
- package/dist/browser/seo/rules/local.js +289 -0
- package/dist/browser/seo/rules/meta.d.ts +2 -0
- package/dist/browser/seo/rules/meta.js +805 -0
- package/dist/browser/seo/rules/mobile.d.ts +2 -0
- package/dist/browser/seo/rules/mobile.js +161 -0
- package/dist/browser/seo/rules/performance.d.ts +2 -0
- package/dist/browser/seo/rules/performance.js +738 -0
- package/dist/browser/seo/rules/pwa.d.ts +2 -0
- package/dist/browser/seo/rules/pwa.js +299 -0
- package/dist/browser/seo/rules/readability.d.ts +2 -0
- package/dist/browser/seo/rules/readability.js +264 -0
- package/dist/browser/seo/rules/redirects.d.ts +16 -0
- package/dist/browser/seo/rules/redirects.js +199 -0
- package/dist/browser/seo/rules/resources.d.ts +2 -0
- package/dist/browser/seo/rules/resources.js +390 -0
- package/dist/browser/seo/rules/schema.d.ts +2 -0
- package/dist/browser/seo/rules/schema.js +379 -0
- package/dist/browser/seo/rules/security.d.ts +2 -0
- package/dist/browser/seo/rules/security.js +877 -0
- package/dist/browser/seo/rules/social.d.ts +2 -0
- package/dist/browser/seo/rules/social.js +603 -0
- package/dist/browser/seo/rules/structural.d.ts +2 -0
- package/dist/browser/seo/rules/structural.js +223 -0
- package/dist/browser/seo/rules/technical-advanced.d.ts +10 -0
- package/dist/browser/seo/rules/technical-advanced.js +289 -0
- package/dist/browser/seo/rules/technical.d.ts +2 -0
- package/dist/browser/seo/rules/technical.js +480 -0
- package/dist/browser/seo/rules/thresholds.d.ts +196 -0
- package/dist/browser/seo/rules/thresholds.js +118 -0
- package/dist/browser/seo/rules/types.d.ts +498 -0
- package/dist/browser/seo/rules/types.js +11 -0
- package/dist/browser/seo/types.d.ts +211 -0
- package/dist/browser/seo/types.js +1 -0
- package/dist/browser/transport/curl.d.ts +4 -0
- package/dist/browser/transport/curl.js +101 -0
- package/dist/browser/transport/undici.js +1 -2
- package/dist/browser/transport/worker.d.ts +18 -0
- package/dist/browser/transport/worker.js +278 -0
- package/dist/browser/types/index.d.ts +4 -1
- package/dist/browser/utils/binary-manager.d.ts +4 -0
- package/dist/browser/utils/binary-manager.js +72 -0
- package/dist/browser/utils/user-agent.js +2 -13
- package/dist/cache/indexed-db.d.ts +10 -0
- package/dist/cache/indexed-db.js +88 -0
- package/dist/cache/service-worker-cache.d.ts +18 -0
- package/dist/cache/service-worker-cache.js +103 -0
- package/dist/cli/commands/ai.d.ts +2 -0
- package/dist/cli/commands/ai.js +162 -0
- package/dist/cli/commands/bench.d.ts +2 -0
- package/dist/cli/commands/bench.js +51 -0
- package/dist/cli/commands/dns.d.ts +2 -0
- package/dist/cli/commands/dns.js +295 -0
- package/dist/cli/commands/har.d.ts +2 -0
- package/dist/cli/commands/har.js +171 -0
- package/dist/cli/commands/hls.d.ts +2 -0
- package/dist/cli/commands/hls.js +192 -0
- package/dist/cli/commands/network.d.ts +2 -0
- package/dist/cli/commands/network.js +288 -0
- package/dist/cli/commands/protocols.d.ts +2 -0
- package/dist/cli/commands/protocols.js +344 -0
- package/dist/cli/commands/scrape.d.ts +2 -0
- package/dist/cli/commands/scrape.js +176 -0
- package/dist/cli/commands/security.d.ts +2 -0
- package/dist/cli/commands/security.js +57 -0
- package/dist/cli/commands/seo.d.ts +2 -0
- package/dist/cli/commands/seo.js +125 -0
- package/dist/cli/commands/serve.d.ts +2 -0
- package/dist/cli/commands/serve.js +531 -0
- package/dist/cli/commands/spider.d.ts +3 -0
- package/dist/cli/commands/spider.js +456 -0
- package/dist/cli/commands/utils.d.ts +2 -0
- package/dist/cli/commands/utils.js +176 -0
- package/dist/cli/commands/vector.d.ts +2 -0
- package/dist/cli/commands/vector.js +158 -0
- package/dist/cli/handler.d.ts +2 -2
- package/dist/cli/handler.js +6 -6
- package/dist/cli/helpers.d.ts +7 -0
- package/dist/cli/helpers.js +128 -0
- package/dist/cli/index.js +96 -5228
- package/dist/cli/parser/help.d.ts +2 -0
- package/dist/cli/parser/help.js +52 -0
- package/dist/cli/parser/index.d.ts +3 -0
- package/dist/cli/parser/index.js +3 -0
- package/dist/cli/parser/parser.d.ts +4 -0
- package/dist/cli/parser/parser.js +146 -0
- package/dist/cli/parser/types.d.ts +41 -0
- package/dist/cli/parser/types.js +1 -0
- package/dist/cli/presets.d.ts +1 -1
- package/dist/cli/presets.js +1 -1
- package/dist/cli/router.d.ts +36 -0
- package/dist/cli/router.js +195 -0
- package/dist/cli/tui/ai-chat.js +1 -1
- package/dist/cli/tui/commands/context.d.ts +9 -0
- package/dist/cli/tui/commands/context.js +1 -0
- package/dist/cli/tui/commands/dns.d.ts +10 -0
- package/dist/cli/tui/commands/dns.js +461 -0
- package/dist/cli/tui/commands/hls.d.ts +2 -0
- package/dist/cli/tui/commands/hls.js +162 -0
- package/dist/cli/tui/commands/ip.d.ts +2 -0
- package/dist/cli/tui/commands/ip.js +45 -0
- package/dist/cli/tui/commands/network.d.ts +3 -0
- package/dist/cli/tui/commands/network.js +81 -0
- package/dist/cli/tui/commands/protocols.d.ts +6 -0
- package/dist/cli/tui/commands/protocols.js +531 -0
- package/dist/cli/tui/commands/security.d.ts +2 -0
- package/dist/cli/tui/commands/security.js +48 -0
- package/dist/cli/tui/commands/seo.d.ts +2 -0
- package/dist/cli/tui/commands/seo.js +74 -0
- package/dist/cli/tui/context.d.ts +12 -0
- package/dist/cli/tui/context.js +1 -0
- package/dist/cli/tui/shell.d.ts +11 -20
- package/dist/cli/tui/shell.js +216 -1873
- package/dist/constants/user-agents.d.ts +7 -0
- package/dist/constants/user-agents.js +7 -0
- package/dist/core/client.d.ts +2 -0
- package/dist/core/client.js +19 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/cli.js +2 -3
- package/dist/mcp/data/embeddings.json +1 -1
- package/dist/mcp/tools/network.js +298 -158
- package/dist/plugins/har-player.d.ts +23 -0
- package/dist/plugins/har-player.js +49 -0
- package/dist/plugins/har-recorder.d.ts +37 -3
- package/dist/plugins/har-recorder.js +116 -63
- package/dist/plugins/network-simulation.d.ts +7 -0
- package/dist/plugins/network-simulation.js +13 -0
- package/dist/presets/android.d.ts +2 -0
- package/dist/presets/android.js +16 -0
- package/dist/presets/chaturbate.d.ts +2 -0
- package/dist/presets/chaturbate.js +17 -0
- package/dist/presets/elevenlabs.d.ts +6 -0
- package/dist/presets/elevenlabs.js +20 -0
- package/dist/presets/enhancers.d.ts +20 -0
- package/dist/presets/enhancers.js +85 -0
- package/dist/presets/hubspot.d.ts +9 -0
- package/dist/presets/hubspot.js +28 -0
- package/dist/presets/index.d.ts +10 -0
- package/dist/presets/index.js +10 -0
- package/dist/presets/ios.d.ts +2 -0
- package/dist/presets/ios.js +13 -0
- package/dist/presets/pinecone.d.ts +8 -0
- package/dist/presets/pinecone.js +42 -0
- package/dist/presets/registry.js +60 -0
- package/dist/presets/sendgrid.d.ts +6 -0
- package/dist/presets/sendgrid.js +20 -0
- package/dist/presets/sentry.d.ts +11 -0
- package/dist/presets/sentry.js +48 -0
- package/dist/presets/square.d.ts +10 -0
- package/dist/presets/square.js +33 -0
- package/dist/recker.d.ts +3 -0
- package/dist/recker.js +4 -0
- package/dist/scrape/document.d.ts +5 -4
- package/dist/scrape/document.js +89 -76
- package/dist/scrape/element.d.ts +10 -8
- package/dist/scrape/element.js +295 -81
- package/dist/scrape/extractors.d.ts +11 -11
- package/dist/scrape/extractors.js +145 -113
- package/dist/scrape/index.d.ts +2 -0
- package/dist/scrape/index.js +1 -0
- package/dist/scrape/parser/back.d.ts +1 -0
- package/dist/scrape/parser/back.js +3 -0
- package/dist/scrape/parser/index.d.ts +20 -0
- package/dist/scrape/parser/index.js +19 -0
- package/dist/scrape/parser/matcher.d.ts +30 -0
- package/dist/scrape/parser/matcher.js +99 -0
- package/dist/scrape/parser/nodes/comment.d.ts +12 -0
- package/dist/scrape/parser/nodes/comment.js +21 -0
- package/dist/scrape/parser/nodes/html.d.ts +110 -0
- package/dist/scrape/parser/nodes/html.js +978 -0
- package/dist/scrape/parser/nodes/node.d.ts +18 -0
- package/dist/scrape/parser/nodes/node.js +31 -0
- package/dist/scrape/parser/nodes/text.d.ts +14 -0
- package/dist/scrape/parser/nodes/text.js +30 -0
- package/dist/scrape/parser/nodes/type.d.ts +6 -0
- package/dist/scrape/parser/nodes/type.js +7 -0
- package/dist/scrape/parser/parse.d.ts +1 -0
- package/dist/scrape/parser/parse.js +1 -0
- package/dist/scrape/parser/valid.d.ts +2 -0
- package/dist/scrape/parser/valid.js +5 -0
- package/dist/scrape/parser/void-tag.d.ts +7 -0
- package/dist/scrape/parser/void-tag.js +43 -0
- package/dist/scrape/spider.d.ts +19 -0
- package/dist/scrape/spider.js +28 -3
- package/dist/scrape/types.d.ts +7 -0
- package/dist/seo/analyzer.d.ts +15 -5
- package/dist/seo/analyzer.js +636 -175
- package/dist/seo/formatter.d.ts +16 -0
- package/dist/seo/formatter.js +228 -0
- package/dist/seo/index.d.ts +2 -0
- package/dist/seo/index.js +1 -0
- package/dist/seo/keywords.d.ts +16 -0
- package/dist/seo/keywords.js +55 -0
- package/dist/seo/rules/accessibility.js +96 -57
- package/dist/seo/rules/ai-search.js +44 -31
- package/dist/seo/rules/analytics.d.ts +2 -0
- package/dist/seo/rules/analytics.js +306 -0
- package/dist/seo/rules/best-practices.js +21 -14
- package/dist/seo/rules/canonical.js +53 -32
- package/dist/seo/rules/content.js +317 -31
- package/dist/seo/rules/crawl.js +55 -40
- package/dist/seo/rules/cwv.js +21 -15
- package/dist/seo/rules/ecommerce.js +82 -22
- package/dist/seo/rules/i18n.js +75 -36
- package/dist/seo/rules/images.js +109 -30
- package/dist/seo/rules/index.js +2 -0
- package/dist/seo/rules/internal-linking.js +58 -39
- package/dist/seo/rules/links.js +79 -52
- package/dist/seo/rules/local.js +49 -25
- package/dist/seo/rules/meta.js +339 -81
- package/dist/seo/rules/mobile.js +112 -2
- package/dist/seo/rules/performance.js +434 -66
- package/dist/seo/rules/pwa.js +36 -39
- package/dist/seo/rules/readability.js +31 -22
- package/dist/seo/rules/redirects.js +21 -15
- package/dist/seo/rules/resources.js +59 -42
- package/dist/seo/rules/schema.js +333 -8
- package/dist/seo/rules/security.js +142 -80
- package/dist/seo/rules/social.js +277 -47
- package/dist/seo/rules/structural.js +87 -19
- package/dist/seo/rules/technical-advanced.js +30 -24
- package/dist/seo/rules/technical.js +243 -42
- package/dist/seo/rules/types.d.ts +53 -1
- package/dist/seo/seo-spider.d.ts +22 -0
- package/dist/seo/seo-spider.js +77 -13
- package/dist/seo/types.d.ts +8 -1
- package/dist/seo/validators/llms-txt.js +19 -0
- package/dist/seo/validators/rss.d.ts +11 -0
- package/dist/seo/validators/rss.js +93 -0
- package/dist/seo/validators/sitemap.js +36 -26
- package/dist/transport/curl.d.ts +4 -0
- package/dist/transport/curl.js +101 -0
- package/dist/transport/udp.js +0 -1
- package/dist/transport/undici.js +1 -2
- package/dist/transport/worker.d.ts +18 -0
- package/dist/transport/worker.js +278 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/utils/binary-manager.d.ts +4 -0
- package/dist/utils/binary-manager.js +72 -0
- package/dist/utils/optional-require.d.ts +7 -8
- package/dist/utils/optional-require.js +2 -21
- package/dist/utils/upload.d.ts +6 -0
- package/dist/utils/upload.js +11 -0
- package/dist/utils/user-agent.js +2 -13
- package/dist/version.js +1 -1
- package/package.json +12 -6
- package/dist/browser/utils/optional-require.d.ts +0 -19
- package/dist/browser/utils/optional-require.js +0 -105
package/dist/cli/tui/shell.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import readline from 'node:readline';
|
|
2
|
-
import { promises as dns } from 'node:dns';
|
|
3
2
|
import { promises as fs } from 'node:fs';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import { requireOptional } from '../../utils/optional-require.js';
|
|
@@ -7,18 +6,22 @@ import { createClient } from '../../core/client.js';
|
|
|
7
6
|
import { startInteractiveWebSocket } from './websocket.js';
|
|
8
7
|
import { whois, isDomainAvailable } from '../../utils/whois.js';
|
|
9
8
|
import { inspectTLS } from '../../utils/tls-inspector.js';
|
|
10
|
-
import { getSecurityRecords } from '../../utils/dns-toolkit.js';
|
|
11
|
-
import { rdap } from '../../utils/rdap.js';
|
|
12
9
|
import { ScrapeDocument } from '../../scrape/document.js';
|
|
13
|
-
import { Spider } from '../../scrape/spider.js';
|
|
14
10
|
import colors from '../../utils/colors.js';
|
|
15
11
|
import { getShellSearch } from './shell-search.js';
|
|
16
12
|
import { openSearchPanel } from './search-panel.js';
|
|
17
13
|
import { ScrollBuffer, parseScrollKey, parseMouseScroll, disableMouseReporting } from './scroll-buffer.js';
|
|
18
|
-
import { analyzeSeo, SeoSpider } from '../../seo/index.js';
|
|
19
14
|
import { resolvePreset } from '../presets.js';
|
|
20
|
-
import { summarizeErrors, formatErrorSummary } from '../helpers.js';
|
|
21
15
|
import { getVersion } from '../../version.js';
|
|
16
|
+
import { parseEnhancerPresets } from '../helpers.js';
|
|
17
|
+
import { runHls } from './commands/hls.js';
|
|
18
|
+
import { runSeo } from './commands/seo.js';
|
|
19
|
+
import { runIpIntelligence } from './commands/ip.js';
|
|
20
|
+
import { runSecurityGrader } from './commands/security.js';
|
|
21
|
+
import { runSpider } from '../commands/spider.js';
|
|
22
|
+
import { runRDAP, runPing } from './commands/network.js';
|
|
23
|
+
import { runDns, runDnsPropagation, runDnsEmailCheck, runDnsHealth, runDnsSpf, runDnsDmarc, runDnsDkim, runDnsDig, runDnsGenerate } from './commands/dns.js';
|
|
24
|
+
import { runFtp, runTelnet, runGraphQL, runJsonRpc, runHar } from './commands/protocols.js';
|
|
22
25
|
let highlight;
|
|
23
26
|
async function initDependencies() {
|
|
24
27
|
if (!highlight) {
|
|
@@ -45,6 +48,12 @@ export class RekShell {
|
|
|
45
48
|
currentDocUrl = '';
|
|
46
49
|
scrollBuffer;
|
|
47
50
|
originalStdoutWrite = null;
|
|
51
|
+
printResponse(res) {
|
|
52
|
+
console.log(JSON.stringify(res, null, 2));
|
|
53
|
+
}
|
|
54
|
+
printError(err) {
|
|
55
|
+
console.error(colors.red(err.message || String(err)));
|
|
56
|
+
}
|
|
48
57
|
inScrollMode = false;
|
|
49
58
|
aiClients = new Map();
|
|
50
59
|
constructor() {
|
|
@@ -349,7 +358,13 @@ export class RekShell {
|
|
|
349
358
|
}
|
|
350
359
|
if (input.includes('=') && !input.includes(' ') && !input.startsWith('http')) {
|
|
351
360
|
}
|
|
352
|
-
const
|
|
361
|
+
const rawParts = this.parseLine(input);
|
|
362
|
+
const { clientOptions, remainingArgs: parts } = await parseEnhancerPresets(rawParts);
|
|
363
|
+
const enhancerHeaders = clientOptions.headers || {};
|
|
364
|
+
const headerArgs = Object.entries(enhancerHeaders).map(([k, v]) => `${k}:${v}`);
|
|
365
|
+
if (headerArgs.length > 0) {
|
|
366
|
+
parts.push(...headerArgs);
|
|
367
|
+
}
|
|
353
368
|
const cmd = parts[0].toLowerCase();
|
|
354
369
|
switch (cmd) {
|
|
355
370
|
case 'help':
|
|
@@ -397,13 +412,13 @@ export class RekShell {
|
|
|
397
412
|
await this.runTLS(parts[1], parts[2] ? parseInt(parts[2]) : 443);
|
|
398
413
|
return;
|
|
399
414
|
case 'security':
|
|
400
|
-
await
|
|
415
|
+
await runSecurityGrader(this, parts[1]);
|
|
401
416
|
return;
|
|
402
417
|
case 'seo':
|
|
403
|
-
await
|
|
418
|
+
await runSeo(this, parts.slice(1));
|
|
404
419
|
return;
|
|
405
420
|
case 'ip':
|
|
406
|
-
await
|
|
421
|
+
await runIpIntelligence(this, parts[1]);
|
|
407
422
|
return;
|
|
408
423
|
case 'dns':
|
|
409
424
|
await this.runDNS(parts[1]);
|
|
@@ -430,31 +445,31 @@ export class RekShell {
|
|
|
430
445
|
await this.runDnsDig(parts.slice(1));
|
|
431
446
|
return;
|
|
432
447
|
case 'dns:generate':
|
|
433
|
-
await
|
|
448
|
+
await runDnsGenerate(this, parts.slice(1));
|
|
434
449
|
return;
|
|
435
450
|
case 'rdap':
|
|
436
|
-
await
|
|
451
|
+
await runRDAP(this, parts[1]);
|
|
437
452
|
return;
|
|
438
453
|
case 'ping':
|
|
439
|
-
await
|
|
454
|
+
await runPing(this, parts[1]);
|
|
440
455
|
return;
|
|
441
456
|
case 'ftp':
|
|
442
|
-
await
|
|
457
|
+
await runFtp(this, parts.slice(1));
|
|
443
458
|
return;
|
|
444
459
|
case 'telnet':
|
|
445
|
-
await
|
|
460
|
+
await runTelnet(this, parts[1], parts[2]);
|
|
446
461
|
return;
|
|
447
462
|
case 'graphql':
|
|
448
|
-
await
|
|
463
|
+
await runGraphQL(this, parts.slice(1));
|
|
449
464
|
return;
|
|
450
465
|
case 'jsonrpc':
|
|
451
|
-
await
|
|
466
|
+
await runJsonRpc(this, parts.slice(1));
|
|
452
467
|
return;
|
|
453
468
|
case 'hls':
|
|
454
|
-
await
|
|
469
|
+
await runHls(this, parts.slice(1));
|
|
455
470
|
return;
|
|
456
471
|
case 'har':
|
|
457
|
-
await
|
|
472
|
+
await runHar(this, parts.slice(1));
|
|
458
473
|
return;
|
|
459
474
|
case 'har:record':
|
|
460
475
|
await this.runHarRecord(parts.slice(1));
|
|
@@ -502,7 +517,7 @@ export class RekShell {
|
|
|
502
517
|
await this.runScrap(parts[1]);
|
|
503
518
|
return;
|
|
504
519
|
case 'spider':
|
|
505
|
-
await
|
|
520
|
+
await runSpider(parts.slice(1), this.baseUrl);
|
|
506
521
|
return;
|
|
507
522
|
case '$':
|
|
508
523
|
await this.runSelect(parts.slice(1).join(' '));
|
|
@@ -643,7 +658,7 @@ export class RekShell {
|
|
|
643
658
|
try {
|
|
644
659
|
let client = this.aiClients.get(presetName);
|
|
645
660
|
if (!client) {
|
|
646
|
-
const presetConfig = resolvePreset(presetName);
|
|
661
|
+
const presetConfig = await resolvePreset(presetName);
|
|
647
662
|
if (!presetConfig) {
|
|
648
663
|
console.log(colors.red(`Unknown AI preset: @${presetName}`));
|
|
649
664
|
console.log(colors.gray('Available AI presets: openai, anthropic, groq, google, xai, mistral, cohere, deepseek, fireworks, together, perplexity'));
|
|
@@ -911,19 +926,22 @@ export class RekShell {
|
|
|
911
926
|
}
|
|
912
927
|
return;
|
|
913
928
|
}
|
|
914
|
-
if (url.startsWith('udp')) {
|
|
915
|
-
const { UDPTransport } = await import('../../transport/udp.js');
|
|
916
|
-
const transport = new UDPTransport(url);
|
|
929
|
+
if (url.startsWith('udp://')) {
|
|
917
930
|
const msg = Object.keys(body).length ? JSON.stringify(body) : 'ping';
|
|
918
931
|
console.log(colors.gray(`UDP packet -> ${url}`));
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
932
|
+
try {
|
|
933
|
+
const res = await this.client.request(url, {
|
|
934
|
+
method: 'POST',
|
|
935
|
+
body: msg
|
|
936
|
+
});
|
|
937
|
+
const text = await res.text();
|
|
938
|
+
console.log(colors.green('✔ Sent/Received'));
|
|
939
|
+
if (text)
|
|
940
|
+
console.log(text);
|
|
941
|
+
}
|
|
942
|
+
catch (e) {
|
|
943
|
+
console.error(colors.red(e.message));
|
|
944
|
+
}
|
|
927
945
|
return;
|
|
928
946
|
}
|
|
929
947
|
console.log(colors.gray(`${method} ${url}...`));
|
|
@@ -1127,1541 +1145,227 @@ export class RekShell {
|
|
|
1127
1145
|
}
|
|
1128
1146
|
console.log('');
|
|
1129
1147
|
}
|
|
1130
|
-
async
|
|
1148
|
+
async runDNS(domain) {
|
|
1149
|
+
await runDns(this, domain);
|
|
1150
|
+
}
|
|
1151
|
+
async runDNSPropagation(domain, type = 'A') {
|
|
1152
|
+
await runDnsPropagation(this, domain, type);
|
|
1153
|
+
}
|
|
1154
|
+
async runDnsEmailCheck(domain, selector) {
|
|
1155
|
+
await runDnsEmailCheck(this, domain, selector);
|
|
1156
|
+
}
|
|
1157
|
+
async runDnsHealth(domain) {
|
|
1158
|
+
await runDnsHealth(this, domain);
|
|
1159
|
+
}
|
|
1160
|
+
async runDnsSpf(domain) {
|
|
1161
|
+
await runDnsSpf(this, domain);
|
|
1162
|
+
}
|
|
1163
|
+
async runDnsDmarc(domain) {
|
|
1164
|
+
await runDnsDmarc(this, domain);
|
|
1165
|
+
}
|
|
1166
|
+
async runDnsDkim(domain, selector) {
|
|
1167
|
+
await runDnsDkim(this, domain, selector);
|
|
1168
|
+
}
|
|
1169
|
+
async runDnsDig(args) {
|
|
1170
|
+
await runDnsDig(this, args);
|
|
1171
|
+
}
|
|
1172
|
+
async runDnsGenerate(args) {
|
|
1173
|
+
await runDnsGenerate(this, args);
|
|
1174
|
+
}
|
|
1175
|
+
async runScrap(url) {
|
|
1131
1176
|
if (!url) {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
console.log(colors.
|
|
1135
|
-
console.log(colors.gray(' Examples: security google.com | security https://example.com'));
|
|
1177
|
+
if (!this.baseUrl) {
|
|
1178
|
+
console.log(colors.yellow('Usage: scrap <url>'));
|
|
1179
|
+
console.log(colors.gray(' Examples: scrap https://news.ycombinator.com'));
|
|
1136
1180
|
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
1137
1181
|
return;
|
|
1138
1182
|
}
|
|
1183
|
+
url = this.baseUrl;
|
|
1139
1184
|
}
|
|
1140
1185
|
else if (!url.startsWith('http')) {
|
|
1141
|
-
url = `https://${url}`;
|
|
1186
|
+
url = this.baseUrl ? `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}` : `https://${url}`;
|
|
1142
1187
|
}
|
|
1143
|
-
console.log(colors.gray(`
|
|
1188
|
+
console.log(colors.gray(`Fetching ${url}...`));
|
|
1189
|
+
const startTime = performance.now();
|
|
1144
1190
|
try {
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1168
|
-
});
|
|
1169
|
-
console.log('');
|
|
1170
|
-
this.lastResponse = report;
|
|
1191
|
+
const response = await this.client.get(url);
|
|
1192
|
+
const html = await response.text();
|
|
1193
|
+
const duration = Math.round(performance.now() - startTime);
|
|
1194
|
+
this.currentDoc = await ScrapeDocument.create(html, { baseUrl: url });
|
|
1195
|
+
this.currentDocUrl = url;
|
|
1196
|
+
const title = this.currentDoc.selectFirst('title').text() || 'No title';
|
|
1197
|
+
const og = this.currentDoc.openGraph();
|
|
1198
|
+
console.log(colors.green(`✔ Loaded`) + colors.gray(` (${duration}ms)`));
|
|
1199
|
+
console.log(` ${colors.cyan('Title')}: ${title}`);
|
|
1200
|
+
if (og.title)
|
|
1201
|
+
console.log(` ${colors.cyan('OG Title')}: ${og.title}`);
|
|
1202
|
+
if (og.description)
|
|
1203
|
+
console.log(` ${colors.cyan('OG Desc')}: ${og.description}`);
|
|
1204
|
+
if (og.image) {
|
|
1205
|
+
const images = Array.isArray(og.image) ? og.image : [og.image];
|
|
1206
|
+
console.log(` ${colors.magenta('Image')}: ${images[0]}`);
|
|
1207
|
+
if (images.length > 1)
|
|
1208
|
+
console.log(colors.gray(` (+${images.length - 1} more)`));
|
|
1209
|
+
}
|
|
1210
|
+
if (og.url && og.url !== url)
|
|
1211
|
+
console.log(` ${colors.magenta('URL')}: ${og.url}`);
|
|
1212
|
+
console.log(colors.gray('\n Use $ <selector> to query, $text, $attr, $links, $images, $scripts, $css, $sourcemaps, $table'));
|
|
1171
1213
|
}
|
|
1172
1214
|
catch (error) {
|
|
1173
|
-
console.error(colors.red(`
|
|
1215
|
+
console.error(colors.red(`Scrape failed: ${error.message}`));
|
|
1174
1216
|
}
|
|
1175
1217
|
console.log('');
|
|
1176
1218
|
}
|
|
1177
|
-
async
|
|
1178
|
-
if (!
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
console.log(colors.yellow('Usage: seo <url> [-a] [--format json]'));
|
|
1182
|
-
console.log(colors.gray(' Examples: seo google.com | seo https://example.com -a'));
|
|
1183
|
-
console.log(colors.gray(' -a, --all Show all checks (including passed)'));
|
|
1184
|
-
console.log(colors.gray(' --format json Output raw JSON for programmatic use'));
|
|
1185
|
-
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
1186
|
-
return;
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
else if (!url.startsWith('http') && !url.startsWith('-')) {
|
|
1190
|
-
url = `https://${url}`;
|
|
1219
|
+
async runSelect(selector) {
|
|
1220
|
+
if (!this.currentDoc) {
|
|
1221
|
+
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
1222
|
+
return;
|
|
1191
1223
|
}
|
|
1192
|
-
if (!
|
|
1193
|
-
console.log(colors.
|
|
1224
|
+
if (!selector) {
|
|
1225
|
+
console.log(colors.yellow('Usage: $ <selector>'));
|
|
1226
|
+
console.log(colors.gray(' Examples: $ h1 | $ .title | $ a[href*="article"]'));
|
|
1227
|
+
return;
|
|
1194
1228
|
}
|
|
1195
|
-
const startTime = performance.now();
|
|
1196
1229
|
try {
|
|
1197
|
-
const
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
dns: t?.dns ? Math.round(t.dns) : undefined,
|
|
1206
|
-
tcp: t?.tcp ? Math.round(t.tcp) : undefined,
|
|
1207
|
-
tls: t?.tls ? Math.round(t.tls) : undefined,
|
|
1208
|
-
download: t?.content ? Math.round(t.content) : undefined,
|
|
1209
|
-
};
|
|
1210
|
-
if (jsonOutput) {
|
|
1211
|
-
const jsonResult = {
|
|
1212
|
-
url,
|
|
1213
|
-
analyzedAt: new Date().toISOString(),
|
|
1214
|
-
timing: report.timing,
|
|
1215
|
-
score: report.score,
|
|
1216
|
-
grade: report.grade,
|
|
1217
|
-
title: report.title,
|
|
1218
|
-
metaDescription: report.metaDescription,
|
|
1219
|
-
content: report.content,
|
|
1220
|
-
headings: report.headings,
|
|
1221
|
-
links: report.links,
|
|
1222
|
-
images: report.images,
|
|
1223
|
-
openGraph: report.openGraph,
|
|
1224
|
-
twitterCard: report.twitterCard,
|
|
1225
|
-
social: report.social,
|
|
1226
|
-
structuredData: report.structuredData,
|
|
1227
|
-
technical: report.technical,
|
|
1228
|
-
checks: report.checks,
|
|
1229
|
-
summary: {
|
|
1230
|
-
total: report.checks.length,
|
|
1231
|
-
passed: report.checks.filter(c => c.status === 'pass').length,
|
|
1232
|
-
warnings: report.checks.filter(c => c.status === 'warn').length,
|
|
1233
|
-
errors: report.checks.filter(c => c.status === 'fail').length,
|
|
1234
|
-
info: report.checks.filter(c => c.status === 'info').length,
|
|
1235
|
-
},
|
|
1236
|
-
};
|
|
1237
|
-
console.log(JSON.stringify(jsonResult, null, 2));
|
|
1238
|
-
this.lastResponse = jsonResult;
|
|
1239
|
-
return;
|
|
1240
|
-
}
|
|
1241
|
-
let gradeColor = colors.red;
|
|
1242
|
-
if (report.grade === 'A')
|
|
1243
|
-
gradeColor = colors.green;
|
|
1244
|
-
else if (report.grade === 'B')
|
|
1245
|
-
gradeColor = colors.blue;
|
|
1246
|
-
else if (report.grade === 'C')
|
|
1247
|
-
gradeColor = colors.yellow;
|
|
1248
|
-
else if (report.grade === 'D')
|
|
1249
|
-
gradeColor = colors.magenta;
|
|
1250
|
-
console.log(`
|
|
1251
|
-
${colors.bold(colors.cyan('🔍 SEO Analysis Report'))} ${colors.gray(`(${duration}ms)`)}
|
|
1252
|
-
Grade: ${gradeColor(colors.bold(report.grade))} (${report.score}/100)
|
|
1253
|
-
`);
|
|
1254
|
-
if (report.title) {
|
|
1255
|
-
console.log(colors.bold('Title:') + ` ${report.title.text} ` + colors.gray(`(${report.title.length} chars)`));
|
|
1256
|
-
}
|
|
1257
|
-
if (report.metaDescription) {
|
|
1258
|
-
const desc = report.metaDescription.text.length > 80
|
|
1259
|
-
? report.metaDescription.text.slice(0, 77) + '...'
|
|
1260
|
-
: report.metaDescription.text;
|
|
1261
|
-
console.log(colors.bold('Description:') + ` ${desc} ` + colors.gray(`(${report.metaDescription.length} chars)`));
|
|
1262
|
-
}
|
|
1263
|
-
if (report.openGraph && Object.values(report.openGraph).some(v => v)) {
|
|
1264
|
-
console.log('');
|
|
1265
|
-
console.log(colors.bold(colors.cyan('OpenGraph:')));
|
|
1266
|
-
if (report.openGraph.title) {
|
|
1267
|
-
const ogTitle = report.openGraph.title.length > 60
|
|
1268
|
-
? report.openGraph.title.slice(0, 57) + '...'
|
|
1269
|
-
: report.openGraph.title;
|
|
1270
|
-
console.log(` ${colors.gray('og:title:')} ${ogTitle}`);
|
|
1271
|
-
}
|
|
1272
|
-
if (report.openGraph.description) {
|
|
1273
|
-
const ogDesc = report.openGraph.description.length > 60
|
|
1274
|
-
? report.openGraph.description.slice(0, 57) + '...'
|
|
1275
|
-
: report.openGraph.description;
|
|
1276
|
-
console.log(` ${colors.gray('og:description:')} ${ogDesc}`);
|
|
1277
|
-
}
|
|
1278
|
-
if (report.openGraph.image) {
|
|
1279
|
-
const ogImg = report.openGraph.image.length > 50
|
|
1280
|
-
? '...' + report.openGraph.image.slice(-47)
|
|
1281
|
-
: report.openGraph.image;
|
|
1282
|
-
console.log(` ${colors.gray('og:image:')} ${colors.blue(ogImg)}`);
|
|
1283
|
-
}
|
|
1284
|
-
if (report.openGraph.type) {
|
|
1285
|
-
console.log(` ${colors.gray('og:type:')} ${report.openGraph.type}`);
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
if (report.timing) {
|
|
1289
|
-
const t = report.timing;
|
|
1290
|
-
console.log('');
|
|
1291
|
-
console.log(colors.bold('Timing:'));
|
|
1292
|
-
const timings = [];
|
|
1293
|
-
if (t.dns !== undefined)
|
|
1294
|
-
timings.push(`DNS ${t.dns}ms`);
|
|
1295
|
-
if (t.tcp !== undefined)
|
|
1296
|
-
timings.push(`TCP ${t.tcp}ms`);
|
|
1297
|
-
if (t.tls !== undefined)
|
|
1298
|
-
timings.push(`TLS ${t.tls}ms`);
|
|
1299
|
-
if (t.ttfb !== undefined)
|
|
1300
|
-
timings.push(`TTFB ${t.ttfb}ms`);
|
|
1301
|
-
if (t.download !== undefined)
|
|
1302
|
-
timings.push(`Download ${t.download}ms`);
|
|
1303
|
-
if (t.total !== undefined)
|
|
1304
|
-
timings.push(`Total ${t.total}ms`);
|
|
1305
|
-
console.log(` ${timings.join(' → ')}`);
|
|
1306
|
-
}
|
|
1307
|
-
if (report.content) {
|
|
1308
|
-
console.log(colors.bold('Content:') + ` ${report.content.wordCount} words, ${report.content.paragraphCount} paragraphs, ~${report.content.readingTimeMinutes} min read`);
|
|
1309
|
-
}
|
|
1310
|
-
console.log('');
|
|
1311
|
-
console.log(colors.bold('Checks:'));
|
|
1312
|
-
const checksToShow = showAll
|
|
1313
|
-
? report.checks
|
|
1314
|
-
: report.checks.filter(c => c.status !== 'pass');
|
|
1315
|
-
const failed = checksToShow.filter(c => c.status === 'fail');
|
|
1316
|
-
const warnings = checksToShow.filter(c => c.status === 'warn');
|
|
1317
|
-
const info = checksToShow.filter(c => c.status === 'info');
|
|
1318
|
-
const passed = showAll ? checksToShow.filter(c => c.status === 'pass') : [];
|
|
1319
|
-
const displayCheck = (check) => {
|
|
1320
|
-
let icon;
|
|
1321
|
-
let nameColor;
|
|
1322
|
-
switch (check.status) {
|
|
1323
|
-
case 'pass':
|
|
1324
|
-
icon = colors.green('✔');
|
|
1325
|
-
nameColor = colors.green;
|
|
1326
|
-
break;
|
|
1327
|
-
case 'warn':
|
|
1328
|
-
icon = colors.yellow('⚠');
|
|
1329
|
-
nameColor = colors.yellow;
|
|
1330
|
-
break;
|
|
1331
|
-
case 'fail':
|
|
1332
|
-
icon = colors.red('✖');
|
|
1333
|
-
nameColor = colors.red;
|
|
1334
|
-
break;
|
|
1335
|
-
default:
|
|
1336
|
-
icon = colors.blue('ℹ');
|
|
1337
|
-
nameColor = colors.blue;
|
|
1338
|
-
}
|
|
1339
|
-
console.log(` ${icon} ${nameColor(check.name.padEnd(22))} ${check.message}`);
|
|
1340
|
-
if (check.recommendation && check.status !== 'pass') {
|
|
1341
|
-
console.log(` ${colors.gray('→')} ${colors.gray(check.recommendation)}`);
|
|
1342
|
-
}
|
|
1343
|
-
const evidence = check.evidence;
|
|
1344
|
-
if (evidence && check.status !== 'pass') {
|
|
1345
|
-
if (evidence.found && Array.isArray(evidence.found) && evidence.found.length > 0) {
|
|
1346
|
-
const items = evidence.found.slice(0, 3);
|
|
1347
|
-
console.log(` ${colors.gray('Found:')} ${colors.red(items.join(', '))}${evidence.found.length > 3 ? ` (+${evidence.found.length - 3} more)` : ''}`);
|
|
1348
|
-
}
|
|
1349
|
-
if (evidence.example) {
|
|
1350
|
-
console.log(` ${colors.gray('Example:')} ${colors.cyan(evidence.example.split('\n')[0])}`);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
};
|
|
1354
|
-
if (failed.length > 0) {
|
|
1355
|
-
console.log(colors.red(`\n Errors (${failed.length}):`));
|
|
1356
|
-
failed.forEach(displayCheck);
|
|
1357
|
-
}
|
|
1358
|
-
if (warnings.length > 0) {
|
|
1359
|
-
console.log(colors.yellow(`\n Warnings (${warnings.length}):`));
|
|
1360
|
-
warnings.forEach(displayCheck);
|
|
1361
|
-
}
|
|
1362
|
-
if (info.length > 0) {
|
|
1363
|
-
console.log(colors.blue(`\n Info (${info.length}):`));
|
|
1364
|
-
info.forEach(displayCheck);
|
|
1365
|
-
}
|
|
1366
|
-
if (passed.length > 0) {
|
|
1367
|
-
console.log(colors.green(`\n Passed (${passed.length}):`));
|
|
1368
|
-
passed.forEach(displayCheck);
|
|
1230
|
+
const elements = this.currentDoc.select(selector);
|
|
1231
|
+
const count = elements.length;
|
|
1232
|
+
console.log(colors.cyan(`Found ${count} element(s)`));
|
|
1233
|
+
if (count > 0 && count <= 10) {
|
|
1234
|
+
elements.each((el, i) => {
|
|
1235
|
+
const text = el.text().slice(0, 80).replace(/\s+/g, ' ').trim();
|
|
1236
|
+
console.log(` ${colors.gray(`${i + 1}.`)} ${text}${text.length >= 80 ? '...' : ''}`);
|
|
1237
|
+
});
|
|
1369
1238
|
}
|
|
1370
|
-
if (
|
|
1371
|
-
console.log(colors.gray(
|
|
1239
|
+
else if (count > 10) {
|
|
1240
|
+
console.log(colors.gray(' (showing first 10)'));
|
|
1241
|
+
let shown = 0;
|
|
1242
|
+
elements.each((el, i) => {
|
|
1243
|
+
if (shown >= 10)
|
|
1244
|
+
return;
|
|
1245
|
+
const text = el.text().slice(0, 80).replace(/\s+/g, ' ').trim();
|
|
1246
|
+
console.log(` ${colors.gray(`${i + 1}.`)} ${text}${text.length >= 80 ? '...' : ''}`);
|
|
1247
|
+
shown++;
|
|
1248
|
+
});
|
|
1372
1249
|
}
|
|
1373
|
-
|
|
1374
|
-
this.lastResponse = report;
|
|
1250
|
+
this.lastResponse = { count, selector };
|
|
1375
1251
|
}
|
|
1376
1252
|
catch (error) {
|
|
1377
|
-
console.error(colors.red(`
|
|
1253
|
+
console.error(colors.red(`Query failed: ${error.message}`));
|
|
1378
1254
|
}
|
|
1379
1255
|
console.log('');
|
|
1380
1256
|
}
|
|
1381
|
-
async
|
|
1382
|
-
if (!
|
|
1383
|
-
console.log(colors.yellow('
|
|
1384
|
-
|
|
1257
|
+
async runSelectText(selector) {
|
|
1258
|
+
if (!this.currentDoc) {
|
|
1259
|
+
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (!selector) {
|
|
1263
|
+
console.log(colors.yellow('Usage: $text <selector>'));
|
|
1385
1264
|
return;
|
|
1386
1265
|
}
|
|
1387
|
-
console.log(colors.gray(`Looking up ${address} using local GeoLite2 database...`));
|
|
1388
1266
|
try {
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
}
|
|
1400
|
-
console.log(`
|
|
1401
|
-
${colors.bold(colors.cyan('🌍 IP Intelligence Report'))}
|
|
1402
|
-
|
|
1403
|
-
${colors.bold('Location:')}
|
|
1404
|
-
${colors.gray('City:')} ${info.city || 'N/A'}
|
|
1405
|
-
${colors.gray('Region:')} ${info.region || 'N/A'}
|
|
1406
|
-
${colors.gray('Country:')} ${info.country || 'N/A'} ${info.countryCode ? `(${info.countryCode})` : ''}
|
|
1407
|
-
${colors.gray('Continent:')} ${info.continent || 'N/A'}
|
|
1408
|
-
${colors.gray('Timezone:')} ${info.timezone || 'N/A'}
|
|
1409
|
-
${colors.gray('Coords:')} ${info.loc ? colors.cyan(info.loc) : 'N/A'}
|
|
1410
|
-
${colors.gray('Accuracy:')} ${info.accuracy ? `~${info.accuracy} km` : 'N/A'}
|
|
1411
|
-
|
|
1412
|
-
${colors.bold('Network:')}
|
|
1413
|
-
${colors.gray('IP:')} ${info.ip}
|
|
1414
|
-
${colors.gray('Type:')} ${info.isIPv6 ? 'IPv6' : 'IPv4'}
|
|
1415
|
-
${colors.gray('Postal:')} ${info.postal || 'N/A'}
|
|
1416
|
-
`);
|
|
1417
|
-
this.lastResponse = info;
|
|
1267
|
+
const elements = this.currentDoc.select(selector);
|
|
1268
|
+
const texts = [];
|
|
1269
|
+
elements.each((el, i) => {
|
|
1270
|
+
const text = el.text().trim();
|
|
1271
|
+
if (text) {
|
|
1272
|
+
texts.push(text);
|
|
1273
|
+
console.log(`${colors.gray(`${i + 1}.`)} ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
this.lastResponse = texts;
|
|
1277
|
+
console.log(colors.gray(`\n ${texts.length} text item(s) extracted`));
|
|
1418
1278
|
}
|
|
1419
1279
|
catch (error) {
|
|
1420
|
-
console.error(colors.red(`
|
|
1280
|
+
console.error(colors.red(`Query failed: ${error.message}`));
|
|
1421
1281
|
}
|
|
1422
1282
|
console.log('');
|
|
1423
1283
|
}
|
|
1424
|
-
async
|
|
1425
|
-
if (!
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1284
|
+
async runSelectAttr(attrName, selector) {
|
|
1285
|
+
if (!this.currentDoc) {
|
|
1286
|
+
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (!attrName || !selector) {
|
|
1290
|
+
console.log(colors.yellow('Usage: $attr <attribute> <selector>'));
|
|
1291
|
+
console.log(colors.gray(' Examples: $attr href a | $attr src img'));
|
|
1292
|
+
return;
|
|
1433
1293
|
}
|
|
1434
|
-
console.log(colors.gray(`Resolving DNS for ${domain}...`));
|
|
1435
|
-
const startTime = performance.now();
|
|
1436
1294
|
try {
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
console.log(colors.bold(' A Records (IPv4):'));
|
|
1449
|
-
a.forEach(ip => console.log(` ${colors.cyan('→')} ${ip}`));
|
|
1450
|
-
}
|
|
1451
|
-
if (aaaa.length) {
|
|
1452
|
-
console.log(colors.bold(' AAAA Records (IPv6):'));
|
|
1453
|
-
aaaa.forEach(ip => console.log(` ${colors.cyan('→')} ${ip}`));
|
|
1454
|
-
}
|
|
1455
|
-
if (ns.length) {
|
|
1456
|
-
console.log(colors.bold(' NS Records:'));
|
|
1457
|
-
ns.forEach(n => console.log(` ${colors.cyan('→')} ${n}`));
|
|
1458
|
-
}
|
|
1459
|
-
if (mx.length) {
|
|
1460
|
-
console.log(colors.bold(' MX Records:'));
|
|
1461
|
-
mx.sort((a, b) => a.priority - b.priority)
|
|
1462
|
-
.forEach(m => console.log(` ${colors.cyan(String(m.priority).padStart(3))} ${m.exchange}`));
|
|
1463
|
-
}
|
|
1464
|
-
const sec = security;
|
|
1465
|
-
if (sec.spf?.length) {
|
|
1466
|
-
console.log(colors.bold(' SPF:'));
|
|
1467
|
-
console.log(` ${colors.gray(sec.spf[0].slice(0, 80))}${sec.spf[0].length > 80 ? '...' : ''}`);
|
|
1468
|
-
}
|
|
1469
|
-
if (sec.dmarc) {
|
|
1470
|
-
console.log(colors.bold(' DMARC:'));
|
|
1471
|
-
console.log(` ${colors.gray(sec.dmarc.slice(0, 80))}${sec.dmarc.length > 80 ? '...' : ''}`);
|
|
1472
|
-
}
|
|
1473
|
-
if (sec.caa?.issue?.length) {
|
|
1474
|
-
console.log(colors.bold(' CAA:'));
|
|
1475
|
-
sec.caa.issue.forEach((ca) => console.log(` ${colors.cyan('issue')} ${ca}`));
|
|
1476
|
-
}
|
|
1477
|
-
this.lastResponse = { a, aaaa, mx, ns, txt, security };
|
|
1295
|
+
const elements = this.currentDoc.select(selector);
|
|
1296
|
+
const attrs = [];
|
|
1297
|
+
elements.each((el, i) => {
|
|
1298
|
+
const value = el.attr(attrName);
|
|
1299
|
+
if (value) {
|
|
1300
|
+
attrs.push(value);
|
|
1301
|
+
console.log(`${colors.gray(`${i + 1}.`)} ${value}`);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
this.lastResponse = attrs;
|
|
1305
|
+
console.log(colors.gray(`\n ${attrs.length} attribute(s) extracted`));
|
|
1478
1306
|
}
|
|
1479
1307
|
catch (error) {
|
|
1480
|
-
console.error(colors.red(`
|
|
1308
|
+
console.error(colors.red(`Query failed: ${error.message}`));
|
|
1481
1309
|
}
|
|
1482
1310
|
console.log('');
|
|
1483
1311
|
}
|
|
1484
|
-
async
|
|
1485
|
-
if (!
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
console.log(colors.yellow('Usage: dns:propagate <domain> [type]'));
|
|
1489
|
-
console.log(colors.gray(' Examples: dns:propagate google.com | dns:propagate github.com TXT'));
|
|
1490
|
-
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
console.log(colors.gray(`Checking DNS propagation for ${domain} (${type})...`));
|
|
1495
|
-
try {
|
|
1496
|
-
const { checkPropagation, formatPropagationReport } = await import('../../dns/propagation.js');
|
|
1497
|
-
const results = await checkPropagation(domain, type);
|
|
1498
|
-
console.log(formatPropagationReport(results, domain, type));
|
|
1499
|
-
this.lastResponse = results;
|
|
1500
|
-
}
|
|
1501
|
-
catch (error) {
|
|
1502
|
-
console.error(colors.red(`Propagation check failed: ${error.message}`));
|
|
1312
|
+
async runSelectHtml(selector) {
|
|
1313
|
+
if (!this.currentDoc) {
|
|
1314
|
+
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
1315
|
+
return;
|
|
1503
1316
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
domain = this.getBaseDomain() || '';
|
|
1508
|
-
if (!domain) {
|
|
1509
|
-
console.log(colors.yellow('Usage: dns:email <domain> [dkim-selector]'));
|
|
1510
|
-
console.log(colors.gray(' Examples: dns:email google.com | dns:email github.com google'));
|
|
1511
|
-
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
1512
|
-
return;
|
|
1513
|
-
}
|
|
1317
|
+
if (!selector) {
|
|
1318
|
+
console.log(colors.yellow('Usage: $html <selector>'));
|
|
1319
|
+
return;
|
|
1514
1320
|
}
|
|
1515
|
-
console.log(colors.gray(`Checking email security for ${domain}...`));
|
|
1516
|
-
const startTime = performance.now();
|
|
1517
1321
|
try {
|
|
1518
|
-
const
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
const duration = Math.round(performance.now() - startTime);
|
|
1525
|
-
console.log(colors.green(`✔ Email security check completed`) + colors.gray(` (${duration}ms)\n`));
|
|
1526
|
-
console.log(colors.bold('SPF:'));
|
|
1527
|
-
if (spf.valid) {
|
|
1528
|
-
console.log(` ${colors.green('✔')} ${spf.record || 'No record'}`);
|
|
1529
|
-
}
|
|
1530
|
-
else {
|
|
1531
|
-
console.log(` ${colors.red('✖')} ${spf.errors?.join(', ') || 'Invalid'}`);
|
|
1532
|
-
}
|
|
1533
|
-
if (spf.warnings?.length) {
|
|
1534
|
-
spf.warnings.forEach((w) => console.log(` ${colors.yellow('⚠')} ${w}`));
|
|
1535
|
-
}
|
|
1536
|
-
console.log(colors.bold('\nDMARC:'));
|
|
1537
|
-
if (dmarc.valid) {
|
|
1538
|
-
console.log(` ${colors.green('✔')} Policy: ${dmarc.policy || 'none'}`);
|
|
1539
|
-
if (dmarc.percentage !== undefined && dmarc.percentage < 100) {
|
|
1540
|
-
console.log(` ${colors.yellow('⚠')} Only ${dmarc.percentage}% of emails affected`);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
else {
|
|
1544
|
-
console.log(` ${colors.red('✖')} No DMARC record found`);
|
|
1545
|
-
}
|
|
1546
|
-
if (dmarc.warnings?.length) {
|
|
1547
|
-
dmarc.warnings.forEach((w) => console.log(` ${colors.yellow('⚠')} ${w}`));
|
|
1548
|
-
}
|
|
1549
|
-
console.log(colors.bold(`\nDKIM (${selector || 'default'}):`));
|
|
1550
|
-
if (dkim.found) {
|
|
1551
|
-
console.log(` ${colors.green('✔')} Record found`);
|
|
1552
|
-
if (dkim.publicKey) {
|
|
1553
|
-
const keyPreview = dkim.publicKey.substring(0, 40) + '...';
|
|
1554
|
-
console.log(` ${colors.gray('Key:')} ${keyPreview}`);
|
|
1322
|
+
const element = this.currentDoc.selectFirst(selector);
|
|
1323
|
+
const html = element.html();
|
|
1324
|
+
if (html) {
|
|
1325
|
+
console.log(html.slice(0, 1000));
|
|
1326
|
+
if (html.length > 1000) {
|
|
1327
|
+
console.log(colors.gray(`\n ... (${html.length} chars total)`));
|
|
1555
1328
|
}
|
|
1329
|
+
this.lastResponse = html;
|
|
1556
1330
|
}
|
|
1557
1331
|
else {
|
|
1558
|
-
console.log(
|
|
1559
|
-
console.log(` ${colors.gray('Try: dns:email ' + domain + ' <selector>')}`);
|
|
1332
|
+
console.log(colors.gray('No element found'));
|
|
1560
1333
|
}
|
|
1561
|
-
console.log('');
|
|
1562
|
-
this.lastResponse = { spf, dmarc, dkim };
|
|
1563
1334
|
}
|
|
1564
1335
|
catch (error) {
|
|
1565
|
-
console.error(colors.red(`
|
|
1336
|
+
console.error(colors.red(`Query failed: ${error.message}`));
|
|
1566
1337
|
}
|
|
1338
|
+
console.log('');
|
|
1567
1339
|
}
|
|
1568
|
-
async
|
|
1569
|
-
if (!
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
console.log(colors.yellow('Usage: dns:health <domain>'));
|
|
1573
|
-
console.log(colors.gray(' Example: dns:health google.com'));
|
|
1574
|
-
return;
|
|
1575
|
-
}
|
|
1340
|
+
async runSelectLinks(selector) {
|
|
1341
|
+
if (!this.currentDoc) {
|
|
1342
|
+
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
1343
|
+
return;
|
|
1576
1344
|
}
|
|
1577
|
-
console.log(colors.gray(`Checking DNS health for ${domain}...`));
|
|
1578
|
-
const startTime = performance.now();
|
|
1579
1345
|
try {
|
|
1580
|
-
const
|
|
1581
|
-
const
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
});
|
|
1346
|
+
const linkSelector = selector || 'a[href]';
|
|
1347
|
+
const elements = this.currentDoc.select(linkSelector);
|
|
1348
|
+
const links = [];
|
|
1349
|
+
elements.each((el, i) => {
|
|
1350
|
+
const href = el.attr('href');
|
|
1351
|
+
const text = el.text().trim().slice(0, 50);
|
|
1352
|
+
if (href) {
|
|
1353
|
+
links.push({ text, href });
|
|
1354
|
+
if (i < 20) {
|
|
1355
|
+
console.log(`${colors.gray(`${i + 1}.`)} ${colors.cyan(text || '(no text)')} ${colors.gray('→')} ${href}`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
if (links.length > 20) {
|
|
1360
|
+
console.log(colors.gray(` ... and ${links.length - 20} more links`));
|
|
1596
1361
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1362
|
+
this.lastResponse = links;
|
|
1363
|
+
console.log(colors.gray(`\n ${links.length} link(s) found`));
|
|
1599
1364
|
}
|
|
1600
1365
|
catch (error) {
|
|
1601
|
-
console.error(colors.red(`
|
|
1366
|
+
console.error(colors.red(`Query failed: ${error.message}`));
|
|
1602
1367
|
}
|
|
1603
|
-
|
|
1604
|
-
async runDnsSpf(domain) {
|
|
1605
|
-
if (!domain) {
|
|
1606
|
-
domain = this.getBaseDomain() || '';
|
|
1607
|
-
if (!domain) {
|
|
1608
|
-
console.log(colors.yellow('Usage: dns:spf <domain>'));
|
|
1609
|
-
console.log(colors.gray(' Example: dns:spf google.com'));
|
|
1610
|
-
return;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
console.log(colors.gray(`Validating SPF for ${domain}...`));
|
|
1614
|
-
try {
|
|
1615
|
-
const { validateSpf } = await import('../../utils/dns-toolkit.js');
|
|
1616
|
-
const result = await validateSpf(domain);
|
|
1617
|
-
console.log('');
|
|
1618
|
-
console.log(colors.bold('SPF Validation'));
|
|
1619
|
-
if (result.valid) {
|
|
1620
|
-
console.log(` ${colors.green('✔')} Valid SPF record`);
|
|
1621
|
-
}
|
|
1622
|
-
else {
|
|
1623
|
-
console.log(` ${colors.red('✖')} Invalid SPF record`);
|
|
1624
|
-
}
|
|
1625
|
-
if (result.record) {
|
|
1626
|
-
console.log(` ${colors.gray('Record:')} ${result.record}`);
|
|
1627
|
-
}
|
|
1628
|
-
if (result.lookupCount !== undefined) {
|
|
1629
|
-
const lookupColor = result.lookupCount > 10 ? colors.red : result.lookupCount > 7 ? colors.yellow : colors.green;
|
|
1630
|
-
console.log(` ${colors.gray('DNS Lookups:')} ${lookupColor(result.lookupCount.toString())}/10`);
|
|
1631
|
-
}
|
|
1632
|
-
if (result.mechanisms && result.mechanisms.length > 0) {
|
|
1633
|
-
console.log(` ${colors.gray('Mechanisms:')} ${result.mechanisms.join(', ')}`);
|
|
1634
|
-
}
|
|
1635
|
-
if (result.includes && result.includes.length > 0) {
|
|
1636
|
-
console.log(` ${colors.gray('Includes:')} ${result.includes.join(', ')}`);
|
|
1637
|
-
}
|
|
1638
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
1639
|
-
console.log('');
|
|
1640
|
-
result.warnings.forEach((w) => console.log(` ${colors.yellow('⚠')} ${w}`));
|
|
1641
|
-
}
|
|
1642
|
-
if (result.errors && result.errors.length > 0) {
|
|
1643
|
-
console.log('');
|
|
1644
|
-
result.errors.forEach((e) => console.log(` ${colors.red('✖')} ${e}`));
|
|
1645
|
-
}
|
|
1646
|
-
console.log('');
|
|
1647
|
-
this.lastResponse = result;
|
|
1648
|
-
}
|
|
1649
|
-
catch (error) {
|
|
1650
|
-
console.error(colors.red(`SPF validation failed: ${error.message}`));
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
async runDnsDmarc(domain) {
|
|
1654
|
-
if (!domain) {
|
|
1655
|
-
domain = this.getBaseDomain() || '';
|
|
1656
|
-
if (!domain) {
|
|
1657
|
-
console.log(colors.yellow('Usage: dns:dmarc <domain>'));
|
|
1658
|
-
console.log(colors.gray(' Example: dns:dmarc google.com'));
|
|
1659
|
-
return;
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
console.log(colors.gray(`Validating DMARC for ${domain}...`));
|
|
1663
|
-
try {
|
|
1664
|
-
const { validateDmarc } = await import('../../utils/dns-toolkit.js');
|
|
1665
|
-
const result = await validateDmarc(domain);
|
|
1666
|
-
console.log('');
|
|
1667
|
-
console.log(colors.bold('DMARC Validation'));
|
|
1668
|
-
if (result.valid) {
|
|
1669
|
-
console.log(` ${colors.green('✔')} Valid DMARC record`);
|
|
1670
|
-
}
|
|
1671
|
-
else {
|
|
1672
|
-
console.log(` ${colors.red('✖')} No DMARC record found`);
|
|
1673
|
-
}
|
|
1674
|
-
if (result.record) {
|
|
1675
|
-
console.log(` ${colors.gray('Record:')} ${result.record}`);
|
|
1676
|
-
}
|
|
1677
|
-
if (result.policy) {
|
|
1678
|
-
const policyColor = result.policy === 'reject' ? colors.green :
|
|
1679
|
-
result.policy === 'quarantine' ? colors.yellow : colors.gray;
|
|
1680
|
-
console.log(` ${colors.gray('Policy:')} ${policyColor(result.policy)}`);
|
|
1681
|
-
}
|
|
1682
|
-
if (result.subdomainPolicy) {
|
|
1683
|
-
console.log(` ${colors.gray('Subdomain Policy:')} ${result.subdomainPolicy}`);
|
|
1684
|
-
}
|
|
1685
|
-
if (result.percentage !== undefined && result.percentage < 100) {
|
|
1686
|
-
console.log(` ${colors.yellow('⚠')} Only ${result.percentage}% of emails affected`);
|
|
1687
|
-
}
|
|
1688
|
-
if (result.rua) {
|
|
1689
|
-
console.log(` ${colors.gray('Aggregate Reports:')} ${result.rua}`);
|
|
1690
|
-
}
|
|
1691
|
-
if (result.ruf) {
|
|
1692
|
-
console.log(` ${colors.gray('Forensic Reports:')} ${result.ruf}`);
|
|
1693
|
-
}
|
|
1694
|
-
if (result.warnings && result.warnings.length > 0) {
|
|
1695
|
-
console.log('');
|
|
1696
|
-
result.warnings.forEach((w) => console.log(` ${colors.yellow('⚠')} ${w}`));
|
|
1697
|
-
}
|
|
1698
|
-
console.log('');
|
|
1699
|
-
this.lastResponse = result;
|
|
1700
|
-
}
|
|
1701
|
-
catch (error) {
|
|
1702
|
-
console.error(colors.red(`DMARC validation failed: ${error.message}`));
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
async runDnsDkim(domain, selector) {
|
|
1706
|
-
if (!domain) {
|
|
1707
|
-
domain = this.getBaseDomain() || '';
|
|
1708
|
-
if (!domain) {
|
|
1709
|
-
console.log(colors.yellow('Usage: dns:dkim <domain> [selector]'));
|
|
1710
|
-
console.log(colors.gray(' Example: dns:dkim google.com | dns:dkim google.com google'));
|
|
1711
|
-
return;
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
const dkimSelector = selector || 'default';
|
|
1715
|
-
console.log(colors.gray(`Checking DKIM for ${domain} (selector: ${dkimSelector})...`));
|
|
1716
|
-
try {
|
|
1717
|
-
const { checkDkim } = await import('../../utils/dns-toolkit.js');
|
|
1718
|
-
const result = await checkDkim(domain, dkimSelector);
|
|
1719
|
-
console.log('');
|
|
1720
|
-
console.log(colors.bold(`DKIM Check (selector: ${dkimSelector})`));
|
|
1721
|
-
if (result.found) {
|
|
1722
|
-
console.log(` ${colors.green('✔')} DKIM record found`);
|
|
1723
|
-
if (result.publicKey) {
|
|
1724
|
-
const keyPreview = result.publicKey.substring(0, 50) + '...';
|
|
1725
|
-
console.log(` ${colors.gray('Public Key:')} ${keyPreview}`);
|
|
1726
|
-
}
|
|
1727
|
-
if (result.record) {
|
|
1728
|
-
console.log(` ${colors.gray('Record:')} ${result.record.substring(0, 80)}...`);
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
else {
|
|
1732
|
-
console.log(` ${colors.yellow('⚠')} No DKIM record found for selector "${dkimSelector}"`);
|
|
1733
|
-
console.log(` ${colors.gray('Common selectors: google, selector1, selector2, k1, default')}`);
|
|
1734
|
-
}
|
|
1735
|
-
console.log('');
|
|
1736
|
-
this.lastResponse = result;
|
|
1737
|
-
}
|
|
1738
|
-
catch (error) {
|
|
1739
|
-
console.error(colors.red(`DKIM check failed: ${error.message}`));
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
async runDnsDig(args) {
|
|
1743
|
-
let server = '';
|
|
1744
|
-
let domain = '';
|
|
1745
|
-
let recordType = 'A';
|
|
1746
|
-
let shortMode = false;
|
|
1747
|
-
for (const arg of args) {
|
|
1748
|
-
if (arg.startsWith('@')) {
|
|
1749
|
-
server = arg.slice(1);
|
|
1750
|
-
}
|
|
1751
|
-
else if (arg === '+short') {
|
|
1752
|
-
shortMode = true;
|
|
1753
|
-
}
|
|
1754
|
-
else if (['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA', 'CAA', 'SRV', 'PTR', 'ANY'].includes(arg.toUpperCase())) {
|
|
1755
|
-
recordType = arg.toUpperCase();
|
|
1756
|
-
}
|
|
1757
|
-
else if (!domain) {
|
|
1758
|
-
domain = arg;
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
if (!domain) {
|
|
1762
|
-
domain = this.getBaseDomain() || '';
|
|
1763
|
-
if (!domain) {
|
|
1764
|
-
console.log(colors.yellow('Usage: dns:dig [@server] <domain> [type] [+short]'));
|
|
1765
|
-
console.log(colors.gray(' Examples:'));
|
|
1766
|
-
console.log(colors.gray(' dns:dig google.com'));
|
|
1767
|
-
console.log(colors.gray(' dns:dig google.com MX'));
|
|
1768
|
-
console.log(colors.gray(' dns:dig @8.8.8.8 google.com A'));
|
|
1769
|
-
console.log(colors.gray(' dns:dig google.com TXT +short'));
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
console.log(colors.gray(`Querying ${recordType} record for ${domain}${server ? ` via ${server}` : ''}...`));
|
|
1774
|
-
try {
|
|
1775
|
-
const { dig, formatDigOutput } = await import('../../utils/dns-toolkit.js');
|
|
1776
|
-
const result = await dig(domain, { type: recordType, server: server || undefined });
|
|
1777
|
-
console.log('');
|
|
1778
|
-
if (shortMode) {
|
|
1779
|
-
if (result.answer && result.answer.length > 0) {
|
|
1780
|
-
result.answer.forEach((ans) => {
|
|
1781
|
-
console.log(ans.data || ans.address || ans.exchange || JSON.stringify(ans));
|
|
1782
|
-
});
|
|
1783
|
-
}
|
|
1784
|
-
else {
|
|
1785
|
-
console.log(colors.gray('(no results)'));
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
else {
|
|
1789
|
-
console.log(formatDigOutput(result, shortMode));
|
|
1790
|
-
}
|
|
1791
|
-
this.lastResponse = result;
|
|
1792
|
-
}
|
|
1793
|
-
catch (error) {
|
|
1794
|
-
console.error(colors.red(`DNS lookup failed: ${error.message}`));
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
async runDnsGenerate(args) {
|
|
1798
|
-
if (args.length === 0 || args[0] === 'help') {
|
|
1799
|
-
console.log(colors.bold('DMARC Record Generator'));
|
|
1800
|
-
console.log('');
|
|
1801
|
-
console.log(colors.yellow('Usage: dns:generate <policy> [options]'));
|
|
1802
|
-
console.log('');
|
|
1803
|
-
console.log(colors.gray('Policies:'));
|
|
1804
|
-
console.log(' none - Monitor only, take no action');
|
|
1805
|
-
console.log(' quarantine - Mark suspicious emails as spam');
|
|
1806
|
-
console.log(' reject - Block suspicious emails');
|
|
1807
|
-
console.log('');
|
|
1808
|
-
console.log(colors.gray('Options (key=value format):'));
|
|
1809
|
-
console.log(' rua=<email> - Aggregate report address(es), comma-separated');
|
|
1810
|
-
console.log(' ruf=<email> - Forensic report address(es), comma-separated');
|
|
1811
|
-
console.log(' sp=<policy> - Subdomain policy (none|quarantine|reject)');
|
|
1812
|
-
console.log(' pct=<0-100> - Percentage of messages to apply policy');
|
|
1813
|
-
console.log(' adkim=<s|r> - DKIM alignment (s=strict, r=relaxed)');
|
|
1814
|
-
console.log(' aspf=<s|r> - SPF alignment (s=strict, r=relaxed)');
|
|
1815
|
-
console.log(' ri=<seconds> - Report interval (default: 86400 = 1 day)');
|
|
1816
|
-
console.log('');
|
|
1817
|
-
console.log(colors.gray('Examples:'));
|
|
1818
|
-
console.log(' dns:generate reject');
|
|
1819
|
-
console.log(' dns:generate reject rua=reports@example.com');
|
|
1820
|
-
console.log(' dns:generate quarantine sp=reject pct=50');
|
|
1821
|
-
console.log(' dns:generate reject rua=dmarc@example.com,backup@example.com');
|
|
1822
|
-
return;
|
|
1823
|
-
}
|
|
1824
|
-
const policy = args[0].toLowerCase();
|
|
1825
|
-
if (!['none', 'quarantine', 'reject'].includes(policy)) {
|
|
1826
|
-
console.log(colors.red(`Invalid policy: ${policy}`));
|
|
1827
|
-
console.log(colors.gray('Valid policies: none, quarantine, reject'));
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
const options = {};
|
|
1831
|
-
for (let i = 1; i < args.length; i++) {
|
|
1832
|
-
const [key, ...valueParts] = args[i].split('=');
|
|
1833
|
-
if (valueParts.length > 0) {
|
|
1834
|
-
options[key.toLowerCase()] = valueParts.join('=');
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
try {
|
|
1838
|
-
const { generateDmarc } = await import('../../utils/dns-toolkit.js');
|
|
1839
|
-
const dmarcOptions = {
|
|
1840
|
-
policy: policy,
|
|
1841
|
-
};
|
|
1842
|
-
if (options.sp) {
|
|
1843
|
-
dmarcOptions.subdomainPolicy = options.sp;
|
|
1844
|
-
}
|
|
1845
|
-
if (options.pct) {
|
|
1846
|
-
dmarcOptions.percentage = parseInt(options.pct, 10);
|
|
1847
|
-
}
|
|
1848
|
-
if (options.rua) {
|
|
1849
|
-
dmarcOptions.aggregateReports = options.rua.split(',').map(e => e.trim());
|
|
1850
|
-
}
|
|
1851
|
-
if (options.ruf) {
|
|
1852
|
-
dmarcOptions.forensicReports = options.ruf.split(',').map(e => e.trim());
|
|
1853
|
-
}
|
|
1854
|
-
if (options.adkim) {
|
|
1855
|
-
dmarcOptions.alignmentDkim = options.adkim === 's' ? 'strict' : 'relaxed';
|
|
1856
|
-
}
|
|
1857
|
-
if (options.aspf) {
|
|
1858
|
-
dmarcOptions.alignmentSpf = options.aspf === 's' ? 'strict' : 'relaxed';
|
|
1859
|
-
}
|
|
1860
|
-
if (options.ri) {
|
|
1861
|
-
dmarcOptions.reportInterval = parseInt(options.ri, 10);
|
|
1862
|
-
}
|
|
1863
|
-
const record = generateDmarc(dmarcOptions);
|
|
1864
|
-
console.log('');
|
|
1865
|
-
console.log(colors.bold(colors.green('Generated DMARC Record')));
|
|
1866
|
-
console.log('');
|
|
1867
|
-
console.log(colors.gray('DNS Record Name:'));
|
|
1868
|
-
console.log(` _dmarc.yourdomain.com`);
|
|
1869
|
-
console.log('');
|
|
1870
|
-
console.log(colors.gray('TXT Record Value:'));
|
|
1871
|
-
console.log(` ${colors.cyan(record)}`);
|
|
1872
|
-
console.log('');
|
|
1873
|
-
console.log(colors.gray('Policy Summary:'));
|
|
1874
|
-
console.log(` ${colors.gray('Policy:')} ${policy}`);
|
|
1875
|
-
if (dmarcOptions.subdomainPolicy) {
|
|
1876
|
-
console.log(` ${colors.gray('Subdomain Policy:')} ${dmarcOptions.subdomainPolicy}`);
|
|
1877
|
-
}
|
|
1878
|
-
if (dmarcOptions.percentage !== undefined && dmarcOptions.percentage !== 100) {
|
|
1879
|
-
console.log(` ${colors.gray('Percentage:')} ${dmarcOptions.percentage}%`);
|
|
1880
|
-
}
|
|
1881
|
-
if (dmarcOptions.aggregateReports) {
|
|
1882
|
-
console.log(` ${colors.gray('Aggregate Reports:')} ${dmarcOptions.aggregateReports.join(', ')}`);
|
|
1883
|
-
}
|
|
1884
|
-
if (dmarcOptions.forensicReports) {
|
|
1885
|
-
console.log(` ${colors.gray('Forensic Reports:')} ${dmarcOptions.forensicReports.join(', ')}`);
|
|
1886
|
-
}
|
|
1887
|
-
console.log('');
|
|
1888
|
-
this.lastResponse = { record, options: dmarcOptions };
|
|
1889
|
-
}
|
|
1890
|
-
catch (error) {
|
|
1891
|
-
console.error(colors.red(`DMARC generation failed: ${error.message}`));
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
async runRDAP(domain) {
|
|
1895
|
-
if (!domain) {
|
|
1896
|
-
domain = this.getRootDomain() || '';
|
|
1897
|
-
if (!domain) {
|
|
1898
|
-
console.log(colors.yellow('Usage: rdap <domain>'));
|
|
1899
|
-
console.log(colors.gray(' Examples: rdap google.com | rdap 8.8.8.8'));
|
|
1900
|
-
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
console.log(colors.gray(`RDAP lookup for ${domain}...`));
|
|
1905
|
-
const startTime = performance.now();
|
|
1906
|
-
try {
|
|
1907
|
-
const result = await rdap(this.client, domain);
|
|
1908
|
-
const duration = Math.round(performance.now() - startTime);
|
|
1909
|
-
console.log(colors.green(`✔ RDAP lookup completed`) + colors.gray(` (${duration}ms)\n`));
|
|
1910
|
-
if (result.status?.length) {
|
|
1911
|
-
console.log(colors.bold(' Status:'));
|
|
1912
|
-
result.status.forEach((s) => console.log(` ${colors.cyan('→')} ${s}`));
|
|
1913
|
-
}
|
|
1914
|
-
if (result.events?.length) {
|
|
1915
|
-
console.log(colors.bold(' Events:'));
|
|
1916
|
-
result.events.forEach((e) => {
|
|
1917
|
-
const date = new Date(e.eventDate).toISOString().split('T')[0];
|
|
1918
|
-
console.log(` ${colors.cyan(e.eventAction.padEnd(15))} ${date}`);
|
|
1919
|
-
});
|
|
1920
|
-
}
|
|
1921
|
-
if (result.entities?.length) {
|
|
1922
|
-
console.log(colors.bold(' Entities:'));
|
|
1923
|
-
result.entities.forEach((e) => {
|
|
1924
|
-
const roles = e.roles?.join(', ') || 'unknown';
|
|
1925
|
-
console.log(` ${colors.cyan(roles.padEnd(15))} ${e.handle || 'N/A'}`);
|
|
1926
|
-
});
|
|
1927
|
-
}
|
|
1928
|
-
if (result.handle) {
|
|
1929
|
-
console.log(` ${colors.cyan('Handle')}: ${result.handle}`);
|
|
1930
|
-
}
|
|
1931
|
-
if (result.name) {
|
|
1932
|
-
console.log(` ${colors.cyan('Name')}: ${result.name}`);
|
|
1933
|
-
}
|
|
1934
|
-
if (result.startAddress && result.endAddress) {
|
|
1935
|
-
console.log(` ${colors.cyan('Range')}: ${result.startAddress} - ${result.endAddress}`);
|
|
1936
|
-
}
|
|
1937
|
-
this.lastResponse = result;
|
|
1938
|
-
}
|
|
1939
|
-
catch (error) {
|
|
1940
|
-
console.error(colors.red(`RDAP lookup failed: ${error.message}`));
|
|
1941
|
-
console.log(colors.gray(' Tip: RDAP may not be available for all TLDs. Try "whois" instead.'));
|
|
1942
|
-
}
|
|
1943
|
-
console.log('');
|
|
1944
|
-
}
|
|
1945
|
-
async runPing(host) {
|
|
1946
|
-
if (!host) {
|
|
1947
|
-
host = this.getBaseDomain() || '';
|
|
1948
|
-
if (!host) {
|
|
1949
|
-
console.log(colors.yellow('Usage: ping <host>'));
|
|
1950
|
-
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
else {
|
|
1955
|
-
host = host.replace(/^https?:\/\//, '').split('/')[0];
|
|
1956
|
-
}
|
|
1957
|
-
console.log(colors.gray(`Pinging ${host}...`));
|
|
1958
|
-
try {
|
|
1959
|
-
const { connect } = await import('node:net');
|
|
1960
|
-
const port = 443;
|
|
1961
|
-
const startTime = performance.now();
|
|
1962
|
-
await new Promise((resolve, reject) => {
|
|
1963
|
-
const socket = connect(port, host, () => {
|
|
1964
|
-
const duration = Math.round(performance.now() - startTime);
|
|
1965
|
-
console.log(colors.green(`✔ ${host}:${port} is reachable`) + colors.gray(` (${duration}ms)`));
|
|
1966
|
-
socket.end();
|
|
1967
|
-
resolve();
|
|
1968
|
-
});
|
|
1969
|
-
socket.on('error', reject);
|
|
1970
|
-
socket.setTimeout(5000, () => {
|
|
1971
|
-
socket.destroy();
|
|
1972
|
-
reject(new Error('Connection timed out'));
|
|
1973
|
-
});
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
1976
|
-
catch (error) {
|
|
1977
|
-
console.error(colors.red(`✖ ${host} is unreachable: ${error.message}`));
|
|
1978
|
-
}
|
|
1979
|
-
console.log('');
|
|
1980
|
-
}
|
|
1981
|
-
async runFtp(args) {
|
|
1982
|
-
if (args.length === 0 || args[0] === 'help') {
|
|
1983
|
-
console.log(colors.bold('FTP Client'));
|
|
1984
|
-
console.log('');
|
|
1985
|
-
console.log(colors.yellow('Usage: ftp <host> [command] [args...]'));
|
|
1986
|
-
console.log('');
|
|
1987
|
-
console.log(colors.gray('Commands:'));
|
|
1988
|
-
console.log(' ftp <host> ls [path] - List directory');
|
|
1989
|
-
console.log(' ftp <host> get <remote> - Download file');
|
|
1990
|
-
console.log(' ftp <host> put <local> [remote]- Upload file');
|
|
1991
|
-
console.log(' ftp <host> rm <path> - Delete file');
|
|
1992
|
-
console.log(' ftp <host> mkdir <path> - Create directory');
|
|
1993
|
-
console.log('');
|
|
1994
|
-
console.log(colors.gray('Options (add after host):'));
|
|
1995
|
-
console.log(' user=<username> - FTP username (default: anonymous)');
|
|
1996
|
-
console.log(' pass=<password> - FTP password (default: anonymous@)');
|
|
1997
|
-
console.log(' port=<number> - Port number (default: 21)');
|
|
1998
|
-
console.log(' secure - Use FTPS (explicit TLS)');
|
|
1999
|
-
console.log('');
|
|
2000
|
-
console.log(colors.gray('Examples:'));
|
|
2001
|
-
console.log(' ftp ftp.example.com ls');
|
|
2002
|
-
console.log(' ftp ftp.example.com ls /pub');
|
|
2003
|
-
console.log(' ftp ftp.example.com get /pub/file.txt');
|
|
2004
|
-
console.log(' ftp ftp.example.com user=admin pass=secret ls');
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
const host = args[0];
|
|
2008
|
-
let command = 'ls';
|
|
2009
|
-
let commandArgs = [];
|
|
2010
|
-
const options = {};
|
|
2011
|
-
for (let i = 1; i < args.length; i++) {
|
|
2012
|
-
const arg = args[i];
|
|
2013
|
-
if (arg.includes('=')) {
|
|
2014
|
-
const [key, value] = arg.split('=');
|
|
2015
|
-
options[key] = value;
|
|
2016
|
-
}
|
|
2017
|
-
else if (['ls', 'get', 'put', 'rm', 'mkdir'].includes(arg)) {
|
|
2018
|
-
command = arg;
|
|
2019
|
-
commandArgs = args.slice(i + 1).filter(a => !a.includes('='));
|
|
2020
|
-
break;
|
|
2021
|
-
}
|
|
2022
|
-
else {
|
|
2023
|
-
command = arg;
|
|
2024
|
-
commandArgs = args.slice(i + 1).filter(a => !a.includes('='));
|
|
2025
|
-
break;
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
const { createFTP } = await import('../../protocols/ftp.js');
|
|
2029
|
-
const client = createFTP({
|
|
2030
|
-
host,
|
|
2031
|
-
port: parseInt(options.port || '21'),
|
|
2032
|
-
user: options.user || 'anonymous',
|
|
2033
|
-
password: options.pass || 'anonymous@',
|
|
2034
|
-
secure: args.includes('secure'),
|
|
2035
|
-
});
|
|
2036
|
-
console.log(colors.gray(`Connecting to ${host}...`));
|
|
2037
|
-
try {
|
|
2038
|
-
const connectResult = await client.connect();
|
|
2039
|
-
if (!connectResult.success) {
|
|
2040
|
-
console.error(colors.red(`Connection failed: ${connectResult.message}`));
|
|
2041
|
-
return;
|
|
2042
|
-
}
|
|
2043
|
-
console.log(colors.green('Connected'));
|
|
2044
|
-
switch (command) {
|
|
2045
|
-
case 'ls': {
|
|
2046
|
-
const path = commandArgs[0] || '/';
|
|
2047
|
-
console.log(colors.gray(`Listing ${path}...`));
|
|
2048
|
-
const result = await client.list(path);
|
|
2049
|
-
if (!result.success || !result.data) {
|
|
2050
|
-
console.error(colors.red(`List failed: ${result.message}`));
|
|
2051
|
-
break;
|
|
2052
|
-
}
|
|
2053
|
-
console.log('');
|
|
2054
|
-
for (const item of result.data) {
|
|
2055
|
-
const typeChar = item.type === 'directory' ? 'd' : item.type === 'link' ? 'l' : '-';
|
|
2056
|
-
const perms = item.permissions || 'rwxr-xr-x';
|
|
2057
|
-
const size = item.size.toString().padStart(10);
|
|
2058
|
-
const date = item.rawModifiedAt || '';
|
|
2059
|
-
const nameColor = item.type === 'directory' ? colors.blue : item.type === 'link' ? colors.cyan : (t) => t;
|
|
2060
|
-
console.log(`${typeChar}${perms} ${size} ${date.padEnd(12)} ${nameColor(item.name)}`);
|
|
2061
|
-
}
|
|
2062
|
-
console.log('');
|
|
2063
|
-
console.log(colors.gray(`Total: ${result.data.length} items`));
|
|
2064
|
-
this.lastResponse = result.data;
|
|
2065
|
-
break;
|
|
2066
|
-
}
|
|
2067
|
-
case 'get': {
|
|
2068
|
-
const remote = commandArgs[0];
|
|
2069
|
-
if (!remote) {
|
|
2070
|
-
console.log(colors.yellow('Usage: ftp <host> get <remote-path>'));
|
|
2071
|
-
break;
|
|
2072
|
-
}
|
|
2073
|
-
const path = await import('node:path');
|
|
2074
|
-
const local = commandArgs[1] || path.basename(remote);
|
|
2075
|
-
console.log(colors.gray(`Downloading ${remote} → ${local}...`));
|
|
2076
|
-
const result = await client.download(remote, local);
|
|
2077
|
-
if (!result.success) {
|
|
2078
|
-
console.error(colors.red(`Download failed: ${result.message}`));
|
|
2079
|
-
}
|
|
2080
|
-
else {
|
|
2081
|
-
console.log(colors.green(`✔ Downloaded to ${local}`));
|
|
2082
|
-
}
|
|
2083
|
-
break;
|
|
2084
|
-
}
|
|
2085
|
-
case 'put': {
|
|
2086
|
-
const local = commandArgs[0];
|
|
2087
|
-
if (!local) {
|
|
2088
|
-
console.log(colors.yellow('Usage: ftp <host> put <local-path> [remote-path]'));
|
|
2089
|
-
break;
|
|
2090
|
-
}
|
|
2091
|
-
const path = await import('node:path');
|
|
2092
|
-
const remote = commandArgs[1] || '/' + path.basename(local);
|
|
2093
|
-
console.log(colors.gray(`Uploading ${local} → ${remote}...`));
|
|
2094
|
-
const result = await client.upload(local, remote);
|
|
2095
|
-
if (!result.success) {
|
|
2096
|
-
console.error(colors.red(`Upload failed: ${result.message}`));
|
|
2097
|
-
}
|
|
2098
|
-
else {
|
|
2099
|
-
console.log(colors.green(`✔ Uploaded to ${remote}`));
|
|
2100
|
-
}
|
|
2101
|
-
break;
|
|
2102
|
-
}
|
|
2103
|
-
case 'rm': {
|
|
2104
|
-
const remotePath = commandArgs[0];
|
|
2105
|
-
if (!remotePath) {
|
|
2106
|
-
console.log(colors.yellow('Usage: ftp <host> rm <remote-path>'));
|
|
2107
|
-
break;
|
|
2108
|
-
}
|
|
2109
|
-
console.log(colors.gray(`Deleting ${remotePath}...`));
|
|
2110
|
-
const result = await client.delete(remotePath);
|
|
2111
|
-
if (!result.success) {
|
|
2112
|
-
console.error(colors.red(`Delete failed: ${result.message}`));
|
|
2113
|
-
}
|
|
2114
|
-
else {
|
|
2115
|
-
console.log(colors.green(`✔ Deleted ${remotePath}`));
|
|
2116
|
-
}
|
|
2117
|
-
break;
|
|
2118
|
-
}
|
|
2119
|
-
case 'mkdir': {
|
|
2120
|
-
const remotePath = commandArgs[0];
|
|
2121
|
-
if (!remotePath) {
|
|
2122
|
-
console.log(colors.yellow('Usage: ftp <host> mkdir <remote-path>'));
|
|
2123
|
-
break;
|
|
2124
|
-
}
|
|
2125
|
-
console.log(colors.gray(`Creating ${remotePath}...`));
|
|
2126
|
-
const result = await client.mkdir(remotePath);
|
|
2127
|
-
if (!result.success) {
|
|
2128
|
-
console.error(colors.red(`Mkdir failed: ${result.message}`));
|
|
2129
|
-
}
|
|
2130
|
-
else {
|
|
2131
|
-
console.log(colors.green(`✔ Created ${remotePath}`));
|
|
2132
|
-
}
|
|
2133
|
-
break;
|
|
2134
|
-
}
|
|
2135
|
-
default:
|
|
2136
|
-
console.log(colors.yellow(`Unknown FTP command: ${command}`));
|
|
2137
|
-
console.log(colors.gray('Valid commands: ls, get, put, rm, mkdir'));
|
|
2138
|
-
}
|
|
2139
|
-
await client.close();
|
|
2140
|
-
}
|
|
2141
|
-
catch (error) {
|
|
2142
|
-
console.error(colors.red(`FTP Error: ${error.message}`));
|
|
2143
|
-
}
|
|
2144
|
-
console.log('');
|
|
2145
|
-
}
|
|
2146
|
-
async runTelnet(host, portStr) {
|
|
2147
|
-
if (!host) {
|
|
2148
|
-
console.log(colors.bold('Telnet Client'));
|
|
2149
|
-
console.log('');
|
|
2150
|
-
console.log(colors.yellow('Usage: telnet <host> [port]'));
|
|
2151
|
-
console.log('');
|
|
2152
|
-
console.log(colors.gray('Examples:'));
|
|
2153
|
-
console.log(' telnet towel.blinkenlights.nl');
|
|
2154
|
-
console.log(' telnet localhost 8023');
|
|
2155
|
-
console.log(' telnet mail.example.com 25');
|
|
2156
|
-
console.log('');
|
|
2157
|
-
console.log(colors.gray('Note: Type "exit" or Ctrl+C to disconnect'));
|
|
2158
|
-
return;
|
|
2159
|
-
}
|
|
2160
|
-
const port = parseInt(portStr || '23');
|
|
2161
|
-
console.log(colors.gray(`Connecting to ${host}:${port}...`));
|
|
2162
|
-
try {
|
|
2163
|
-
const { createTelnet } = await import('../../protocols/telnet.js');
|
|
2164
|
-
const client = createTelnet({
|
|
2165
|
-
host,
|
|
2166
|
-
port,
|
|
2167
|
-
timeout: 30000,
|
|
2168
|
-
});
|
|
2169
|
-
await client.connect();
|
|
2170
|
-
console.log(colors.green(`Connected to ${host}:${port}`));
|
|
2171
|
-
console.log(colors.gray('Interactive mode. Type "exit" to disconnect.'));
|
|
2172
|
-
console.log('');
|
|
2173
|
-
const originalPrompt = this.rl.getPrompt();
|
|
2174
|
-
client.on('data', (data) => {
|
|
2175
|
-
process.stdout.write(data);
|
|
2176
|
-
});
|
|
2177
|
-
client.on('close', () => {
|
|
2178
|
-
console.log(colors.yellow('\nConnection closed'));
|
|
2179
|
-
this.rl.setPrompt(originalPrompt);
|
|
2180
|
-
this.prompt();
|
|
2181
|
-
});
|
|
2182
|
-
const telnetPrompt = () => {
|
|
2183
|
-
this.rl.question('', async (input) => {
|
|
2184
|
-
if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
|
|
2185
|
-
console.log(colors.yellow('Disconnecting...'));
|
|
2186
|
-
await client.close();
|
|
2187
|
-
this.rl.setPrompt(originalPrompt);
|
|
2188
|
-
this.prompt();
|
|
2189
|
-
return;
|
|
2190
|
-
}
|
|
2191
|
-
await client.send(input + '\r\n');
|
|
2192
|
-
telnetPrompt();
|
|
2193
|
-
});
|
|
2194
|
-
};
|
|
2195
|
-
telnetPrompt();
|
|
2196
|
-
}
|
|
2197
|
-
catch (error) {
|
|
2198
|
-
console.error(colors.red(`Telnet Error: ${error.message}`));
|
|
2199
|
-
console.log('');
|
|
2200
|
-
}
|
|
2201
|
-
}
|
|
2202
|
-
async runScrap(url) {
|
|
2203
|
-
if (!url) {
|
|
2204
|
-
if (!this.baseUrl) {
|
|
2205
|
-
console.log(colors.yellow('Usage: scrap <url>'));
|
|
2206
|
-
console.log(colors.gray(' Examples: scrap https://news.ycombinator.com'));
|
|
2207
|
-
console.log(colors.gray(' Or set a base URL first: url https://example.com'));
|
|
2208
|
-
return;
|
|
2209
|
-
}
|
|
2210
|
-
url = this.baseUrl;
|
|
2211
|
-
}
|
|
2212
|
-
else if (!url.startsWith('http')) {
|
|
2213
|
-
url = this.baseUrl ? `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}` : `https://${url}`;
|
|
2214
|
-
}
|
|
2215
|
-
console.log(colors.gray(`Fetching ${url}...`));
|
|
2216
|
-
const startTime = performance.now();
|
|
2217
|
-
try {
|
|
2218
|
-
const response = await this.client.get(url);
|
|
2219
|
-
const html = await response.text();
|
|
2220
|
-
const duration = Math.round(performance.now() - startTime);
|
|
2221
|
-
this.currentDoc = await ScrapeDocument.create(html, { baseUrl: url });
|
|
2222
|
-
this.currentDocUrl = url;
|
|
2223
|
-
const elementCount = this.currentDoc.select('*').length;
|
|
2224
|
-
const title = this.currentDoc.selectFirst('title').text() || 'No title';
|
|
2225
|
-
const meta = this.currentDoc.meta();
|
|
2226
|
-
const og = this.currentDoc.openGraph();
|
|
2227
|
-
console.log(colors.green(`✔ Loaded`) + colors.gray(` (${duration}ms)`));
|
|
2228
|
-
console.log(` ${colors.cyan('Title')}: ${title}`);
|
|
2229
|
-
console.log(` ${colors.cyan('Elements')}: ${elementCount}`);
|
|
2230
|
-
console.log(` ${colors.cyan('Size')}: ${(html.length / 1024).toFixed(1)}kb`);
|
|
2231
|
-
if (meta.description) {
|
|
2232
|
-
const desc = meta.description.length > 100 ? meta.description.slice(0, 100) + '...' : meta.description;
|
|
2233
|
-
console.log(` ${colors.cyan('Description')}: ${desc}`);
|
|
2234
|
-
}
|
|
2235
|
-
const hasOg = og.title || og.description || og.image || og.siteName;
|
|
2236
|
-
if (hasOg) {
|
|
2237
|
-
console.log(colors.bold('\n OpenGraph:'));
|
|
2238
|
-
if (og.siteName)
|
|
2239
|
-
console.log(` ${colors.magenta('Site')}: ${og.siteName}`);
|
|
2240
|
-
if (og.title && og.title !== title)
|
|
2241
|
-
console.log(` ${colors.magenta('Title')}: ${og.title}`);
|
|
2242
|
-
if (og.type)
|
|
2243
|
-
console.log(` ${colors.magenta('Type')}: ${og.type}`);
|
|
2244
|
-
if (og.description) {
|
|
2245
|
-
const ogDesc = og.description.length > 80 ? og.description.slice(0, 80) + '...' : og.description;
|
|
2246
|
-
console.log(` ${colors.magenta('Description')}: ${ogDesc}`);
|
|
2247
|
-
}
|
|
2248
|
-
if (og.image) {
|
|
2249
|
-
const images = Array.isArray(og.image) ? og.image : [og.image];
|
|
2250
|
-
console.log(` ${colors.magenta('Image')}: ${images[0]}`);
|
|
2251
|
-
if (images.length > 1)
|
|
2252
|
-
console.log(colors.gray(` (+${images.length - 1} more)`));
|
|
2253
|
-
}
|
|
2254
|
-
if (og.url && og.url !== url)
|
|
2255
|
-
console.log(` ${colors.magenta('URL')}: ${og.url}`);
|
|
2256
|
-
}
|
|
2257
|
-
console.log(colors.gray('\n Use $ <selector> to query, $text, $attr, $links, $images, $scripts, $css, $sourcemaps, $table'));
|
|
2258
|
-
}
|
|
2259
|
-
catch (error) {
|
|
2260
|
-
console.error(colors.red(`Scrape failed: ${error.message}`));
|
|
2261
|
-
}
|
|
2262
|
-
console.log('');
|
|
2263
|
-
}
|
|
2264
|
-
async runSpider(args) {
|
|
2265
|
-
let url = '';
|
|
2266
|
-
let maxDepth = 5;
|
|
2267
|
-
let maxPages = 100;
|
|
2268
|
-
let concurrency = 5;
|
|
2269
|
-
let seoEnabled = false;
|
|
2270
|
-
let outputFile = '';
|
|
2271
|
-
for (let i = 0; i < args.length; i++) {
|
|
2272
|
-
const arg = args[i];
|
|
2273
|
-
if (arg.startsWith('depth=')) {
|
|
2274
|
-
maxDepth = parseInt(arg.split('=')[1]) || 5;
|
|
2275
|
-
}
|
|
2276
|
-
else if (arg.startsWith('limit=')) {
|
|
2277
|
-
maxPages = parseInt(arg.split('=')[1]) || 100;
|
|
2278
|
-
}
|
|
2279
|
-
else if (arg.startsWith('concurrency=')) {
|
|
2280
|
-
concurrency = parseInt(arg.split('=')[1]) || 5;
|
|
2281
|
-
}
|
|
2282
|
-
else if (arg === 'seo') {
|
|
2283
|
-
seoEnabled = true;
|
|
2284
|
-
}
|
|
2285
|
-
else if (arg.startsWith('output=')) {
|
|
2286
|
-
outputFile = arg.split('=')[1] || '';
|
|
2287
|
-
}
|
|
2288
|
-
else if (!arg.includes('=')) {
|
|
2289
|
-
url = arg;
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
if (!url) {
|
|
2293
|
-
if (!this.baseUrl) {
|
|
2294
|
-
console.log(colors.yellow('Usage: spider <url> [options]'));
|
|
2295
|
-
console.log(colors.gray(' Options:'));
|
|
2296
|
-
console.log(colors.gray(' depth=5 Max crawl depth'));
|
|
2297
|
-
console.log(colors.gray(' limit=100 Max pages to crawl'));
|
|
2298
|
-
console.log(colors.gray(' concurrency=5 Concurrent requests'));
|
|
2299
|
-
console.log(colors.gray(' seo Enable SEO analysis'));
|
|
2300
|
-
console.log(colors.gray(' output=file.json Save JSON report'));
|
|
2301
|
-
console.log(colors.gray(' Examples:'));
|
|
2302
|
-
console.log(colors.gray(' spider example.com'));
|
|
2303
|
-
console.log(colors.gray(' spider example.com depth=2 limit=50'));
|
|
2304
|
-
console.log(colors.gray(' spider example.com seo output=seo-report.json'));
|
|
2305
|
-
return;
|
|
2306
|
-
}
|
|
2307
|
-
url = this.baseUrl;
|
|
2308
|
-
}
|
|
2309
|
-
else if (!url.startsWith('http')) {
|
|
2310
|
-
url = `https://${url}`;
|
|
2311
|
-
}
|
|
2312
|
-
console.log(colors.cyan(`\nSpider starting: ${url}`));
|
|
2313
|
-
const modeLabel = seoEnabled ? colors.magenta(' + SEO') : '';
|
|
2314
|
-
console.log(colors.gray(` Depth: ${maxDepth} | Limit: ${maxPages} | Concurrency: ${concurrency}${modeLabel}`));
|
|
2315
|
-
if (outputFile) {
|
|
2316
|
-
console.log(colors.gray(` Output: ${outputFile}`));
|
|
2317
|
-
}
|
|
2318
|
-
console.log('');
|
|
2319
|
-
if (seoEnabled) {
|
|
2320
|
-
const seoSpider = new SeoSpider({
|
|
2321
|
-
maxDepth,
|
|
2322
|
-
maxPages,
|
|
2323
|
-
concurrency,
|
|
2324
|
-
sameDomain: true,
|
|
2325
|
-
delay: 100,
|
|
2326
|
-
seo: true,
|
|
2327
|
-
output: outputFile || undefined,
|
|
2328
|
-
onProgress: (progress) => {
|
|
2329
|
-
process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
|
|
2330
|
-
},
|
|
2331
|
-
});
|
|
2332
|
-
try {
|
|
2333
|
-
const result = await seoSpider.crawl(url);
|
|
2334
|
-
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
2335
|
-
console.log(colors.green(`\n✔ SEO Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
|
|
2336
|
-
console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
|
|
2337
|
-
console.log(` ${colors.cyan('Unique URLs')}: ${result.visited.size}`);
|
|
2338
|
-
console.log(` ${colors.cyan('Avg SEO Score')}: ${result.summary.avgScore}/100`);
|
|
2339
|
-
const responseTimes = result.pages.filter(p => p.duration > 0).map(p => p.duration);
|
|
2340
|
-
const avgResponseTime = responseTimes.length > 0
|
|
2341
|
-
? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length)
|
|
2342
|
-
: 0;
|
|
2343
|
-
const minResponseTime = responseTimes.length > 0 ? Math.min(...responseTimes) : 0;
|
|
2344
|
-
const maxResponseTime = responseTimes.length > 0 ? Math.max(...responseTimes) : 0;
|
|
2345
|
-
const reqPerSec = result.duration > 0 ? (result.pages.length / (result.duration / 1000)).toFixed(1) : '0';
|
|
2346
|
-
const statusCounts = new Map();
|
|
2347
|
-
for (const page of result.pages) {
|
|
2348
|
-
const status = page.status || 0;
|
|
2349
|
-
statusCounts.set(status, (statusCounts.get(status) || 0) + 1);
|
|
2350
|
-
}
|
|
2351
|
-
let totalInternalLinks = 0;
|
|
2352
|
-
let totalExternalLinks = 0;
|
|
2353
|
-
let totalImages = 0;
|
|
2354
|
-
let imagesWithoutAlt = 0;
|
|
2355
|
-
let pagesWithoutTitle = 0;
|
|
2356
|
-
let pagesWithoutDescription = 0;
|
|
2357
|
-
for (const page of result.pages) {
|
|
2358
|
-
if (page.seoReport) {
|
|
2359
|
-
totalInternalLinks += page.seoReport.links?.internal || 0;
|
|
2360
|
-
totalExternalLinks += page.seoReport.links?.external || 0;
|
|
2361
|
-
totalImages += page.seoReport.images?.total || 0;
|
|
2362
|
-
imagesWithoutAlt += page.seoReport.images?.withoutAlt || 0;
|
|
2363
|
-
if (!page.seoReport.title?.text)
|
|
2364
|
-
pagesWithoutTitle++;
|
|
2365
|
-
if (!page.seoReport.metaDescription?.text)
|
|
2366
|
-
pagesWithoutDescription++;
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
|
-
console.log(colors.bold('\n Performance:'));
|
|
2370
|
-
console.log(` ${colors.gray('Avg Response:')} ${avgResponseTime}ms`);
|
|
2371
|
-
console.log(` ${colors.gray('Min/Max:')} ${minResponseTime}ms / ${maxResponseTime}ms`);
|
|
2372
|
-
console.log(` ${colors.gray('Throughput:')} ${reqPerSec} req/s`);
|
|
2373
|
-
console.log(colors.bold('\n HTTP Status:'));
|
|
2374
|
-
const sortedStatuses = Array.from(statusCounts.entries()).sort((a, b) => b[1] - a[1]);
|
|
2375
|
-
for (const [status, count] of sortedStatuses.slice(0, 5)) {
|
|
2376
|
-
const statusLabel = status === 0 ? 'Error' : status.toString();
|
|
2377
|
-
const statusColor = status >= 400 || status === 0 ? colors.red :
|
|
2378
|
-
status >= 300 ? colors.yellow : colors.green;
|
|
2379
|
-
const pct = ((count / result.pages.length) * 100).toFixed(0);
|
|
2380
|
-
console.log(` ${statusColor(statusLabel.padEnd(5))} ${count.toString().padStart(3)} (${pct}%)`);
|
|
2381
|
-
}
|
|
2382
|
-
console.log(colors.bold('\n Content:'));
|
|
2383
|
-
console.log(` ${colors.gray('Internal links:')} ${totalInternalLinks.toLocaleString()}`);
|
|
2384
|
-
console.log(` ${colors.gray('External links:')} ${totalExternalLinks.toLocaleString()}`);
|
|
2385
|
-
console.log(` ${colors.gray('Images:')} ${totalImages.toLocaleString()} (${imagesWithoutAlt} missing alt)`);
|
|
2386
|
-
console.log(` ${colors.gray('Missing title:')} ${pagesWithoutTitle}`);
|
|
2387
|
-
console.log(` ${colors.gray('Missing desc:')} ${pagesWithoutDescription}`);
|
|
2388
|
-
console.log(colors.bold('\n SEO Summary:'));
|
|
2389
|
-
const { summary } = result;
|
|
2390
|
-
console.log(` ${colors.red('✗')} Pages with errors: ${summary.pagesWithErrors}`);
|
|
2391
|
-
console.log(` ${colors.yellow('⚠')} Pages with warnings: ${summary.pagesWithWarnings}`);
|
|
2392
|
-
console.log(` ${colors.magenta('⚐')} Duplicate titles: ${summary.duplicateTitles}`);
|
|
2393
|
-
console.log(` ${colors.magenta('⚐')} Duplicate descriptions:${summary.duplicateDescriptions}`);
|
|
2394
|
-
console.log(` ${colors.magenta('⚐')} Duplicate H1s: ${summary.duplicateH1s}`);
|
|
2395
|
-
console.log(` ${colors.gray('○')} Orphan pages: ${summary.orphanPages}`);
|
|
2396
|
-
if (result.siteWideIssues.length > 0) {
|
|
2397
|
-
console.log(colors.bold('\n Site-Wide Issues:'));
|
|
2398
|
-
for (const issue of result.siteWideIssues.slice(0, 10)) {
|
|
2399
|
-
const icon = issue.severity === 'error' ? colors.red('✗') :
|
|
2400
|
-
issue.severity === 'warning' ? colors.yellow('⚠') : colors.gray('○');
|
|
2401
|
-
console.log(` ${icon} ${issue.message}`);
|
|
2402
|
-
if (issue.value) {
|
|
2403
|
-
const truncatedValue = issue.value.length > 50 ? issue.value.slice(0, 47) + '...' : issue.value;
|
|
2404
|
-
console.log(` ${colors.gray(`"${truncatedValue}"`)}`);
|
|
2405
|
-
}
|
|
2406
|
-
const uniquePaths = [...new Set(issue.affectedUrls.map(u => new URL(u).pathname))];
|
|
2407
|
-
if (uniquePaths.length <= 3) {
|
|
2408
|
-
for (const path of uniquePaths) {
|
|
2409
|
-
console.log(` ${colors.gray('→')} ${path}`);
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
|
-
else {
|
|
2413
|
-
console.log(` ${colors.gray(`→ ${uniquePaths.length} pages affected`)}`);
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
if (result.siteWideIssues.length > 10) {
|
|
2417
|
-
console.log(colors.gray(` ... and ${result.siteWideIssues.length - 10} more issues`));
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
const pagesWithScores = result.pages
|
|
2421
|
-
.filter(p => p.seoReport)
|
|
2422
|
-
.sort((a, b) => (a.seoReport?.score || 0) - (b.seoReport?.score || 0));
|
|
2423
|
-
const seenPaths = new Set();
|
|
2424
|
-
const uniquePages = pagesWithScores.filter(page => {
|
|
2425
|
-
const path = new URL(page.url).pathname;
|
|
2426
|
-
if (seenPaths.has(path))
|
|
2427
|
-
return false;
|
|
2428
|
-
seenPaths.add(path);
|
|
2429
|
-
return true;
|
|
2430
|
-
});
|
|
2431
|
-
if (uniquePages.length > 0) {
|
|
2432
|
-
console.log(colors.bold('\n Pages by SEO Score:'));
|
|
2433
|
-
const worstPages = uniquePages.slice(0, 5);
|
|
2434
|
-
for (const page of worstPages) {
|
|
2435
|
-
const score = page.seoReport?.score || 0;
|
|
2436
|
-
const grade = page.seoReport?.grade || '?';
|
|
2437
|
-
const path = new URL(page.url).pathname;
|
|
2438
|
-
const scoreColor = score >= 80 ? colors.green : score >= 60 ? colors.yellow : colors.red;
|
|
2439
|
-
console.log(` ${scoreColor(`${score.toString().padStart(3)}`)} ${colors.gray(`[${grade}]`)} ${path.slice(0, 50)}`);
|
|
2440
|
-
}
|
|
2441
|
-
if (uniquePages.length > 5) {
|
|
2442
|
-
console.log(colors.gray(` ... and ${uniquePages.length - 5} more pages`));
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
if (outputFile) {
|
|
2446
|
-
console.log(colors.green(`\n Report saved to: ${outputFile}`));
|
|
2447
|
-
}
|
|
2448
|
-
this.lastResponse = result;
|
|
2449
|
-
console.log(colors.gray('\n Result stored in lastResponse.'));
|
|
2450
|
-
}
|
|
2451
|
-
catch (error) {
|
|
2452
|
-
console.error(colors.red(`SEO Spider failed: ${error.message}`));
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
else {
|
|
2456
|
-
const spider = new Spider({
|
|
2457
|
-
maxDepth,
|
|
2458
|
-
maxPages,
|
|
2459
|
-
concurrency,
|
|
2460
|
-
sameDomain: true,
|
|
2461
|
-
delay: 100,
|
|
2462
|
-
onProgress: (progress) => {
|
|
2463
|
-
process.stdout.write(`\r${colors.gray(' Crawling:')} ${colors.cyan(progress.crawled.toString())} pages | ${colors.gray('Queue:')} ${progress.queued} | ${colors.gray('Depth:')} ${progress.depth} `);
|
|
2464
|
-
},
|
|
2465
|
-
});
|
|
2466
|
-
try {
|
|
2467
|
-
const result = await spider.crawl(url);
|
|
2468
|
-
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
2469
|
-
console.log(colors.green(`\n✔ Spider complete`) + colors.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
|
|
2470
|
-
console.log(` ${colors.cyan('Pages crawled')}: ${result.pages.length}`);
|
|
2471
|
-
console.log(` ${colors.cyan('Unique URLs')}: ${result.visited.size}`);
|
|
2472
|
-
console.log(` ${colors.cyan('Errors')}: ${result.errors.length}`);
|
|
2473
|
-
const byDepth = new Map();
|
|
2474
|
-
for (const page of result.pages) {
|
|
2475
|
-
byDepth.set(page.depth, (byDepth.get(page.depth) || 0) + 1);
|
|
2476
|
-
}
|
|
2477
|
-
console.log(colors.bold('\n Pages by depth:'));
|
|
2478
|
-
for (const [depth, count] of Array.from(byDepth.entries()).sort((a, b) => a[0] - b[0])) {
|
|
2479
|
-
const bar = '█'.repeat(Math.min(count, 40));
|
|
2480
|
-
console.log(` ${colors.gray(`d${depth}:`)} ${bar} ${count}`);
|
|
2481
|
-
}
|
|
2482
|
-
const topPages = [...result.pages]
|
|
2483
|
-
.filter(p => !p.error)
|
|
2484
|
-
.sort((a, b) => b.links.length - a.links.length)
|
|
2485
|
-
.slice(0, 10);
|
|
2486
|
-
if (topPages.length > 0) {
|
|
2487
|
-
console.log(colors.bold('\n Top pages by outgoing links:'));
|
|
2488
|
-
for (const page of topPages) {
|
|
2489
|
-
const title = page.title.slice(0, 40) || new URL(page.url).pathname;
|
|
2490
|
-
console.log(` ${colors.cyan(page.links.length.toString().padStart(3))} ${title}`);
|
|
2491
|
-
}
|
|
2492
|
-
}
|
|
2493
|
-
if (result.errors.length > 0) {
|
|
2494
|
-
const errorSummary = summarizeErrors(result.errors);
|
|
2495
|
-
console.log(formatErrorSummary(errorSummary));
|
|
2496
|
-
}
|
|
2497
|
-
if (outputFile) {
|
|
2498
|
-
const reportData = {
|
|
2499
|
-
...result,
|
|
2500
|
-
visited: Array.from(result.visited),
|
|
2501
|
-
generatedAt: new Date().toISOString(),
|
|
2502
|
-
};
|
|
2503
|
-
await fs.writeFile(outputFile, JSON.stringify(reportData, null, 2), 'utf-8');
|
|
2504
|
-
console.log(colors.green(`\n Report saved to: ${outputFile}`));
|
|
2505
|
-
}
|
|
2506
|
-
this.lastResponse = result;
|
|
2507
|
-
console.log(colors.gray('\n Result stored in lastResponse. Use $links to explore.'));
|
|
2508
|
-
}
|
|
2509
|
-
catch (error) {
|
|
2510
|
-
console.error(colors.red(`Spider failed: ${error.message}`));
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
console.log('');
|
|
2514
|
-
}
|
|
2515
|
-
async runSelect(selector) {
|
|
2516
|
-
if (!this.currentDoc) {
|
|
2517
|
-
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
2518
|
-
return;
|
|
2519
|
-
}
|
|
2520
|
-
if (!selector) {
|
|
2521
|
-
console.log(colors.yellow('Usage: $ <selector>'));
|
|
2522
|
-
console.log(colors.gray(' Examples: $ h1 | $ .title | $ a[href*="article"]'));
|
|
2523
|
-
return;
|
|
2524
|
-
}
|
|
2525
|
-
try {
|
|
2526
|
-
const elements = this.currentDoc.select(selector);
|
|
2527
|
-
const count = elements.length;
|
|
2528
|
-
console.log(colors.cyan(`Found ${count} element(s)`));
|
|
2529
|
-
if (count > 0 && count <= 10) {
|
|
2530
|
-
elements.each((el, i) => {
|
|
2531
|
-
const text = el.text().slice(0, 80).replace(/\s+/g, ' ').trim();
|
|
2532
|
-
console.log(` ${colors.gray(`${i + 1}.`)} ${text}${text.length >= 80 ? '...' : ''}`);
|
|
2533
|
-
});
|
|
2534
|
-
}
|
|
2535
|
-
else if (count > 10) {
|
|
2536
|
-
console.log(colors.gray(' (showing first 10)'));
|
|
2537
|
-
let shown = 0;
|
|
2538
|
-
elements.each((el, i) => {
|
|
2539
|
-
if (shown >= 10)
|
|
2540
|
-
return;
|
|
2541
|
-
const text = el.text().slice(0, 80).replace(/\s+/g, ' ').trim();
|
|
2542
|
-
console.log(` ${colors.gray(`${i + 1}.`)} ${text}${text.length >= 80 ? '...' : ''}`);
|
|
2543
|
-
shown++;
|
|
2544
|
-
});
|
|
2545
|
-
}
|
|
2546
|
-
this.lastResponse = { count, selector };
|
|
2547
|
-
}
|
|
2548
|
-
catch (error) {
|
|
2549
|
-
console.error(colors.red(`Query failed: ${error.message}`));
|
|
2550
|
-
}
|
|
2551
|
-
console.log('');
|
|
2552
|
-
}
|
|
2553
|
-
async runSelectText(selector) {
|
|
2554
|
-
if (!this.currentDoc) {
|
|
2555
|
-
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
2556
|
-
return;
|
|
2557
|
-
}
|
|
2558
|
-
if (!selector) {
|
|
2559
|
-
console.log(colors.yellow('Usage: $text <selector>'));
|
|
2560
|
-
return;
|
|
2561
|
-
}
|
|
2562
|
-
try {
|
|
2563
|
-
const elements = this.currentDoc.select(selector);
|
|
2564
|
-
const texts = [];
|
|
2565
|
-
elements.each((el, i) => {
|
|
2566
|
-
const text = el.text().trim();
|
|
2567
|
-
if (text) {
|
|
2568
|
-
texts.push(text);
|
|
2569
|
-
console.log(`${colors.gray(`${i + 1}.`)} ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`);
|
|
2570
|
-
}
|
|
2571
|
-
});
|
|
2572
|
-
this.lastResponse = texts;
|
|
2573
|
-
console.log(colors.gray(`\n ${texts.length} text item(s) extracted`));
|
|
2574
|
-
}
|
|
2575
|
-
catch (error) {
|
|
2576
|
-
console.error(colors.red(`Query failed: ${error.message}`));
|
|
2577
|
-
}
|
|
2578
|
-
console.log('');
|
|
2579
|
-
}
|
|
2580
|
-
async runSelectAttr(attrName, selector) {
|
|
2581
|
-
if (!this.currentDoc) {
|
|
2582
|
-
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
2583
|
-
return;
|
|
2584
|
-
}
|
|
2585
|
-
if (!attrName || !selector) {
|
|
2586
|
-
console.log(colors.yellow('Usage: $attr <attribute> <selector>'));
|
|
2587
|
-
console.log(colors.gray(' Examples: $attr href a | $attr src img'));
|
|
2588
|
-
return;
|
|
2589
|
-
}
|
|
2590
|
-
try {
|
|
2591
|
-
const elements = this.currentDoc.select(selector);
|
|
2592
|
-
const attrs = [];
|
|
2593
|
-
elements.each((el, i) => {
|
|
2594
|
-
const value = el.attr(attrName);
|
|
2595
|
-
if (value) {
|
|
2596
|
-
attrs.push(value);
|
|
2597
|
-
console.log(`${colors.gray(`${i + 1}.`)} ${value}`);
|
|
2598
|
-
}
|
|
2599
|
-
});
|
|
2600
|
-
this.lastResponse = attrs;
|
|
2601
|
-
console.log(colors.gray(`\n ${attrs.length} attribute(s) extracted`));
|
|
2602
|
-
}
|
|
2603
|
-
catch (error) {
|
|
2604
|
-
console.error(colors.red(`Query failed: ${error.message}`));
|
|
2605
|
-
}
|
|
2606
|
-
console.log('');
|
|
2607
|
-
}
|
|
2608
|
-
async runSelectHtml(selector) {
|
|
2609
|
-
if (!this.currentDoc) {
|
|
2610
|
-
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
2611
|
-
return;
|
|
2612
|
-
}
|
|
2613
|
-
if (!selector) {
|
|
2614
|
-
console.log(colors.yellow('Usage: $html <selector>'));
|
|
2615
|
-
return;
|
|
2616
|
-
}
|
|
2617
|
-
try {
|
|
2618
|
-
const element = this.currentDoc.selectFirst(selector);
|
|
2619
|
-
const html = element.html();
|
|
2620
|
-
if (html) {
|
|
2621
|
-
console.log(html.slice(0, 1000));
|
|
2622
|
-
if (html.length > 1000) {
|
|
2623
|
-
console.log(colors.gray(`\n ... (${html.length} chars total)`));
|
|
2624
|
-
}
|
|
2625
|
-
this.lastResponse = html;
|
|
2626
|
-
}
|
|
2627
|
-
else {
|
|
2628
|
-
console.log(colors.gray('No element found'));
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
|
-
catch (error) {
|
|
2632
|
-
console.error(colors.red(`Query failed: ${error.message}`));
|
|
2633
|
-
}
|
|
2634
|
-
console.log('');
|
|
2635
|
-
}
|
|
2636
|
-
async runSelectLinks(selector) {
|
|
2637
|
-
if (!this.currentDoc) {
|
|
2638
|
-
console.log(colors.yellow('No document loaded. Use "scrap <url>" first.'));
|
|
2639
|
-
return;
|
|
2640
|
-
}
|
|
2641
|
-
try {
|
|
2642
|
-
const linkSelector = selector || 'a[href]';
|
|
2643
|
-
const elements = this.currentDoc.select(linkSelector);
|
|
2644
|
-
const links = [];
|
|
2645
|
-
elements.each((el, i) => {
|
|
2646
|
-
const href = el.attr('href');
|
|
2647
|
-
const text = el.text().trim().slice(0, 50);
|
|
2648
|
-
if (href) {
|
|
2649
|
-
links.push({ text, href });
|
|
2650
|
-
if (i < 20) {
|
|
2651
|
-
console.log(`${colors.gray(`${i + 1}.`)} ${colors.cyan(text || '(no text)')} ${colors.gray('→')} ${href}`);
|
|
2652
|
-
}
|
|
2653
|
-
}
|
|
2654
|
-
});
|
|
2655
|
-
if (links.length > 20) {
|
|
2656
|
-
console.log(colors.gray(` ... and ${links.length - 20} more links`));
|
|
2657
|
-
}
|
|
2658
|
-
this.lastResponse = links;
|
|
2659
|
-
console.log(colors.gray(`\n ${links.length} link(s) found`));
|
|
2660
|
-
}
|
|
2661
|
-
catch (error) {
|
|
2662
|
-
console.error(colors.red(`Query failed: ${error.message}`));
|
|
2663
|
-
}
|
|
2664
|
-
console.log('');
|
|
1368
|
+
console.log('');
|
|
2665
1369
|
}
|
|
2666
1370
|
async runSelectImages(selector) {
|
|
2667
1371
|
if (!this.currentDoc) {
|
|
@@ -3378,44 +2082,6 @@ ${colors.bold('Network:')}
|
|
|
3378
2082
|
harRecording = false;
|
|
3379
2083
|
harFile = '';
|
|
3380
2084
|
harEntries = [];
|
|
3381
|
-
async runHar(args) {
|
|
3382
|
-
if (args.length === 0 || args[0] === 'help') {
|
|
3383
|
-
console.log(colors.bold('HAR Recording & Playback'));
|
|
3384
|
-
console.log('');
|
|
3385
|
-
console.log(colors.yellow('Commands:'));
|
|
3386
|
-
console.log(' har:record <file> Start recording requests to HAR file');
|
|
3387
|
-
console.log(' har:play <file> Replay requests from HAR file');
|
|
3388
|
-
console.log(' har:info <file> Show HAR file information');
|
|
3389
|
-
console.log('');
|
|
3390
|
-
console.log(colors.gray('Examples:'));
|
|
3391
|
-
console.log(' har:record api.har Start recording session');
|
|
3392
|
-
console.log(' har:play api.har Replay recorded requests');
|
|
3393
|
-
console.log(' har:info api.har Inspect HAR file contents');
|
|
3394
|
-
console.log('');
|
|
3395
|
-
console.log(colors.gray('Recording mode:'));
|
|
3396
|
-
console.log(' While recording, all HTTP requests are saved to the HAR file.');
|
|
3397
|
-
console.log(' Use "har:stop" to end recording.');
|
|
3398
|
-
return;
|
|
3399
|
-
}
|
|
3400
|
-
const [subCmd, ...rest] = args;
|
|
3401
|
-
switch (subCmd) {
|
|
3402
|
-
case 'record':
|
|
3403
|
-
await this.runHarRecord(rest);
|
|
3404
|
-
break;
|
|
3405
|
-
case 'play':
|
|
3406
|
-
await this.runHarPlay(rest);
|
|
3407
|
-
break;
|
|
3408
|
-
case 'info':
|
|
3409
|
-
await this.runHarInfo(rest[0]);
|
|
3410
|
-
break;
|
|
3411
|
-
case 'stop':
|
|
3412
|
-
await this.runHarStop();
|
|
3413
|
-
break;
|
|
3414
|
-
default:
|
|
3415
|
-
console.log(colors.yellow(`Unknown HAR command: ${subCmd}`));
|
|
3416
|
-
console.log(colors.gray('Use "har help" for usage'));
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
2085
|
async runHarRecord(args) {
|
|
3420
2086
|
if (args.length === 0) {
|
|
3421
2087
|
console.log(colors.yellow('Usage: har:record <file>'));
|
|
@@ -3547,329 +2213,6 @@ ${colors.bold('Network:')}
|
|
|
3547
2213
|
}
|
|
3548
2214
|
console.log('');
|
|
3549
2215
|
}
|
|
3550
|
-
async runGraphQL(args) {
|
|
3551
|
-
if (args.length === 0 || args[0] === 'help') {
|
|
3552
|
-
console.log(colors.bold('GraphQL Client'));
|
|
3553
|
-
console.log('');
|
|
3554
|
-
console.log(colors.yellow('Usage: graphql <endpoint> <query> [variables...]'));
|
|
3555
|
-
console.log('');
|
|
3556
|
-
console.log(colors.gray('Arguments:'));
|
|
3557
|
-
console.log(' <endpoint> GraphQL endpoint URL or path');
|
|
3558
|
-
console.log(' <query> GraphQL query string (inline or @file.graphql)');
|
|
3559
|
-
console.log(' [variables] Variables as key=value pairs');
|
|
3560
|
-
console.log('');
|
|
3561
|
-
console.log(colors.gray('Examples:'));
|
|
3562
|
-
console.log(' graphql /graphql "{ users { id name } }"');
|
|
3563
|
-
console.log(' graphql https://api.example.com/graphql "query GetUser($id: ID!) { user(id: $id) { name } }" id=123');
|
|
3564
|
-
console.log(' graphql /graphql @query.graphql userId=abc');
|
|
3565
|
-
console.log('');
|
|
3566
|
-
console.log(colors.gray('Notes:'));
|
|
3567
|
-
console.log(' - Use @filename to load query from file');
|
|
3568
|
-
console.log(' - Variables are passed as key=value, JSON supported');
|
|
3569
|
-
return;
|
|
3570
|
-
}
|
|
3571
|
-
let endpoint = args[0];
|
|
3572
|
-
let query = args[1];
|
|
3573
|
-
const variables = {};
|
|
3574
|
-
if (!query) {
|
|
3575
|
-
console.log(colors.yellow('Missing GraphQL query. Use "graphql help" for usage.'));
|
|
3576
|
-
return;
|
|
3577
|
-
}
|
|
3578
|
-
if (!endpoint.startsWith('http')) {
|
|
3579
|
-
if (this.baseUrl) {
|
|
3580
|
-
endpoint = `${this.baseUrl}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
|
|
3581
|
-
}
|
|
3582
|
-
else {
|
|
3583
|
-
console.log(colors.yellow('No base URL set. Provide full URL or use "url <baseUrl>" first.'));
|
|
3584
|
-
return;
|
|
3585
|
-
}
|
|
3586
|
-
}
|
|
3587
|
-
if (query.startsWith('@')) {
|
|
3588
|
-
const filePath = query.slice(1);
|
|
3589
|
-
try {
|
|
3590
|
-
const { promises: fs } = await import('node:fs');
|
|
3591
|
-
query = await fs.readFile(filePath, 'utf-8');
|
|
3592
|
-
console.log(colors.gray(`Loaded query from ${filePath}`));
|
|
3593
|
-
}
|
|
3594
|
-
catch (err) {
|
|
3595
|
-
console.log(colors.red(`Failed to load query file: ${err.message}`));
|
|
3596
|
-
return;
|
|
3597
|
-
}
|
|
3598
|
-
}
|
|
3599
|
-
for (let i = 2; i < args.length; i++) {
|
|
3600
|
-
const arg = args[i];
|
|
3601
|
-
const eqIndex = arg.indexOf('=');
|
|
3602
|
-
if (eqIndex > 0) {
|
|
3603
|
-
const key = arg.slice(0, eqIndex);
|
|
3604
|
-
let value = arg.slice(eqIndex + 1);
|
|
3605
|
-
try {
|
|
3606
|
-
value = JSON.parse(value);
|
|
3607
|
-
}
|
|
3608
|
-
catch {
|
|
3609
|
-
}
|
|
3610
|
-
variables[key] = value;
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
console.log(colors.gray(`POST ${endpoint}`));
|
|
3614
|
-
if (Object.keys(variables).length > 0) {
|
|
3615
|
-
console.log(colors.gray(`Variables: ${JSON.stringify(variables)}`));
|
|
3616
|
-
}
|
|
3617
|
-
const startTime = performance.now();
|
|
3618
|
-
try {
|
|
3619
|
-
const response = await this.client.post(endpoint, {
|
|
3620
|
-
query,
|
|
3621
|
-
variables: Object.keys(variables).length > 0 ? variables : undefined,
|
|
3622
|
-
});
|
|
3623
|
-
const duration = Math.round(performance.now() - startTime);
|
|
3624
|
-
const result = await response.json();
|
|
3625
|
-
if (result.errors && result.errors.length > 0) {
|
|
3626
|
-
console.log(colors.yellow(`\nGraphQL Errors (${duration}ms):`));
|
|
3627
|
-
for (const error of result.errors) {
|
|
3628
|
-
console.log(colors.red(` • ${error.message}`));
|
|
3629
|
-
}
|
|
3630
|
-
if (result.data) {
|
|
3631
|
-
console.log(colors.gray('\nPartial data:'));
|
|
3632
|
-
console.log(highlight(JSON.stringify(result.data, null, 2)));
|
|
3633
|
-
}
|
|
3634
|
-
}
|
|
3635
|
-
else {
|
|
3636
|
-
console.log(colors.green(`\n✔ Success`) + colors.gray(` (${duration}ms)`));
|
|
3637
|
-
console.log('');
|
|
3638
|
-
console.log(highlight(JSON.stringify(result.data, null, 2)));
|
|
3639
|
-
}
|
|
3640
|
-
this.lastResponse = result;
|
|
3641
|
-
}
|
|
3642
|
-
catch (error) {
|
|
3643
|
-
console.error(colors.red(`GraphQL Error: ${error.message}`));
|
|
3644
|
-
}
|
|
3645
|
-
console.log('');
|
|
3646
|
-
}
|
|
3647
|
-
async runJsonRpc(args) {
|
|
3648
|
-
if (args.length === 0 || args[0] === 'help') {
|
|
3649
|
-
console.log(colors.bold('JSON-RPC 2.0 Client'));
|
|
3650
|
-
console.log('');
|
|
3651
|
-
console.log(colors.yellow('Usage: jsonrpc <endpoint> <method> [params...]'));
|
|
3652
|
-
console.log('');
|
|
3653
|
-
console.log(colors.gray('Arguments:'));
|
|
3654
|
-
console.log(' <endpoint> JSON-RPC endpoint URL or path');
|
|
3655
|
-
console.log(' <method> RPC method name');
|
|
3656
|
-
console.log(' [params] Positional args or key=value for named params');
|
|
3657
|
-
console.log('');
|
|
3658
|
-
console.log(colors.gray('Examples:'));
|
|
3659
|
-
console.log(' jsonrpc /rpc add 1 2');
|
|
3660
|
-
console.log(' jsonrpc /rpc getUser id=123');
|
|
3661
|
-
console.log(' jsonrpc https://api.example.com/rpc eth_blockNumber');
|
|
3662
|
-
console.log('');
|
|
3663
|
-
console.log(colors.gray('Notes:'));
|
|
3664
|
-
console.log(' - Positional params: jsonrpc /rpc add 1 2 → params: [1, 2]');
|
|
3665
|
-
console.log(' - Named params: jsonrpc /rpc getUser id=123 → params: {id: 123}');
|
|
3666
|
-
console.log(' - Values are auto-parsed (numbers, booleans, JSON)');
|
|
3667
|
-
return;
|
|
3668
|
-
}
|
|
3669
|
-
let endpoint = args[0];
|
|
3670
|
-
const method = args[1];
|
|
3671
|
-
if (!method) {
|
|
3672
|
-
console.log(colors.yellow('Missing RPC method. Use "jsonrpc help" for usage.'));
|
|
3673
|
-
return;
|
|
3674
|
-
}
|
|
3675
|
-
if (!endpoint.startsWith('http')) {
|
|
3676
|
-
if (this.baseUrl) {
|
|
3677
|
-
endpoint = `${this.baseUrl}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
|
|
3678
|
-
}
|
|
3679
|
-
else {
|
|
3680
|
-
console.log(colors.yellow('No base URL set. Provide full URL or use "url <baseUrl>" first.'));
|
|
3681
|
-
return;
|
|
3682
|
-
}
|
|
3683
|
-
}
|
|
3684
|
-
let params;
|
|
3685
|
-
const positional = [];
|
|
3686
|
-
const named = {};
|
|
3687
|
-
let hasNamed = false;
|
|
3688
|
-
for (let i = 2; i < args.length; i++) {
|
|
3689
|
-
const arg = args[i];
|
|
3690
|
-
const eqIndex = arg.indexOf('=');
|
|
3691
|
-
if (eqIndex > 0) {
|
|
3692
|
-
hasNamed = true;
|
|
3693
|
-
const key = arg.slice(0, eqIndex);
|
|
3694
|
-
let value = arg.slice(eqIndex + 1);
|
|
3695
|
-
try {
|
|
3696
|
-
value = JSON.parse(value);
|
|
3697
|
-
}
|
|
3698
|
-
catch {
|
|
3699
|
-
}
|
|
3700
|
-
named[key] = value;
|
|
3701
|
-
}
|
|
3702
|
-
else {
|
|
3703
|
-
let value = arg;
|
|
3704
|
-
try {
|
|
3705
|
-
value = JSON.parse(arg);
|
|
3706
|
-
}
|
|
3707
|
-
catch {
|
|
3708
|
-
}
|
|
3709
|
-
positional.push(value);
|
|
3710
|
-
}
|
|
3711
|
-
}
|
|
3712
|
-
if (hasNamed && positional.length === 0) {
|
|
3713
|
-
params = named;
|
|
3714
|
-
}
|
|
3715
|
-
else if (!hasNamed && positional.length > 0) {
|
|
3716
|
-
params = positional;
|
|
3717
|
-
}
|
|
3718
|
-
else if (hasNamed && positional.length > 0) {
|
|
3719
|
-
console.log(colors.yellow('Warning: Mixed params detected. Using named params only.'));
|
|
3720
|
-
params = named;
|
|
3721
|
-
}
|
|
3722
|
-
const rpcRequest = {
|
|
3723
|
-
jsonrpc: '2.0',
|
|
3724
|
-
method,
|
|
3725
|
-
params,
|
|
3726
|
-
id: Date.now(),
|
|
3727
|
-
};
|
|
3728
|
-
console.log(colors.gray(`POST ${endpoint}`));
|
|
3729
|
-
console.log(colors.gray(`Method: ${method}`));
|
|
3730
|
-
if (params) {
|
|
3731
|
-
console.log(colors.gray(`Params: ${JSON.stringify(params)}`));
|
|
3732
|
-
}
|
|
3733
|
-
const startTime = performance.now();
|
|
3734
|
-
try {
|
|
3735
|
-
const response = await this.client.post(endpoint, rpcRequest);
|
|
3736
|
-
const duration = Math.round(performance.now() - startTime);
|
|
3737
|
-
const result = await response.json();
|
|
3738
|
-
if (result.error) {
|
|
3739
|
-
console.log(colors.red(`\nRPC Error (${duration}ms):`));
|
|
3740
|
-
console.log(colors.red(` Code: ${result.error.code}`));
|
|
3741
|
-
console.log(colors.red(` Message: ${result.error.message}`));
|
|
3742
|
-
if (result.error.data) {
|
|
3743
|
-
console.log(colors.gray(' Data:'));
|
|
3744
|
-
console.log(highlight(JSON.stringify(result.error.data, null, 2)));
|
|
3745
|
-
}
|
|
3746
|
-
}
|
|
3747
|
-
else {
|
|
3748
|
-
console.log(colors.green(`\n✔ Success`) + colors.gray(` (${duration}ms)`));
|
|
3749
|
-
console.log('');
|
|
3750
|
-
console.log(highlight(JSON.stringify(result.result, null, 2)));
|
|
3751
|
-
}
|
|
3752
|
-
this.lastResponse = result;
|
|
3753
|
-
}
|
|
3754
|
-
catch (error) {
|
|
3755
|
-
console.error(colors.red(`JSON-RPC Error: ${error.message}`));
|
|
3756
|
-
}
|
|
3757
|
-
console.log('');
|
|
3758
|
-
}
|
|
3759
|
-
async runHls(args) {
|
|
3760
|
-
if (args.length === 0 || args[0] === 'help') {
|
|
3761
|
-
console.log(colors.bold('HLS Streaming Client'));
|
|
3762
|
-
console.log('');
|
|
3763
|
-
console.log(colors.yellow('Usage: hls <url> [command] [options...]'));
|
|
3764
|
-
console.log('');
|
|
3765
|
-
console.log(colors.gray('Commands:'));
|
|
3766
|
-
console.log(' info Show stream information (default)');
|
|
3767
|
-
console.log(' download Download the stream to a file');
|
|
3768
|
-
console.log('');
|
|
3769
|
-
console.log(colors.gray('Options:'));
|
|
3770
|
-
console.log(' output=<file> Output file for download (default: stream.ts)');
|
|
3771
|
-
console.log(' quality=<level> Quality selection: highest, lowest (default: highest)');
|
|
3772
|
-
console.log('');
|
|
3773
|
-
console.log(colors.gray('Examples:'));
|
|
3774
|
-
console.log(' hls https://example.com/stream.m3u8');
|
|
3775
|
-
console.log(' hls https://example.com/live.m3u8 info');
|
|
3776
|
-
console.log(' hls https://example.com/vod.m3u8 download output=video.ts');
|
|
3777
|
-
console.log(' hls https://example.com/stream.m3u8 download quality=lowest');
|
|
3778
|
-
return;
|
|
3779
|
-
}
|
|
3780
|
-
let url = args[0];
|
|
3781
|
-
let command = 'info';
|
|
3782
|
-
const options = {};
|
|
3783
|
-
for (let i = 1; i < args.length; i++) {
|
|
3784
|
-
const arg = args[i];
|
|
3785
|
-
if (arg.includes('=')) {
|
|
3786
|
-
const [key, value] = arg.split('=');
|
|
3787
|
-
options[key] = value;
|
|
3788
|
-
}
|
|
3789
|
-
else if (['info', 'download'].includes(arg)) {
|
|
3790
|
-
command = arg;
|
|
3791
|
-
}
|
|
3792
|
-
}
|
|
3793
|
-
if (!url.startsWith('http')) {
|
|
3794
|
-
if (this.baseUrl) {
|
|
3795
|
-
url = `${this.baseUrl}${url.startsWith('/') ? '' : '/'}${url}`;
|
|
3796
|
-
}
|
|
3797
|
-
else {
|
|
3798
|
-
url = `https://${url}`;
|
|
3799
|
-
}
|
|
3800
|
-
}
|
|
3801
|
-
console.log(colors.gray(`Fetching playlist: ${url}`));
|
|
3802
|
-
try {
|
|
3803
|
-
const { hls } = await import('../../plugins/hls.js');
|
|
3804
|
-
const hlsClient = hls(this.client, url, {
|
|
3805
|
-
quality: options.quality || 'highest',
|
|
3806
|
-
});
|
|
3807
|
-
if (command === 'info') {
|
|
3808
|
-
const info = await hlsClient.info();
|
|
3809
|
-
console.log(colors.green('\n✔ HLS Stream Info'));
|
|
3810
|
-
console.log('');
|
|
3811
|
-
if (info.master) {
|
|
3812
|
-
console.log(colors.bold(' Master Playlist:'));
|
|
3813
|
-
console.log(` ${colors.cyan('Variants')}: ${info.master.variants.length}`);
|
|
3814
|
-
console.log('');
|
|
3815
|
-
info.master.variants.forEach((v, i) => {
|
|
3816
|
-
const bandwidth = v.bandwidth ? `${Math.round(v.bandwidth / 1000)}kbps` : 'unknown';
|
|
3817
|
-
const resolution = v.resolution || 'unknown';
|
|
3818
|
-
const selected = info.selectedVariant?.url === v.url ? colors.green(' ★ selected') : '';
|
|
3819
|
-
console.log(` ${colors.gray(`${i + 1}.`)} ${bandwidth} @ ${resolution}${selected}`);
|
|
3820
|
-
if (v.codecs) {
|
|
3821
|
-
console.log(` ${colors.gray('Codecs:')} ${v.codecs}`);
|
|
3822
|
-
}
|
|
3823
|
-
});
|
|
3824
|
-
console.log('');
|
|
3825
|
-
}
|
|
3826
|
-
if (info.playlist) {
|
|
3827
|
-
console.log(colors.bold(' Media Playlist:'));
|
|
3828
|
-
console.log(` ${colors.cyan('Segments')}: ${info.playlist.segments.length}`);
|
|
3829
|
-
console.log(` ${colors.cyan('Target Duration')}: ${info.playlist.targetDuration}s`);
|
|
3830
|
-
console.log(` ${colors.cyan('Type')}: ${info.isLive ? colors.yellow('LIVE') : colors.green('VOD')}`);
|
|
3831
|
-
if (info.totalDuration) {
|
|
3832
|
-
const minutes = Math.floor(info.totalDuration / 60);
|
|
3833
|
-
const seconds = Math.round(info.totalDuration % 60);
|
|
3834
|
-
console.log(` ${colors.cyan('Total Duration')}: ${minutes}m ${seconds}s`);
|
|
3835
|
-
}
|
|
3836
|
-
if (info.playlist.segments.length > 0) {
|
|
3837
|
-
const firstSeg = info.playlist.segments[0];
|
|
3838
|
-
const lastSeg = info.playlist.segments[info.playlist.segments.length - 1];
|
|
3839
|
-
console.log(` ${colors.cyan('Sequence Range')}: ${firstSeg.sequence} - ${lastSeg.sequence}`);
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
this.lastResponse = info;
|
|
3843
|
-
}
|
|
3844
|
-
else if (command === 'download') {
|
|
3845
|
-
const outputFile = options.output || 'stream.ts';
|
|
3846
|
-
console.log(colors.gray(`Downloading to ${outputFile}...`));
|
|
3847
|
-
console.log('');
|
|
3848
|
-
let lastProgress = 0;
|
|
3849
|
-
const startTime = Date.now();
|
|
3850
|
-
await hls(this.client, url, {
|
|
3851
|
-
quality: options.quality || 'highest',
|
|
3852
|
-
onProgress: (progress) => {
|
|
3853
|
-
const now = Date.now();
|
|
3854
|
-
if (now - lastProgress > 500) {
|
|
3855
|
-
lastProgress = now;
|
|
3856
|
-
const kb = Math.round(progress.downloadedBytes / 1024);
|
|
3857
|
-
const total = progress.totalSegments ? ` / ${progress.totalSegments}` : '';
|
|
3858
|
-
process.stdout.write(`\r ${colors.cyan('Segments')}: ${progress.downloadedSegments}${total} | ${colors.cyan('Downloaded')}: ${kb}kb `);
|
|
3859
|
-
}
|
|
3860
|
-
},
|
|
3861
|
-
}).download(outputFile);
|
|
3862
|
-
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
3863
|
-
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
3864
|
-
console.log(colors.green(`✔ Downloaded to ${outputFile}`) + colors.gray(` (${duration}s)`));
|
|
3865
|
-
this.lastResponse = { file: outputFile, duration };
|
|
3866
|
-
}
|
|
3867
|
-
}
|
|
3868
|
-
catch (error) {
|
|
3869
|
-
console.error(colors.red(`HLS Error: ${error.message}`));
|
|
3870
|
-
}
|
|
3871
|
-
console.log('');
|
|
3872
|
-
}
|
|
3873
2216
|
async runRobots(url) {
|
|
3874
2217
|
const targetUrl = url || this.baseUrl;
|
|
3875
2218
|
if (!targetUrl) {
|