mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-10 23:19:09 +02:00
fix: pre-landing review fixes for the Aside-first branch
Review army + adversarial passes (Claude and Codex) on the merged branch:
setup
- _prune_stale_generated scans the host dirs too (the generator already
removed the render before setup ran, so the host branch was dead), skips
symlinks in the render tree (rm -rf on a slash-terminated link empties its
target), removes a host symlink only when it resolves into gstack, cleans a
bannered real dir through _cleanup_weak_dir, recognizes frontmatter-renamed
skills, and logs through log. The always-run codex render passes every host
dir that may link to it.
- NEEDS_BUILD checks all three binaries (with $_EXE) and lib/ sources; the
browser hint and the bootstrap summary honor GSTACK_SKIP_ASIDE, treat a
requested skip as a request, and derive one skill list.
lib/aside-render.ts + bin/gstack-render.ts
- The loopback server carries a per-render secret path, checks containment on
the real path (symlink escapes are 403), and rejects malformed encoding.
- Inline eval results are one base64 line, so page text cannot forge
ASIDE_DIR= or the sentinel; the last ASIDE_DIR wins.
- runProc escalates SIGTERM to SIGKILL, bounds every wait, and clears every
timer (an uncleared one kept gstack-render alive after printing OK).
- renderTmpDir refuses a shared /tmp name owned by someone else; the work dir
and server are created inside try; goto's budget follows the render budget.
- probeAside classifies a present-but-failing CLI as ASIDE_NOT_RUNNING like
the skills' bash probe; render() retries on gstack's own browser when Aside
could not start or its private CDP bridge is gone (never on a page error
or a timeout of a running script); the CLI reports the engine that actually
rendered, exits 0 on --help, rejects non-numeric flags, documents
--wait-timeout, fences EVAL/PAGE_ERRORS as untrusted content, and names the
daemon's cookie-import JS lock remedy.
- The browse path passes --scale only when asked (a scale change rebuilds
the daemon context) and restores the viewport after a sized screenshot.
resolvers / templates
- The bash probe honors GSTACK_SKIP_ASIDE and has a perl deadline on stock
macOS; .local is no longer LOCAL (mDNS); same-origin filters compare parsed
origins; link status is HEAD-checked only on LOCAL targets; every
aside exec goes through the receipted _aside_exec prelude
({{ASIDE_EXEC_PRELUDE}}), including nine template blocks that called it
bare; the design sketch and diagram staging use private directories.
- The generator prunes only bannered renders and never a host whose
generation failed.
Docs, stale comments and dead code cleaned; goldens re-rendered; tests
updated and added for every behavior above.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ea61bd65be
commit
444f8feff8
@@ -12,11 +12,13 @@ import * as path from "node:path";
|
||||
|
||||
import {
|
||||
NO_BROWSER,
|
||||
PAGE_NUMBER_FOOTER,
|
||||
lengthToInches,
|
||||
paperInches,
|
||||
render,
|
||||
renderTmpDir,
|
||||
type PdfStepOptions,
|
||||
type RenderEngine,
|
||||
} from "../../lib/aside-render";
|
||||
import { BrowserUnavailableError } from "./types";
|
||||
|
||||
@@ -40,12 +42,6 @@ export interface PdfOptions {
|
||||
toc?: boolean;
|
||||
}
|
||||
|
||||
const PAGE_NUMBER_FOOTER =
|
||||
'<div style="font-size:9pt; font-family:Helvetica,Arial,sans-serif; color:#666; ' +
|
||||
'width:100%; text-align:center;">' +
|
||||
'<span class="pageNumber"></span> of <span class="totalPages"></span>' +
|
||||
"</div>";
|
||||
|
||||
/**
|
||||
* make-pdf's option shape → CDP Page.printToPDF options (inches). Same
|
||||
* mapping the browse `pdf` command applies: Letter when no size is
|
||||
@@ -94,7 +90,7 @@ export async function renderPdf(
|
||||
html: string,
|
||||
opts: PdfOptions,
|
||||
renderFn: typeof render = render,
|
||||
): Promise<void> {
|
||||
): Promise<RenderEngine | undefined> {
|
||||
const dir = fs.mkdtempSync(path.join(renderTmpDir(), "make-pdf-"));
|
||||
try {
|
||||
const file = path.join(dir, "document.html");
|
||||
@@ -104,6 +100,7 @@ export async function renderPdf(
|
||||
steps: [{ kind: "pdf", out: path.resolve(opts.output), options: pdfStepOptions(opts) }],
|
||||
});
|
||||
if (!result.ok) throw renderFailure(result.error ?? "unknown error");
|
||||
return result.engine;
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ export async function generate(opts: GenerateOptions): Promise<string> {
|
||||
const engine = pickEngine().engine;
|
||||
const via = engine === "aside" ? "Aside" : engine === "browse" ? "gstack's browser" : "a browser";
|
||||
progress.begin(`Rendering PDF through ${via}`);
|
||||
await renderPdf(finalHtml, {
|
||||
const used = await renderPdf(finalHtml, {
|
||||
output: outputPath,
|
||||
format: opts.pageSize ?? "letter",
|
||||
marginTop: opts.marginTop ?? opts.margins ?? "1in",
|
||||
@@ -244,6 +244,12 @@ export async function generate(opts: GenerateOptions): Promise<string> {
|
||||
toc: opts.toc,
|
||||
});
|
||||
progress.end(`Rendering PDF through ${via}`);
|
||||
if (used && used !== engine) {
|
||||
// render() fell back mid-run (Aside quit or its CLI could not start): say
|
||||
// which browser actually produced the file, since the label above was
|
||||
// decided before the render.
|
||||
process.stderr.write(" Aside was unavailable mid-run; the PDF was rendered through gstack's own browser.\n");
|
||||
}
|
||||
|
||||
const kb = Math.round(fs.statSync(outputPath).size / 1024);
|
||||
progress.done(`${rendered.meta.wordCount} words · ${kb}KB · ${outputPath}`);
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
import { pickEngine, render, renderTmpDir } from "../../lib/aside-render";
|
||||
import { NO_BROWSER_HELP, pickEngine, render, renderTmpDir } from "../../lib/aside-render";
|
||||
import { ExitCode } from "./types";
|
||||
import { resolvePdftotext, PdftotextUnavailableError } from "./pdftotext";
|
||||
import { OUTPUT_TMP_DIR, generate } from "./orchestrator";
|
||||
|
||||
@@ -25,7 +26,7 @@ export async function runSetup(): Promise<void> {
|
||||
if (!engine.engine) {
|
||||
process.stderr.write(" FAIL\n");
|
||||
process.stderr.write(`\n${engine.error}\n`);
|
||||
process.exit(4);
|
||||
process.exit(ExitCode.BrowserUnavailable);
|
||||
}
|
||||
const via = engine.engine === "aside" ? `Aside ${engine.version}` : "gstack browser";
|
||||
process.stderr.write(engine.engine === "aside"
|
||||
@@ -46,7 +47,8 @@ export async function runSetup(): Promise<void> {
|
||||
} catch (err: any) {
|
||||
process.stderr.write(" FAIL\n");
|
||||
process.stderr.write(`\n${via} could not render a page: ${err.message}\n`);
|
||||
process.exit(4);
|
||||
process.stderr.write(`To fix: ${NO_BROWSER_HELP}\n`);
|
||||
process.exit(ExitCode.BrowserUnavailable);
|
||||
} finally {
|
||||
fs.rmSync(smokeDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user