Compare commits

...
5 Commits
Author SHA1 Message Date
noether 7ea7652f36 content: restore GatGPT/JabFreeJobs links, fix Surveillance Report URL
Un-comment GatGPT (defense) and JabFreeJobs (job-boards) now that their
domains respond; fix the https://ttps:// typo'd Surveillance Report URL and
the 'the New Oil' capitalization (privacy).
2026-10-12 22:53:15 -06:00
noether 2d7c724a48 docs: README quick-start for fresh pulls (bootstrap, rebuild, serve-lan)
Top section now leads with the rebuild-after-pull workflow (public/ is never
in git) including make bootstrap/tool-versions and the error-127 hint; §2
recommends bootstrap over manual installs; §4 documents the new targets; §7
gains the pagefind/livereload-404 and command-not-found bullets; §8 lists
.tools/.
2026-10-12 22:53:14 -06:00
noether 8e8958274a fix: ship external-fix script + lychee JSON save so make external-fix works fresh
scripts/fix_dead_external_links.py was referenced by the Makefile but never
committed, and check_external_links.py did not yet save linkcheck/external.json
(both were working-tree only), so external-fix/external-fix-apply failed on
any fresh clone. The fixer is deliberately conservative: only DNS-failed /
connection-failed / host-unreachable links are auto-commented; .onion links
and ambiguous failures (404/5xx/SSL/refused) go to manual review.
2026-10-12 22:53:09 -06:00
noether f90e694c3e fix: set real baseURL/title so canonical/og/RSS URLs stop pointing at example.org
Every rendered page carried <link rel=canonical>, og:url and RSS <link> for
http://example.org/... and an empty <title>. Set baseURL to the actual site
(https://libertytools.io/) and title to 'Liberty Tools'; verified zero
example.org references in the rebuilt public/.
2026-10-12 22:53:09 -06:00
noether be9c92f6d2 build: pin toolchain via make bootstrap + tool-versions/preflight (repo-local .tools/)
Make targets now resolve a repo-local, gitignored .tools/bin (Hugo extended
0.167.0, Node 22.11.0, lychee 0.24.2) via a recipe-scoped PATH, so machines
with old/missing tools can no longer produce error-127 mysteries or version
drift. System PATH is never touched.

  make bootstrap     one-time idempotent install (sha256-verified Hugo)
  make tool-versions pinned vs PATH-first diagnostic table
  make preflight     strict check, wired ahead of build/serve/serve-lan
2026-10-12 22:53:03 -06:00
11 changed files with 439 additions and 14 deletions
+1
View File
@@ -3,3 +3,4 @@ linkcheck/
resources/_gen/ resources/_gen/
__pycache__/ __pycache__/
.hermes/ .hermes/
.tools/
+48 -5
View File
@@ -3,12 +3,37 @@ PAGEFIND := npx -y pagefind
LYCHEE := lychee LYCHEE := lychee
OUT := public OUT := public
.PHONY: all build index serve run link-check link-fix external-check serve-lan clean # --- Pinned toolchain (repo-local .tools/; gitignored) ---
# `make bootstrap` installs these exact versions into .tools/bin. The PATH
# prepend below applies to every recipe, so make targets always use the pinned
# tools; your system PATH is never modified. `make tool-versions` shows both.
HUGO_VERSION := 0.167.0
NODE_VERSION := 22.11.0
LYCHEE_VERSION := 0.24.2
TOOLS_BIN := $(CURDIR)/.tools/bin
export PATH := $(TOOLS_BIN):$(PATH)
.PHONY: all build index serve run link-check link-fix external-check external-fix external-fix-apply serve-lan clean bootstrap tool-versions preflight
all: index all: index
# Build Hugo content -> public/ # ONE-TIME per machine: install the pinned hugo/node/lychee into .tools/bin.
build: # Idempotent (present-at-pin tools are skipped); touches nothing outside .tools/.
bootstrap:
scripts/bootstrap.sh
# Diagnostic: pinned (.tools/bin) vs PATH-first (system) tool versions.
tool-versions:
scripts/tool_versions.sh
# Strict tool check; runs automatically before all/serve/serve-lan and
# fails with a `make bootstrap` hint instead of a bare "error 127".
preflight:
scripts/tool_versions.sh --strict
# Build Hugo content -> public/ (preflight first so a missing tool fails fast
# and in order even under `make -j`).
build: preflight
$(HUGO) --gc $(HUGO) --gc
# (Re)build the Pagefind search index from public/ -> public/pagefind/ # (Re)build the Pagefind search index from public/ -> public/pagefind/
@@ -39,14 +64,32 @@ link-fix:
$(PAGEFIND) --site $(OUT) $(PAGEFIND) --site $(OUT)
# DEAD LINKS -- external (network): scan all http/https links in content. Report-only. # DEAD LINKS -- external (network): scan all http/https links in content. Report-only.
# Always exits 0; the report is the point. # Always exits 0; the report is the point. Also saves the full lychee JSON to
# linkcheck/external.json so external-fix can act without re-pinging the web.
external-check: external-check:
python3 scripts/check_external_links.py python3 scripts/check_external_links.py
# Preview which EXTERNAL links are truly dead (domain/DNS no longer exists) and
# would be commented out. Reports only; changes nothing. Run external-check first.
external-fix:
python3 scripts/fix_dead_external_links.py --report linkcheck/external.json
# Comment out the truly-dead EXTERNAL links (DNS failed / host unreachable), then
# rebuild content + index so they leave the live page AND the search index.
# 404s, SSL errors, 5xx, redirects, refused/reset connections are NOT auto-commented
# (domain is still alive / ambiguous) -- they go to linkcheck/manual_review_external.txt.
external-fix-apply:
python3 scripts/fix_dead_external_links.py --report linkcheck/external.json --apply
$(HUGO) --gc
$(PAGEFIND) --site $(OUT)
# Serve the BUILT site on all interfaces so another LAN machine can open it: # Serve the BUILT site on all interfaces so another LAN machine can open it:
# http://<this-machine-LAN-IP>:8090/ (IP printed at startup; also `hostname -I | head -1`) # http://<this-machine-LAN-IP>:8090/ (IP printed at startup; also `hostname -I | head -1`)
serve-lan: serve-lan: preflight
python3 scripts/serve_lan.py python3 scripts/serve_lan.py
# NOTE: clean does NOT remove .tools/ -- the pinned toolchain is expensive to
# reinstall and independent of build output. Remove it manually if ever needed.
clean: clean:
rm -rf $(OUT) linkcheck rm -rf $(OUT) linkcheck
+48 -3
View File
@@ -11,8 +11,35 @@ previewing, searching, and maintaining the site.
--- ---
## Fresh git pull? Rebuild before serving
`public/` (the built site) is **never in git** — it's regenerated locally and
gitignored (§8). A `git pull` brings you source only: content, theme, Makefile,
scripts. Until you rebuild, there is nothing to serve — and a stale `public/`
left over from an older workflow (e.g. `hugo server`) will render pages with a
broken search box and spurious `pagefind/*` / `livereload.js` 404s in the server
log.
From the project root:
```
make bootstrap # ONCE per machine: pinned Hugo/Node/lychee into .tools/ (no system changes)
make tool-versions # diagnostic: pinned vs system tool versions
make clean && make all # wipe stale output, then hugo --gc + pagefind index
make serve-lan # serve on http://<LAN-IP>:8090/ (or: make serve, local :1414)
```
Success check: the `make all` log prints `Indexed N pages` (N > 0) and
`public/pagefind/pagefind-ui.js` exists. `error 127` / "command not found"
means a tool is missing: `make tool-versions` shows which, `make bootstrap`
fixes it. Full details: [Prerequisites (§2)](#2-prerequisites-install-once),
[Build & preview (§4)](#4-build--preview-make-commands).
---
## Table of contents ## Table of contents
- [Fresh git pull? Rebuild before serving](#fresh-git-pull-rebuild-before-serving)
- [Architecture](#architecture) - [Architecture](#architecture)
- [1. Clone](#1-clone) - [1. Clone](#1-clone)
- [2. Prerequisites (install once)](#2-prerequisites-install-once) - [2. Prerequisites (install once)](#2-prerequisites-install-once)
@@ -196,6 +223,13 @@ cd libertytoolsio
## 2. Prerequisites (install once) ## 2. Prerequisites (install once)
**Recommended: `make bootstrap`.** It installs pinned versions of Hugo extended
(0.167.0), Node (22.11.0 — provides `npx` for Pagefind), and lychee (0.24.2)
into the repo-local `.tools/bin` (gitignored). Make targets always use these
pinned tools via a recipe-scoped PATH; your system PATH and any system-installed
tools (e.g. an older Hugo) are never touched. Run `make tool-versions` to see
pinned vs system versions side by side. The manual per-tool installs below are
the fallback (they install into `~/.local/bin`).
All tools install into `~/.local/bin` (no sudo). Make sure `~/.local/bin` is on your All tools install into `~/.local/bin` (no sudo). Make sure `~/.local/bin` is on your
`PATH` (it usually is; if not: `export PATH="$HOME/.local/bin:$PATH"`). `PATH` (it usually is; if not: `export PATH="$HOME/.local/bin:$PATH"`).
@@ -235,11 +269,14 @@ All tools install into `~/.local/bin` (no sudo). Make sure `~/.local/bin` is on
## 4. Build & preview (make commands) ## 4. Build & preview (make commands)
Run from the project root. `public/`, `linkcheck/`, and `resources/_gen/` are Run from the project root. `public/`, `linkcheck/`, `resources/_gen/`, and
generated and gitignored. `.tools/` are generated and gitignored.
| Target | What it does | | Target | What it does |
|-----------------------|--------------| |-----------------------|--------------|
| `make bootstrap` | **Once per machine.** Install the pinned toolchain (Hugo 0.167.0 extended, Node 22.11.0, lychee 0.24.2) into repo-local `.tools/bin`. Idempotent; never touches your system. |
| `make tool-versions` | Diagnostic table: pinned (`.tools/bin`) vs PATH-first (system) tool versions. |
| `make preflight` | Strict tool check (runs automatically before `all`/`serve`/`serve-lan`); on failure it says `make bootstrap` instead of a bare `error 127`. |
| `make build` | Build Hugo content into `public/` (`hugo --gc`). | | `make build` | Build Hugo content into `public/` (`hugo --gc`). |
| `make index` | `build` + build the Pagefind search index into `public/pagefind/`. **This is the normal "I edited content, re-make it" command.** | | `make index` | `build` + build the Pagefind search index into `public/pagefind/`. **This is the normal "I edited content, re-make it" command.** |
| `make serve` / `make run` | `index` + serve the built site **with search** at `http://localhost:1414/` (local only). Uses Pagefind's own server, which binds 127.0.0.1. | | `make serve` / `make run` | `index` + serve the built site **with search** at `http://localhost:1414/` (local only). Uses Pagefind's own server, which binds 127.0.0.1. |
@@ -377,6 +414,12 @@ hermes cronjob list # confirm it exists and is enabled
## 7. Troubleshooting ## 7. Troubleshooting
- **Server log shows 404s for `/pagefind/*` or `/livereload.js`** → stale or
unindexed `public/`: run `make clean && make all` and restart the server (see
[Fresh git pull? Rebuild before serving](#fresh-git-pull-rebuild-before-serving)).
- **`error 127` / "command not found"** → a build tool is missing. Run
`make tool-versions` to see which, then `make bootstrap` to install the
pinned toolchain into `.tools/`.
- **`hugo: command not found`** → you're using the non-extended build or it isn't on - **`hugo: command not found`** → you're using the non-extended build or it isn't on
PATH. Re-run the Hugo install above and confirm `hugo version` says `+extended`. PATH. Re-run the Hugo install above and confirm `hugo version` says `+extended`.
- **Search shows no results** → the index wasn't built. Run `make index`, confirm the - **Search shows no results** → the index wasn't built. Run `make index`, confirm the
@@ -404,9 +447,11 @@ All changes from a maintenance pass are left in the working tree for review. Com
**only after you have validated the result** (`make build` exits 0 and you've checked **only after you have validated the result** (`make build` exits 0 and you've checked
the rendered output). The following are generated and gitignored — never commit them: the rendered output). The following are generated and gitignored — never commit them:
- `public/` — built site output - `public/` — built site output; a fresh clone/pull contains none of it (rebuild
per [Fresh git pull? Rebuild before serving](#fresh-git-pull-rebuild-before-serving))
- `linkcheck/` — link-check reports - `linkcheck/` — link-check reports
- `resources/_gen/` — Hugo asset-generation cache - `resources/_gen/` — Hugo asset-generation cache
- `.tools/` — pinned local toolchain installed by `make bootstrap`
- `.hermes/` — agent working files - `.hermes/` — agent working files
## Repository layout ## Repository layout
+2 -2
View File
@@ -1,6 +1,6 @@
baseURL = 'http://example.org/' baseURL = 'https://libertytools.io/'
languageCode = 'en-us' languageCode = 'en-us'
title = '' title = 'Liberty Tools'
theme = "hugo-book" theme = "hugo-book"
[params] [params]
+1 -1
View File
@@ -90,7 +90,7 @@ Documentaries, talks, and other media.
- [**The Unshackled Englishman**](https://www.youtube.com/watch?v=rB6oK_FS8JA) - [**The Unshackled Englishman**](https://www.youtube.com/watch?v=rB6oK_FS8JA)
## Resources ## Resources
<!-- - [**GatGPT**](https://gatgpt.defcad.com/) - A deneutered version of ChatGPT focused on firearms. --> - [**GatGPT**](https://gatgpt.defcad.com/) - A deneutered version of ChatGPT focused on firearms.
- [**Shootingbot**](https://www.shootingbot.com/) - Monitors price and availability of ammunition and reloading components. - [**Shootingbot**](https://www.shootingbot.com/) - Monitors price and availability of ammunition and reloading components.
## Training ## Training
+3 -2
View File
@@ -43,5 +43,6 @@ Why work for a company you hate? If you believe in liberty many of these compani
- [**Vaccinefreejobs.org**](http://vaccinefreejobs.org/) - This site is for the people, the employees and candidates, who believe that your access and free movement in society should not be determined by your vaccination status. - [**Vaccinefreejobs.org**](http://vaccinefreejobs.org/) - This site is for the people, the employees and candidates, who believe that your access and free movement in society should not be determined by your vaccination status.
<!--- [https://www.jabfreejobs.in](https://www.jabfreejobs.in)fo/ - [**JabFreeJobs**](https://www.jabfreejobs.in/) - No jab job board.
http://www.jobsnotjabs.com/ http://novaxjobsusa.com www.provisionjobs.com --->
<!--- http://www.jobsnotjabs.com/ http://novaxjobsusa.com www.provisionjobs.com --->
+1 -1
View File
@@ -525,7 +525,7 @@ Password managers are one of the few security tools that makes your life easier.
- [**Privacy Security and Osint**](https://inteltechniques.com/podcast.html) - Hosted by the king of privacy, Micahel Bazzell, covers a wide range of privacy, security and Osint. Currently on a hiatus. - [**Privacy Security and Osint**](https://inteltechniques.com/podcast.html) - Hosted by the king of privacy, Micahel Bazzell, covers a wide range of privacy, security and Osint. Currently on a hiatus.
- [**Surveillance Report**](https://ttps://surveillancereport.tech/) - Joint hosted by Privacy advocates Henry from Techlore and Nate from the New Oil. Break down of weekly data leaks and privacy incursions. - [**Surveillance Report**](https://surveillancereport.tech/) - Joint hosted by Privacy advocates Henry from Techlore and Nate from The New Oil. Break down of weekly data leaks and privacy incursions.
- [**WatchMan Privacy**](https://watchmanprivacy.com/) - Hosted by Gabriel Custodiet, a principled privacy and freedom advocate. - [**WatchMan Privacy**](https://watchmanprivacy.com/) - Hosted by Gabriel Custodiet, a principled privacy and freedom advocate.
+69
View File
@@ -0,0 +1,69 @@
#!/bin/sh
# Install the pinned toolchain into the repo-local .tools/bin (gitignored):
# hugo extended 0.167.0, node 22.11.0 (provides npx/npm), lychee 0.24.2
# Idempotent: a tool already present at the pinned version is skipped, so
# re-running with everything installed is an instant no-op. Touches nothing
# outside .tools/ and never modifies your system PATH.
# Requires: curl, tar, sha256sum, and network on first run.
set -eu
ROOT=$(cd "$(dirname "$0")/.." && pwd)
BIN="$ROOT/.tools/bin"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
HUGO_VERSION=0.167.0
HUGO_SHA256=0163f5c3deddac1f494a1629ddc40c65d18de9d5794facd98f7f96ac2c7d8957
NODE_VERSION=22.11.0
LYCHEE_VERSION=0.24.2
case "$(uname -s)/$(uname -m)" in
Linux/x86_64) HARCH=amd64; NARCH=x64; RARCH=x86_64 ;;
Linux/aarch64) HARCH=arm64; NARCH=arm64; RARCH=aarch64 ;;
*) echo "bootstrap: unsupported platform $(uname -s) $(uname -m) (Linux x86_64/aarch64 only)" >&2; exit 1 ;;
esac
command -v curl >/dev/null 2>&1 || { echo "bootstrap: curl is required (downloads the pinned tarballs)" >&2; exit 1; }
mkdir -p "$BIN"
# --- hugo extended ---
# Release builds report "v<ver>-<40-hex-commit>+extended"; strip the commit so
# the pinned comparison is exact.
hugo_norm() { "$1" version 2>/dev/null | awk '{print $2}' | sed 's/-[0-9a-f]\{40,\}//'; }
if [ -x "$BIN/hugo" ] && [ "$(hugo_norm "$BIN/hugo")" = "v$HUGO_VERSION+extended" ]; then
echo "hugo v$HUGO_VERSION+extended: present, skipping"
else
echo "hugo v$HUGO_VERSION+extended: downloading..."
curl -fsSL -o "$TMP/h.tgz" \
"https://github.com/gohugoio/hugo/releases/download/v$HUGO_VERSION/hugo_extended_${HUGO_VERSION}_linux-${HARCH}.tar.gz"
echo "$HUGO_SHA256 $TMP/h.tgz" | sha256sum -c -
tar xzf "$TMP/h.tgz" -C "$TMP"
install -m 0755 "$TMP/hugo" "$BIN/hugo"
fi
# --- node (+ npm/npx) ---
if [ -x "$BIN/node" ] && [ "$("$BIN/node" --version 2>/dev/null)" = "v$NODE_VERSION" ]; then
echo "node v$NODE_VERSION: present, skipping"
else
echo "node v$NODE_VERSION: downloading..."
curl -fsSL -o "$TMP/n.tar.xz" \
"https://nodejs.org/dist/v$NODE_VERSION/node-v${NODE_VERSION}-linux-${NARCH}.tar.xz"
rm -rf "$ROOT"/.tools/node-v*-linux-*
tar xJf "$TMP/n.tar.xz" -C "$ROOT/.tools"
for t in node npm npx; do
ln -sfn "../node-v${NODE_VERSION}-linux-${NARCH}/bin/$t" "$BIN/$t"
done
fi
# --- lychee ---
if [ -x "$BIN/lychee" ] && "$BIN/lychee" --version 2>/dev/null | grep -q "$LYCHEE_VERSION"; then
echo "lychee $LYCHEE_VERSION: present, skipping"
else
echo "lychee $LYCHEE_VERSION: downloading..."
curl -fsSL -o "$TMP/l.tgz" \
"https://github.com/lycheeverse/lychee/releases/download/lychee-v$LYCHEE_VERSION/lychee-${RARCH}-unknown-linux-musl.tar.gz"
tar xzf "$TMP/l.tgz" -C "$TMP"
install -m 0755 "$TMP/lychee-${RARCH}-unknown-linux-musl/lychee" "$BIN/lychee"
fi
echo "OK: pinned toolchain ready in $BIN (verify with: make tool-versions)"
+4
View File
@@ -89,6 +89,10 @@ def main():
for src, line, url, status in sorted(dead): for src, line, url, status in sorted(dead):
f.write(f"{src}:{line}\t{url}\t[{status}]\n") f.write(f"{src}:{line}\t{url}\t[{status}]\n")
# Save the full lychee JSON so `make external-fix` can act on this run's data
# without re-pinging the web (see scripts/fix_dead_external_links.py).
(outdir / "external.json").write_text(json.dumps(data, indent=2))
print(f"External link check: {total_checked} links checked, {ok} ok, {len(dead)} dead.") print(f"External link check: {total_checked} links checked, {ok} ok, {len(dead)} dead.")
if dead: if dead:
print("\nDead external links:") print("\nDead external links:")
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Comment out EXTERNAL links that are truly dead — i.e. the host/domain no longer
exists (DNS does not resolve / host unreachable / connection failed).
This is the external analogue of fix_dead_links.py (which does the same for internal
page links). It is deliberately conservative:
* ONLY links whose lychee reason matches the "domain-gone" class are auto-commented:
- "Connection failed" (lychee's umbrella for "couldn't even connect" —
the exact reason a dead domain like jabfreejobs.in
produces)
- "Could not resolve host" (explicit DNS failure)
- "HostUnreachable" (host unreachable)
* Everything else is LEFT ALONE and written to a manual-review file:
- HTTP status rejections (404 / 429 / 500 / redirects) -> the domain is alive
and the server responded; a 404 is "page gone", not "domain gone".
- SSL / TLS / certificate / HTTP2 errors -> the site is up.
- Connection refused / reset / closed -> weaker, possibly
transient; not the "domain doesn't exist" class.
- "Error (cached)" -> a stale lychee cache
entry, not a fresh signal.
Commenting (NOT deleting) wraps the markdown link in an HTML comment:
[Old](/gone/) -> <!-- [Old](/gone/) -->
so it disappears from the rendered page and the Pagefind index, and is no longer
detected by lychee on the next run (idempotent).
Usage:
python3 scripts/fix_dead_external_links.py --report linkcheck/external.json # DRY RUN (default)
python3 scripts/fix_dead_external_links.py --report linkcheck/external.json --apply # actually edit
"""
import argparse
import json
import re
import time
from pathlib import Path
from urllib.parse import urlsplit
# Case-insensitive substrings that mark a link as "the domain/host no longer exists".
DOMAIN_GONE_PATTERNS = [
"connection failed",
"could not resolve host",
"hostunreachable",
]
def is_domain_gone(status_text: str) -> bool:
s = status_text.lower()
return any(p in s for p in DOMAIN_GONE_PATTERNS)
def is_onion(url: str) -> bool:
"""True for .onion hosts — only reachable through Tor. This checker runs
off-Tor, so every .onion link 'fails' here by construction; they must
never be auto-commented (manual: verify via Tor)."""
host = urlsplit(url).hostname or ""
return host.endswith(".onion")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--report", required=True,
help="lychee JSON report (linkcheck/external.json)")
ap.add_argument("--apply", action="store_true",
help="actually edit the files (default is a dry-run preview)")
ap.add_argument("--repo", default=".",
help="repo root the error_map file paths are relative to")
args = ap.parse_args()
repo = Path(args.repo).resolve()
data = json.loads(Path(args.report).read_text())
err_map = data.get("error_map", {})
to_comment = [] # (rel_md_file, line, url)
manual = [] # (rel_md_file, line, url, status_text)
for md_file, items in err_map.items():
rel = _rel(md_file, repo)
for it in items:
url = it.get("url", "")
status = it.get("status", {}).get("text", "")
line = it.get("span", {}).get("line")
if not url.startswith(("http://", "https://")):
continue
if is_onion(url):
manual.append((rel, line, url,
"(.onion — only reachable via Tor; not routable from "
"this network. Verify in Tor; do NOT auto-comment.)"))
continue
if is_domain_gone(status):
to_comment.append((rel, line, url))
else:
manual.append((rel, line, url, status))
# Group edits by file, apply bottom-up so line numbers stay valid.
by_file = {}
for rel, line, url in to_comment:
by_file.setdefault(rel, []).append((line, url))
for rel in by_file:
by_file[rel].sort(reverse=True)
plan = 0
for rel in sorted(by_file):
p = repo / rel
if not p.exists():
print(f" ! missing source, skipping: {rel}")
continue
lines = p.read_text().splitlines(keepends=True)
changed = False
for line_no, url in by_file[rel]:
if not line_no:
continue
idx = line_no - 1
if idx < 0 or idx >= len(lines):
continue
cur = lines[idx]
# Find the markdown link on this line whose href matches the dead URL
# (allow a trailing-slash mismatch).
link = _find_link(cur, url)
if not link:
print(f" ? could not locate link on {rel}:{line_no} for {url}")
continue
link_txt = link.group(0)
# Skip if it is already inside an HTML comment (idempotency guard).
if _already_commented(cur, link.start()):
continue
lines[idx] = cur[:link.start()] + "!-- " + link_txt + " --" + cur[link.end():]
changed = True
plan += 1
print(f" {'APPLY' if args.apply else 'DRY '} {rel}:{line_no} {link_txt}")
if changed and args.apply:
p.write_text("".join(lines))
# Manual-review report for everything we deliberately did NOT touch.
outdir = repo / "linkcheck"
outdir.mkdir(parents=True, exist_ok=True)
mr = outdir / "manual_review_external.txt"
with mr.open("w") as f:
f.write("External links NOT auto-commented (domain still alive / ambiguous).\n")
f.write("Auto-comment only covers the domain-gone class (DNS failed / host\n")
f.write("unreachable). Review these by hand.\n\n")
for rel, line, url, status in sorted(manual):
f.write(f"{rel}:{line}\t{url}\t[{status}]\n")
print(f"\nAuto-comment {'applied' if args.apply else 'planned'}: {plan}")
print(f"Manual review entries: {len(manual)} -> {mr}")
if not args.apply:
print("\nDRY RUN — nothing was changed. Re-run with --apply to comment these out.")
def _rel(path: str, repo: Path) -> str:
try:
return str(Path(path).resolve().relative_to(repo))
except ValueError:
return path
def _find_link(line: str, url: str):
"""Return a match for the markdown link [text](url) whose href equals url
(ignoring a trailing-slash difference)."""
# Escape the url for regex, then allow an optional trailing slash on either side.
esc = re.escape(url)
patterns = [
r"\[[^\]]*\]\(" + esc + r"\s*\)",
r"\[[^\]]*\]\(" + esc.rstrip("/") + r"/?\s*\)",
]
for pat in patterns:
m = re.search(pat, line)
if m:
return m
return None
def _already_commented(line: str, link_start: int) -> bool:
"""True if an opening <!-- appears before the link with no matching --> between
them (i.e. the link sits inside an existing HTML comment)."""
pre = line[:link_start]
opens = pre.count("<!--")
closes = pre.count("-->")
return opens > closes
if __name__ == "__main__":
main()
+79
View File
@@ -0,0 +1,79 @@
#!/bin/sh
# Print a table of tool versions: pinned (.tools/bin -- what make targets use,
# because the Makefile prepends it to recipe PATH) vs PATH-first (what your own
# shell resolves outside make).
#
# scripts/tool_versions.sh informational; always exits 0
# scripts/tool_versions.sh --strict exit 1 if a pinned tool is missing or
# has the wrong version (used by `make
# preflight`, which runs before
# all/serve/serve-lan)
set -u
ROOT=$(cd "$(dirname "$0")/.." && pwd)
BIN="$ROOT/.tools/bin"
MODE=${1:-}
HUGO_PIN="v0.167.0+extended"
NODE_PIN="v22.11.0"
LYCHEE_PIN="0.24.2"
# PATH with .tools/bin stripped, so "PATH-first" shows what the user's shell
# would resolve, even though make exported .tools/bin into our PATH.
SYSPATH=$(printf '%s' "$PATH" | tr ':' '\n' | grep -vx "$BIN" | tr '\n' ':' | sed 's/:$//')
pinned_ver() {
case "$1" in
# hugo release builds report "v<ver>-<40-hex-commit>+extended"; strip the commit
hugo) [ -x "$BIN/hugo" ] && "$BIN/hugo" version 2>/dev/null | awk '{print $2}' | sed 's/-[0-9a-f]\{40,\}//' ;;
node) [ -x "$BIN/node" ] && "$BIN/node" --version 2>/dev/null ;;
npx) [ -x "$BIN/npx" ] && "$BIN/npx" --version 2>/dev/null ;;
lychee) [ -x "$BIN/lychee" ] && "$BIN/lychee" --version 2>/dev/null | awk '{print $2}' ;;
esac
}
first_path() { PATH="$SYSPATH" command -v "$1" 2>/dev/null; }
first_ver() { # first_ver <tool> <path>
[ -n "${2:-}" ] || return 0
case "$1" in
hugo) "$2" version 2>/dev/null | awk '{print $2}' | sed 's/-[0-9a-f]\{40,\}//' ;;
node) "$2" --version 2>/dev/null ;;
npx) "$2" --version 2>/dev/null ;;
lychee) "$2" --version 2>/dev/null | awk '{print $2}' ;;
python3) "$2" --version 2>/dev/null | awk '{print $2}' ;;
esac
}
if [ "$MODE" = "--strict" ]; then
fail=0
[ "$(pinned_ver hugo)" = "$HUGO_PIN" ] || { echo "preflight: pinned hugo $HUGO_PIN missing/wrong in $BIN" >&2; fail=1; }
[ "$(pinned_ver node)" = "$NODE_PIN" ] || { echo "preflight: pinned node $NODE_PIN missing/wrong in $BIN" >&2; fail=1; }
[ -n "$(pinned_ver npx)" ] || { echo "preflight: pinned npx missing in $BIN" >&2; fail=1; }
[ "$(pinned_ver lychee)" = "$LYCHEE_PIN" ] || { echo "preflight: pinned lychee $LYCHEE_PIN missing/wrong in $BIN" >&2; fail=1; }
command -v python3 >/dev/null 2>&1 || { echo "preflight: python3 not found on PATH (needed by serve-lan)" >&2; fail=1; }
if [ "$fail" = 1 ]; then
echo "preflight: fix with: make bootstrap" >&2
exit 1
fi
echo "preflight: OK (pinned toolchain present in $BIN)"
exit 0
fi
row() { # row <tool> <pinned disp> <first disp> <first path>
printf '%-8s %-27s %-27s %s\n' "$1" "$2" "$3" "${4:--}"
}
echo "tool pinned (.tools/bin) PATH-first (your shell)"
for t in hugo node npx lychee; do
pv=$(pinned_ver "$t")
fp=$(first_path "$t")
fv=$(first_ver "$t" "$fp")
[ -n "$pv" ] || pv="(missing)"
[ -n "$fv" ] || fv="(not installed)"
row "$t" "$pv" "$fv" "$fp"
done
fp=$(first_path python3); fv=$(first_ver python3 "$fp")
[ -n "$fv" ] || fv="(not installed)"
row "python3" "(not pinned)" "$fv" "$fp"
echo
echo "make targets always use the pinned column. 'make bootstrap' installs it."