Compare commits
5
Commits
cfaab13e45
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ea7652f36 | ||
|
|
2d7c724a48 | ||
|
|
8e8958274a | ||
|
|
f90e694c3e | ||
|
|
be9c92f6d2 |
@@ -3,3 +3,4 @@ linkcheck/
|
||||
resources/_gen/
|
||||
__pycache__/
|
||||
.hermes/
|
||||
.tools/
|
||||
|
||||
@@ -3,12 +3,37 @@ PAGEFIND := npx -y pagefind
|
||||
LYCHEE := lychee
|
||||
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
|
||||
|
||||
# Build Hugo content -> public/
|
||||
build:
|
||||
# ONE-TIME per machine: install the pinned hugo/node/lychee into .tools/bin.
|
||||
# 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
|
||||
|
||||
# (Re)build the Pagefind search index from public/ -> public/pagefind/
|
||||
@@ -39,14 +64,32 @@ link-fix:
|
||||
$(PAGEFIND) --site $(OUT)
|
||||
|
||||
# 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:
|
||||
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:
|
||||
# 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
|
||||
|
||||
# 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:
|
||||
rm -rf $(OUT) linkcheck
|
||||
|
||||
@@ -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
|
||||
|
||||
- [Fresh git pull? Rebuild before serving](#fresh-git-pull-rebuild-before-serving)
|
||||
- [Architecture](#architecture)
|
||||
- [1. Clone](#1-clone)
|
||||
- [2. Prerequisites (install once)](#2-prerequisites-install-once)
|
||||
@@ -196,6 +223,13 @@ cd libertytoolsio
|
||||
|
||||
## 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
|
||||
`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)
|
||||
|
||||
Run from the project root. `public/`, `linkcheck/`, and `resources/_gen/` are
|
||||
generated and gitignored.
|
||||
Run from the project root. `public/`, `linkcheck/`, `resources/_gen/`, and
|
||||
`.tools/` are generated and gitignored.
|
||||
|
||||
| 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 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. |
|
||||
@@ -377,6 +414,12 @@ hermes cronjob list # confirm it exists and is enabled
|
||||
|
||||
## 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
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
- `resources/_gen/` — Hugo asset-generation cache
|
||||
- `.tools/` — pinned local toolchain installed by `make bootstrap`
|
||||
- `.hermes/` — agent working files
|
||||
|
||||
## Repository layout
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
baseURL = 'http://example.org/'
|
||||
baseURL = 'https://libertytools.io/'
|
||||
languageCode = 'en-us'
|
||||
title = ''
|
||||
title = 'Liberty Tools'
|
||||
theme = "hugo-book"
|
||||
|
||||
[params]
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ Documentaries, talks, and other media.
|
||||
- [**The Unshackled Englishman**](https://www.youtube.com/watch?v=rB6oK_FS8JA)
|
||||
|
||||
## 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.
|
||||
|
||||
## Training
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
<!--- [https://www.jabfreejobs.in](https://www.jabfreejobs.in)fo/
|
||||
http://www.jobsnotjabs.com/ http://novaxjobsusa.com www.provisionjobs.com --->
|
||||
- [**JabFreeJobs**](https://www.jabfreejobs.in/) - No jab job board.
|
||||
|
||||
<!--- http://www.jobsnotjabs.com/ http://novaxjobsusa.com www.provisionjobs.com --->
|
||||
+1
-1
@@ -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.
|
||||
|
||||
- [**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.
|
||||
|
||||
|
||||
Executable
+69
@@ -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)"
|
||||
@@ -89,6 +89,10 @@ def main():
|
||||
for src, line, url, status in sorted(dead):
|
||||
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.")
|
||||
if dead:
|
||||
print("\nDead external links:")
|
||||
|
||||
@@ -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()
|
||||
Executable
+79
@@ -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."
|
||||
Reference in New Issue
Block a user