meodp 0.0.9 → 0.1.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/README.md +204 -22
- package/bin/index.mjs +10 -2
- package/dist/check/cli.cjs +166 -0
- package/dist/check/cli.d.cts +5 -0
- package/dist/check/cli.d.mts +5 -0
- package/dist/check/cli.d.ts +5 -0
- package/dist/check/cli.mjs +158 -0
- package/dist/check/index.cjs +25 -0
- package/dist/check/index.d.cts +107 -0
- package/dist/check/index.d.mts +107 -0
- package/dist/check/index.d.ts +107 -0
- package/dist/check/index.mjs +11 -0
- package/dist/cli/index.cjs +2 -2
- package/dist/cli/index.d.cts +1 -1
- package/dist/cli/index.d.mts +1 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/cli/main.cjs +220 -0
- package/dist/cli/main.d.cts +4 -0
- package/dist/cli/main.d.mts +4 -0
- package/dist/cli/main.d.ts +4 -0
- package/dist/cli/main.mjs +214 -0
- package/dist/shared/meodp.a0bcc3e7.cjs +580 -0
- package/dist/shared/meodp.d0916bc5.mjs +569 -0
- package/package.json +58 -31
- package/bin/index.ts +0 -6
package/README.md
CHANGED
|
@@ -1,46 +1,228 @@
|
|
|
1
1
|
# MEODP (Mystic Eyes of Death Perception)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/meodp)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[English](https://yunyoujun.github.io/meodp/) | [简体中文](https://yunyoujun.github.io/meodp/zh/)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Check website and friend-link availability from Node.js or the command line. Built on [linkinator](https://github.com/JustinBeckwith/linkinator), with interactive HTML, JSON, and Markdown reports and optional history across runs.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Requires **Node.js 22.19+**. The `meodp/check` entry, `meodp check`, and `meodp sitemap` commands do not require Playwright or a browser installation.
|
|
10
|
+
|
|
11
|
+
Guides: [English](https://yunyoujun.github.io/meodp/guide/quick-start.html) · [简体中文](https://yunyoujun.github.io/meodp/zh/guide/quick-start.html). The repository's VitePress site includes API/CLI references and workspace development instructions in both languages.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add meodp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Library
|
|
10
20
|
|
|
11
21
|
```ts
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
import { checkLinks, formatReport, readReport, saveReport, writeReports } from 'meodp/check'
|
|
23
|
+
|
|
24
|
+
const historyFile = '.cache/link-check/local.json'
|
|
25
|
+
const report = await checkLinks([
|
|
26
|
+
{ name: '云游君', url: 'https://www.yunyoujun.cn/' },
|
|
27
|
+
'https://example.com/',
|
|
28
|
+
], {
|
|
29
|
+
observer: 'my-local-network',
|
|
30
|
+
previousReport: await readReport(historyFile),
|
|
31
|
+
concurrency: 5,
|
|
32
|
+
timeoutMs: 10000,
|
|
33
|
+
retries: 1,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
console.log(formatReport(report))
|
|
37
|
+
await writeReports(report, 'reports/links')
|
|
38
|
+
await saveReport(report, historyFile)
|
|
39
|
+
```
|
|
14
40
|
|
|
15
|
-
|
|
41
|
+
`checkLinks()` returns a report without writing files. URL strings and objects with `url` and optional `name` are accepted; duplicate normalized URLs are checked once, in input order. Other fields such as friend email addresses are not copied into reports. Invalid input rejects before any requests are made.
|
|
16
42
|
|
|
43
|
+
| Option | Default | Meaning |
|
|
44
|
+
| ---------------- | ---------------- | --------------------------------------------------------- |
|
|
45
|
+
| `concurrency` | `5` | Simultaneous site checks, from 1 to 100 |
|
|
46
|
+
| `timeoutMs` | `10000` | Per-request timeout in milliseconds, from 1 to 300000 |
|
|
47
|
+
| `retries` | `1` | Additional attempts for transport/5xx errors, from 0 to 5 |
|
|
48
|
+
| `maxRedirects` | `5` | Maximum followed redirects per attempt, from 0 to 20 |
|
|
49
|
+
| `observer` | Machine hostname | Identifies the network used for these observations |
|
|
50
|
+
| `previousReport` | None | Previous report from the same observer |
|
|
51
|
+
| `onResult` | None | Optional callback when each site's observation is ready |
|
|
52
|
+
|
|
53
|
+
Each observation includes its HTTP status, final URL, redirect chain, duration, attempt count, failure reason, last success, consecutive failed runs, and recovery/change flags. `formatReport(report, 'html')` returns a self-contained interactive HTML report; `'json'` returns JSON; the default is Markdown. `writeReports()` writes `report.html`, `report.json`, and `report.md` and returns `{ html, json, markdown }`.
|
|
54
|
+
|
|
55
|
+
## Sitemap page checks
|
|
56
|
+
|
|
57
|
+
Check all pages listed in an XML sitemap, reusing the same HTTP observations, history, and reports:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { checkSitemap, readSitemapUrls, writeReports } from 'meodp/check'
|
|
61
|
+
|
|
62
|
+
const report = await checkSitemap('https://example.com/sitemap.xml', {
|
|
63
|
+
concurrency: 5,
|
|
64
|
+
timeoutMs: 10000,
|
|
65
|
+
maxUrls: 10000,
|
|
17
66
|
})
|
|
67
|
+
await writeReports(report, 'reports/pages')
|
|
68
|
+
|
|
69
|
+
// Start from a site: read Sitemap directives in robots.txt, then /sitemap.xml if none exist.
|
|
70
|
+
const siteReport = await checkSitemap('https://example.com/', { discover: true })
|
|
71
|
+
|
|
72
|
+
// Inspect or select the page list before making any page requests.
|
|
73
|
+
const urls = await readSitemapUrls('https://example.com/sitemap.xml')
|
|
18
74
|
```
|
|
19
75
|
|
|
20
|
-
|
|
76
|
+
```bash
|
|
77
|
+
pnpm exec meodp sitemap https://example.com/sitemap.xml --output reports/pages
|
|
78
|
+
pnpm exec meodp sitemap https://example.com/ --discover --output reports/pages \
|
|
79
|
+
--history .cache/sitemap/local.json --observer my-local-network
|
|
80
|
+
pnpm exec meodp sitemap --help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Supports XML URL sets, nested sitemap indexes, namespaces, CDATA, XML entities, gzip files, redirects, and multiple `Sitemap:` directives in `robots.txt`. Relative entries resolve against the final sitemap URL. Pages and indexes are deduplicated; fragment identifiers are removed and query strings are preserved. Cyclic indexes terminate. Sitemaps may list pages on other HTTP(S) origins; those explicit entries are included.
|
|
84
|
+
|
|
85
|
+
All `checkLinks()` options also apply. `readSitemapUrls()` only requests discovery documents; `checkSitemap()` then checks the complete list with the existing linkinator adapter. Sitemap documents are read sequentially; `concurrency` controls page checks. HTTP timeouts, retries, and redirect limits apply to both phases. Only transport/5xx discovery failures are retried. Robots discovery falls back for 404/410 or a successful file with no sitemap declarations; other errors are surfaced.
|
|
86
|
+
|
|
87
|
+
| Additional option | Default | Meaning |
|
|
88
|
+
| ----------------------------------------- | ---------- | --------------------------------------------------------------------------- |
|
|
89
|
+
| `discover` / `--discover` | `false` | Treat the input as a site URL; otherwise it is an explicit sitemap URL |
|
|
90
|
+
| `maxUrls` / `--max-urls` | `10000` | Maximum unique pages, configurable up to 50000 |
|
|
91
|
+
| `maxSitemaps` / `--max-sitemaps` | `100` | Maximum sitemap documents, configurable up to 10000 |
|
|
92
|
+
| `maxSitemapBytes` / `--max-sitemap-bytes` | `10485760` | Per-document downloaded/decompressed byte limit, configurable up to 100 MiB |
|
|
93
|
+
|
|
94
|
+
Discovery is complete before any page checks begin. A missing or unreadable nested sitemap, invalid XML/URL, empty page list, or exceeded limit rejects the run. The CLI exits with `2` and preserves existing reports/history, even with `--fail-on none`; it never saves a silently truncated page list. Page findings use the same `0`/`1` exit policies as `meodp check`.
|
|
95
|
+
|
|
96
|
+
This mode measures HTTP availability of sitemap-listed pages. It does not follow their article links, check embedded assets, execute JavaScript, or apply robots `Disallow` rules. XML parsing and validation use [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser); MEODP handles bounded discovery and delegates page checks to linkinator. Linkinator also offers a [native sitemap scan](https://github.com/JustinBeckwith/linkinator#command-usage), but its public API combines discovery with page/link scanning. Keeping discovery separate preserves MEODP's URL-only probes and existing redirect/history semantics.
|
|
97
|
+
|
|
98
|
+
## CLI
|
|
99
|
+
|
|
100
|
+
Use explicit subcommands to distinguish a fresh network check from rendering saved data:
|
|
21
101
|
|
|
22
|
-
|
|
102
|
+
| Command | Input | Behavior |
|
|
103
|
+
| -------------- | -------------------------------- | -------------------------------------------------------------- |
|
|
104
|
+
| `meodp check` | URL array in JSON/YAML | Request sites and write fresh HTML, Markdown, and JSON reports |
|
|
105
|
+
| `meodp report` | Saved `report.json`, or no input | Export a static report viewer; no site-check requests |
|
|
106
|
+
| `meodp scan` | Legacy config directory | Run the experimental Playwright scanner |
|
|
23
107
|
|
|
24
|
-
|
|
108
|
+
`meodp`, `meodp --help`, and `meodp help` display the command overview. Use `meodp check -h` or `meodp help report` for details; `meodp --version` / `-v` prints the version. Unknown commands exit with code `2` instead of starting a scan. Help and version do not require Playwright.
|
|
109
|
+
|
|
110
|
+
Create `links.yml` (JSON arrays also work):
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
- name: 云游君
|
|
114
|
+
url: https://www.yunyoujun.cn/
|
|
115
|
+
- name: Example
|
|
116
|
+
url: https://example.com/
|
|
117
|
+
```
|
|
25
118
|
|
|
26
119
|
```bash
|
|
27
|
-
|
|
120
|
+
pnpm exec meodp check links.yml --output reports/links
|
|
121
|
+
|
|
122
|
+
# Persist observations from one network across separate runs.
|
|
123
|
+
pnpm exec meodp check links.yml --output reports/links \
|
|
124
|
+
--history .cache/link-check/local.json --observer my-local-network
|
|
125
|
+
|
|
126
|
+
# Generate a maintenance report without failing because a friend is unavailable.
|
|
127
|
+
pnpm exec meodp check links.yml --fail-on none
|
|
128
|
+
|
|
129
|
+
pnpm exec meodp check --help
|
|
28
130
|
```
|
|
29
131
|
|
|
30
|
-
|
|
132
|
+
`meodp check` inputs are local `.json`, `.yml`, or `.yaml` files. A friends-style array containing additional fields is supported directly. Only its `url` and `name` are used. Download remote list data explicitly before checking it; use `meodp sitemap` for remote sitemap inputs.
|
|
133
|
+
|
|
134
|
+
Exit codes:
|
|
135
|
+
|
|
136
|
+
- `0`: the selected policy passed.
|
|
137
|
+
- `1`: observations matched the policy. Default `--fail-on unavailable` fails on unavailable sites; `--fail-on review` also includes restricted access and redirects.
|
|
138
|
+
- `2`: invalid input/options, incompatible or corrupt history, or an execution/report-writing error. `--fail-on none` does not suppress these errors.
|
|
31
139
|
|
|
32
|
-
###
|
|
140
|
+
### Project scripts
|
|
33
141
|
|
|
34
|
-
|
|
35
|
-
它的命令行应该能满足你使用命令行的大部分需求。
|
|
142
|
+
Keep the package CLI general and name project scripts after their specific subject:
|
|
36
143
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"scripts": {
|
|
147
|
+
"check:links": "meodp check public/links.yml --output reports/links --fail-on none",
|
|
148
|
+
"report:links": "meodp report reports/links/report.json --output reports/site"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Use `pnpm run check:links` and `pnpm run report:links`; keep `lint`, `typecheck`, and `test` for code validation. `report:links` should render existing observations rather than implicitly invoke `check:links`. This makes exporting a report reproducible without another network scan. In CI, select the failure policy explicitly; friends uses `check:links:ci` with its own observer and history file.
|
|
154
|
+
|
|
155
|
+
Documentation and CI use explicit `pnpm run <script>` for project scripts and `pnpm exec meodp <command>` for the package CLI. [pnpm documents](https://pnpm.io/cli/run) script-name shorthand only when it does not conflict with a built-in command; options after the script name are passed to the script.
|
|
156
|
+
|
|
157
|
+
## Interactive report and static site
|
|
158
|
+
|
|
159
|
+
Open `report.html` directly in a browser. It embeds the data, script, and styles in one file and works offline. The viewer supports status filters, name/URL search, sorting, pagination, expandable HTTP/error/redirect/history details, local JSON import (including drag-and-drop), JSON URL loading, and downloading the loaded report. Mobile layouts keep technical fields inside expandable details.
|
|
160
|
+
|
|
161
|
+
Export an existing report as a static site, **without checking the sites again**:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pnpm exec meodp report reports/links/report.json --output reports/site
|
|
165
|
+
|
|
166
|
+
# A reusable viewer with no bundled data; users can load their own JSON.
|
|
167
|
+
pnpm exec meodp report --output reports/viewer
|
|
168
|
+
|
|
169
|
+
# Read data from another location (cross-origin sources must allow CORS).
|
|
170
|
+
pnpm exec meodp report --output reports/viewer --data-url https://example.com/report.json
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Upload the contents of `reports/site/` to any static host, including a subdirectory. No Node.js server, database, browser automation, or CDN assets are needed at runtime. The output contains:
|
|
174
|
+
|
|
175
|
+
- `index.html`: the interactive viewer with an embedded snapshot.
|
|
176
|
+
- `report.json`: the data loaded on each hosted page visit, with cache bypassed. Replace it after a new scan to update the hosted view.
|
|
177
|
+
|
|
178
|
+
Opening `index.html` through `file://` uses the embedded snapshot; choose a JSON file to load newer data offline. A hosted data-load failure displays an error and retains the embedded/current report. `--data-url` overrides the hosted source. The page reads reports; scanning still runs in Node.js or CI. Displayed timestamps always come from the observations, not the page load time.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { parseReport, writeReportSite } from 'meodp/check'
|
|
182
|
+
|
|
183
|
+
const report = parseReport(JSON.parse(jsonText))
|
|
184
|
+
await writeReportSite(report, 'reports/site')
|
|
185
|
+
await writeReportSite(undefined, 'reports/viewer', { dataUrl: './data/report.json' })
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The viewer accepts MEODP `schemaVersion: 1` reports, validates observations and summary counts, and renders names/errors as text. Local files stay in the browser. Browser imports are limited to 10 MiB and 50,000 sites; HTTP data loads time out after 15 seconds. No remote scanning occurs in the viewer. Publish only the report data you intend to share: reports include observer names, site URLs, times, and error details.
|
|
189
|
+
|
|
190
|
+
## Interpreting results
|
|
191
|
+
|
|
192
|
+
| Status | Meaning |
|
|
193
|
+
| ------------- | ------------------------------------------------------------------------------------- |
|
|
194
|
+
| `reachable` | The requested URL, possibly after redirects, returned HTTP 2xx |
|
|
195
|
+
| `restricted` | HTTP 401, 403, 429, 451, or 999; access needs review |
|
|
196
|
+
| `unavailable` | Other HTTP errors, transport/DNS/TLS failures, timeouts, or invalid/looping redirects |
|
|
197
|
+
|
|
198
|
+
`checkLinks()` requests only the supplied URLs and their redirect destinations. `checkSitemap()` first reads discovery documents, then uses those same probes for the listed pages. The checker uses GET for the pages and does not request discovered article links, images, scripts, or stylesheets. Redirects are recorded separately from availability; a redirect to a working site is still reachable.
|
|
199
|
+
|
|
200
|
+
Results describe **this observer at this time**. HTTP 2xx does not verify content, detect expired-domain parking, execute JavaScript, or prove that a blog is still owned by the same person. Restricted results require manual or browser verification. Sites using JavaScript redirects or returning a challenge page with HTTP 200 need separate verification.
|
|
201
|
+
|
|
202
|
+
`consecutiveFailures` counts separate completed runs, not retries within one run or days of continuous downtime. A reachable or restricted observation breaks that failure streak. Previous success is retained. History must use the same observer; use distinct files for different machines or CI networks. Missing history starts a new record; corrupt history is an error. Use one writer per history file.
|
|
203
|
+
|
|
204
|
+
MEODP reports observations; it does not remove friends, change their addresses, or send notifications.
|
|
205
|
+
|
|
206
|
+
## Existing browser scanner
|
|
207
|
+
|
|
208
|
+
The original Playwright-based scanner remains available through the root library entry and `meodp scan [root]` with `meodp.config.ts`. Install `playwright` and its browsers separately to use it. Its experimental resource scanning and legacy report format are separate from `meodp/check`; its CLI does not implement the new failure policies. The old `meodp export` command is retained for that legacy report format; use `meodp report` for a new `schemaVersion: 1` JSON report.
|
|
209
|
+
|
|
210
|
+
**CLI migration for 0.1:** change a bare `meodp` scan to `meodp scan`, and `meodp ./project` to `meodp scan ./project`. The bare command now displays help. Existing `meodp check` / `meodp report` invocations and library APIs are unchanged.
|
|
211
|
+
|
|
212
|
+
The former development-only `meodp-ts` executable is no longer shipped. Run `pnpm --filter meodp exec tsx bin/index.ts` from the workspace root when developing.
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
pnpm install
|
|
218
|
+
pnpm test
|
|
219
|
+
pnpm typecheck
|
|
220
|
+
pnpm build
|
|
221
|
+
pnpm pack
|
|
222
|
+
```
|
|
40
223
|
|
|
41
|
-
|
|
224
|
+
The viewer source lives in `packages/meodp/src/check/viewer/` in the repository. Build, test, and typecheck commands bundle its browser TypeScript and CSS into an ignored generated module; published HTML needs no runtime dependencies.
|
|
42
225
|
|
|
43
|
-
|
|
226
|
+
Tests use local HTTP servers for redirects, restricted access, transient failures, timeouts, history, report output, CLI exit policies, and sitemap discovery (indexes, gzip, namespaces, cycles, limits, and partial failures). They do not scan external sites. Library type checking and declaration generation use the package's `tsconfig.json`, independently of the experimental client template.
|
|
44
227
|
|
|
45
|
-
|
|
46
|
-
- [ ] HTML 报告
|
|
228
|
+
See [competitive analysis](https://github.com/YunYouJun/meodp/blob/main/docs/competitive-analysis.md) for the project scope and alternatives.
|
package/bin/index.mjs
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
-
import
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { runCli } from '../dist/cli/main.mjs'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
async function main() {
|
|
8
|
+
process.exitCode = await runCli()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
main().catch((error) => {
|
|
12
|
+
console.error(error)
|
|
13
|
+
process.exitCode = 2
|
|
14
|
+
})
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const promises = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const process = require('node:process');
|
|
6
|
+
const node_util = require('node:util');
|
|
7
|
+
const jsYaml = require('js-yaml');
|
|
8
|
+
const check_index = require('../shared/meodp.a0bcc3e7.cjs');
|
|
9
|
+
require('node:timers/promises');
|
|
10
|
+
require('linkinator');
|
|
11
|
+
require('node:os');
|
|
12
|
+
require('node:crypto');
|
|
13
|
+
require('node:buffer');
|
|
14
|
+
require('node:zlib');
|
|
15
|
+
require('fast-xml-parser');
|
|
16
|
+
|
|
17
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
|
|
18
|
+
|
|
19
|
+
const process__default = /*#__PURE__*/_interopDefaultCompat(process);
|
|
20
|
+
|
|
21
|
+
const help = `Usage: meodp check <links.json|links.yml> [options]
|
|
22
|
+
|
|
23
|
+
Check only the listed HTTP(S) URLs, with no browser or recursive resource scan.
|
|
24
|
+
Input: an array of URL strings or objects with url and optional name.
|
|
25
|
+
This makes network requests. To render saved data instead, use meodp report.
|
|
26
|
+
|
|
27
|
+
--output <directory> Write report.json, report.md, report.html (default: reports/meodp)
|
|
28
|
+
--history <file> Read previous observations and save the completed report
|
|
29
|
+
--observer <name> Identify this network/environment (default: hostname)
|
|
30
|
+
--concurrency <n> Concurrent sites (default: 5)
|
|
31
|
+
--timeout <ms> Timeout per HTTP request (default: 10000)
|
|
32
|
+
--retries <n> Retries for transport/5xx failures (default: 1)
|
|
33
|
+
--max-redirects <n> Follow at most n redirects (default: 5)
|
|
34
|
+
--fail-on <policy> unavailable (default), review, or none
|
|
35
|
+
-h, --help Show this help
|
|
36
|
+
|
|
37
|
+
Exit codes: 0 = policy passed; 1 = findings match --fail-on; 2 = input/execution error.
|
|
38
|
+
Restricted results and redirects require review; they do not prove a dead site.
|
|
39
|
+
`;
|
|
40
|
+
async function runCheckCli(args = process__default.argv.slice(3)) {
|
|
41
|
+
return runScanCli("links", args);
|
|
42
|
+
}
|
|
43
|
+
async function runSitemapCli(args = process__default.argv.slice(3)) {
|
|
44
|
+
return runScanCli("sitemap", args);
|
|
45
|
+
}
|
|
46
|
+
async function runScanCli(mode, args) {
|
|
47
|
+
try {
|
|
48
|
+
const { values, positionals } = node_util.parseArgs({
|
|
49
|
+
args,
|
|
50
|
+
allowPositionals: true,
|
|
51
|
+
options: {
|
|
52
|
+
"output": { type: "string", default: "reports/meodp" },
|
|
53
|
+
"history": { type: "string" },
|
|
54
|
+
"observer": { type: "string" },
|
|
55
|
+
"concurrency": { type: "string" },
|
|
56
|
+
"timeout": { type: "string" },
|
|
57
|
+
"retries": { type: "string" },
|
|
58
|
+
"max-redirects": { type: "string" },
|
|
59
|
+
"fail-on": { type: "string", default: "unavailable" },
|
|
60
|
+
"help": { type: "boolean", short: "h" },
|
|
61
|
+
...mode === "sitemap" ? {
|
|
62
|
+
"discover": { type: "boolean" },
|
|
63
|
+
"max-urls": { type: "string" },
|
|
64
|
+
"max-sitemaps": { type: "string" },
|
|
65
|
+
"max-sitemap-bytes": { type: "string" }
|
|
66
|
+
} : {}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
if (values.help) {
|
|
70
|
+
console.log(mode === "links" ? help : `Usage: meodp sitemap <sitemap-url> [options]
|
|
71
|
+
|
|
72
|
+
Read an XML sitemap or nested index, then check each listed page via HTTP.
|
|
73
|
+
--discover Treat the URL as a site: read robots.txt or /sitemap.xml
|
|
74
|
+
--max-urls <n> Reject discovery above n unique pages (default: 10000)
|
|
75
|
+
--max-sitemaps <n> Limit sitemap documents (default: 100)
|
|
76
|
+
--max-sitemap-bytes <n> Limit each downloaded/decompressed document (default: 10485760)
|
|
77
|
+
|
|
78
|
+
${help.slice(help.indexOf(" --output"))}`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
if (positionals.length !== 1)
|
|
82
|
+
throw new Error(`Provide exactly one ${mode === "links" ? "JSON or YAML input file" : "HTTP(S) URL"}. Use --help for examples.`);
|
|
83
|
+
if (!["none", "unavailable", "review"].includes(values["fail-on"]))
|
|
84
|
+
throw new Error("--fail-on must be none, unavailable, or review");
|
|
85
|
+
const numeric = (value) => typeof value === "string" ? Number(value) : void 0;
|
|
86
|
+
const options = {
|
|
87
|
+
observer: values.observer,
|
|
88
|
+
previousReport: values.history ? await check_index.readReport(values.history) : void 0,
|
|
89
|
+
concurrency: numeric(values.concurrency),
|
|
90
|
+
timeoutMs: numeric(values.timeout),
|
|
91
|
+
retries: numeric(values.retries),
|
|
92
|
+
maxRedirects: numeric(values["max-redirects"]),
|
|
93
|
+
onResult(result) {
|
|
94
|
+
console.log(`[${result.status}] ${result.httpStatus ?? result.reason} ${result.url}`);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const input = positionals[0];
|
|
98
|
+
const report = mode === "sitemap" ? await check_index.checkSitemap(input, {
|
|
99
|
+
...options,
|
|
100
|
+
discover: values.discover === true,
|
|
101
|
+
maxUrls: numeric(values["max-urls"]),
|
|
102
|
+
maxSitemaps: numeric(values["max-sitemaps"]),
|
|
103
|
+
maxSitemapBytes: numeric(values["max-sitemap-bytes"])
|
|
104
|
+
}) : await check_index.checkLinks(await readTargets(input), options);
|
|
105
|
+
const paths = await check_index.writeReports(report, values.output);
|
|
106
|
+
if (values.history)
|
|
107
|
+
await check_index.saveReport(report, values.history);
|
|
108
|
+
console.log(`${report.summary.total} URLs: ${report.summary.reachable} reachable, ${report.summary.restricted} restricted, ${report.summary.unavailable} unavailable`);
|
|
109
|
+
console.log(`Interactive report: ${path.resolve(paths.html)}`);
|
|
110
|
+
console.log(`Reports: ${path.resolve(paths.markdown)} and ${path.resolve(paths.json)}`);
|
|
111
|
+
if (values["fail-on"] === "none")
|
|
112
|
+
return 0;
|
|
113
|
+
const findings = report.summary.unavailable > 0 || values["fail-on"] === "review" && (report.summary.restricted > 0 || report.summary.redirected > 0);
|
|
114
|
+
return findings ? 1 : 0;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error(`meodp ${mode === "links" ? "check" : "sitemap"}: ${error instanceof Error ? error.message : String(error)}`);
|
|
117
|
+
return 2;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function readTargets(input) {
|
|
121
|
+
const extension = path.extname(input).toLowerCase();
|
|
122
|
+
if (![".json", ".yml", ".yaml"].includes(extension))
|
|
123
|
+
throw new Error("Input file must use .json, .yml, or .yaml");
|
|
124
|
+
const source = await promises.readFile(input, "utf8");
|
|
125
|
+
return check_index.normalizeTargets(extension === ".json" ? JSON.parse(source) : jsYaml.load(source));
|
|
126
|
+
}
|
|
127
|
+
async function runReportCli(args = process__default.argv.slice(3)) {
|
|
128
|
+
try {
|
|
129
|
+
const { values, positionals } = node_util.parseArgs({
|
|
130
|
+
args,
|
|
131
|
+
allowPositionals: true,
|
|
132
|
+
options: {
|
|
133
|
+
"output": { type: "string", default: "reports/site" },
|
|
134
|
+
"data-url": { type: "string" },
|
|
135
|
+
"help": { type: "boolean", short: "h" }
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
if (values.help) {
|
|
139
|
+
console.log(`Usage: meodp report [report.json] [--output reports/site] [--data-url ./report.json]
|
|
140
|
+
|
|
141
|
+
Export a static interactive viewer without making any site-check requests.
|
|
142
|
+
With an input report, write index.html and report.json. The hosted viewer loads
|
|
143
|
+
report.json on each visit; opening index.html as a local file uses its embedded snapshot.
|
|
144
|
+
Without input, export an empty viewer supporting file upload and JSON URL loading.
|
|
145
|
+
--data-url overrides the data source loaded by the hosted viewer (cross-origin needs CORS).
|
|
146
|
+
-h, --help shows this help. To collect fresh observations, use meodp check.
|
|
147
|
+
Exit codes: 0 = exported; 2 = invalid input or execution error.`);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
if (positionals.length > 1)
|
|
151
|
+
throw new Error("Provide at most one report.json file. Use --help for examples.");
|
|
152
|
+
const report = positionals[0] ? await check_index.readReport(positionals[0]) : void 0;
|
|
153
|
+
if (positionals[0] && !report)
|
|
154
|
+
throw new Error(`Report not found: ${positionals[0]}`);
|
|
155
|
+
const paths = await check_index.writeReportSite(report, values.output, values["data-url"] ? { dataUrl: values["data-url"] } : {});
|
|
156
|
+
console.log(`Static report site: ${path.resolve(paths.index)}`);
|
|
157
|
+
return 0;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(`meodp report: ${error instanceof Error ? error.message : String(error)}`);
|
|
160
|
+
return 2;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
exports.runCheckCli = runCheckCli;
|
|
165
|
+
exports.runReportCli = runReportCli;
|
|
166
|
+
exports.runSitemapCli = runSitemapCli;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve, extname } from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { parseArgs } from 'node:util';
|
|
5
|
+
import { load } from 'js-yaml';
|
|
6
|
+
import { r as readReport, c as checkSitemap, a as checkLinks, w as writeReports, s as saveReport, n as normalizeTargets, b as writeReportSite } from '../shared/meodp.d0916bc5.mjs';
|
|
7
|
+
import 'node:timers/promises';
|
|
8
|
+
import 'linkinator';
|
|
9
|
+
import 'node:os';
|
|
10
|
+
import 'node:crypto';
|
|
11
|
+
import 'node:buffer';
|
|
12
|
+
import 'node:zlib';
|
|
13
|
+
import 'fast-xml-parser';
|
|
14
|
+
|
|
15
|
+
const help = `Usage: meodp check <links.json|links.yml> [options]
|
|
16
|
+
|
|
17
|
+
Check only the listed HTTP(S) URLs, with no browser or recursive resource scan.
|
|
18
|
+
Input: an array of URL strings or objects with url and optional name.
|
|
19
|
+
This makes network requests. To render saved data instead, use meodp report.
|
|
20
|
+
|
|
21
|
+
--output <directory> Write report.json, report.md, report.html (default: reports/meodp)
|
|
22
|
+
--history <file> Read previous observations and save the completed report
|
|
23
|
+
--observer <name> Identify this network/environment (default: hostname)
|
|
24
|
+
--concurrency <n> Concurrent sites (default: 5)
|
|
25
|
+
--timeout <ms> Timeout per HTTP request (default: 10000)
|
|
26
|
+
--retries <n> Retries for transport/5xx failures (default: 1)
|
|
27
|
+
--max-redirects <n> Follow at most n redirects (default: 5)
|
|
28
|
+
--fail-on <policy> unavailable (default), review, or none
|
|
29
|
+
-h, --help Show this help
|
|
30
|
+
|
|
31
|
+
Exit codes: 0 = policy passed; 1 = findings match --fail-on; 2 = input/execution error.
|
|
32
|
+
Restricted results and redirects require review; they do not prove a dead site.
|
|
33
|
+
`;
|
|
34
|
+
async function runCheckCli(args = process.argv.slice(3)) {
|
|
35
|
+
return runScanCli("links", args);
|
|
36
|
+
}
|
|
37
|
+
async function runSitemapCli(args = process.argv.slice(3)) {
|
|
38
|
+
return runScanCli("sitemap", args);
|
|
39
|
+
}
|
|
40
|
+
async function runScanCli(mode, args) {
|
|
41
|
+
try {
|
|
42
|
+
const { values, positionals } = parseArgs({
|
|
43
|
+
args,
|
|
44
|
+
allowPositionals: true,
|
|
45
|
+
options: {
|
|
46
|
+
"output": { type: "string", default: "reports/meodp" },
|
|
47
|
+
"history": { type: "string" },
|
|
48
|
+
"observer": { type: "string" },
|
|
49
|
+
"concurrency": { type: "string" },
|
|
50
|
+
"timeout": { type: "string" },
|
|
51
|
+
"retries": { type: "string" },
|
|
52
|
+
"max-redirects": { type: "string" },
|
|
53
|
+
"fail-on": { type: "string", default: "unavailable" },
|
|
54
|
+
"help": { type: "boolean", short: "h" },
|
|
55
|
+
...mode === "sitemap" ? {
|
|
56
|
+
"discover": { type: "boolean" },
|
|
57
|
+
"max-urls": { type: "string" },
|
|
58
|
+
"max-sitemaps": { type: "string" },
|
|
59
|
+
"max-sitemap-bytes": { type: "string" }
|
|
60
|
+
} : {}
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
if (values.help) {
|
|
64
|
+
console.log(mode === "links" ? help : `Usage: meodp sitemap <sitemap-url> [options]
|
|
65
|
+
|
|
66
|
+
Read an XML sitemap or nested index, then check each listed page via HTTP.
|
|
67
|
+
--discover Treat the URL as a site: read robots.txt or /sitemap.xml
|
|
68
|
+
--max-urls <n> Reject discovery above n unique pages (default: 10000)
|
|
69
|
+
--max-sitemaps <n> Limit sitemap documents (default: 100)
|
|
70
|
+
--max-sitemap-bytes <n> Limit each downloaded/decompressed document (default: 10485760)
|
|
71
|
+
|
|
72
|
+
${help.slice(help.indexOf(" --output"))}`);
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
if (positionals.length !== 1)
|
|
76
|
+
throw new Error(`Provide exactly one ${mode === "links" ? "JSON or YAML input file" : "HTTP(S) URL"}. Use --help for examples.`);
|
|
77
|
+
if (!["none", "unavailable", "review"].includes(values["fail-on"]))
|
|
78
|
+
throw new Error("--fail-on must be none, unavailable, or review");
|
|
79
|
+
const numeric = (value) => typeof value === "string" ? Number(value) : void 0;
|
|
80
|
+
const options = {
|
|
81
|
+
observer: values.observer,
|
|
82
|
+
previousReport: values.history ? await readReport(values.history) : void 0,
|
|
83
|
+
concurrency: numeric(values.concurrency),
|
|
84
|
+
timeoutMs: numeric(values.timeout),
|
|
85
|
+
retries: numeric(values.retries),
|
|
86
|
+
maxRedirects: numeric(values["max-redirects"]),
|
|
87
|
+
onResult(result) {
|
|
88
|
+
console.log(`[${result.status}] ${result.httpStatus ?? result.reason} ${result.url}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const input = positionals[0];
|
|
92
|
+
const report = mode === "sitemap" ? await checkSitemap(input, {
|
|
93
|
+
...options,
|
|
94
|
+
discover: values.discover === true,
|
|
95
|
+
maxUrls: numeric(values["max-urls"]),
|
|
96
|
+
maxSitemaps: numeric(values["max-sitemaps"]),
|
|
97
|
+
maxSitemapBytes: numeric(values["max-sitemap-bytes"])
|
|
98
|
+
}) : await checkLinks(await readTargets(input), options);
|
|
99
|
+
const paths = await writeReports(report, values.output);
|
|
100
|
+
if (values.history)
|
|
101
|
+
await saveReport(report, values.history);
|
|
102
|
+
console.log(`${report.summary.total} URLs: ${report.summary.reachable} reachable, ${report.summary.restricted} restricted, ${report.summary.unavailable} unavailable`);
|
|
103
|
+
console.log(`Interactive report: ${resolve(paths.html)}`);
|
|
104
|
+
console.log(`Reports: ${resolve(paths.markdown)} and ${resolve(paths.json)}`);
|
|
105
|
+
if (values["fail-on"] === "none")
|
|
106
|
+
return 0;
|
|
107
|
+
const findings = report.summary.unavailable > 0 || values["fail-on"] === "review" && (report.summary.restricted > 0 || report.summary.redirected > 0);
|
|
108
|
+
return findings ? 1 : 0;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(`meodp ${mode === "links" ? "check" : "sitemap"}: ${error instanceof Error ? error.message : String(error)}`);
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function readTargets(input) {
|
|
115
|
+
const extension = extname(input).toLowerCase();
|
|
116
|
+
if (![".json", ".yml", ".yaml"].includes(extension))
|
|
117
|
+
throw new Error("Input file must use .json, .yml, or .yaml");
|
|
118
|
+
const source = await readFile(input, "utf8");
|
|
119
|
+
return normalizeTargets(extension === ".json" ? JSON.parse(source) : load(source));
|
|
120
|
+
}
|
|
121
|
+
async function runReportCli(args = process.argv.slice(3)) {
|
|
122
|
+
try {
|
|
123
|
+
const { values, positionals } = parseArgs({
|
|
124
|
+
args,
|
|
125
|
+
allowPositionals: true,
|
|
126
|
+
options: {
|
|
127
|
+
"output": { type: "string", default: "reports/site" },
|
|
128
|
+
"data-url": { type: "string" },
|
|
129
|
+
"help": { type: "boolean", short: "h" }
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (values.help) {
|
|
133
|
+
console.log(`Usage: meodp report [report.json] [--output reports/site] [--data-url ./report.json]
|
|
134
|
+
|
|
135
|
+
Export a static interactive viewer without making any site-check requests.
|
|
136
|
+
With an input report, write index.html and report.json. The hosted viewer loads
|
|
137
|
+
report.json on each visit; opening index.html as a local file uses its embedded snapshot.
|
|
138
|
+
Without input, export an empty viewer supporting file upload and JSON URL loading.
|
|
139
|
+
--data-url overrides the data source loaded by the hosted viewer (cross-origin needs CORS).
|
|
140
|
+
-h, --help shows this help. To collect fresh observations, use meodp check.
|
|
141
|
+
Exit codes: 0 = exported; 2 = invalid input or execution error.`);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
if (positionals.length > 1)
|
|
145
|
+
throw new Error("Provide at most one report.json file. Use --help for examples.");
|
|
146
|
+
const report = positionals[0] ? await readReport(positionals[0]) : void 0;
|
|
147
|
+
if (positionals[0] && !report)
|
|
148
|
+
throw new Error(`Report not found: ${positionals[0]}`);
|
|
149
|
+
const paths = await writeReportSite(report, values.output, values["data-url"] ? { dataUrl: values["data-url"] } : {});
|
|
150
|
+
console.log(`Static report site: ${resolve(paths.index)}`);
|
|
151
|
+
return 0;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error(`meodp report: ${error instanceof Error ? error.message : String(error)}`);
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export { runCheckCli, runReportCli, runSitemapCli };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const check_index = require('../shared/meodp.a0bcc3e7.cjs');
|
|
4
|
+
require('node:timers/promises');
|
|
5
|
+
require('linkinator');
|
|
6
|
+
require('node:os');
|
|
7
|
+
require('node:crypto');
|
|
8
|
+
require('node:fs/promises');
|
|
9
|
+
require('node:path');
|
|
10
|
+
require('node:buffer');
|
|
11
|
+
require('node:util');
|
|
12
|
+
require('node:zlib');
|
|
13
|
+
require('fast-xml-parser');
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
exports.checkLinks = check_index.checkLinks;
|
|
18
|
+
exports.checkSitemap = check_index.checkSitemap;
|
|
19
|
+
exports.formatReport = check_index.formatReport;
|
|
20
|
+
exports.parseReport = check_index.parseReport;
|
|
21
|
+
exports.readReport = check_index.readReport;
|
|
22
|
+
exports.readSitemapUrls = check_index.readSitemapUrls;
|
|
23
|
+
exports.saveReport = check_index.saveReport;
|
|
24
|
+
exports.writeReportSite = check_index.writeReportSite;
|
|
25
|
+
exports.writeReports = check_index.writeReports;
|