fix: resolve npx gateway daemon install

This commit is contained in:
Peter Steinberger
2026-01-05 02:48:25 +01:00
parent b779029517
commit 8791e46cf3
3 changed files with 136 additions and 9 deletions

View File

@@ -4,6 +4,9 @@
## Unreleased
### Fixes
- Onboarding: resolve CLI entrypoint when running via `npx` so gateway daemon install works without a build step.
## 2026.1.5
### Highlights

View File

@@ -0,0 +1,71 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const access = vi.fn();
const realpath = vi.fn();
vi.mock("node:fs/promises", () => ({
default: { access, realpath },
access,
realpath,
}));
import { resolveGatewayProgramArguments } from "./program-args.js";
const originalArgv = [...process.argv];
afterEach(() => {
process.argv = [...originalArgv];
vi.resetAllMocks();
});
describe("resolveGatewayProgramArguments", () => {
it("uses realpath-resolved dist entry when running via npx shim", async () => {
process.argv = [
"node",
"/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot",
];
realpath.mockResolvedValue(
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js",
);
access.mockImplementation(async (target: string) => {
if (target === "/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js") {
return;
}
throw new Error("missing");
});
const result = await resolveGatewayProgramArguments({ port: 18789 });
expect(result.programArguments).toEqual([
process.execPath,
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/entry.js",
"gateway-daemon",
"--port",
"18789",
]);
});
it("falls back to node_modules package dist when .bin path is not resolved", async () => {
process.argv = [
"node",
"/tmp/.npm/_npx/63c3/node_modules/.bin/clawdbot",
];
realpath.mockRejectedValue(new Error("no realpath"));
access.mockImplementation(async (target: string) => {
if (target === "/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/index.js") {
return;
}
throw new Error("missing");
});
const result = await resolveGatewayProgramArguments({ port: 18789 });
expect(result.programArguments).toEqual([
process.execPath,
"/tmp/.npm/_npx/63c3/node_modules/clawdbot/dist/index.js",
"gateway-daemon",
"--port",
"18789",
]);
});
});

View File

@@ -16,18 +16,14 @@ async function resolveCliEntrypointPathForService(): Promise<string> {
if (!argv1) throw new Error("Unable to resolve CLI entrypoint path");
const normalized = path.resolve(argv1);
const looksLikeDist = /[/\\]dist[/\\].+\.(cjs|js|mjs)$/.test(normalized);
const resolvedPath = await resolveRealpathSafe(normalized);
const looksLikeDist = /[/\\]dist[/\\].+\.(cjs|js|mjs)$/.test(resolvedPath);
if (looksLikeDist) {
await fs.access(normalized);
return normalized;
await fs.access(resolvedPath);
return resolvedPath;
}
const distCandidates = [
path.resolve(path.dirname(normalized), "..", "dist", "index.js"),
path.resolve(path.dirname(normalized), "..", "dist", "index.mjs"),
path.resolve(path.dirname(normalized), "dist", "index.js"),
path.resolve(path.dirname(normalized), "dist", "index.mjs"),
];
const distCandidates = buildDistCandidates(resolvedPath, normalized);
for (const candidate of distCandidates) {
try {
@@ -43,6 +39,63 @@ async function resolveCliEntrypointPathForService(): Promise<string> {
);
}
async function resolveRealpathSafe(inputPath: string): Promise<string> {
try {
return await fs.realpath(inputPath);
} catch {
return inputPath;
}
}
function buildDistCandidates(...inputs: string[]): string[] {
const candidates: string[] = [];
const seen = new Set<string>();
for (const inputPath of inputs) {
if (!inputPath) continue;
const baseDir = path.dirname(inputPath);
appendDistCandidates(candidates, seen, path.resolve(baseDir, ".."));
appendDistCandidates(candidates, seen, baseDir);
appendNodeModulesBinCandidates(candidates, seen, inputPath);
}
return candidates;
}
function appendDistCandidates(
candidates: string[],
seen: Set<string>,
baseDir: string,
): void {
const distDir = path.resolve(baseDir, "dist");
const distEntries = [
path.join(distDir, "index.js"),
path.join(distDir, "index.mjs"),
path.join(distDir, "entry.js"),
path.join(distDir, "entry.mjs"),
];
for (const entry of distEntries) {
if (seen.has(entry)) continue;
seen.add(entry);
candidates.push(entry);
}
}
function appendNodeModulesBinCandidates(
candidates: string[],
seen: Set<string>,
inputPath: string,
): void {
const parts = inputPath.split(path.sep);
const binIndex = parts.lastIndexOf(".bin");
if (binIndex <= 0) return;
if (parts[binIndex - 1] !== "node_modules") return;
const binName = path.basename(inputPath);
const nodeModulesDir = parts.slice(0, binIndex).join(path.sep);
const packageRoot = path.join(nodeModulesDir, binName);
appendDistCandidates(candidates, seen, packageRoot);
}
function resolveRepoRootForDev(): string {
const argv1 = process.argv[1];
if (!argv1) throw new Error("Unable to resolve repo root");