meodp 0.0.9 → 0.2.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.
Files changed (61) hide show
  1. package/README.md +230 -21
  2. package/bin/index.mjs +10 -2
  3. package/dist/check/cli.cjs +257 -0
  4. package/dist/check/cli.d.cts +5 -0
  5. package/dist/check/cli.d.mts +5 -0
  6. package/dist/check/cli.d.ts +5 -0
  7. package/dist/check/cli.mjs +249 -0
  8. package/dist/check/index.cjs +29 -0
  9. package/dist/check/index.d.cts +44 -0
  10. package/dist/check/index.d.mts +44 -0
  11. package/dist/check/index.d.ts +44 -0
  12. package/dist/check/index.mjs +13 -0
  13. package/dist/cli/index.cjs +2 -2
  14. package/dist/cli/index.d.cts +1 -1
  15. package/dist/cli/index.d.mts +1 -1
  16. package/dist/cli/index.d.ts +1 -1
  17. package/dist/cli/index.mjs +2 -2
  18. package/dist/cli/main.cjs +272 -0
  19. package/dist/cli/main.d.cts +4 -0
  20. package/dist/cli/main.d.mts +4 -0
  21. package/dist/cli/main.d.ts +4 -0
  22. package/dist/cli/main.mjs +266 -0
  23. package/dist/config/index.cjs +7 -0
  24. package/dist/config/index.d.cts +47 -0
  25. package/dist/config/index.d.mts +47 -0
  26. package/dist/config/index.d.ts +47 -0
  27. package/dist/config/index.mjs +5 -0
  28. package/dist/notify/cli.cjs +107 -0
  29. package/dist/notify/cli.d.cts +3 -0
  30. package/dist/notify/cli.d.mts +3 -0
  31. package/dist/notify/cli.d.ts +3 -0
  32. package/dist/notify/cli.mjs +101 -0
  33. package/dist/notify/email.cjs +38 -0
  34. package/dist/notify/email.d.cts +29 -0
  35. package/dist/notify/email.d.mts +29 -0
  36. package/dist/notify/email.d.ts +29 -0
  37. package/dist/notify/email.mjs +35 -0
  38. package/dist/notify/feishu.cjs +162 -0
  39. package/dist/notify/feishu.d.cts +74 -0
  40. package/dist/notify/feishu.d.mts +74 -0
  41. package/dist/notify/feishu.d.ts +74 -0
  42. package/dist/notify/feishu.mjs +157 -0
  43. package/dist/notify/index.cjs +64 -0
  44. package/dist/notify/index.d.cts +27 -0
  45. package/dist/notify/index.d.mts +27 -0
  46. package/dist/notify/index.d.ts +27 -0
  47. package/dist/notify/index.mjs +62 -0
  48. package/dist/shared/meodp.2b00b0ae.cjs +228 -0
  49. package/dist/shared/meodp.4f1da070.d.cts +41 -0
  50. package/dist/shared/meodp.63483e20.cjs +30 -0
  51. package/dist/shared/meodp.80ff918d.mjs +465 -0
  52. package/dist/shared/meodp.8369c4c1.d.ts +41 -0
  53. package/dist/shared/meodp.93e06e00.d.cts +64 -0
  54. package/dist/shared/meodp.93e06e00.d.mts +64 -0
  55. package/dist/shared/meodp.93e06e00.d.ts +64 -0
  56. package/dist/shared/meodp.9c706389.mjs +220 -0
  57. package/dist/shared/meodp.a704f8db.d.mts +41 -0
  58. package/dist/shared/meodp.c4c70565.cjs +477 -0
  59. package/dist/shared/meodp.f96d0510.mjs +24 -0
  60. package/package.json +104 -31
  61. package/bin/index.ts +0 -6
package/README.md CHANGED
@@ -1,46 +1,255 @@
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
20
+
21
+ ```ts
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
+ ```
40
+
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.
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
+ ## Project configuration
56
+
57
+ Starting with 0.2.0, HTTP commands read `meodp.config.ts`:
10
58
 
