feat(api): accept a lane tool on create, start and binary probe
This commit is contained in:
@@ -988,3 +988,23 @@ exit 1
|
||||
assert.equal(r.body.checks[0].name, "tests");
|
||||
});
|
||||
});
|
||||
|
||||
describe("/api/lanes tool tests", () => {
|
||||
it("POST /api/lanes/ensure stores the requested tool", async () => {
|
||||
const res = await request("POST", "/api/lanes/ensure", {
|
||||
cwd: "/tmp/codex-lane",
|
||||
tool: "codex",
|
||||
});
|
||||
assert.equal(res.status, 201);
|
||||
assert.equal(res.body.lane.tool, "codex");
|
||||
});
|
||||
|
||||
it("POST /api/lanes/ensure rejects an unknown tool", async () => {
|
||||
const res = await request("POST", "/api/lanes/ensure", {
|
||||
cwd: "/tmp/bad-tool",
|
||||
tool: "cursor",
|
||||
});
|
||||
assert.equal(res.status, 400);
|
||||
assert.equal(res.body.error.code, "EBADTOOL");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,4 +174,15 @@ describe("/api/run", () => {
|
||||
assert.equal(status, 200);
|
||||
assert.ok(Array.isArray(body.items));
|
||||
});
|
||||
|
||||
it("GET /api/run/binary?tool=codex probes codex, not claude", async () => {
|
||||
const res = await fetchJson("/api/run/binary?tool=codex");
|
||||
assert.equal(res.status, 200);
|
||||
assert.equal(typeof res.body.found, "boolean");
|
||||
});
|
||||
|
||||
it("GET /api/run/binary rejects an unknown tool", async () => {
|
||||
const res = await fetchJson("/api/run/binary?tool=rm");
|
||||
assert.equal(res.status, 400);
|
||||
});
|
||||
});
|
||||
|
||||
+20
-1
@@ -260,6 +260,11 @@ router.post("/ensure", sameOriginGuard, (req, res) => {
|
||||
// path and lane uniqueness stays meaningful. Callers that do care pass one
|
||||
// through unchanged.
|
||||
const cwd = typeof body.cwd === "string" && body.cwd.trim() ? body.cwd.trim() : os.homedir();
|
||||
|
||||
if (body.tool !== undefined && !lanesLib.VALID_TOOLS.has(body.tool)) {
|
||||
return res.status(400).json({ error: { code: "EBADTOOL", message: "unknown tool" } });
|
||||
}
|
||||
|
||||
const owner = lanesLib.resolveLaneByCwd(cwd);
|
||||
if (owner) {
|
||||
// First-write wins on the title: an ensure-with-title on an existing lane
|
||||
@@ -276,11 +281,12 @@ router.post("/ensure", sameOriginGuard, (req, res) => {
|
||||
cwd,
|
||||
title: body.title || "",
|
||||
...(body.pipeline ? { pipeline: body.pipeline } : {}),
|
||||
...(body.tool ? { tool: body.tool } : {}),
|
||||
});
|
||||
broadcastLane(lane.id);
|
||||
return res.status(201).json({ lane: payload(lane), created: true });
|
||||
} catch (err) {
|
||||
if (err.code === "EBADCWD" || err.code === "EBADPIPELINE") {
|
||||
if (err.code === "EBADCWD" || err.code === "EBADPIPELINE" || err.code === "EBADTOOL") {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
// The cwd UNIQUE constraint is the arbiter: someone else won the race, so
|
||||
@@ -641,6 +647,11 @@ router.get("/:id/ci/status", async (req, res) => {
|
||||
*/
|
||||
router.post("/worktree", sameOriginGuard, async (req, res) => {
|
||||
const body = req.body || {};
|
||||
|
||||
if (body.tool !== undefined && !lanesLib.VALID_TOOLS.has(body.tool)) {
|
||||
return res.status(400).json({ error: { code: "EBADTOOL", message: "unknown tool" } });
|
||||
}
|
||||
|
||||
const sourceRepo = body.sourceRepo;
|
||||
if (
|
||||
typeof sourceRepo !== "string" ||
|
||||
@@ -706,6 +717,7 @@ router.post("/worktree", sameOriginGuard, async (req, res) => {
|
||||
base_branch: body.base || null,
|
||||
slug,
|
||||
...(body.pipeline ? { pipeline: body.pipeline } : {}),
|
||||
...(body.tool ? { tool: body.tool } : {}),
|
||||
});
|
||||
lane = lanesLib.updateLane(lane.id, { status: "provisioning" });
|
||||
} catch (err) {
|
||||
@@ -1230,9 +1242,16 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
laneId: current.id,
|
||||
initialPrompt: body.prompt || "",
|
||||
cwd: current.cwd,
|
||||
// The tool is the LANE's fact, never the request's: a client that
|
||||
// asked for the other CLI would otherwise get a pane that doesn't
|
||||
// match the lane's badge, its history, or its binary probe.
|
||||
tool: current.tool || "claude",
|
||||
model: body.model,
|
||||
permissionMode: body.permissionMode,
|
||||
effort: body.effort,
|
||||
sandbox: body.sandbox,
|
||||
approval: body.approval,
|
||||
resumeLast: body.resumeLast === true,
|
||||
resumeSessionId: body.resumeSessionId || (body.resume ? current.session_id : undefined),
|
||||
});
|
||||
lanesLib.updateLane(current.id, { run_id: handle.id, status: "running" });
|
||||
|
||||
+14
-9
@@ -24,6 +24,7 @@ const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const runs = require("../lib/pty-run");
|
||||
const tmux = require("../lib/tmux");
|
||||
const lanesLib = require("../lib/lanes");
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -236,20 +237,22 @@ router.get("/files", (req, res) => {
|
||||
res.json({ items: results.slice(0, MAX_RESULTS) });
|
||||
});
|
||||
|
||||
router.get("/binary", (_req, res) => {
|
||||
// Surface whether `claude` is on PATH so the UI can show a helpful error
|
||||
// before the user clicks Run. We don't actually invoke it — just let the
|
||||
// user know the spawn will work.
|
||||
router.get("/binary", (req, res) => {
|
||||
// Surface whether the lane's CLI is on PATH so the UI can show a helpful
|
||||
// error before the user clicks Run. We don't actually invoke it. The name is
|
||||
// taken from a fixed allowlist, never from the query string directly — this
|
||||
// string reaches `which`.
|
||||
const tool = req.query.tool === undefined ? "claude" : String(req.query.tool);
|
||||
if (tool !== "claude" && tool !== "codex") {
|
||||
return res.status(400).json({ error: { code: "EBADTOOL", message: "unknown tool" } });
|
||||
}
|
||||
const which = require("node:child_process").spawnSync(
|
||||
process.platform === "win32" ? "where" : "which",
|
||||
["claude"],
|
||||
[tool],
|
||||
{ encoding: "utf8" }
|
||||
);
|
||||
const stdout = (which.stdout || "").trim();
|
||||
res.json({
|
||||
found: which.status === 0 && stdout.length > 0,
|
||||
path: stdout || null,
|
||||
});
|
||||
res.json({ found: which.status === 0 && stdout.length > 0, path: stdout || null });
|
||||
});
|
||||
|
||||
router.get("/tmux", (_req, res) => {
|
||||
@@ -269,9 +272,11 @@ router.post("/", (req, res) => {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
try {
|
||||
const lane = lanesLib.getLane(laneId);
|
||||
const handle = runs.spawnRun({
|
||||
laneId,
|
||||
cwd,
|
||||
tool: lane?.tool || "claude",
|
||||
model: typeof body.model === "string" && body.model ? body.model : null,
|
||||
permissionMode:
|
||||
typeof body.permissionMode === "string" && ALLOWED_PERMISSION_MODES.has(body.permissionMode)
|
||||
|
||||
Reference in New Issue
Block a user