@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.
- package/README.md +174 -2
- package/bin/crust +64 -0
- package/lib/install.mjs +330 -0
- package/lib/skills-prompt.mjs +134 -0
- package/package.json +55 -4
- package/scripts/postinstall.mjs +36 -0
- package/skills/crust-api-testing/SKILL.md +299 -0
- package/skills/crust-builtins/SKILL.md +238 -0
- package/skills/crust-cli/SKILL.md +215 -0
- package/skills/crust-load-testing/SKILL.md +161 -0
- package/skills/crust-logs/SKILL.md +99 -0
- package/skills/crust-mcp/SKILL.md +136 -0
- package/skills/crust-mock-server/SKILL.md +138 -0
- package/skills/crust-pipelines/SKILL.md +209 -0
- package/skills/crust-procs/SKILL.md +108 -0
- package/skills/crust-verify-web-links/SKILL.md +160 -0
|
@@ -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.
|