11
59
  ```ts
12
- // Create a config file: `meodp.config.ts` in your project root.
13
- import { defineConfig } from 'meodp'
60
+ import { defineConfig } from 'meodp/config'
14
61
 
15
62
  export default defineConfig({
63
+ check: {
64
+ reporter: ['json', 'markdown', ['html', { outputFolder: 'reports/site' }]],
65
+ input: 'public/links.yml',
66
+ output: 'reports/links',
67
+ history: '.cache/links.json',
68
+ failOn: 'none',
69
+ },
70
+ report: { input: 'reports/links/report.json', reporter: 'html', output: 'dist/status' },
71
+ })
72
+ ```
73
+
74
+ Run `meodp check` to collect observations or `meodp report` to render saved data. `check.reporter` selects check outputs; `report.input` is the saved JSON to read, and `report.reporter` selects export formats. Use top-level `reporter` only for shared defaults. CLI options override config values. Config file paths are relative to the config; CLI paths are relative to your working directory.
75
+
76
+ The same config supports history seeding, deployment verification (`meodp report --verify`), and optional Feishu / SMTP notifications (`meodp notify`). Reuse the pure policy from `meodp/notify` and delivery adapters from `meodp/notify/feishu` or `meodp/notify/email`. Email requires the optional Nodemailer peer only for sending. Notification channels default to off; use `--mode changes|weekly` to enable and `--dry-run` to preview.
16
77
 
