commit bd0565c54944b3ff6c5c42220176b9cf8cecb67a
parent 6fcadfffea368687ee00a9ed64ed3eb6e01d8ad5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 09:07:52 -0400
common: `archilyzer doctor` reports stagit beside git-filter-repo — its path, or not found with the install line; a STAGIT_BIN that names nothing is a warning, never a failure
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 41 insertions(+), 9 deletions(-)
diff --git a/common/bin/doctor.test.ts b/common/bin/doctor.test.ts
@@ -244,12 +244,19 @@ test("the source publish block: the tools, the operator files by count and mode
sourceTools: async () => tools,
});
// A clone that never publishes: notes, not warnings.
- let r = await collectDoctorReport(deps({ filterRepo: null, gitleaks: null }));
+ let r = await collectDoctorReport(deps({ filterRepo: null, gitleaks: null, stagit: null }));
assert.equal(r.ok, true, renderDoctorReport(r));
- for (const id of ["filter-repo", "gitleaks", "scrub rules", "denylist", "published"]) {
+ for (const id of ["filter-repo", "gitleaks", "stagit", "scrub rules", "denylist", "published"]) {
assert.equal(status(r, id), "info", id);
}
assert.match(r.checks.find((x) => x.id === "filter-repo")!.detail, /install: `pipx install git-filter-repo`/);
+ // stagit, beside git-filter-repo: where it is, or not found and how to install it.
+ assert.equal(
+ r.checks.find((x) => x.id === "stagit")!.detail,
+ "not found (PATH, ~/.local/bin) — the source is published without its history pages (/source/git/); install it once: git clone git://git.codemadness.org/stagit && make -C stagit && cp stagit/stagit ~/.local/bin/",
+ );
+ const order = r.checks.filter((x) => x.section === "source publish").map((x) => x.id);
+ assert.equal(order.indexOf("stagit"), order.indexOf("filter-repo") + 1, "beside git-filter-repo");
// The operator's files exist (one readable by others), pipx only, a publish.
mkdirSync(path.dirname(c.paths.sourceScrubFile), { recursive: true });
@@ -267,11 +274,13 @@ test("the source publish block: the tools, the operator files by count and mode
audit: { objects: 1, commits: 1, gitleaks: "clean" }, tools: {},
}));
const before = tree(c.root);
- r = await collectDoctorReport(deps({ filterRepo: { via: "pipx", version: "1.15.0" }, gitleaks: { version: "8.28.0" } }));
+ r = await collectDoctorReport(deps({ filterRepo: { via: "pipx", version: "1.15.0" }, gitleaks: { version: "8.28.0" }, stagit: "/opt/stagit/stagit" }));
assert.equal(r.ok, true, renderDoctorReport(r));
assert.equal(status(r, "filter-repo"), "warn");
assert.match(r.checks.find((x) => x.id === "filter-repo")!.detail, /pipx run --spec git-filter-repo==2\.47\.0/);
assert.equal(status(r, "gitleaks"), "ok");
+ assert.equal(status(r, "stagit"), "ok");
+ assert.equal(r.checks.find((x) => x.id === "stagit")!.detail, "/opt/stagit/stagit");
assert.equal(status(r, "scrub rules"), "ok");
assert.match(r.checks.find((x) => x.id === "scrub rules")!.detail, /\(2 rules, mode 600\)$/);
assert.equal(status(r, "denylist"), "warn");
@@ -282,8 +291,14 @@ test("the source publish block: the tools, the operator files by count and mode
assert.ok(!/PLANTED/.test(text), "the doctor never prints an operator file's contents");
assert.deepEqual(tree(c.root), before);
- r = await collectDoctorReport(deps({ filterRepo: { via: "git", version: "a40bce548d2c" }, gitleaks: null }));
+ r = await collectDoctorReport(deps({ filterRepo: { via: "git", version: "a40bce548d2c" }, gitleaks: null, stagit: null }));
assert.equal(status(r, "filter-repo"), "ok");
+
+ // A STAGIT_BIN that names nothing: a warning, never a failure.
+ r = await collectDoctorReport({ ...deps({ filterRepo: null, gitleaks: null, stagit: null }), env: { PATH: c.bin, STAGIT_BIN: "/nowhere/stagit" } });
+ assert.equal(status(r, "stagit"), "warn");
+ assert.match(r.checks.find((x) => x.id === "stagit")!.detail, /^not found: STAGIT_BIN=\/nowhere\/stagit names no executable/);
+ assert.equal(r.ok, true);
});
// A stored icon the checker refuses is named by file and label with its
@@ -309,7 +324,7 @@ test("social icons: a refused stored icon is named by file and label, not its ma
portInUse: async () => false,
portBlock: async () => null,
umtoolTools: async () => null,
- sourceTools: async () => ({ filterRepo: null, gitleaks: null }),
+ sourceTools: async () => ({ filterRepo: null, gitleaks: null, stagit: null }),
});
const line = report.checks.find((x) => x.section === "social icons")!;
assert.equal(line.status, "warn");
diff --git a/common/bin/doctor.ts b/common/bin/doctor.ts
@@ -74,10 +74,12 @@ export type DoctorDeps = {
sourceTools?: () => Promise<SourceTools>;
};
-// Which git-filter-repo `archilyzer source publish` would run, and gitleaks.
+// Which git-filter-repo `archilyzer source publish` would run, gitleaks, and
+// the stagit that renders the history pages (its path, or null).
export type SourceTools = {
filterRepo: { via: "git"; version: string } | { via: "pipx"; version: string } | null;
gitleaks: { version: string } | null;
+ stagit: string | null;
};
const MIN_NODE = [20, 9, 0] as const; // next 16's engines field
@@ -291,7 +293,7 @@ export async function collectDoctorReport(deps: DoctorDeps): Promise<DoctorRepor
// are never printed.
const SP = "source publish";
const src = await import("../publish/source");
- const tools = await (deps.sourceTools ?? (() => probeSourceTools(env)))();
+ const tools = await (deps.sourceTools ?? (() => probeSourceTools(env, paths)))();
const scrubFile = paths.sourceScrubFile;
const denylistFile = paths.sourceDenylistFile;
const intends = [scrubFile, denylistFile].some((f) => f && existsSync(f));
@@ -304,6 +306,18 @@ export async function collectDoctorReport(deps: DoctorDeps): Promise<DoctorRepor
add(SP, "filter-repo", intends ? "warn" : "info",
`neither git-filter-repo nor pipx — \`archilyzer build homepage\` refuses; install: \`${src.FILTER_REPO_INSTALL}\``);
}
+ // stagit renders the history pages (/source/git/); without it the publish
+ // goes on without them. A STAGIT_BIN that names nothing is a warning.
+ const hist = await import("../publish/sourceHistory");
+ if (tools.stagit) {
+ add(SP, "stagit", "ok", tools.stagit);
+ } else if (env.STAGIT_BIN) {
+ add(SP, "stagit", "warn",
+ `not found: STAGIT_BIN=${env.STAGIT_BIN} names no executable — the source is published without its history pages (/source/git/)`);
+ } else {
+ add(SP, "stagit", "info",
+ `not found (PATH, ~/.local/bin) — the source is published without its history pages (/source/git/); install it once: ${hist.STAGIT_INSTALL}`);
+ }
add(SP, "gitleaks", tools.gitleaks ? "ok" : "info",
tools.gitleaks ? `gitleaks ${tools.gitleaks.version}` : "absent — the gate skips the secret scan with a WARNING (the literal audit still runs)");
for (const [id, file, unit] of [
@@ -518,8 +532,9 @@ async function worktreePortBlock(
// What `source publish` would run, by version flags only: `git filter-repo
// --version` answering 0 is an installed filter-repo; otherwise pipx's own
-// version (never `pipx run`, which would download). gitleaks likewise.
-async function probeSourceTools(env: NodeJS.ProcessEnv): Promise<SourceTools> {
+// version (never `pipx run`, which would download). gitleaks likewise. stagit
+// has no version flag: it is looked up the way the publish looks it up.
+async function probeSourceTools(env: NodeJS.ProcessEnv, paths: Paths): Promise<SourceTools> {
const version = async (bin: string, args: string[]): Promise<string | null> => {
try {
const { stdout, stderr } = await execFileP(bin, args, { env, timeout: 10_000 });
@@ -531,9 +546,11 @@ async function probeSourceTools(env: NodeJS.ProcessEnv): Promise<SourceTools> {
const git = await version("git", ["filter-repo", "--version"]);
const pipx = git === null ? await version("pipx", ["--version"]) : null;
const leaks = await version("gitleaks", ["version"]);
+ const { resolveStagit } = await import("../publish/sourceHistory");
return {
filterRepo: git !== null ? { via: "git", version: git } : pipx !== null ? { via: "pipx", version: pipx } : null,
gitleaks: leaks !== null ? { version: leaks } : null,
+ stagit: resolveStagit(paths.stagitBin ?? "stagit", env),
};
}