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 CHANGED
@@ -1,46 +1,228 @@
1
1
  # MEODP (Mystic Eyes of Death Perception)
2
2
 
3
- Written by Typescript, based on [Playwright](https://playwright.dev/).
3
+ [![npm version](https://img.shields.io/npm/v/meodp)](https://www.npmjs.com/package/meodp)
4
4
 
5
- ## Usage
5
+ [English](https://yunyoujun.github.io/meodp/) | [简体中文](https://yunyoujun.github.io/meodp/zh/)
6
6
 
7
- You can use it like a lib or a cli.
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
- Or like an application:
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
- // Create a config file: `meodp.config.ts` in your project root.
13
- import { defineConfig } from 'meodp'
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
- export default defineConfig({
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
- ## Ref
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
- - [lychee](https://lychee.cli.rs/)
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
- ## Logs
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
- logs/meodp
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
- ## FAQ
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
- ### lychee 的区别
140
+ ### Project scripts
33
141
 
34
- lychee 是一个使用 Rust 编写的快速检测链接的命令行。
35
- 它的命令行应该能满足你使用命令行的大部分需求。
142
+ Keep the package CLI general and name project scripts after their specific subject:
36
143
 
37
- 但我希望能够通过脚本/配置自由地定制检测流程、日志,并记录相关内容,执行对应的函数。
38
- 因此我们需要一个类似 SDK 的库,来实现类似的功能。
39
- 同时基于 Typescript 可以获得更好的开发灵活性,而使用 Playwright 则可以模拟浏览器以检测页面中的资源加载。
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
- ## TODO
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 { run } from '../dist/cli/index.mjs'
4
+ import process from 'node:process'
5
+ import { runCli } from '../dist/cli/main.mjs'
5
6
 
6
- run()
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,5 @@
1
+ declare function runCheckCli(args?: string[]): Promise<number>;
2
+ declare function runSitemapCli(args?: string[]): Promise<number>;
3
+ declare function runReportCli(args?: string[]): Promise<number>;
4
+
5
+ export { runCheckCli, runReportCli, runSitemapCli };
@@ -0,0 +1,5 @@
1
+ declare function runCheckCli(args?: string[]): Promise<number>;
2
+ declare function runSitemapCli(args?: string[]): Promise<number>;
3
+ declare function runReportCli(args?: string[]): Promise<number>;
4
+
5
+ export { runCheckCli, runReportCli, runSitemapCli };
@@ -0,0 +1,5 @@
1
+ declare function runCheckCli(args?: string[]): Promise<number>;
2
+ declare function runSitemapCli(args?: string[]): Promise<number>;
3
+ declare function runReportCli(args?: string[]): Promise<number>;
4
+
5
+ export { runCheckCli, runReportCli, 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;