@lariocpt/crust-mongo 0.0.0-stage → 0.3.6

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.
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: crust-procs
3
+ description: Run and supervise dev processes with crust — procs() merges multiple long-lived commands into one pipeable stream with per-proc env, restart backoff, readiness probes (ready:), dependency ordering (after:), and process-group teardown; wait blocks until a port/URL is up. Use for dev runners ("start db, api, web in order"), readiness gating in CI, tailing multiple services, or mining merged process logs.
4
+ ---
5
+
6
+ # crust procs & wait
7
+
8
+ ## procs() — the one-tail dev runner
9
+
10
+ ```crust
11
+ procs({web: "bun run dev", api: "bun api.ts"}) | (l => l.proc + " | " + l.line)
12
+ ```
13
+
14
+ Every stdout/stderr line becomes `{proc, stream, line}` — a PIPELINE ITEM,
15
+ not text. Grep it, transform it, count it. Streams: `stdout`, `stderr`,
16
+ `exit`, `ready`, `live`.
17
+
18
+ Full spec form:
19
+
20
+ ```crust
21
+ procs({ db: {cmd: "docker compose up pg", ready: "port:5432"}, api: {cmd: "bun api.ts", after: "db", ready: ":3001/health", live: {url: ":3001/health", failures: 3}, restart: {max: 3}}, web: {cmd: "bun run dev", after: "api", env: {PORT: "3001"}} })
22
+ ```
23
+
24
+ - `env` — per-proc overlay on the inherited environment.
25
+ - `restart: true` — respawn on unexpected exit (backoff 250ms→2s; >10s
26
+ uptime while READY resets it — a proc that hangs un-ready until its
27
+ ready-timeout kill never does, and with `live:` uptime ends when a fatal
28
+ probe streak BEGAN, so a proc that wedges right after ready still accrues
29
+ strikes). `restart: {max: N}` gives up after N consecutive restarts.
30
+ Ctrl-C never respawns. **Giving up is not a failure**: `giving up after N
31
+ restart(s)` is an `exit`-stream line and the line still exits 0 (same for a
32
+ child that exits nonzero with no `restart:` — its stream just ends). Gate it
33
+ in CI: `| (l => { if (/giving up|unhealthy/.test(l.line)) throw new Error(l.line); return null })`.
34
+ - `ready:` — `":3001/health"` / `"http(s)://…"` (any 2xx) or `"port:5432"`
35
+ (TCP connect). Long form `{url?, port?, timeoutMs?, intervalMs?,
36
+ probeTimeoutMs?}` (defaults 30s / 250ms; each probe capped at
37
+ `min(intervalMs*4, 2s)` unless `probeTimeoutMs` raises it). `timeoutMs`
38
+ belongs INSIDE the ready object: as a sibling of `cmd` it is ignored
39
+ silently — `{cmd: "sleep 40", ready: "port:4199", timeoutMs: 1200}` waits
40
+ the full default 30s. Timeout: a restartable proc is killed and respawned
41
+ (readiness re-awaited each spawn); a non-restartable one FAILS the whole
42
+ pipeline — CI semantics.
43
+ - `live:` — liveness for the unhealthy-but-alive case. Same target forms;
44
+ long form `{url?, port?, intervalMs?, probeTimeoutMs?, failures?,
45
+ graceMs?}` (defaults 5s / 3 failures / 0 grace). Use `url:` here: a
46
+ `port:` probe is a TCP connect, so it cannot see a wedged listener — a
47
+ server answering 503 for four seconds with `live: {port, failures: 3}`
48
+ logged nothing and the run exited 0. Arms once the proc is up
49
+ (after `ready:`, else at spawn); `live`-stream lines: `probe failed
50
+ (k/N)`, `recovered after k failed probe(s)`, `unhealthy after N
51
+ consecutive failed probe(s)`. Fatal streak: restartable → killed +
52
+ respawned (re-arms after next ready); non-restartable → FAILS the
53
+ pipeline, like a ready-timeout.
54
+ - `after:` — name or list; the proc spawns only once its dependencies are
55
+ READY (no `ready:` on the dep = ready at spawn). One-shot gate; a dep
56
+ that dies before ever becoming ready fails the pipeline. Unknown names /
57
+ self-deps / cycles throw before anything spawns.
58
+ - Teardown: children run in their own process groups; SIGTERM to all, 3s
59
+ grace, then SIGKILL — grandchildren from `sh -c` don't survive.
60
+
61
+ ## Log mining on the merged stream
62
+
63
+ ```crust
64
+ procs({api: "bun api.ts", worker: "bun worker.ts"}) | (l => { try { return JSON.parse(l.line) } catch { return { level: 30, msg: l.line, proc: l.proc } } }) | filter (e => e.level >= 40) | (e => "WARN+ " + (e.msg ?? ""))
65
+ ```
66
+
67
+ (`filter` drops the sub-WARN entries; a plain lambda with `: null` would
68
+ print a literal `null` line for every one of them.)
69
+
70
+ Objects printed to stdout are JSON lines — pipe the whole thing into
71
+ `pino-pretty` via a shell stage, or count errors with `grep`/`wc`.
72
+
73
+ To iterate on these filters WITHOUT restarting the dev stack, hold it in a
74
+ `logs` session instead: `logs procs({api: "bun api.ts", worker: "bun
75
+ worker.ts"})` keeps the group running and buffers its recent output; each
76
+ line you type (`filter (l => l.proc === "api")`, `(l => l.line) | grep -i
77
+ error`) runs over the buffer, then live until Ctrl-C. Items stay `{proc,
78
+ stream, line}` objects. `exit`/Ctrl-D tears the group down (SIGTERM→SIGKILL).
79
+
80
+ ## wait — readiness as a one-liner (CI's friend)
81
+
82
+ ```crust
83
+ wait :3001/api/health --timeout 40s --interval 2s
84
+ ```
85
+
86
+ Blocks until the target answers (URL = any 2xx; `port:5432` = TCP connect),
87
+ then emits `{target, ready: true, ms, attempts}` and exits 0. Not ready in
88
+ time → error, exit 1. Durations: `300ms`, `30s`, `2m` (defaults 30s/500ms).
89
+ `--probe-timeout 5s` raises the per-probe cap (default `min(interval*4, 2s)`)
90
+ for targets that take longer than that to first byte.
91
+ Works in the REPL, `crust -c` (replaces hand-rolled curl-sleep retry loops
92
+ in CI), and `.pipes` files. Env-expanded args work:
93
+ `wait "$BASE_URL/health" --timeout 40s`.
94
+
95
+ ## Traps
96
+
97
+ - procs is source-only (first stage). Spec keys are JS object-literal keys, so
98
+ a duplicate collapses BEFORE crust sees it (`{a: "x", a: "y"}` runs only
99
+ `y`) — nothing warns you.
100
+ - The merged stream never ends while a `restart: true` child keeps
101
+ respawning — consume it with `for await`/pipe, bound it with a downstream
102
+ condition, or stop it with Ctrl-C: at the REPL that cancels the running
103
+ line and tears the whole process group down (SIGTERM, then SIGKILL).
104
+ - `FORCE_COLOR=0` is forced on children so lines stay parseable.
105
+ - To capture the stream, END with a shell stage: `procs({…}) | cat > file`
106
+ writes each item as its JSON line. A bare `procs(...) > file` is refused —
107
+ crust has no redirect on its own stages (the `>` would be read as data by a
108
+ pipeline stage); the outer shell can still wrap the whole invocation.
@@ -0,0 +1,160 @@
1
+ ---
2
+ name: crust-verify-web-links
3
+ description: Crawl a site from its sitemap and verify every link, #anchor, redirect chain, OG image and meta tag against .crust.ts fixtures. `--base-url` auto-discovers the sitemap (robots.txt → /sitemap.xml → /sitemap-index.xml), `--site-map-url` takes a local sitemap so you can check build output, `--fixtures` diffs meta. Every failure names its source page and element (structured `sources`/`meta`), and every failing run ends with an aligned fix-list. Use when link or SEO health must gate a deploy, when a built site should be checked before publishing, or when OG/meta regressions need catching.
4
+ ---
5
+
6
+ # crust verify-web-links
7
+
8
+ ## What it verifies
9
+
10
+ | Check | Fails when |
11
+ | --- | --- |
12
+ | internal + external links | any status that is not 2xx |
13
+ | `#fragment` targets | the id/name is not on the fetched page |
14
+ | redirect chains | a link 301s/302s somewhere (informational with `--no-redirect-warnings`) |
15
+ | OG / twitter images | the image URL is unreachable |
16
+ | meta vs `--fixtures` | a keyed value differs, or a predicate returns falsy |
17
+
18
+ Exit codes: `0` all clear, `1` verification failures, `2` bad args or an
19
+ unreachable sitemap. With `--strict`, `1` also covers a run that finished
20
+ having left discovered links unchecked. `2` also covers `--site-map-url`
21
+ together with `--base-url`, neither of them given, `no fixture files matched
22
+ <glob>`, `--concurrency must be >= 1`, and `unexpected argument`.
23
+
24
+ ## Every failure names its source (new in 0.3.6)
25
+
26
+ Failures are **one per failing target**, and each answers "which page did I
27
+ find this on, and which element":
28
+
29
+ - `sources[]` — every link that pointed at the failing target, as
30
+ `{fromUrl, kind, href}`. In text each prints as an indented
31
+ `from <page> (<kind>)` line under the failure; in `--json` it is the full
32
+ array. A broken URL linked from fifty pages is still **one** failure with
33
+ fifty sources — `totals.failures` and exit codes are unchanged.
34
+ `kind: "sitemap"` means the URL arrived as a sitemap `<loc>` and no page
35
+ linked it; the sitemap URL is named as `fromUrl` instead of the source being
36
+ omitted.
37
+ - `meta` — structured `{key, expected, actual}` for `meta-mismatch`
38
+ (`meta-fixture-no-page` carries `key: "fixture"`).
39
+ - **Fix-list**: every failing text run ends with a separate aligned section —
40
+ one line per (page, element, broken-thing) triple, nothing else (excerpt of
41
+ the real output; the full failing report is in USAGE and on the website):
42
+
43
+ ```text
44
+ fix list
45
+ page element broken link / diff
46
+ http://localhost:47831/page-a a http://localhost:47831/broken
47
+ http://localhost:47831/page-a a http://localhost:47831/page-x#ghost
48
+ http://localhost:47831/page-b img http://localhost:47831/broken
49
+ http://localhost:47831/page-x meta:og:title got "Page X — Real Title" (expected "Page X — Wrong Title")
50
+ ```
51
+
52
+ A clean run prints no fix-list section. `--json` carries the structured
53
+ `sources`/`meta` fields instead — the data *is* the fix-list there.
54
+
55
+ The `detail` prose is unchanged from v0.3.5: the structured fields are purely
56
+ additive.
57
+
58
+ ## Getting it pointed at the right thing
59
+
60
+ ```crust
61
+ verify-web-links --base-url https://example.com
62
+ verify-web-links --site-map-url https://example.com/sitemap.xml
63
+ verify-web-links --site-map-url ./dist/sitemap.xml --no-recurse
64
+ verify-web-links --base-url https://staging.example.com --fixtures site/*.meta.crust.ts
65
+ ```
66
+
67
+ `--base-url` probes `robots.txt` for `Sitemap:` lines, then `/sitemap.xml`, then
68
+ `/sitemap-index.xml`; when none answer it names all three. `--site-map-url`
69
+ (aliased `--sitemap`) also takes a **local path**, which is the CI move: crawl
70
+ the build output rather than a deployed site. With `--no-recurse` only the URLs
71
+ in the sitemap are fetched.
72
+
73
+ ## Green does not mean checked
74
+
75
+ Three ways to get exit 0 with nothing actually verified. Read the note lines and
76
+ `totals` before believing a green run:
77
+
78
+ | Symptom | What it means |
79
+ | --- | --- |
80
+ | `N page(s) had to be recognised from their body` | the server sends HTML without saying `text/html`. crust followed the links anyway, but the mislabel is the site's bug |
81
+ | `N page(s) NOT read as HTML` | a sitemap entry or `<a>` target that is neither declared HTML nor smells like it (a JSON endpoint). Its links were **not** followed and the crawl ended there |
82
+ | a note naming a count (`anchors skipped…`, `stopped at --max-pages…`) | that work did not happen. `--no-recurse` and `--no-anchors` produce it deliberately |
83
+ | `"parsed": false` on a page in `--json` | no links and no ids were extracted from that page |
84
+
85
+ `totals.complete` is the one field that means "nothing was left unchecked":
86
+ false on any of the above. `--max-pages N` is a safety valve, not a fast mode —
87
+ the crawl stops early, the unchecked count is reported, and the exit code still
88
+ says 0 without `--strict`. Treat a `complete: false` run as inconclusive.
89
+
90
+ ## In CI
91
+
92
+ **`--strict` is the CI flag.** It exits 1 when the run finished having left
93
+ discovered links unchecked — a `--max-pages` cut-off, an unchecked `#fragment`,
94
+ a page crust could not read — so a bounded crawl cannot pass as a clean one:
95
+
96
+ ```crust
97
+ verify-web-links --base-url https://example.com --strict --max-pages 500
98
+ ```
99
+
100
+ A builtin runs in-process and cannot sit mid-pipeline —
101
+ `verify-web-links … | assert (…)` is refused with *"give it a line of its own"*.
102
+ Redirect the whole invocation instead, then judge the report:
103
+
104
+ ```bash
105
+ crust -c 'verify-web-links --base-url https://example.com --json --no-progress' > report.json
106
+ ```
107
+
108
+ ```crust
109
+ read report.json | (s => JSON.parse(s)) | assert (r => r.totals.failures === 0)
110
+ read report.json | (s => JSON.parse(s)) | assert (r => r.totals.complete)
111
+ ```
112
+
113
+ `read` yields each file's **contents as one item**, so `JSON.parse` receives the
114
+ whole document. The first line fails on a real failure; the second catches every
115
+ inconclusive case at once — `complete` is false if anything was dropped, any
116
+ `#fragment` went unchecked, or any page could not be read.
117
+
118
+ ## Meta fixtures
119
+
120
+ ```ts
121
+ export default {
122
+ url: "https://example.com/about",
123
+ meta: {
124
+ title: "About Us",
125
+ description: (d: string) => d.length > 50 && d.length < 160,
126
+ "og:image": (u: string) => u.endsWith(".png") || u.endsWith(".jpg"),
127
+ "twitter:card": "summary_large_image",
128
+ },
129
+ };
130
+ ```
131
+
132
+ A value is compared for equality; a function is a predicate over the extracted
133
+ value. A key a page does not have is **asked about**, not ignored — so asserting
134
+ `og:image` on a page that lost it is a failure, which is the point.
135
+
136
+ Fixture `url`s match **URL-normalized**: scheme+host case and default ports
137
+ don't matter, `https://x.com` and `https://x.com/` are the same page, but a
138
+ trailing slash on a non-root path stays significant (`/login/` ≠ `/login`). A
139
+ fixture whose url never appears in the crawl fails as `meta-fixture-no-page` —
140
+ a fixture for a page you deleted is a failure, not a skip.
141
+
142
+ ## Tuning
143
+
144
+ ```crust
145
+ verify-web-links --base-url https://example.com --exclude /checkout/ --exclude /go/
146
+ verify-web-links --base-url https://example.com --include-external --max-depth 2
147
+ verify-web-links --base-url https://example.com --concurrency 16 --timeout 3000
148
+ ```
149
+
150
+ - `--exclude <substring>` is repeatable. Use it for subtrees that redirect **by
151
+ design** (short-link senders, locale switches); redirect chains are failures
152
+ otherwise. `--no-redirect-warnings` silences them globally — only when you
153
+ mean it.
154
+ - Off-origin links are not queued at all without `--include-external`, and even
155
+ then they are never recursed into.
156
+ - `--max-depth` (default 5) bounds the crawl, `--concurrency` (default 4) and
157
+ `--timeout` (default 10000 ms) bound each fetch.
158
+ - Anchors come from the crawl: with `--no-recurse` the internal links found on
159
+ sitemap pages are not fetched, so their `#fragments` are not checked — the
160
+ report says how many were skipped.