feat(ui): call lanes "workspaces" in the UI, CLI output and docs
Copy-only rename (workspace redesign, phase 0). The word a user reads changes — "Lane" → "Workspace", "Add lane" → "Add workspace" — in both locales, in agenthub CLI prose and help, in the server's error messages and in the docs. Every identifier stays: /api/lanes, `agenthub lanes`, --lane, AGENTHUB_LANE_ID, error codes, tmux names, DB tables, file, component and i18n key names. docs/LANES.md keeps its name, retitled "Workspaces (lanes)".
This commit is contained in:
+49
-30
@@ -229,7 +229,9 @@ router.get("/pipeline-stats", (req, res) => {
|
||||
if (laneFilter !== undefined && laneFilter !== "" && laneFilter !== "all") {
|
||||
laneId = Number(laneFilter);
|
||||
if (!Number.isInteger(laneId) || laneId < 1) {
|
||||
return res.status(400).json({ error: { code: "EBADLANE", message: "lane must be an id" } });
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: { code: "EBADLANE", message: "workspace must be an id" } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +502,8 @@ router.get("/browse", (req, res) => {
|
||||
|
||||
router.get("/:id", (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
res.json({ lane: payload(lane) });
|
||||
});
|
||||
|
||||
@@ -523,7 +526,7 @@ router.post("/", sameOriginGuard, (req, res) => {
|
||||
if (err.code === "SQLITE_CONSTRAINT_UNIQUE" || String(err.message).includes("UNIQUE")) {
|
||||
return res
|
||||
.status(409)
|
||||
.json({ error: { code: "EDUPCWD", message: "a lane already owns that cwd" } });
|
||||
.json({ error: { code: "EDUPCWD", message: "a workspace already owns that cwd" } });
|
||||
}
|
||||
res.status(500).json({ error: { message: err.message } });
|
||||
}
|
||||
@@ -531,7 +534,7 @@ router.post("/", sameOriginGuard, (req, res) => {
|
||||
|
||||
router.patch("/:id", sameOriginGuard, (req, res) => {
|
||||
if (!lanesLib.getLane(req.params.id)) {
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
}
|
||||
let lane;
|
||||
try {
|
||||
@@ -558,7 +561,7 @@ router.patch("/:id", sameOriginGuard, (req, res) => {
|
||||
router.post("/:id/stage", sameOriginGuard, (req, res) => {
|
||||
const current = lanesLib.getLane(req.params.id);
|
||||
if (!current) {
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
}
|
||||
// A stage naming no node of this lane's pipeline used to be stored and then
|
||||
// silently ignored by every reader — `agenthub stage test` on a ship-feature
|
||||
@@ -575,7 +578,7 @@ router.post("/:id/stage", sameOriginGuard, (req, res) => {
|
||||
error: {
|
||||
code: "ENOPIPELINE",
|
||||
message:
|
||||
"no pipeline template is installed, so this lane has no stages to declare " +
|
||||
"no pipeline template is installed, so this workspace has no stages to declare " +
|
||||
"(drop a template into DASHBOARD_PIPELINES_DIR to turn stages on)",
|
||||
stages: [],
|
||||
},
|
||||
@@ -600,7 +603,8 @@ router.post("/:id/stage", sameOriginGuard, (req, res) => {
|
||||
|
||||
router.get("/:id/preflight", async (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
const action = String(req.query.action || "");
|
||||
if (!["reset", "remove", "purge"].includes(action)) {
|
||||
return res
|
||||
@@ -623,13 +627,15 @@ router.get("/:id/preflight", async (req, res) => {
|
||||
*/
|
||||
router.get("/:id/features", (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
res.json({ features: laneFeatures.listFeatures(lane.id).map(featurePayload) });
|
||||
});
|
||||
|
||||
router.get("/:id/features/:slug", (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
const feature = laneFeatures.getFeature(lane.id, req.params.slug);
|
||||
if (!feature) {
|
||||
return res.status(404).json({ error: { code: "ENOFEATURE", message: "no such feature" } });
|
||||
@@ -639,7 +645,8 @@ router.get("/:id/features/:slug", (req, res) => {
|
||||
|
||||
router.post("/:id/features/activate", sameOriginGuard, (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
const slug = typeof req.body?.slug === "string" ? req.body.slug : "";
|
||||
if (!slug) {
|
||||
return res.status(400).json({ error: { code: "EBADSLUG", message: "slug is required" } });
|
||||
@@ -660,7 +667,8 @@ router.post("/:id/features/activate", sameOriginGuard, (req, res) => {
|
||||
|
||||
router.get("/:id/proof", (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
res.json({ features: proofLib.listProof(lane.id) });
|
||||
});
|
||||
|
||||
@@ -677,7 +685,8 @@ router.get("/:id/proof/:slug/:group/:file", (req, res) => {
|
||||
|
||||
router.delete("/:id/proof/:slug", sameOriginGuard, (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
try {
|
||||
const result = proofLib.deleteProof(lane.id, {
|
||||
slug: req.params.slug,
|
||||
@@ -698,7 +707,8 @@ router.delete("/:id/proof/:slug", sameOriginGuard, (req, res) => {
|
||||
|
||||
router.post("/:id/proof-link", sameOriginGuard, (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
res.json(proofLib.ensureProofLink(lane));
|
||||
});
|
||||
|
||||
@@ -712,7 +722,8 @@ router.post("/:id/proof-link", sameOriginGuard, (req, res) => {
|
||||
*/
|
||||
router.post("/:id/mcp/sync", sameOriginGuard, async (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
try {
|
||||
res.json(await syncMcp(lane));
|
||||
} catch (err) {
|
||||
@@ -734,7 +745,8 @@ router.post("/:id/mcp/sync", sameOriginGuard, async (req, res) => {
|
||||
*/
|
||||
router.post("/:id/router/apply", sameOriginGuard, (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
const profile = (req.body || {}).profile;
|
||||
if (typeof profile !== "string" || !profile) {
|
||||
return res.status(400).json({ error: { code: "EBADREQ", message: "profile is required" } });
|
||||
@@ -775,7 +787,8 @@ router.post("/:id/router/apply", sameOriginGuard, (req, res) => {
|
||||
*/
|
||||
router.post("/:id/profile/init", sameOriginGuard, (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
|
||||
const facts = detectNode(lane.cwd);
|
||||
if (!facts) {
|
||||
@@ -800,7 +813,8 @@ router.post("/:id/profile/init", sameOriginGuard, (req, res) => {
|
||||
*/
|
||||
router.get("/:id/integrations/:name", (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
res.json({ enabled: isIntegrationEnabled(lane, req.params.name) });
|
||||
});
|
||||
|
||||
@@ -821,7 +835,8 @@ router.get("/:id/integrations/:name", (req, res) => {
|
||||
*/
|
||||
router.get("/:id/git", async (req, res) => {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
try {
|
||||
res.json({ available: true, ...(await gitFacts(lane.cwd)) });
|
||||
} catch {
|
||||
@@ -1138,7 +1153,7 @@ router.post("/worktree", sameOriginGuard, async (req, res) => {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
code: "EEMPTYREPO",
|
||||
message: "sourceRepo has no commits yet — add it as a Repo lane and commit first",
|
||||
message: "sourceRepo has no commits yet — add it as a Repo workspace and commit first",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1201,7 +1216,7 @@ router.post("/worktree", sameOriginGuard, async (req, res) => {
|
||||
if (err.code === "SQLITE_CONSTRAINT_UNIQUE" || String(err.message).includes("UNIQUE")) {
|
||||
return res
|
||||
.status(409)
|
||||
.json({ error: { code: "EDUPCWD", message: "a lane already owns that cwd" } });
|
||||
.json({ error: { code: "EDUPCWD", message: "a workspace already owns that cwd" } });
|
||||
}
|
||||
return res.status(500).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
@@ -1273,7 +1288,7 @@ function assertExpectedPreflight(action, current, expected) {
|
||||
}
|
||||
const changed = fields.some((field) => current[field] !== expected[field]);
|
||||
if (changed) {
|
||||
const err = lifecycleError("ESTALE", "lane state changed since preflight");
|
||||
const err = lifecycleError("ESTALE", "workspace state changed since preflight");
|
||||
err.expected = expected;
|
||||
err.current = current;
|
||||
throw err;
|
||||
@@ -1316,7 +1331,7 @@ async function stopLaneRun(lane) {
|
||||
if (Date.now() >= deadline) {
|
||||
throw lifecycleError(
|
||||
"ERUNTIMEOUT",
|
||||
`lane run ${id} did not exit within ${RUN_EXIT_TIMEOUT_MS / 1000} seconds`
|
||||
`workspace run ${id} did not exit within ${RUN_EXIT_TIMEOUT_MS / 1000} seconds`
|
||||
);
|
||||
}
|
||||
await wait(RUN_EXIT_POLL_MS);
|
||||
@@ -1410,7 +1425,7 @@ function gitErrorCode(err) {
|
||||
function laneOr404(req, res) {
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) {
|
||||
res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
return null;
|
||||
}
|
||||
return lane;
|
||||
@@ -1556,7 +1571,7 @@ router.post("/:id/hook/:name", sameOriginGuard, (req, res) => {
|
||||
}
|
||||
if (!lane.slot) {
|
||||
return res.status(409).json({
|
||||
error: { code: "ENOSLOT", message: "lane has no runtime yet — bring it up first" },
|
||||
error: { code: "ENOSLOT", message: "workspace has no runtime yet — bring it up first" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1635,7 +1650,8 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
.json({ error: { code: "EBADACTION", message: `unknown action ${action}` } });
|
||||
}
|
||||
const lane = lanesLib.getLane(req.params.id);
|
||||
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!lane)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
const body = req.body || {};
|
||||
|
||||
if (DESTRUCTIVE_ACTIONS.has(action)) {
|
||||
@@ -1647,7 +1663,7 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
try {
|
||||
const result = await withLaneLock(lane.id, async () => {
|
||||
const lockedLane = lanesLib.getLane(lane.id);
|
||||
if (!lockedLane) throw lifecycleError("ENOLANE", "lane not found");
|
||||
if (!lockedLane) throw lifecycleError("ENOLANE", "workspace not found");
|
||||
|
||||
await stopLaneRun(lockedLane);
|
||||
// A running stack holds files open in the very directory `reset` is about
|
||||
@@ -1782,12 +1798,14 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
return {};
|
||||
});
|
||||
if (outcome.missing) {
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
return res
|
||||
.status(404)
|
||||
.json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
}
|
||||
if (outcome.conflict) {
|
||||
return res
|
||||
.status(409)
|
||||
.json({ error: { code: "ERUNLIVE", message: "lane already has a live run" } });
|
||||
.json({ error: { code: "ERUNLIVE", message: "workspace already has a live run" } });
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1808,7 +1826,7 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
error: {
|
||||
code: "EUNSUPPORTED",
|
||||
message:
|
||||
"sending input to a lane's run is no longer supported via REST — open the lane's terminal in Workspace and type directly (attaches over WebSocket to the same tmux session)",
|
||||
"sending input to a workspace's run is no longer supported via REST — open the workspace's terminal in Workspace and type directly (attaches over WebSocket to the same tmux session)",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1839,7 +1857,8 @@ router.post("/:id/:action", sameOriginGuard, async (req, res) => {
|
||||
|
||||
router.delete("/:id", sameOriginGuard, (req, res) => {
|
||||
const ok = lanesLib.deleteLane(req.params.id);
|
||||
if (!ok) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
|
||||
if (!ok)
|
||||
return res.status(404).json({ error: { code: "ENOLANE", message: "workspace not found" } });
|
||||
broadcast("lane_update", { removed: Number(req.params.id) });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user