78
+ See the [configuration guide](https://yunyoujun.github.io/meodp/guide/configuration) ([中文](https://yunyoujun.github.io/meodp/zh/guide/configuration)). The older browser `defineConfig` at the package root remains unchanged; HTTP projects import from `meodp/config`.
79
+
80
+ Override formats with `--reporter=json,markdown,html` or repeated `--reporter` flags. JSON / Markdown tuples accept `outputFile`; HTML accepts `outputFolder` or a standalone `outputFile`. See [reporter formats and path precedence](https://yunyoujun.github.io/meodp/guide/reports#reporters).
81
+
82
+ ## Sitemap page checks
83
+
84
+ Check all pages listed in an XML sitemap, reusing the same HTTP observations, history, and reports:
85
+
86
+ ```ts
87
+ import { checkSitemap, readSitemapUrls, writeReports } from 'meodp/check'
88
+
89
+ const report = await checkSitemap('https://example.com/sitemap.xml', {
90
+ concurrency: 5,
91
+ timeoutMs: 10000,
92
+ maxUrls: 10000,
17
93
  })
94
+ await writeReports(report, 'reports/pages')
95
+
96
+ // Start from a site: read Sitemap directives in robots.txt, then /sitemap.xml if none exist.
97
+ const siteReport = await checkSitemap('https://example.com/', { discover: true })
98
+
99
+ // Inspect or select the page list before making any page requests.
100
+ const urls = await readSitemapUrls('https://example.com/sitemap.xml')
101
+ ```
102
+
103
+ ```bash
104
+ pnpm exec meodp sitemap https://example.com/sitemap.xml --output reports/pages
105
+ pnpm exec meodp sitemap https://example.com/ --discover --output reports/pages \
106
+ --history .cache/sitemap/local.json --observer my-local-network
107
+ pnpm exec meodp sitemap --help
108
+ ```
109
+
110
+ 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.
111
+
112
+ 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.
113
+
114
+ | Additional option | Default | Meaning |
115
+ | ----------------------------------------- | ---------- | --------------------------------------------------------------------------- |
116
+ | `discover` / `--discover` | `false` | Treat the input as a site URL; otherwise it is an explicit sitemap URL |
117
+ | `maxUrls` / `--max-urls` | `10000` | Maximum unique pages, configurable up to 50000 |
118
+ | `maxSitemaps` / `--max-sitemaps` | `100` | Maximum sitemap documents, configurable up to 10000 |
119
+ | `maxSitemapBytes` / `--max-sitemap-bytes` | `10485760` | Per-document downloaded/decompressed byte limit, configurable up to 100 MiB |
120
+
121
+ 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`.
122
+
123
+ 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.
124
+
125
+ ## CLI
126
+
127
+ Use explicit subcommands to distinguish a fresh network check from rendering saved data:
128
+
129
+ | Command | Input | Behavior |
130
+ | -------------- | -------------------------------- | -------------------------------------------------------------- |
131
+ | `meodp check` | URL array in JSON/YAML | Request sites and write fresh HTML, Markdown, and JSON reports |
132
+ | `meodp report` | Saved `report.json`, or no input | Render JSON, Markdown or HTML; no site-check requests |
133
+ | `meodp scan` | Legacy config directory | Run the experimental Playwright scanner |
134
+
135
+ `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.
136
+
137
+ Create `links.yml` (JSON arrays also work):
138
+
139
+ ```yaml
140
+ - name: 云游君
141
+ url: https://www.yunyoujun.cn/
142
+ - name: Example
143
+ url: https://example.com/
144
+ ```
145
+
146
+ ```bash
147
+ pnpm exec meodp check links.yml --output reports/links
148
+
149
+ # Persist observations from one network across separate runs.
150
+ pnpm exec meodp check links.yml --output reports/links \
151
+ --history .cache/link-check/local.json --observer my-local-network
152
+
153
+ # Generate a maintenance report without failing because a friend is unavailable.
154
+ pnpm exec meodp check links.yml --fail-on none
155
+
156
+ pnpm exec meodp check --help
18
157
  ```
19
158
 
20
- ## Ref
159
+ `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.
21
160
 
22
- - [lychee](https://lychee.cli.rs/)
161
+ Exit codes:
23
162
 
24
- ## Logs
163
+ - `0`: the selected policy passed.
164
+ - `1`: observations matched the policy. Default `--fail-on unavailable` fails on unavailable sites; `--fail-on review` also includes restricted access and redirects.
165
+ - `2`: invalid input/options, incompatible or corrupt history, or an execution/report-writing error. `--fail-on none` does not suppress these errors.
166
+
167
+ ### Project scripts
168
+
169
+ Keep the package CLI general and name project scripts after their specific subject:
170
+
171
+ ```json
172
+ {
173
+ "scripts": {
174
+ "check:links": "meodp check public/links.yml --output reports/links --fail-on none",
175
+ "report:links": "meodp report reports/links/report.json --output reports/site"
176
+ }
177
+ }
178
+ ```
179
+
180
+ 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.
181
+
182
+ 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.
183
+
184
+ ## Interactive report and static site
185
+
186
+ 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.
187
+
188
+ Export an existing report as a static site, **without checking the sites again**:
25
189
 
26
190
  ```bash
27
- logs/meodp
191
+ pnpm exec meodp report reports/links/report.json --output reports/site
192
+
193
+ # A reusable viewer with no bundled data; users can load their own JSON.
194
+ pnpm exec meodp report --output reports/viewer
195
+
196
+ # Read data from another location (cross-origin sources must allow CORS).
197
+ pnpm exec meodp report --output reports/viewer --data-url https://example.com/report.json
198
+ ```
199
+
200
+ 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:
201
+
202
+ - `index.html`: the interactive viewer with an embedded snapshot.
203
+ - `report.json`: the data loaded on each hosted page visit, with cache bypassed. Replace it after a new scan to update the hosted view.
204
+
205
+ 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.
206
+
207
+ ```ts
208
+ import { parseReport, writeReportSite } from 'meodp/check'
209
+
210
+ const report = parseReport(JSON.parse(jsonText))
211
+ await writeReportSite(report, 'reports/site')
212
+ await writeReportSite(undefined, 'reports/viewer', { dataUrl: './data/report.json' })
28
213
  ```
29
214
 
30
- ## FAQ
215
+ 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.
216
+
217
+ ## Interpreting results
218
+
219
+ | Status | Meaning |
220
+ | ------------- | ------------------------------------------------------------------------------------- |
221
+ | `reachable` | The requested URL, possibly after redirects, returned HTTP 2xx |
222
+ | `restricted` | HTTP 401, 403, 429, 451, or 999; access needs review |
223
+ | `unavailable` | Other HTTP errors, transport/DNS/TLS failures, timeouts, or invalid/looping redirects |
31
224
 
32
- ### 与 lychee 的区别
225
+ `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.
33
226
 
34
- lychee 是一个使用 Rust 编写的快速检测链接的命令行。
35
- 它的命令行应该能满足你使用命令行的大部分需求。
227
+ 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.
36
228
 
37
- 但我希望能够通过脚本/配置自由地定制检测流程、日志,并记录相关内容,执行对应的函数。
38
- 因此我们需要一个类似 SDK 的库,来实现类似的功能。
39
- 同时基于 Typescript 可以获得更好的开发灵活性,而使用 Playwright 则可以模拟浏览器以检测页面中的资源加载。
229
+ `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.
230
+
231
+ MEODP reports observations; it does not remove friends, change their addresses, or send notifications.
232
+
233
+ ## Existing browser scanner
234
+
235
+ 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.
236
+
237
+ **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.
238
+
239
+ 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.
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ pnpm install
245
+ pnpm test
246
+ pnpm typecheck
247
+ pnpm build
248
+ pnpm pack
249
+ ```
40
250
 
41
- 如果可能,它未来也许可以支持插件或预置配置。
251
+ 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
252
 
43
- ## TODO
253
+ 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
254
 
45
- - [ ] 文件下载
46
- - [ ] HTML 报告
255
+ 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,257 @@
1
+ 'use strict';
2
+
3
+ const promises = require('node:fs/promises');
4
+ const node_os = require('node:os');
5
+ const path = require('node:path');
6
+ const process = require('node:process');
7
+ const node_util = require('node:util');
8
+ const jsYaml = require('js-yaml');
9
+ const load = require('../shared/meodp.63483e20.cjs');
10
+ const check_index = require('../shared/meodp.c4c70565.cjs');
11
+ const report = require('../shared/meodp.2b00b0ae.cjs');
12
+ require('c12');
13
+ require('node:timers/promises');
14
+ require('linkinator');
15
+ require('node:buffer');
16
+ require('node:zlib');
17
+ require('fast-xml-parser');
18
+ require('node:crypto');
19
+
20
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
21
+
22
+ const process__default = /*#__PURE__*/_interopDefaultCompat(process);
23
+
24
+ const help = `Usage: meodp check <links.json|links.yml> [options]
25
+
26
+ Check only the listed HTTP(S) URLs, with no browser or recursive resource scan.
27
+ Input: an array of URL strings or objects with url and optional name.
28
+ This makes network requests. To render saved data instead, use meodp report.
29
+
30
+ --config <file> Load a project config (default: meodp.config.ts)
31
+ --output <directory> Default report directory (default: reports/meodp)
32
+ --reporter <names> json, markdown, html; comma-separated or repeated
33
+ Without a selection, write report.json, report.md, report.html
34
+ --history <file> Read previous observations and save the completed report
35
+ --observer <name> Identify this network/environment (default: hostname)
36
+ --concurrency <n> Concurrent sites (default: 5)
37
+ --timeout <ms> Timeout per HTTP request (default: 10000)
38
+ --retries <n> Retries for transport/5xx failures (default: 1)
39
+ --max-redirects <n> Follow at most n redirects (default: 5)
40
+ --fail-on <policy> unavailable (default), review, or none
41
+ -h, --help Show this help
42
+
43
+ Exit codes: 0 = policy passed; 1 = findings match --fail-on; 2 = input/execution error.
44
+ Restricted results and redirects require review; they do not prove a dead site.
45
+ `;
46
+ async function runCheckCli(args = process__default.argv.slice(3)) {
47
+ return runScanCli("links", args);
48
+ }
49
+ async function runSitemapCli(args = process__default.argv.slice(3)) {
50
+ return runScanCli("sitemap", args);
51
+ }
52
+ async function runScanCli(mode, args) {
53
+ try {
54
+ const { values, positionals } = node_util.parseArgs({
55
+ args,
56
+ allowPositionals: true,
57
+ options: {
58
+ "config": { type: "string" },
59
+ "reporter": { type: "string", multiple: true },
60
+ "output": { type: "string" },
61
+ "history": { type: "string" },
62
+ "observer": { type: "string" },
63
+ "concurrency": { type: "string" },
64
+ "timeout": { type: "string" },
65
+ "retries": { type: "string" },
66
+ "max-redirects": { type: "string" },
67
+ "fail-on": { type: "string" },
68
+ "help": { type: "boolean", short: "h" },
69
+ ...mode === "sitemap" ? {
70
+ "discover": { type: "boolean" },
71
+ "max-urls": { type: "string" },
72
+ "max-sitemaps": { type: "string" },
73
+ "max-sitemap-bytes": { type: "string" }
74
+ } : {}
75
+ }
76
+ });
77
+ if (values.help) {
78
+ console.log(mode === "links" ? help : `Usage: meodp sitemap <sitemap-url> [options]
79
+
80
+ Read an XML sitemap or nested index, then check each listed page via HTTP.
81
+ --discover Treat the URL as a site: read robots.txt or /sitemap.xml
82
+ --max-urls <n> Reject discovery above n unique pages (default: 10000)
83
+ --max-sitemaps <n> Limit sitemap documents (default: 100)
84
+ --max-sitemap-bytes <n> Limit each downloaded/decompressed document (default: 10485760)
85
+
86
+ ${help.slice(help.indexOf(" --config"))}`);
87
+ return 0;
88
+ }
89
+ const project = await load.loadProjectConfig(values.config);
90
+ const configured = (mode === "links" ? project.config.check : project.config.sitemap) ?? {};
91
+ const input = positionals[0] ?? (configured.input ? mode === "links" ? project.path(configured.input) : configured.input : void 0);
92
+ if (!input || positionals.length > 1)
93
+ throw new Error(`Provide exactly one ${mode === "links" ? "JSON or YAML input file" : "HTTP(S) URL"}. Use --help for examples.`);
94
+ const selection = selectReporters(values.reporter, configured.reporter ?? project.config.reporter);
95
+ const outputDir = path.resolve(values.output ?? (configured.output ? project.path(configured.output) : "reports/meodp"));
96
+ const reporterOptions = { outputDir, cwd: project.path(".") };
97
+ const reporter = typeof selection === "string" ? [selection] : [...selection ?? ["json", "markdown", ["html", { outputFile: path.join(outputDir, "report.html") }]]];
98
+ if (configured.site)
99
+ reporter.push(["html", { outputFolder: project.path(configured.site) }]);
100
+ const plan = check_index.planReporters(reporter, reporterOptions);
101
+ const failOn = values["fail-on"] ?? configured.failOn ?? "unavailable";
102
+ if (!["none", "unavailable", "review"].includes(failOn))
103
+ throw new Error("--fail-on must be none, unavailable, or review");
104
+ const numeric = (value) => typeof value === "string" ? Number(value) : void 0;
105
+ const history = values.history ?? (configured.history ? project.path(configured.history) : void 0);
106
+ const seed = configured.historySeed ? project.path(configured.historySeed) : void 0;
107
+ await protectInputs(plan, [
108
+ ...mode === "links" ? [{ file: input, jsonOnly: false }] : [],
109
+ ...[history, seed].filter((file) => !!file).map((file) => ({ file, jsonOnly: true }))
110
+ ], history);
111
+ let previousReport = history ? await report.readReport(history) : void 0;
112
+ if (!previousReport && seed)
113
+ previousReport = await report.readReport(seed);
114
+ const observer = values.observer ?? configured.observer;
115
+ if (previousReport && configured.observerMismatch === "reset" && previousReport.observer !== (observer ?? node_os.hostname()))
116
+ previousReport = void 0;
117
+ const options = {
118
+ observer,
119
+ previousReport,
120
+ concurrency: numeric(values.concurrency) ?? configured.concurrency,
121
+ timeoutMs: numeric(values.timeout) ?? configured.timeoutMs,
122
+ retries: numeric(values.retries) ?? configured.retries,
123
+ maxRedirects: numeric(values["max-redirects"]) ?? configured.maxRedirects,
124
+ onResult(result) {
125
+ console.log(`[${result.status}] ${result.httpStatus ?? result.reason} ${result.url}`);
126
+ }
127
+ };
128
+ const report$1 = mode === "sitemap" ? await check_index.checkSitemap(input, {
129
+ ...options,
130
+ discover: values.discover === void 0 ? configured.discover : values.discover === true,
131
+ maxUrls: numeric(values["max-urls"]) ?? configured.maxUrls,
132
+ maxSitemaps: numeric(values["max-sitemaps"]) ?? configured.maxSitemaps,
133
+ maxSitemapBytes: numeric(values["max-sitemap-bytes"]) ?? configured.maxSitemapBytes
134
+ }) : await check_index.checkLinks(await readTargets(input), options);
135
+ const outputs = await check_index.writeReporters(report$1, reporter, reporterOptions);
136
+ for (const output of outputs)
137
+ console.log(`${output.reporter}: ${output.files.join(", ")}`);
138
+ if (history)
139
+ await report.saveReport(report$1, history);
140
+ console.log(`${report$1.summary.total} URLs: ${report$1.summary.reachable} reachable, ${report$1.summary.restricted} restricted, ${report$1.summary.unavailable} unavailable`);
141
+ if (failOn === "none")
142
+ return 0;
143
+ const findings = report$1.summary.unavailable > 0 || failOn === "review" && (report$1.summary.restricted > 0 || report$1.summary.redirected > 0);
144
+ return findings ? 1 : 0;
145
+ } catch (error) {
146
+ console.error(`meodp ${mode === "links" ? "check" : "sitemap"}: ${error instanceof Error ? error.message : String(error)}`);
147
+ return 2;
148
+ }
149
+ }
150
+ async function readTargets(input) {
151
+ const extension = path.extname(input).toLowerCase();
152
+ if (![".json", ".yml", ".yaml"].includes(extension))
153
+ throw new Error("Input file must use .json, .yml, or .yaml");
154
+ const source = await promises.readFile(input, "utf8");
155
+ return check_index.normalizeTargets(extension === ".json" ? JSON.parse(source) : jsYaml.load(source));
156
+ }
157
+ async function runReportCli(args = process__default.argv.slice(3)) {
158
+ try {
159
+ const { values, positionals } = node_util.parseArgs({
160
+ args,
161
+ allowPositionals: true,
162
+ options: {
163
+ "config": { type: "string" },
164
+ "reporter": { type: "string", multiple: true },
165
+ "verify": { type: "boolean" },
166
+ "output": { type: "string" },
167
+ "data-url": { type: "string" },
168
+ "help": { type: "boolean", short: "h" }
169
+ }
170
+ });
171
+ if (values.help) {
172
+ console.log(`Usage: meodp report [report.json] [--reporter json,markdown,html] [--output reports/site]
173
+
174
+ Render saved data through selected reporters without making any site-check requests.
175
+ --reporter accepts json, markdown, html; repeat it or separate names with commas.
176
+ With no configured selection, export the HTML static viewer.
177
+ HTML folders with input write index.html and report.json. The hosted viewer loads
178
+ report.json on each visit; opening index.html as a local file uses its embedded snapshot.
179
+ Without input, only HTML is available: an empty viewer with file upload and URL loading.
180
+ --config loads a project config; CLI arguments override config values.
181
+ --verify waits for report.verify.url to serve this report and the matching viewer.
182
+ --data-url overrides the data source loaded by the hosted viewer (cross-origin needs CORS).
183
+ -h, --help shows this help. To collect fresh observations, use meodp check.
184
+ Exit codes: 0 = exported; 2 = invalid input or execution error.`);
185
+ return 0;
186
+ }
187
+ if (positionals.length > 1)
188
+ throw new Error("Provide at most one report.json file. Use --help for examples.");
189
+ const project = await load.loadProjectConfig(values.config);
190
+ const configured = project.config.report ?? {};
191
+ const input = positionals[0] ?? (configured.input ? project.path(configured.input) : void 0);
192
+ const report$1 = input ? await report.readReport(input) : void 0;
193
+ if (input && !report$1)
194
+ throw new Error(`Report not found: ${input}`);
195
+ const reporter = selectReporters(values.reporter, configured.reporter ?? project.config.reporter) ?? "html";
196
+ if (values.verify) {
197
+ if (!report$1 || !configured.verify)
198
+ throw new Error("Verification requires a saved report and report.verify.url in the config.");
199
+ await check_index.waitForReport(report$1, configured.verify);
200
+ console.log("The public status page is serving the expected report.");
201
+ return 0;
202
+ }
203
+ const selected = values["data-url"] === void 0 ? reporter : (typeof reporter === "string" ? [reporter] : reporter).map((entry) => {
204
+ const [name, options] = typeof entry === "string" ? [entry] : entry;
205
+ return name === "html" ? ["html", { ...options, dataUrl: values["data-url"] }] : entry;
206
+ });
207
+ const reporterOptions = {
208
+ cwd: project.path("."),
209
+ outputDir: path.resolve(values.output ?? (configured.output ? project.path(configured.output) : "reports/site")),
210
+ dataUrl: values["data-url"] ?? configured.dataUrl
211
+ };
212
+ await protectInputs(check_index.planReporters(selected, reporterOptions), input ? [{ file: input, jsonOnly: true }] : []);
213
+ const outputs = await check_index.writeReporters(report$1, selected, reporterOptions);
214
+ for (const output of outputs)
215
+ console.log(`${output.reporter}: ${output.files.join(", ")}`);
216
+ return 0;
217
+ } catch (error) {
218
+ console.error(`meodp report: ${error instanceof Error ? error.message : String(error)}`);
219
+ return 2;
220
+ }
221
+ }
222
+ function selectReporters(cli, configured) {
223
+ if (cli === void 0)
224
+ return configured;
225
+ const names = [...new Set(cli.flatMap((value) => value.split(",").map((name) => name.trim())))];
226
+ if (names.some((name) => !["json", "markdown", "html"].includes(name)))
227
+ throw new TypeError("--reporter must select json, markdown, or html.");
228
+ return names;
229
+ }
230
+ async function protectInputs(plan, inputs, history) {
231
+ const identity = async (file) => {
232
+ try {
233
+ return await promises.realpath(file);
234
+ } catch (error) {
235
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
236
+ return path.resolve(file);
237
+ throw error;
238
+ }
239
+ };
240
+ const writes = plan.flatMap((item) => [
241
+ { file: item.file, reporter: item.reporter },
242
+ ...item.json ? [{ file: item.json, reporter: "json" }] : []
243
+ ]);
244
+ if (history)
245
+ writes.push({ file: history, reporter: "json" });
246
+ for (const input of inputs) {
247
+ const source = await identity(input.file);
248
+ for (const output of writes) {
249
+ if (await identity(output.file) === source && (!input.jsonOnly || output.reporter !== "json"))
250
+ throw new Error(`Reporter/history output would overwrite input ${input.file}; choose another output path.`);
251
+ }
252
+ }
253
+ }
254
+
255
+ exports.runCheckCli = runCheckCli;
256
+ exports.runReportCli = runReportCli;
257
+ 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 };