agent-readiness 0.4.0
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/LICENSE +21 -0
- package/README.md +142 -0
- package/bin/agent-ready.mjs +296 -0
- package/lib/core.mjs +971 -0
- package/lib/fix.mjs +564 -0
- package/lib/github.mjs +51 -0
- package/lib/report.mjs +57 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Veldin Salcinovic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# agent-ready
|
|
2
|
+
|
|
3
|
+
[](https://agent-ready-web.vercel.app)
|
|
4
|
+
[](https://www.npmjs.com/package/agent-readiness)
|
|
5
|
+
[](https://github.com/VeldinS/agent-ready/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
**Make any website agent-ready in one command.** Scan a site, get an agent-readiness score, and auto-generate the two things the agentic web now expects — an `llms.txt` and a **WebMCP** tool scaffold — so AI agents can *understand and act on* your site instead of blindly scraping it.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npx agent-readiness https://yoursite.com
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> **No install?** Scan any site in your browser at **[agent-ready-web.vercel.app](https://agent-ready-web.vercel.app)** — paste a URL, get the score + per-check breakdown, and download the generated `llms.txt` + WebMCP scaffold.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
agent-ready — scanning https://yoursite.com
|
|
18
|
+
|
|
19
|
+
✗ llms.txt present (0/22)
|
|
20
|
+
→ Add /llms.txt so agents get a clean, token-cheap map of your site (generated below).
|
|
21
|
+
✗ WebMCP tools (document.modelContext) (0/22)
|
|
22
|
+
→ Expose your key actions as WebMCP tools so agents can ACT, not just scrape (scaffold generated below).
|
|
23
|
+
✔ Structured data (JSON-LD / OpenGraph) (+13/13)
|
|
24
|
+
✔ Semantic landmarks + an H1 (+13/13)
|
|
25
|
+
✔ Title + meta description (+10/10)
|
|
26
|
+
✗ robots.txt + sitemap.xml (0/8)
|
|
27
|
+
✔ Canonical URL (<link rel="canonical">) (+4/4)
|
|
28
|
+
✔ Document language (<html lang>) (+4/4)
|
|
29
|
+
✔ Image alt-text coverage (18/20, 90%) (+4/4)
|
|
30
|
+
|
|
31
|
+
Agent-Readiness: 48/100
|
|
32
|
+
pages: 17 • forms: 2 • inferred actions: 3
|
|
33
|
+
Generated → ./agent-ready-out/llms.txt, ./agent-ready-out/webmcp.tools.js, ./agent-ready-out/structured-data.html, ./agent-ready-out/agent-ready.json
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why this exists
|
|
37
|
+
|
|
38
|
+
Chrome's Lighthouse now ships an **"Agentic Browsing"** audit *in the default config* (13.3, May 2026), and **WebMCP** is a W3C draft in origin trial. Millions of sites are about to see a failing agent-readiness grade with no obvious way to fix it. `agent-ready` is the fix: it doesn't just *diagnose* (every scanner does that) — it **generates the artifacts** and is built to run in CI.
|
|
39
|
+
|
|
40
|
+
- **`llms.txt`** — a clean, machine-readable map of your pages (with one-line summaries) and actions, built from a multi-page crawl (the emerging standard agents read first).
|
|
41
|
+
- **`webmcp.tools.js`** — a [WebMCP](https://webmachinelearning.github.io/webmcp/) scaffold that registers each `<form>` and inferred action (search / login / signup / cart / checkout / contact / subscribe) as a callable agent tool via `document.modelContext.registerTool(...)`, so an agent can *complete the signup / search / booking* instead of guessing at your DOM.
|
|
42
|
+
- **`structured-data.html`** — a JSON-LD + OpenGraph snippet to drop into your `<head>` so machines get explicit, structured meaning.
|
|
43
|
+
|
|
44
|
+
## One command, one PR (the fixer)
|
|
45
|
+
|
|
46
|
+
Every other tool *scans*. `agent-ready` also **fixes** — and opens the fix as a pull request:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx agent-readiness fix ./my-site --pr
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
It detects your framework (Next.js App/Pages Router, Vite, or plain static HTML), writes `llms.txt` + `webmcp.tools.js` into the right place, **injects** the `<script>` tag and the missing `<html lang>` / meta description / JSON-LD + OpenGraph into your entry HTML or layout, then branches, commits, and opens a PR with a clear **before → after** score:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
agent-ready fix — ./my-site
|
|
56
|
+
|
|
57
|
+
Framework: Static / plain HTML
|
|
58
|
+
Score: 17/100 → 88/100
|
|
59
|
+
|
|
60
|
+
Files:
|
|
61
|
+
+ llms.txt (token-cheap site map for agents)
|
|
62
|
+
+ webmcp.tools.js (WebMCP tool scaffold)
|
|
63
|
+
inject → index.html (html-lang, meta-description, structured-data, webmcp-script)
|
|
64
|
+
|
|
65
|
+
✔ Opened pull request: https://github.com/you/my-site/pull/42
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `--dry-run` — print the plan (and the exact git/gh commands) without touching anything.
|
|
69
|
+
- `--url https://yoursite.com` — scan the live site for a richer `llms.txt` (multi-page) and to fill real URLs + a canonical tag.
|
|
70
|
+
- `--base <branch>` / `--branch <name>` — control the PR target and head branch.
|
|
71
|
+
- Injection is **idempotent** — re-running never duplicates a tag. `--pr` needs a git repo with an `origin` remote and the [`gh`](https://cli.github.com/) CLI authenticated.
|
|
72
|
+
|
|
73
|
+
Prefer to apply locally without a PR? `agent-ready fix ./my-site` writes + injects in place; `agent-ready <url> --apply ./my-site` just drops the files into `public/`.
|
|
74
|
+
|
|
75
|
+
## Use it in CI (GitHub Action)
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
permissions:
|
|
79
|
+
contents: read
|
|
80
|
+
pull-requests: write # so it can comment the score on the PR
|
|
81
|
+
steps:
|
|
82
|
+
- uses: VeldinS/agent-ready@v0
|
|
83
|
+
with:
|
|
84
|
+
url: https://yoursite.com
|
|
85
|
+
comment: true # post a sticky agent-readiness comment on the PR (default)
|
|
86
|
+
min-score: 60 # optional: fail the job if the score drops below this
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
On every pull request it posts (and updates) a single comment with the score and per-check breakdown. Add the badge to your README once you're green:
|
|
90
|
+
|
|
91
|
+
```md
|
|
92
|
+

|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Use it from your agent (MCP server)
|
|
96
|
+
|
|
97
|
+
Run *"make my site agent-ready"* right inside Claude Code / Cursor / Claude Desktop. The [`@veldins/agent-ready-mcp`](mcp/) server exposes two tools — `scan_site` (score + per-check breakdown) and `generate_fixes` (the `llms.txt` + WebMCP scaffold + structured-data contents) — over stdio, sharing the same scanner core.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
claude mcp add agent-ready -- npx -y @veldins/agent-ready-mcp
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{ "mcpServers": { "agent-ready": { "command": "npx", "args": ["-y", "@veldins/agent-ready-mcp"] } } }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## What it checks
|
|
108
|
+
|
|
109
|
+
| Check | Weight |
|
|
110
|
+
|-------|:--:|
|
|
111
|
+
| `llms.txt` present | 22 |
|
|
112
|
+
| WebMCP tools (`document.modelContext`) | 22 |
|
|
113
|
+
| Structured data (JSON-LD / OpenGraph) | 13 |
|
|
114
|
+
| Semantic landmarks + an `<h1>` | 13 |
|
|
115
|
+
| Title + meta description | 10 |
|
|
116
|
+
| `robots.txt` + `sitemap.xml` | 8 |
|
|
117
|
+
| Canonical URL (`<link rel="canonical">`) | 4 |
|
|
118
|
+
| Document language (`<html lang>`) | 4 |
|
|
119
|
+
| Image alt-text coverage (≥80%) | 4 |
|
|
120
|
+
|
|
121
|
+
> The score reflects the **server-rendered HTML** an agent first sees; heavily client-rendered SPAs will under-report content checks until they ship meaningful HTML.
|
|
122
|
+
|
|
123
|
+
## Roadmap
|
|
124
|
+
|
|
125
|
+
- [x] `fix` mode: write + inject the artifacts and open a PR with a before → after score
|
|
126
|
+
- [x] Framework adapters (Next.js App/Pages Router, Vite, static HTML)
|
|
127
|
+
- [x] GitHub Action comments the score on every PR
|
|
128
|
+
- [x] [Web scanner page](https://agent-ready-web.vercel.app): paste a URL → scored report + downloadable fixes (no install)
|
|
129
|
+
- [x] MCP server so Claude Code / Cursor can run "make my site agent-ready" (`@veldins/agent-ready-mcp`)
|
|
130
|
+
- [ ] Re-grade against live Lighthouse Agentic Browsing on each PR
|
|
131
|
+
|
|
132
|
+
## Local dev
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm install
|
|
136
|
+
node bin/agent-ready.mjs examples/sample-site.html
|
|
137
|
+
npm test # node --test — fixtures, generators, crawl, CLI
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
One runtime dependency ([`node-html-parser`](https://www.npmjs.com/package/node-html-parser)); Node ≥20.19; ESM; no build step.
|
|
141
|
+
|
|
142
|
+
MIT.
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { writeFile, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { join, relative } from 'node:path'
|
|
5
|
+
import { crawl, crawlLocal, analyze, score, badgeUrl, writeArtifacts, buildLlmsTxt, buildWebMcp, buildStructuredData } from '../lib/core.mjs'
|
|
6
|
+
import { detectFramework, buildFixPlan, applyFix, gitInfo, ghReady, openPullRequest } from '../lib/fix.mjs'
|
|
7
|
+
import { prBody, commentBody, COMMENT_MARKER } from '../lib/report.mjs'
|
|
8
|
+
import { ghContext, upsertPrComment } from '../lib/github.mjs'
|
|
9
|
+
|
|
10
|
+
const C = { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', d: '\x1b[2m', b: '\x1b[1m', x: '\x1b[0m' }
|
|
11
|
+
const colorFor = (n) => (n >= 80 ? C.g : n >= 50 ? C.y : C.r)
|
|
12
|
+
|
|
13
|
+
const USAGE = `usage:
|
|
14
|
+
agent-ready <url | local.html> [--apply <dir>] [--comment] [--min-score <n>]
|
|
15
|
+
agent-ready fix <repo-dir> [--url <url>] [--pr] [--dry-run] [--base <branch>] [--branch <name>]`
|
|
16
|
+
|
|
17
|
+
// Tiny flag parser. Supports both `--flag value` and `--flag=value`. For a value
|
|
18
|
+
// flag with no value (next token missing or another --flag), the value is null —
|
|
19
|
+
// matching the old --apply guard.
|
|
20
|
+
function parseArgs(args, valueFlags = []) {
|
|
21
|
+
const flags = {}
|
|
22
|
+
const positional = []
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const a = args[i]
|
|
25
|
+
if (a.startsWith('--')) {
|
|
26
|
+
const eq = a.indexOf('=')
|
|
27
|
+
const name = eq === -1 ? a.slice(2) : a.slice(2, eq)
|
|
28
|
+
if (eq !== -1) {
|
|
29
|
+
flags[name] = a.slice(eq + 1)
|
|
30
|
+
} else if (valueFlags.includes(name)) {
|
|
31
|
+
const next = args[i + 1]
|
|
32
|
+
flags[name] = next === undefined || next.startsWith('--') ? null : args[++i]
|
|
33
|
+
} else {
|
|
34
|
+
flags[name] = true
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
positional.push(a)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { flags, positional }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stamp() {
|
|
44
|
+
const d = new Date()
|
|
45
|
+
const p = (n) => String(n).padStart(2, '0')
|
|
46
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function printChecks(sc) {
|
|
50
|
+
for (const c of sc.checks) {
|
|
51
|
+
const mark = c.pass ? `${C.g}✔${C.x}` : `${C.r}✗${C.x}`
|
|
52
|
+
console.log(` ${mark} ${c.label} ${C.d}(${c.pass ? '+' + c.weight : 0}/${c.weight})${C.x}`)
|
|
53
|
+
if (!c.pass) console.log(` ${C.d}→ ${c.fix}${C.x}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// scan (default command) — unchanged behavior + --comment / --min-score
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
async function runScan(args) {
|
|
62
|
+
const { flags, positional } = parseArgs(args, ['apply', 'min-score'])
|
|
63
|
+
const arg = positional[0]
|
|
64
|
+
if (!arg) {
|
|
65
|
+
console.error(USAGE)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
}
|
|
68
|
+
if ('apply' in flags && !flags.apply) {
|
|
69
|
+
console.error('--apply requires a <project-dir>, e.g. agent-ready example.com --apply ./my-site')
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isLocal = existsSync(arg)
|
|
74
|
+
const target = isLocal ? arg : arg.startsWith('http') ? arg : 'https://' + arg
|
|
75
|
+
console.log(`\n${C.b}agent-ready${C.x} ${C.d}— scanning ${target}${C.x}\n`)
|
|
76
|
+
|
|
77
|
+
let raw
|
|
78
|
+
try {
|
|
79
|
+
raw = isLocal ? await crawlLocal(target) : await crawl(target)
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error(`${C.r}✗ ${err.message}${C.x}`)
|
|
82
|
+
process.exit(1)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const a = analyze(raw)
|
|
86
|
+
const sc = score(a)
|
|
87
|
+
printChecks(sc)
|
|
88
|
+
|
|
89
|
+
console.log(`\n ${C.b}Agent-Readiness:${C.x} ${colorFor(sc.total)}${C.b}${sc.total}/100${C.x}`)
|
|
90
|
+
console.log(` ${C.d}badge: ${badgeUrl(sc.total)}${C.x}`)
|
|
91
|
+
console.log(` ${C.d}pages: ${a.pages.length} • forms: ${a.forms.length} • inferred actions: ${a.actions.length}${C.x}`)
|
|
92
|
+
|
|
93
|
+
const out = './agent-ready-out'
|
|
94
|
+
await writeArtifacts(out, a, sc)
|
|
95
|
+
console.log(`\n ${C.b}Generated${C.x} → ${out}/llms.txt, ${out}/webmcp.tools.js, ${out}/structured-data.html, ${out}/agent-ready.json`)
|
|
96
|
+
|
|
97
|
+
const applyDir = flags.apply
|
|
98
|
+
if (applyDir) {
|
|
99
|
+
const dest = existsSync(join(applyDir, 'public')) ? join(applyDir, 'public') : applyDir
|
|
100
|
+
await mkdir(dest, { recursive: true })
|
|
101
|
+
await Promise.all([
|
|
102
|
+
writeFile(join(dest, 'llms.txt'), buildLlmsTxt(a)),
|
|
103
|
+
writeFile(join(dest, 'webmcp.tools.js'), buildWebMcp(a)),
|
|
104
|
+
writeFile(join(dest, 'structured-data.html'), buildStructuredData(a))
|
|
105
|
+
])
|
|
106
|
+
console.log(`\n ${C.g}${C.b}✔ Applied the fix${C.x} → wrote llms.txt + webmcp.tools.js + structured-data.html into ${C.b}${dest}/${C.x}`)
|
|
107
|
+
console.log(` ${C.d}Tip: ${C.x}agent-ready fix <repo-dir>${C.d} injects the <script> tag + meta for you and can open a PR.${C.x}`)
|
|
108
|
+
} else {
|
|
109
|
+
console.log(` ${C.d}Re-run with ${C.x}--apply <project-dir>${C.d}, or ${C.x}agent-ready fix <repo-dir>${C.d} to inject + open a PR.${C.x}`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (flags.comment) await maybeComment(target, a, sc)
|
|
113
|
+
|
|
114
|
+
if (flags['min-score'] != null) {
|
|
115
|
+
const min = Number(flags['min-score'])
|
|
116
|
+
if (Number.isFinite(min) && sc.total < min) {
|
|
117
|
+
console.error(`\n${C.r}✗ agent-readiness ${sc.total}/100 is below --min-score ${min}${C.x}`)
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function maybeComment(target, a, sc) {
|
|
124
|
+
const ctx = ghContext()
|
|
125
|
+
if (!ctx) {
|
|
126
|
+
console.log(` ${C.d}(--comment: no GitHub PR context/token — skipping)${C.x}`)
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
const body = commentBody({
|
|
130
|
+
url: target,
|
|
131
|
+
total: sc.total,
|
|
132
|
+
checks: sc.checks,
|
|
133
|
+
counts: { pages: a.pages.length, forms: a.forms.length, actions: a.actions.length }
|
|
134
|
+
})
|
|
135
|
+
try {
|
|
136
|
+
const r = await upsertPrComment(ctx, body, COMMENT_MARKER)
|
|
137
|
+
console.log(` ${C.g}✔ ${r.updated ? 'Updated' : 'Posted'} the agent-ready PR comment${C.x}`)
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.log(` ${C.d}(--comment: could not post — ${err.message})${C.x}`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// fix (the one-click fixer)
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
async function runFix(args) {
|
|
148
|
+
const { flags, positional } = parseArgs(args, ['url', 'base', 'branch'])
|
|
149
|
+
const repoDir = positional[0]
|
|
150
|
+
if (!repoDir) {
|
|
151
|
+
console.error(USAGE)
|
|
152
|
+
process.exit(1)
|
|
153
|
+
}
|
|
154
|
+
if (!existsSync(repoDir)) {
|
|
155
|
+
console.error(`${C.r}✗ fix: directory not found: ${repoDir}${C.x}`)
|
|
156
|
+
process.exit(1)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fw = detectFramework(repoDir)
|
|
160
|
+
|
|
161
|
+
let analysis
|
|
162
|
+
let baseUrl = null
|
|
163
|
+
if (flags.url) {
|
|
164
|
+
const url = flags.url.startsWith('http') ? flags.url : 'https://' + flags.url
|
|
165
|
+
console.log(`\n${C.b}agent-ready fix${C.x} ${C.d}— scanning ${url} to generate artifacts${C.x}`)
|
|
166
|
+
try {
|
|
167
|
+
analysis = analyze(await crawl(url))
|
|
168
|
+
baseUrl = analysis.base
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(`${C.r}✗ fix: could not scan ${url}: ${err.message}${C.x}`)
|
|
171
|
+
process.exit(1)
|
|
172
|
+
}
|
|
173
|
+
} else if (fw.entry && existsSync(fw.entry)) {
|
|
174
|
+
analysis = analyze({ base: '', home: readFileSync(fw.entry, 'utf8') })
|
|
175
|
+
} else {
|
|
176
|
+
analysis = analyze({ base: '', home: '' })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const plan = buildFixPlan({ framework: fw, analysis, baseUrl })
|
|
180
|
+
|
|
181
|
+
console.log(`\n${C.b}agent-ready fix${C.x} ${C.d}— ${fw.root}${C.x}\n`)
|
|
182
|
+
console.log(` ${C.b}Framework:${C.x} ${fw.label}`)
|
|
183
|
+
console.log(` ${C.d}files dir: ${plan.framework.publicDir}${C.x}`)
|
|
184
|
+
console.log(` ${C.d}entry: ${fw.entry || '(none detected)'}${C.x}`)
|
|
185
|
+
console.log(
|
|
186
|
+
`\n ${C.b}Score:${C.x} ${plan.before.total}/100 → ${colorFor(plan.after.total)}${C.b}${plan.after.total}/100${C.x}${plan.after.projected ? C.d + ' (projected after deploy)' + C.x : ''}\n`
|
|
187
|
+
)
|
|
188
|
+
console.log(` ${C.b}Files:${C.x}`)
|
|
189
|
+
for (const w of plan.writes) console.log(` ${w.exists ? '~' : C.g + '+' + C.x} ${w.rel} ${C.d}(${w.label})${C.x}`)
|
|
190
|
+
if (plan.injection) console.log(` ${C.d}inject → ${plan.injection.entryRel} (${plan.injection.applied.join(', ') || 'no changes needed'})${C.x}`)
|
|
191
|
+
if (plan.notes.length) {
|
|
192
|
+
console.log(`\n ${C.b}Notes:${C.x}`)
|
|
193
|
+
for (const n of plan.notes) console.log(` ${C.d}• ${n}${C.x}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (flags['dry-run']) {
|
|
197
|
+
console.log(`\n ${C.y}dry run — no files written.${C.x}`)
|
|
198
|
+
if (flags.pr) {
|
|
199
|
+
const gi = gitInfo(repoDir)
|
|
200
|
+
const base = resolveBase(gi, flags) || 'main'
|
|
201
|
+
const branch = flags.branch || `agent-ready/fix-${stamp()}`
|
|
202
|
+
const filesRel = changedRel(gi, plan)
|
|
203
|
+
const pr = openPullRequest({ root: gi && gi.root, branch, base, files: filesRel, title: prTitle(plan), body: '', dryRun: true })
|
|
204
|
+
console.log(`\n ${C.b}Would open a PR with:${C.x}`)
|
|
205
|
+
for (const cmd of pr.commands) console.log(` ${C.d}$ ${cmd}${C.x}`)
|
|
206
|
+
}
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let gi = null
|
|
211
|
+
let base = null
|
|
212
|
+
if (flags.pr) {
|
|
213
|
+
gi = gitInfo(repoDir)
|
|
214
|
+
if (!gi) {
|
|
215
|
+
console.error(`\n${C.r}✗ fix --pr: not a git repository. Run \`git init\` and add a GitHub remote first.${C.x}`)
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
if (!gi.hasOrigin) {
|
|
219
|
+
console.error(`\n${C.r}✗ fix --pr: no \`origin\` remote — push this repo to GitHub first.${C.x}`)
|
|
220
|
+
process.exit(1)
|
|
221
|
+
}
|
|
222
|
+
if (!ghReady(gi.root)) {
|
|
223
|
+
console.error(`\n${C.r}✗ fix --pr: GitHub CLI (gh) is not installed or not authenticated. Install gh and run \`gh auth login\`.${C.x}`)
|
|
224
|
+
process.exit(1)
|
|
225
|
+
}
|
|
226
|
+
base = resolveBase(gi, flags)
|
|
227
|
+
if (!base) {
|
|
228
|
+
console.error(`\n${C.r}✗ fix --pr: detached HEAD and no default branch found — pass --base <branch>.${C.x}`)
|
|
229
|
+
process.exit(1)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const changed = await applyFix(plan)
|
|
234
|
+
console.log(`\n ${C.g}${C.b}✔ Applied${C.x} — wrote/updated ${changed.length} file(s).`)
|
|
235
|
+
|
|
236
|
+
if (flags.pr) {
|
|
237
|
+
const branch = flags.branch || `agent-ready/fix-${stamp()}`
|
|
238
|
+
const filesRel = changed.map((abs) => relative(gi.root, abs))
|
|
239
|
+
const title = prTitle(plan)
|
|
240
|
+
const body = prBody({ before: plan.before, after: plan.after, framework: fw, writes: plan.writes, injection: plan.injection, notes: plan.notes })
|
|
241
|
+
try {
|
|
242
|
+
const r = openPullRequest({ root: gi.root, branch, base, files: filesRel, title, body, dryRun: false })
|
|
243
|
+
if (r.noChanges) {
|
|
244
|
+
console.log(`\n ${C.g}${C.b}✔ Already agent-ready${C.x} — the artifacts match what's committed; no PR needed.`)
|
|
245
|
+
} else {
|
|
246
|
+
console.log(`\n ${C.g}${C.b}✔ Opened pull request:${C.x} ${r.url}`)
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error(`\n${C.r}✗ Could not open the PR: ${err.message}${C.x}`)
|
|
250
|
+
console.error(`${C.d} ${recoveryHint(err)}${C.x}`)
|
|
251
|
+
process.exit(1)
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
console.log(` ${C.d}Re-run with ${C.x}--pr${C.d} to branch, commit, and open a pull request.${C.x}`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// PR base = explicit --base, else the repo's default branch, else the current
|
|
259
|
+
// branch (unless detached). null means "couldn't determine — ask the user".
|
|
260
|
+
function resolveBase(gi, flags) {
|
|
261
|
+
if (flags.base) return flags.base
|
|
262
|
+
if (!gi) return null
|
|
263
|
+
if (gi.defaultBranch) return gi.defaultBranch
|
|
264
|
+
return gi.detached ? null : gi.branch
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Describe the repo's true state for the stage the PR flow failed at.
|
|
268
|
+
function recoveryHint(err) {
|
|
269
|
+
switch (err.agentReadyStage) {
|
|
270
|
+
case 'pushed':
|
|
271
|
+
return `branch "${err.branch}" was committed and pushed to origin — open the PR from GitHub, or delete the branch and retry.`
|
|
272
|
+
case 'committed':
|
|
273
|
+
return `changes were committed locally on "${err.branch}" but not pushed.`
|
|
274
|
+
default:
|
|
275
|
+
return 'the fix files were written to your working tree; commit + open the PR manually.'
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function prTitle(plan) {
|
|
280
|
+
return `agent-ready: add llms.txt + WebMCP scaffold (score ${plan.before.total}→${plan.after.total})`
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function changedRel(gi, plan) {
|
|
284
|
+
const abs = [...plan.writes.map((w) => w.abs)]
|
|
285
|
+
if (plan.injection && plan.injection.after !== plan.injection.before) abs.push(plan.injection.entryAbs)
|
|
286
|
+
return gi ? abs.map((p) => relative(gi.root, p)) : abs
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
const argv = process.argv.slice(2)
|
|
292
|
+
if (argv[0] === 'fix') {
|
|
293
|
+
await runFix(argv.slice(1))
|
|
294
|
+
} else {
|
|
295
|
+
await runScan(argv)
|
|
296
|
+
}
|