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:
@@ -131,11 +131,11 @@ async function isSecretOnDisk(dir, file) {
|
||||
*/
|
||||
function lanePath(file) {
|
||||
if (typeof file !== "string" || !file || file.includes("\0")) {
|
||||
throw badRequest("EBADPATH", "path must be a relative path inside the lane");
|
||||
throw badRequest("EBADPATH", "path must be a relative path inside the workspace");
|
||||
}
|
||||
// A backslash is an ordinary file-name byte to git on POSIX, not a separator.
|
||||
if (file.startsWith("/") || file.split("/").includes("..")) {
|
||||
throw badRequest("EBADPATH", "path must be a relative path inside the lane");
|
||||
throw badRequest("EBADPATH", "path must be a relative path inside the workspace");
|
||||
}
|
||||
const norm = path.posix
|
||||
.normalize(file)
|
||||
@@ -160,7 +160,7 @@ async function resolveInLane(dir, file) {
|
||||
};
|
||||
// Lexically first, so `../x` is EBADPATH whether or not x exists.
|
||||
if (escapes(path.resolve(dir), path.resolve(dir, file))) {
|
||||
throw badRequest("EBADPATH", "path escapes the lane directory");
|
||||
throw badRequest("EBADPATH", "path escapes the workspace directory");
|
||||
}
|
||||
const root = await fs.promises.realpath(dir);
|
||||
let real;
|
||||
@@ -169,7 +169,7 @@ async function resolveInLane(dir, file) {
|
||||
} catch {
|
||||
throw badRequest("ENOFILE", "no such file in the working tree");
|
||||
}
|
||||
if (escapes(root, real)) throw badRequest("EBADPATH", "path escapes the lane directory");
|
||||
if (escapes(root, real)) throw badRequest("EBADPATH", "path escapes the workspace directory");
|
||||
if (await isSecretOnDisk(root, file))
|
||||
throw badRequest("ESECRET", "that file holds secrets and is not readable here");
|
||||
return real;
|
||||
@@ -821,7 +821,7 @@ async function listTree(dir, { subdir = "" } = {}) {
|
||||
const clean = String(subdir || "")
|
||||
.replace(/^\/+|\/+$/g, "")
|
||||
.trim();
|
||||
if (clean.split("/").includes("..")) throw badRequest("EBADPATH", "path escapes the lane");
|
||||
if (clean.split("/").includes("..")) throw badRequest("EBADPATH", "path escapes the workspace");
|
||||
const [{ all, cut }, st] = await Promise.all([
|
||||
lsFiles(dir, clean ? [...TREE_LS, "--", `${clean}/`] : TREE_LS),
|
||||
treeStatus(dir),
|
||||
|
||||
@@ -200,7 +200,7 @@ async function assertNothingStagedOutside(dir) {
|
||||
if (outside) {
|
||||
throw bad(
|
||||
"ESTAGEDOUTSIDE",
|
||||
`refusing to commit: ${outside} is staged outside this lane's directory`
|
||||
`refusing to commit: ${outside} is staged outside this workspace's directory`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -505,7 +505,7 @@ async function mergeAction(dir, action, body) {
|
||||
if ((await git(dir, ["rev-parse", "--show-prefix"])).stdout.trim()) {
|
||||
throw bad(
|
||||
"ESUBDIR",
|
||||
"a merge changes the whole repository; run it from a lane at the repo root"
|
||||
"a merge changes the whole repository; run it from a workspace at the repo root"
|
||||
);
|
||||
}
|
||||
if (await isDirty(dir)) throw bad("EDIRTY", "working tree has uncommitted changes");
|
||||
|
||||
@@ -150,7 +150,7 @@ function archiveActiveFeature(laneId) {
|
||||
function activateFeature(laneId, rawSlug, options = {}) {
|
||||
const slug = canonicalizeSlug(rawSlug);
|
||||
const lane = lanesLib.getLane(laneId);
|
||||
if (!lane) throw Object.assign(new Error(`no lane ${laneId}`), { code: "ENOLANE" });
|
||||
if (!lane) throw Object.assign(new Error(`no workspace ${laneId}`), { code: "ENOLANE" });
|
||||
|
||||
const current = lane.active_feature_id ? getFeatureById(lane.active_feature_id) : null;
|
||||
if (current && current.slug === slug) {
|
||||
|
||||
@@ -59,7 +59,7 @@ function requireProfile(lane) {
|
||||
if (!profile) {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
`lane has no .agenthub/profile — looked in: ${profileSearchPaths(lane).join(", ")}`
|
||||
`workspace has no .agenthub/profile — looked in: ${profileSearchPaths(lane).join(", ")}`
|
||||
),
|
||||
{ code: "ENOPROFILE", searched: profileSearchPaths(lane) }
|
||||
);
|
||||
@@ -176,7 +176,7 @@ function resolveLaneDirEntry(lane, entry) {
|
||||
const target = path.resolve(lane.cwd, entry);
|
||||
const relative = path.relative(lane.cwd, target);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
throw Object.assign(new Error(`LANE_DIRS entry escapes the lane: ${entry}`), {
|
||||
throw Object.assign(new Error(`LANE_DIRS entry escapes the workspace: ${entry}`), {
|
||||
code: "EBADLANEDIR",
|
||||
entry,
|
||||
});
|
||||
|
||||
@@ -62,7 +62,7 @@ function allocateSlot(laneId) {
|
||||
lanesLib.setProvisioningFacts(laneId, { slot });
|
||||
return slot;
|
||||
}
|
||||
throw Object.assign(new Error(`all ${limit} lane slots are in use`), { code: "ESLOTS" });
|
||||
throw Object.assign(new Error(`all ${limit} workspace slots are in use`), { code: "ESLOTS" });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -282,7 +282,7 @@ async function continueSync(lane, profile, branchArg) {
|
||||
|
||||
const current = (await git(lane.cwd, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
||||
if (current !== branch) {
|
||||
throw badBranch(`--continue: lane is not on '${branch}' (currently on '${current}')`);
|
||||
throw badBranch(`--continue: workspace is not on '${branch}' (currently on '${current}')`);
|
||||
}
|
||||
|
||||
const unresolved = await unmergedFiles(lane.cwd);
|
||||
|
||||
+3
-3
@@ -480,7 +480,7 @@ function appendStageEvent(lane, nodeId, { source, result, evidence } = {}) {
|
||||
|
||||
function setStage(id, { stage, status, evidence, note, result } = {}) {
|
||||
const lane = getLane(id);
|
||||
if (!lane) throw Object.assign(new Error(`no lane ${id}`), { code: "ENOLANE" });
|
||||
if (!lane) throw Object.assign(new Error(`no workspace ${id}`), { code: "ENOLANE" });
|
||||
const next = stage || lane.stage;
|
||||
const changed = next !== lane.stage;
|
||||
const stages = { ...lane.stages };
|
||||
@@ -543,7 +543,7 @@ function setStage(id, { stage, status, evidence, note, result } = {}) {
|
||||
*/
|
||||
function recordDetection(id, { nodeId, signal } = {}) {
|
||||
const lane = getLane(id);
|
||||
if (!lane) throw Object.assign(new Error(`no lane ${id}`), { code: "ENOLANE" });
|
||||
if (!lane) throw Object.assign(new Error(`no workspace ${id}`), { code: "ENOLANE" });
|
||||
const pipeline = getPipeline(lane.pipeline);
|
||||
const nodeIdx = phaseIdx(pipeline, nodeId);
|
||||
if (nodeIdx === -1) return { written: false, reason: "unknown-node" };
|
||||
@@ -839,7 +839,7 @@ function hasActiveLaneSession(lane) {
|
||||
*/
|
||||
function purgeLaneSessions(laneId) {
|
||||
const lane = getLane(laneId);
|
||||
if (!lane) throw Object.assign(new Error(`no lane ${laneId}`), { code: "ENOLANE" });
|
||||
if (!lane) throw Object.assign(new Error(`no workspace ${laneId}`), { code: "ENOLANE" });
|
||||
|
||||
const result = { sessions: 0, events: 0, tokenRows: 0 };
|
||||
|
||||
|
||||
+2
-2
@@ -166,14 +166,14 @@ function proofFile(laneId, slug, group, file) {
|
||||
*/
|
||||
function deleteProof(laneId, { slug, group, images } = {}) {
|
||||
const lane = lanesLib.getLane(laneId);
|
||||
if (!lane) throw Object.assign(new Error(`no lane ${laneId}`), { code: "ENOLANE" });
|
||||
if (!lane) throw Object.assign(new Error(`no workspace ${laneId}`), { code: "ENOLANE" });
|
||||
if (badName(slug)) throw Object.assign(new Error("invalid slug"), { code: "EBADPATH" });
|
||||
|
||||
let base;
|
||||
try {
|
||||
base = fs.realpathSync(proofRoot(lane));
|
||||
} catch {
|
||||
throw Object.assign(new Error("no proof for this lane"), { code: "ENOFEATURE" });
|
||||
throw Object.assign(new Error("no proof for this workspace"), { code: "ENOFEATURE" });
|
||||
}
|
||||
const inBase = (p) => p === base || p.startsWith(base + path.sep);
|
||||
|
||||
|
||||
@@ -388,7 +388,7 @@ function spawnRun(args) {
|
||||
else if (resumeSessionId || initialPrompt) {
|
||||
throw makeErr(
|
||||
"ERUNLIVE",
|
||||
`lane ${laneId}'s pane is already running \`${tmux.paneCommand(id) || "a process"}\` — stop it before resuming a session or sending a prompt`
|
||||
`workspace ${laneId}'s pane is already running \`${tmux.paneCommand(id) || "a process"}\` — stop it before resuming a session or sending a prompt`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ function applyRouterToLane(lane, profileName) {
|
||||
// AI tool runs on its vendor's own settings, so native is the one fit.
|
||||
if (!ROUTER_CAPABLE.has(lane.tool || "claude")) {
|
||||
if (profile.builtin) return { tool: lane.tool, file: null, profile: profile.name };
|
||||
throw Object.assign(new Error(`a ${lane.tool} lane cannot use an AI Router`), {
|
||||
throw Object.assign(new Error(`a ${lane.tool} workspace cannot use an AI Router`), {
|
||||
code: "EUNSUPPORTED",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ async function deleteBranchSafely(sourceRepo, branch, baseBranch) {
|
||||
*/
|
||||
function assertManaged(lane) {
|
||||
if (lane.kind !== "managed") {
|
||||
const err = new Error(`lane kind is ${lane.kind}, not managed`);
|
||||
const err = new Error(`workspace kind is ${lane.kind}, not managed`);
|
||||
err.code = "ENOTMANAGED";
|
||||
throw err;
|
||||
}
|
||||
@@ -348,7 +348,7 @@ async function assertDestroyable(lane) {
|
||||
resolvedCwd = fs.realpathSync(lane.cwd);
|
||||
} catch {
|
||||
// Path doesn't exist, which means it's not a live worktree
|
||||
const err = new Error(`lane cwd does not exist: ${lane.cwd}`);
|
||||
const err = new Error(`workspace cwd does not exist: ${lane.cwd}`);
|
||||
err.code = "EOUTSIDEROOT";
|
||||
throw err;
|
||||
}
|
||||
@@ -365,7 +365,9 @@ async function assertDestroyable(lane) {
|
||||
// Check that cwd is inside LANES_ROOT on a path boundary
|
||||
const relativePath = path.relative(resolvedRoot, resolvedCwd);
|
||||
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
||||
const err = new Error(`lane cwd is outside LANES_ROOT: ${resolvedCwd} not in ${resolvedRoot}`);
|
||||
const err = new Error(
|
||||
`workspace cwd is outside LANES_ROOT: ${resolvedCwd} not in ${resolvedRoot}`
|
||||
);
|
||||
err.code = "EOUTSIDEROOT";
|
||||
throw err;
|
||||
}
|
||||
@@ -384,7 +386,7 @@ async function assertDestroyable(lane) {
|
||||
}
|
||||
});
|
||||
if (!exists) {
|
||||
const err = new Error(`lane is not listed as a worktree in ${lane.source_repo}`);
|
||||
const err = new Error(`workspace is not listed as a worktree in ${lane.source_repo}`);
|
||||
err.code = "ENOTWORKTREE";
|
||||
throw err;
|
||||
}
|
||||
@@ -520,7 +522,7 @@ async function removeWorktree(lane) {
|
||||
if (!fs.existsSync(cwd)) {
|
||||
assertManaged(lane);
|
||||
if (!isInsideLanesRoot(cwd)) {
|
||||
const err = new Error(`lane cwd is outside LANES_ROOT: ${cwd} not in ${LANES_ROOT}`);
|
||||
const err = new Error(`workspace cwd is outside LANES_ROOT: ${cwd} not in ${LANES_ROOT}`);
|
||||
err.code = "EOUTSIDEROOT";
|
||||
throw err;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user