Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions make-pdf/src/browseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
* Binary resolution order (Codex round 2 #4):
* 1. $BROWSE_BIN env override
* 2. sibling dir: dirname(argv[0])/../browse/dist/browse
* 3. ~/.claude/skills/gstack/browse/dist/browse
* 4. PATH lookup: `browse`
* 5. error with setup hint
* 3. ~/.agents/skills/gstack/browse/dist/browse
* 4. ~/.codex/skills/gstack/browse/dist/browse
* 5. ~/.claude/skills/gstack/browse/dist/browse
* 6. PATH lookup: `browse`
* 7. error with setup hint
*/

import { execFileSync } from "node:child_process";
Expand Down Expand Up @@ -74,10 +76,17 @@ export function resolveBrowseBin(): string {
if (isExecutable(candidate)) return candidate;
}

// Global install
// Global installs. Prefer the active Codex/agents install before older
// Claude installs that may still exist on disk.
const home = os.homedir();
const globalPath = path.join(home, ".claude/skills/gstack/browse/dist/browse");
if (isExecutable(globalPath)) return globalPath;
const globalCandidates = [
path.join(home, ".agents/skills/gstack/browse/dist/browse"),
path.join(home, ".codex/skills/gstack/browse/dist/browse"),
path.join(home, ".claude/skills/gstack/browse/dist/browse"),
];
for (const candidate of globalCandidates) {
if (isExecutable(candidate)) return candidate;
}

// PATH lookup
try {
Expand All @@ -97,7 +106,7 @@ export function resolveBrowseBin(): string {
"Tried:",
` - $BROWSE_BIN (${envOverride || "unset"})`,
` - sibling: ${siblingCandidates.join(", ")}`,
` - global: ${globalPath}`,
` - global: ${globalCandidates.join(", ")}`,
" - PATH: `browse`",
"",
"To fix: run gstack setup from the gstack repo:",
Expand All @@ -111,6 +120,8 @@ export function resolveBrowseBin(): string {

function isExecutable(p: string): boolean {
try {
const stat = fs.statSync(p);
if (!stat.isFile()) return false;
fs.accessSync(p, fs.constants.X_OK);
return true;
} catch {
Expand Down
9 changes: 8 additions & 1 deletion make-pdf/src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export async function generate(opts: GenerateOptions): Promise<string> {
}

const outputPath = path.resolve(
opts.output ?? path.join(os.tmpdir(), `${deriveSlug(input)}.pdf`),
opts.output ?? path.join(defaultOutputDir(), `${deriveSlug(input)}.pdf`),
);

// Stage 1: read markdown
Expand Down Expand Up @@ -216,6 +216,13 @@ function tmpFile(ext: string): string {
return path.join(os.tmpdir(), `make-pdf-${process.pid}-${hash}.${ext}`);
}

function defaultOutputDir(): string {
if (process.platform === "darwin" && fs.existsSync("/private/tmp")) {
return "/private/tmp";
}
return os.tmpdir();
}

function tryOpen(pathOrUrl: string): void {
const platform = process.platform;
const cmd = platform === "darwin" ? "open" :
Expand Down
15 changes: 11 additions & 4 deletions make-pdf/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Flow (per the CEO plan CLI UX spec):
* 1. Verify browse binary exists and responds
* 2. Verify Chromium launches via $B goto about:blank
* 2. Verify Chromium launches via a dedicated blank tab
* 3. Verify pdftotext is installed (warn, don't fail)
* 4. Generate a smoke-test PDF from an inline 2-paragraph fixture
* 5. Open it
Expand Down Expand Up @@ -32,11 +32,11 @@ export async function runSetup(): Promise<void> {
process.exit(4);
}

// 2. Chromium smoke (navigate a dedicated tab to about:blank)
// 2. Chromium smoke (open a dedicated blank tab)
process.stderr.write(" [2/5] Launching Chromium...");
let chromiumTab: number | null = null;
try {
chromiumTab = browseClient.newtab("about:blank");
chromiumTab = browseClient.newtab();
process.stderr.write(` OK (tab ${chromiumTab})\n`);
} catch (err: any) {
process.stderr.write(" FAIL\n");
Expand Down Expand Up @@ -78,7 +78,7 @@ export async function runSetup(): Promise<void> {
"",
].join("\n");
const fixturePath = path.join(os.tmpdir(), `make-pdf-smoke-${process.pid}.md`);
const outPath = path.join(os.tmpdir(), `make-pdf-smoke-${process.pid}.pdf`);
const outPath = path.join(defaultOutputDir(), `make-pdf-smoke-${process.pid}.pdf`);
fs.writeFileSync(fixturePath, fixture, "utf8");

try {
Expand Down Expand Up @@ -108,3 +108,10 @@ export async function runSetup(): Promise<void> {
"",
].join("\n"));
}

function defaultOutputDir(): string {
if (process.platform === "darwin" && fs.existsSync("/private/tmp")) {
return "/private/tmp";
}
return os.tmpdir();
}
33 changes: 33 additions & 0 deletions make-pdf/test/browseClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
*/

import { describe, expect, test } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";

import { BrowseClientError } from "../src/types";
import { resolveBrowseBin } from "../src/browseClient";
Expand Down Expand Up @@ -57,6 +60,36 @@ describe("resolveBrowseBin", () => {
}
}
});

test("does not treat executable directories as binaries", () => {
const originalEnv = process.env.BROWSE_BIN;
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "make-pdf-browse-dir-"));
fs.chmodSync(dir, 0o755);
process.env.BROWSE_BIN = dir;

try {
let resolved: string | null = null;
let thrown: any = null;
try {
resolved = resolveBrowseBin();
} catch (err) {
thrown = err;
}

expect(resolved).not.toBe(dir);
if (thrown) {
expect(thrown).toBeInstanceOf(BrowseClientError);
expect(thrown.message).toContain("browse binary not found");
}
} finally {
if (originalEnv === undefined) {
delete process.env.BROWSE_BIN;
} else {
process.env.BROWSE_BIN = originalEnv;
}
fs.rmSync(dir, { recursive: true, force: true });
}
});
});

describe("BrowseClientError", () => {
Expand Down