botvisibility 1.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # BotVisibility CLI
2
+
3
+ Scan any URL to check how visible and usable it is to AI agents. Like Lighthouse for AI agent readiness.
4
+
5
+ ```bash
6
+ npx botvisibility stripe.com
7
+ ```
8
+
9
+ ## What it does
10
+
11
+ BotVisibility runs 28+ automated checks across 4 levels to measure how well your site works with AI agents like Claude, GPT, Copilot, and autonomous agent frameworks.
12
+
13
+ Without agent-ready metadata and APIs, agents burn 5-100x more tokens through HTML scraping, trial-and-error discovery, and retry loops. A fully unoptimized site can cost agents **120,000-500,000+ excess tokens per session**.
14
+
15
+ The CLI tells you exactly what's missing and how to fix it.
16
+
17
+ ## Install & run
18
+
19
+ No install needed. Just run:
20
+
21
+ ```bash
22
+ npx botvisibility <url>
23
+ ```
24
+
25
+ Or install globally:
26
+
27
+ ```bash
28
+ npm install -g botvisibility
29
+ botvisibility stripe.com
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ # Basic URL scan
36
+ npx botvisibility https://example.com
37
+
38
+ # JSON output for CI/CD
39
+ npx botvisibility stripe.com --json
40
+
41
+ # Full scan with local repo analysis (unlocks Level 3 code checks + Level 4)
42
+ npx botvisibility https://myapp.com --repo ./
43
+
44
+ # Combined scan with JSON output
45
+ npx botvisibility mysite.com --repo ../my-backend --json
46
+ ```
47
+
48
+ ## What it checks
49
+
50
+ ### Level 1: Discoverable (12 checks)
51
+
52
+ Bots can find you. These checks verify that AI agents can discover your site's capabilities without scraping HTML.
53
+
54
+ | Check | What it looks for |
55
+ |-------|-------------------|
56
+ | llms.txt | Machine-readable site description at /llms.txt |
57
+ | Agent Card | Capability declaration at /.well-known/agent-card.json |
58
+ | OpenAPI Spec | Published API specification |
59
+ | robots.txt AI Policy | AI crawler directives in robots.txt |
60
+ | Documentation Accessibility | Public dev docs without auth walls |
61
+ | CORS Headers | Cross-origin access for browser-based agents |
62
+ | AI Meta Tags | llms:description, llms:url, llms:instructions meta tags |
63
+ | Skill File | Structured agent instructions at /skill.md |
64
+ | AI Site Profile | Site manifest at /.well-known/ai.json |
65
+ | Skills Index | Skills catalog at /.well-known/skills/index.json |
66
+ | Link Headers | HTML link elements pointing to AI discovery files |
67
+ | MCP Server | Model Context Protocol endpoint discovery |
68
+
69
+ ### Level 2: Usable (9 checks)
70
+
71
+ Your API works for agents. Authentication, error handling, and core operations are agent-compatible.
72
+
73
+ | Check | What it looks for |
74
+ |-------|-------------------|
75
+ | API Read Operations | GET/list/search endpoints in API spec |
76
+ | API Write Operations | POST/PUT/PATCH/DELETE endpoints |
77
+ | API Primary Action | Core value action available via API |
78
+ | API Key Authentication | Simple API key auth (not just OAuth) |
79
+ | Scoped API Keys | Permission-scoped API keys |
80
+ | OpenID Configuration | OIDC discovery document |
81
+ | Structured Error Responses | JSON errors with codes, not HTML error pages |
82
+ | Async Operations | Job ID + polling for long-running operations |
83
+ | Idempotency Support | Idempotency key support on write endpoints |
84
+
85
+ ### Level 3: Optimized (7 checks)
86
+
87
+ Agents work efficiently. Pagination, filtering, caching, and MCP tools reduce token waste.
88
+
89
+ | Check | What it looks for |
90
+ |-------|-------------------|
91
+ | Sparse Fields | fields/select parameter to request only needed data |
92
+ | Cursor Pagination | Cursor-based pagination on list endpoints |
93
+ | Search & Filtering | Server-side filter and search parameters |
94
+ | Bulk Operations | Batch create/update/delete endpoints |
95
+ | Rate Limit Headers | X-RateLimit-* headers on API responses |
96
+ | Caching Headers | ETag, Cache-Control, Last-Modified headers |
97
+ | MCP Tool Quality | Well-described MCP tools with input schemas |
98
+
99
+ With `--repo`, Level 3 checks also scan your codebase for these patterns in code, catching implementations that the web scanner can't detect from HTTP responses alone.
100
+
101
+ ### Level 4: Agent-Native (7 checks, --repo required)
102
+
103
+ First-class agent support. These checks require local code access.
104
+
105
+ | Check | What it looks for |
106
+ |-------|-------------------|
107
+ | Intent-Based Endpoints | High-level action endpoints (e.g., /send-invoice) |
108
+ | Agent Sessions | Persistent session management for multi-step interactions |
109
+ | Scoped Agent Tokens | Agent-specific tokens with capability limits |
110
+ | Agent Audit Logs | API actions logged with agent identifiers |
111
+ | Sandbox Environment | Test environment for safe agent experimentation |
112
+ | Consequence Labels | Annotations marking irreversible/destructive actions |
113
+ | Native Tool Schemas | Ready-to-use tool definitions for agent frameworks |
114
+
115
+ ## Scoring
116
+
117
+ BotVisibility uses a weighted cross-level algorithm:
118
+
119
+ - **L1 Discoverable**: Pass 50%+ of L1 checks
120
+ - **L2 Usable**: Pass 50%+ of L1 AND 50%+ of L2 (or 35%+ L1 with 75%+ L2)
121
+ - **L3 Optimized**: Achieve L2 AND pass 50%+ of L3 (or 35%+ L2 with 75%+ L3)
122
+ - **L4 Agent-Native**: Requires `--repo` flag for code-level analysis
123
+
124
+ This rewards sites that invest in higher-level capabilities even if some lower-level items are still missing.
125
+
126
+ ## CI/CD integration
127
+
128
+ Add to your CI pipeline to catch agent-readiness regressions:
129
+
130
+ ```yaml
131
+ # GitHub Actions
132
+ - name: Check BotVisibility
133
+ run: |
134
+ SCORE=$(npx botvisibility mysite.com --json | jq '.currentLevel')
135
+ if [ "$SCORE" -lt 1 ]; then
136
+ echo "BotVisibility score below Level 1"
137
+ exit 1
138
+ fi
139
+ ```
140
+
141
+ ## The agent tax
142
+
143
+ Every unoptimized interaction costs AI agents extra tokens:
144
+
145
+ | Without | With | Savings |
146
+ |---------|------|---------|
147
+ | Scrape HTML (30,000 tokens) | Read llms.txt (500 tokens) | 98% |
148
+ | Guess API endpoints (100,000 tokens) | Read OpenAPI spec (15,000 tokens) | 85% |
149
+ | Parse HTML errors (10,000 tokens) | Read JSON error (50 tokens) | 99% |
150
+ | Fetch all fields (2,000 tokens) | Sparse fields (200 tokens) | 90% |
151
+
152
+ At Claude Sonnet 4.6 rates, a single unoptimized session costs $0.83 vs $0.07 optimized. At 1,000 agent visits/day, that's **$22,800/month** in wasted tokens.
153
+
154
+ Read the full analysis: [The Agent Tax](https://botvisibility.com/agent-tax)
155
+
156
+ ## Links
157
+
158
+ - **Scanner**: [botvisibility.com](https://botvisibility.com)
159
+ - **Checklist**: [botvisibility.com/checklist](https://botvisibility.com/checklist)
160
+ - **Badge**: [botvisibility.com/badge](https://botvisibility.com/badge)
161
+ - **Agent Tax Whitepaper**: [botvisibility.com/agent-tax](https://botvisibility.com/agent-tax)
162
+ - **GitHub**: [github.com/jjanisheck/botvisibility](https://github.com/jjanisheck/botvisibility)
163
+
164
+ ## License
165
+
166
+ MIT
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "botvisibility",
3
- "version": "1.0.0",
3
+ "version": "1.2.1",
4
4
  "description": "Scan any URL to check if it's ready for AI agents",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "botvisibility": "./dist/index.js"
8
8
  },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
9
13
  "scripts": {
10
14
  "build": "tsc",
11
15
  "dev": "tsc -w",
@@ -24,6 +28,7 @@
24
28
  ],
25
29
  "author": "Joey Janisheck",
26
30
  "license": "MIT",
31
+ "homepage": "https://botvisibility.com",
27
32
  "repository": {
28
33
  "type": "git",
29
34
  "url": "https://github.com/jjanisheck/botvisibility"
@@ -1,2 +0,0 @@
1
- {"type":"edit","file":"unknown","timestamp":1773448122142,"sessionId":null}
2
- {"type":"edit","file":"unknown","timestamp":1773448124561,"sessionId":null}
package/.next/trace DELETED
@@ -1 +0,0 @@
1
- [{"name":"generate-buildid","duration":71,"timestamp":199541658392,"id":4,"parentId":1,"tags":{},"startTime":1773816019614,"traceId":"260b839151d64215"},{"name":"load-custom-routes","duration":107,"timestamp":199541658494,"id":5,"parentId":1,"tags":{},"startTime":1773816019614,"traceId":"260b839151d64215"},{"name":"create-dist-dir","duration":132,"timestamp":199541658609,"id":6,"parentId":1,"tags":{},"startTime":1773816019614,"traceId":"260b839151d64215"},{"name":"clean","duration":82,"timestamp":199541658966,"id":7,"parentId":1,"tags":{},"startTime":1773816019614,"traceId":"260b839151d64215"},{"name":"next-build","duration":314254,"timestamp":199541346562,"id":1,"tags":{"buildMode":"default","version":"16.1.6","bundler":"turbopack"},"startTime":1773816019302,"traceId":"260b839151d64215"}]
package/.next/trace-build DELETED
@@ -1 +0,0 @@
1
- [{"name":"next-build","duration":314254,"timestamp":199541346562,"id":1,"tags":{"buildMode":"default","version":"16.1.6","bundler":"turbopack"},"startTime":1773816019302,"traceId":"260b839151d64215"}]
package/src/index.ts DELETED
@@ -1,402 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { normalizeUrl, runAllChecks } from './scanner.js';
4
- import { runRepoChecks } from './repo-scanner.js';
5
- import { calculateLevelProgress, getCurrentLevel, LEVELS, CLI_CHECKS } from './scoring.js';
6
- import { CheckResult, ScanResult, RepoCheckResult, LevelProgress } from './types.js';
7
- import * as path from 'path';
8
- import * as readline from 'readline';
9
-
10
- // ANSI colors
11
- const colors = {
12
- reset: '\x1b[0m',
13
- bold: '\x1b[1m',
14
- dim: '\x1b[2m',
15
- red: '\x1b[31m',
16
- green: '\x1b[32m',
17
- yellow: '\x1b[33m',
18
- blue: '\x1b[34m',
19
- magenta: '\x1b[35m',
20
- cyan: '\x1b[36m',
21
- white: '\x1b[37m',
22
- };
23
-
24
- function getLevelColor(levelNumber: number): string {
25
- switch (levelNumber) {
26
- case 1: return colors.red;
27
- case 2: return colors.yellow;
28
- case 3: return colors.green;
29
- case 4: return colors.blue;
30
- default: return colors.white;
31
- }
32
- }
33
-
34
- function printHelp() {
35
- console.log(`
36
- ${colors.bold}BotVisibility CLI${colors.reset}
37
- The Speedtest.net for AI agents. Scan any URL to check bot visibility.
38
-
39
- ${colors.bold}USAGE${colors.reset}
40
- npx botvisibility <url> [options]
41
-
42
- ${colors.bold}OPTIONS${colors.reset}
43
- --json Output results as JSON (for CI/CD integration)
44
- --repo <path> Include local repo analysis for deeper checks (unlocks Level 4)
45
- --help, -h Show this help message
46
-
47
- ${colors.bold}EXAMPLES${colors.reset}
48
- ${colors.dim}# Basic URL scan${colors.reset}
49
- npx botvisibility https://example.com
50
-
51
- ${colors.dim}# JSON output for CI/CD${colors.reset}
52
- npx botvisibility stripe.com --json
53
-
54
- ${colors.dim}# Full scan with repo analysis (unlocks Level 4)${colors.reset}
55
- npx botvisibility https://myapp.com --repo ./
56
-
57
- ${colors.dim}# Combined scan with JSON output${colors.reset}
58
- npx botvisibility clone.fyi --repo ../my-backend --json
59
-
60
- ${colors.bold}LEVELS${colors.reset}
61
- ${colors.red}Level 1: Discoverable${colors.reset} Bots can find you via machine-readable metadata (6 checks)
62
- ${colors.yellow}Level 2: Usable${colors.reset} Your API works for agents (9 checks, most require OpenAPI)
63
- ${colors.green}Level 3: Optimized${colors.reset} Your API minimizes token cost and handles scale (6 checks)
64
- ${colors.blue}Level 4: Agent-Native${colors.reset} Your platform treats AI agents as first-class users (7 checks, --repo required)
65
-
66
- ${colors.bold}LEARN MORE${colors.reset}
67
- https://botvisibility.com
68
- https://github.com/jjanisheck/botvisibility
69
- `);
70
- }
71
-
72
- function statusIcon(status: CheckResult['status']): string {
73
- switch (status) {
74
- case 'pass': return `${colors.green}+${colors.reset}`;
75
- case 'fail': return `${colors.red}x${colors.reset}`;
76
- case 'partial': return `${colors.yellow}~${colors.reset}`;
77
- case 'na': return `${colors.dim}-${colors.reset}`;
78
- }
79
- }
80
-
81
- function printLevelSection(
82
- levelNumber: number,
83
- levelName: string,
84
- checks: (CheckResult | RepoCheckResult)[],
85
- hasRepo: boolean,
86
- ) {
87
- const levelColor = getLevelColor(levelNumber);
88
- const applicable = checks.filter(c => c.status !== 'na');
89
- const passed = checks.filter(c => c.status === 'pass').length;
90
- const naCount = checks.filter(c => c.status === 'na').length;
91
-
92
- // Level 4 header when no --repo
93
- if (levelNumber === 4 && !hasRepo) {
94
- console.log('');
95
- console.log(`${levelColor}${colors.bold}LEVEL ${levelNumber}: ${levelName.toUpperCase()}${colors.reset} ${colors.dim}(--repo required)${colors.reset}`);
96
- console.log(`${colors.dim}${'─'.repeat(55)}${colors.reset}`);
97
- console.log(` ${colors.dim}Run with --repo <path> to unlock Level 4 checks${colors.reset}`);
98
- return;
99
- }
100
-
101
- console.log('');
102
- console.log(`${levelColor}${colors.bold}LEVEL ${levelNumber}: ${levelName.toUpperCase()}${colors.reset}${' '.repeat(Math.max(0, 40 - levelName.length - 10))}${colors.bold}${passed}/${applicable.length}${colors.reset}`);
103
- console.log(`${colors.dim}${'─'.repeat(55)}${colors.reset}`);
104
-
105
- // Print non-NA checks
106
- for (const check of checks) {
107
- if (check.status === 'na') continue;
108
- const icon = statusIcon(check.status);
109
- console.log(` ${icon} ${colors.bold}${check.name}${colors.reset}`);
110
- console.log(` ${colors.dim}${check.message}${colors.reset}`);
111
-
112
- if (check.details) {
113
- console.log(` ${colors.dim}${check.details}${colors.reset}`);
114
- }
115
-
116
- if (check.recommendation && check.status !== 'pass') {
117
- console.log(` ${colors.yellow}-> ${check.recommendation}${colors.reset}`);
118
- }
119
-
120
- if ('filePath' in check && check.filePath) {
121
- console.log(` ${colors.dim}File: ${check.filePath}${colors.reset}`);
122
- }
123
- }
124
-
125
- // Collapsed NA count
126
- if (naCount > 0) {
127
- console.log(` ${colors.dim}- ${naCount} check${naCount === 1 ? '' : 's'} require${naCount === 1 ? 's' : ''} an OpenAPI spec to evaluate${colors.reset}`);
128
- }
129
- }
130
-
131
- function printResults(result: ScanResult, repoChecks?: RepoCheckResult[]) {
132
- const allChecks: CheckResult[] = [...result.checks, ...(repoChecks || [])];
133
- const levelProgress = calculateLevelProgress(allChecks);
134
- const currentLevel = getCurrentLevel(levelProgress);
135
-
136
- const totalPassed = allChecks.filter(c => c.status === 'pass').length;
137
- const totalApplicable = allChecks.filter(c => c.status !== 'na').length;
138
-
139
- // Find the "current working level" — first incomplete
140
- const workingLevel = levelProgress.find(lp => !lp.complete) || levelProgress[levelProgress.length - 1];
141
- const workingLevelColor = getLevelColor(workingLevel.level.number);
142
-
143
- console.log('');
144
- console.log(`${colors.bold}+-------------------------------------------------------+${colors.reset}`);
145
- console.log(`${colors.bold}| BOTVISIBILITY SCAN RESULTS |${colors.reset}`);
146
- console.log(`${colors.bold}+-------------------------------------------------------+${colors.reset}`);
147
- console.log('');
148
- console.log(` ${colors.dim}URL:${colors.reset} ${result.url}`);
149
- console.log(` ${colors.dim}Scanned:${colors.reset} ${new Date(result.timestamp).toLocaleString()}`);
150
- console.log('');
151
-
152
- // Score box
153
- console.log(` ${colors.bold}+-------------------------------------+${colors.reset}`);
154
- console.log(` ${colors.bold}|${colors.reset} ${workingLevelColor}${colors.bold}Level ${workingLevel.level.number}: ${workingLevel.level.name}${colors.reset}${' '.repeat(Math.max(0, 24 - workingLevel.level.name.length))}${colors.bold}|${colors.reset}`);
155
- console.log(` ${colors.bold}|${colors.reset} ${colors.bold}${totalPassed}${colors.reset}${colors.dim}/${totalApplicable}${colors.reset} checks passed${' '.repeat(14)}${colors.bold}|${colors.reset}`);
156
- console.log(` ${colors.bold}+-------------------------------------+${colors.reset}`);
157
- console.log('');
158
-
159
- if (currentLevel === 0) {
160
- console.log(` ${colors.dim}Start by making your site discoverable to AI agents.${colors.reset}`);
161
- } else if (currentLevel < 4) {
162
- const nextLevel = LEVELS[currentLevel]; // 0-indexed: currentLevel is the next one
163
- console.log(` ${colors.dim}Level ${currentLevel} complete! Work on Level ${nextLevel.number}: ${nextLevel.name}.${colors.reset}`);
164
- } else {
165
- console.log(` ${colors.green}All levels complete! Maximum agent visibility achieved.${colors.reset}`);
166
- }
167
-
168
- // Group checks by level
169
- const hasRepo = !!repoChecks && repoChecks.length > 0;
170
-
171
- for (const level of LEVELS) {
172
- const levelChecks = allChecks.filter(c => c.level === level.number);
173
-
174
- if (level.number === 4 && !hasRepo) {
175
- printLevelSection(level.number, level.name, [], false);
176
- } else if (levelChecks.length > 0) {
177
- printLevelSection(level.number, level.name, levelChecks, hasRepo);
178
- }
179
- }
180
-
181
- // Summary
182
- console.log('');
183
- console.log(`${colors.bold}${'='.repeat(55)}${colors.reset}`);
184
- const totalPartial = allChecks.filter(c => c.status === 'partial').length;
185
- const totalFailed = allChecks.filter(c => c.status === 'fail').length;
186
- const totalNa = allChecks.filter(c => c.status === 'na').length;
187
-
188
- console.log(` ${colors.green}+ ${totalPassed} passed${colors.reset} ${colors.yellow}~ ${totalPartial} partial${colors.reset} ${colors.red}x ${totalFailed} failed${colors.reset} ${colors.dim}- ${totalNa} n/a${colors.reset}`);
189
- console.log('');
190
- console.log(` ${colors.dim}Full checklist: https://botvisibility.com${colors.reset}`);
191
- console.log('');
192
- }
193
-
194
- // Ask user a yes/no question
195
- function askQuestion(question: string): Promise<boolean> {
196
- return new Promise((resolve) => {
197
- const rl = readline.createInterface({
198
- input: process.stdin,
199
- output: process.stdout
200
- });
201
-
202
- rl.question(question, (answer) => {
203
- rl.close();
204
- const normalized = answer.toLowerCase().trim();
205
- resolve(normalized === 'y' || normalized === 'yes');
206
- });
207
- });
208
- }
209
-
210
- // Generate the publish payload
211
- interface PublishPayload {
212
- domain: string;
213
- scannedAt: string;
214
- currentLevel: number;
215
- checks: Array<{
216
- id: string;
217
- name: string;
218
- passed: boolean;
219
- status: string;
220
- level: number;
221
- }>;
222
- repoChecks?: Array<{
223
- id: string;
224
- name: string;
225
- passed: boolean;
226
- status: string;
227
- level: number;
228
- }>;
229
- }
230
-
231
- function generatePublishPayload(
232
- result: ScanResult,
233
- repoChecks?: RepoCheckResult[]
234
- ): PublishPayload {
235
- const url = new URL(result.url);
236
- const domain = url.hostname.replace(/^www\./, '');
237
-
238
- return {
239
- domain,
240
- scannedAt: result.timestamp,
241
- currentLevel: result.currentLevel,
242
- checks: result.checks.map(c => ({
243
- id: c.id,
244
- name: c.name,
245
- passed: c.passed,
246
- status: c.status,
247
- level: c.level
248
- })),
249
- repoChecks: repoChecks?.map(c => ({
250
- id: c.id,
251
- name: c.name,
252
- passed: c.passed,
253
- status: c.status,
254
- level: c.level
255
- }))
256
- };
257
- }
258
-
259
- async function promptPublish(result: ScanResult, repoChecks?: RepoCheckResult[]) {
260
- console.log('');
261
- console.log(`${colors.bold}Share Your Score${colors.reset}`);
262
- console.log(`${colors.dim}${'─'.repeat(55)}${colors.reset}`);
263
- console.log('');
264
-
265
- const shouldPublish = await askQuestion(
266
- ` Would you like to publish this score to ${colors.cyan}botvisibility.com${colors.reset}? (y/n) `
267
- );
268
-
269
- if (shouldPublish) {
270
- const payload = generatePublishPayload(result, repoChecks);
271
- const domain = payload.domain;
272
- const profileUrl = `https://botvisibility.com/site/${domain}`;
273
-
274
- console.log('');
275
- console.log(` ${colors.green}+${colors.reset} Score ready to publish!`);
276
- console.log('');
277
- console.log(` ${colors.bold}Your public profile will be available at:${colors.reset}`);
278
- console.log(` ${colors.cyan}${profileUrl}${colors.reset}`);
279
- console.log('');
280
- console.log(` ${colors.dim}This page will show:${colors.reset}`);
281
- console.log(` - Level ${payload.currentLevel} badge`);
282
- console.log(` - Check results and recommendations`);
283
- console.log(` - Score history over time`);
284
- console.log('');
285
-
286
- console.log(` ${colors.dim}Payload preview:${colors.reset}`);
287
- console.log(` ${colors.dim}${JSON.stringify({
288
- domain: payload.domain,
289
- currentLevel: payload.currentLevel,
290
- checks: `${payload.checks.length} URL checks` + (payload.repoChecks ? ` + ${payload.repoChecks.length} repo checks` : '')
291
- })}${colors.reset}`);
292
- console.log('');
293
-
294
- // TODO: When API is ready, uncomment this:
295
- // try {
296
- // const response = await fetch('https://botvisibility.com/api/publish', {
297
- // method: 'POST',
298
- // headers: { 'Content-Type': 'application/json' },
299
- // body: JSON.stringify(payload)
300
- // });
301
- // if (response.ok) {
302
- // const data = await response.json();
303
- // console.log(` ${colors.green}+${colors.reset} Published! View at: ${data.profileUrl}`);
304
- // }
305
- // } catch (e) {
306
- // console.log(` ${colors.yellow}!${colors.reset} Could not publish. Try again later.`);
307
- // }
308
-
309
- console.log(` ${colors.yellow}!${colors.reset} Publishing API coming soon! For now, share your results manually.`);
310
- console.log('');
311
- } else {
312
- console.log('');
313
- console.log(` ${colors.dim}No problem! Run with --json to export results.${colors.reset}`);
314
- console.log('');
315
- }
316
- }
317
-
318
- async function main() {
319
- const args = process.argv.slice(2);
320
-
321
- // Parse flags
322
- const jsonOutput = args.includes('--json');
323
- const helpFlag = args.includes('--help') || args.includes('-h');
324
- const repoIndex = args.indexOf('--repo');
325
- const repoPath = repoIndex !== -1 ? args[repoIndex + 1] : null;
326
-
327
- // Filter out flags to get URL
328
- const urlArgs = args.filter((arg, i) =>
329
- !arg.startsWith('--') &&
330
- !arg.startsWith('-') &&
331
- (repoIndex === -1 || i !== repoIndex + 1)
332
- );
333
-
334
- if (helpFlag || urlArgs.length === 0) {
335
- printHelp();
336
- process.exit(0);
337
- }
338
-
339
- const urlInput = urlArgs[0];
340
-
341
- // Normalize URL
342
- let baseUrl: string;
343
- try {
344
- baseUrl = normalizeUrl(urlInput);
345
- } catch (e) {
346
- console.error(`${colors.red}Error: Invalid URL "${urlInput}"${colors.reset}`);
347
- process.exit(1);
348
- }
349
-
350
- if (!jsonOutput) {
351
- console.log('');
352
- console.log(`${colors.cyan}Scanning ${baseUrl}...${colors.reset}`);
353
- }
354
-
355
- // Run URL checks
356
- const checks = await runAllChecks(baseUrl);
357
-
358
- // Run repo checks if specified
359
- let repoChecks: RepoCheckResult[] | undefined;
360
- if (repoPath) {
361
- const absolutePath = path.resolve(repoPath);
362
- if (!jsonOutput) {
363
- console.log(`${colors.cyan}Scanning repo at ${absolutePath}...${colors.reset}`);
364
- }
365
- repoChecks = runRepoChecks(absolutePath);
366
- }
367
-
368
- // Calculate level progress
369
- const allChecks = [...checks, ...(repoChecks || [])];
370
- const levelProgress = calculateLevelProgress(allChecks);
371
- const currentLevel = getCurrentLevel(levelProgress);
372
-
373
- const result: ScanResult = {
374
- url: baseUrl,
375
- timestamp: new Date().toISOString(),
376
- currentLevel,
377
- levels: levelProgress,
378
- checks,
379
- cliChecks: CLI_CHECKS,
380
- };
381
-
382
- // Output
383
- if (jsonOutput) {
384
- const output = {
385
- ...result,
386
- repoChecks: repoChecks || []
387
- };
388
- console.log(JSON.stringify(output, null, 2));
389
- } else {
390
- printResults(result, repoChecks);
391
-
392
- // Prompt to publish (only in interactive mode)
393
- if (process.stdin.isTTY) {
394
- await promptPublish(result, repoChecks);
395
- }
396
- }
397
- }
398
-
399
- main().catch(err => {
400
- console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
401
- process.exit(1);
402
- });