chore: maintenance — dependency majors, rename step 4, member Claude config, OpenAPI coverage

Dependencies
- Security: body-parser and multer DoS advisories fixed (npm audit: 0).
- Server: Express 5 (query parser kept "extended", req.body defaults to {},
  SPA catch-all /{*splat}, send-file helper restores Express 4's dot-folder
  rule for sendFile/download, alerts ack route without inline regex),
  better-sqlite3 13, js-yaml 5 (server/lib/yaml.js keeps js-yaml 4's default
  schema), uuid dropped for crypto.randomUUID, node-pty beta.18, multer 2.5.
- Client: React 19, xterm 6 (WebGL renderer replaces the removed canvas one,
  DOM fallback on context loss; Alt+arrow word moves re-mapped), lucide-react 1,
  TypeScript 7, i18next/react-i18next/codemirror minors.

Workspace rename step 4
- `agenthub lanes` and `--lane` exit 1 with "renamed: use …"; client api.ts,
  the CLI and the MCP server call /api/workspaces; /api/lanes stays mounted.

Server mode (S3): members manage their own Claude Code config
- server/lib/cc-target.js resolves whose config a /cc-config request touches
  and gates the router by allowlist; /profiles* and /plugins/update-all stay
  admin-only. Member writes go through write-as, CLI calls through sudo as the
  member (never -y), reads through openSafe; cc_config_changed reaches only
  its owner. Not verified live on an S3 install yet.

Other
- Chat: an HTML file is an artifact only when written whole and not a project
  page (index.html, or under src/, public/, templates/, …).
- OpenAPI covers every /api route; openapi-coverage.test.js guards it.
- read-pool test keeps its event loop alive (its worker is unref'd).
- Docs: plan statuses, checklists, CLI.md, LANES.md, SERVER-MODE.md, API.md.
This commit is contained in:
2026-10-26 14:22:41 +07:00
parent cc2ece94c3
commit 8546748759
102 changed files with 4165 additions and 2004 deletions
+67 -69
View File
@@ -1281,7 +1281,7 @@ async function cmdDoctor() {
// Asked of the SERVER, not of this shell: a run is a tmux session the server
// spawns, so the only PATH that decides whether Start Run works is its own —
// a systemd --user service never reads your shell profile. Missing tmux is a
// warning, not a failure: it costs the run console and `lanes shell`, nothing
// warning, not a failure: it costs the run console and `workspace shell`, nothing
// else.
try {
const { available, upgrade } = await get("/api/run/tmux");
@@ -1693,7 +1693,7 @@ function cmdOpen() {
}
/**
* `agenthub lanes add` — adopt a working directory or provision a managed worktree.
* `agenthub workspace add` — adopt a working directory or provision a managed worktree.
*/
async function cmdLanesAdd(args) {
const flag = (name) => {
@@ -1719,12 +1719,12 @@ async function cmdLanesAdd(args) {
if (base) body.base = base;
if (slug) body.slug = slug;
if (tool) body.tool = tool;
const { lane: provisioning } = await post("/api/lanes/worktree", body);
const { lane: provisioning } = await post("/api/workspaces/worktree", body);
const deadline = Date.now() + 30_000;
let lane = provisioning;
while (lane.status === "provisioning" && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 250));
({ lane } = await get(`/api/lanes/${provisioning.id}`));
({ lane } = await get(`/api/workspaces/${provisioning.id}`));
}
if (lane.status === "provisioning") {
console.error(
@@ -1755,12 +1755,12 @@ async function cmdLanesAdd(args) {
const body = { cwd, title };
if (pipeline) body.pipeline = pipeline;
if (tool) body.tool = tool;
const { lane } = await post("/api/lanes", body);
const { lane } = await post("/api/workspaces", body);
console.log(`${c.green("✔")} Created workspace #${lane.id}: ${lane.title}`);
}
/**
* `agenthub lanes gc [--dry-run]` — reap orphaned Playwright MCP processes
* `agenthub workspace gc [--dry-run]` — reap orphaned Playwright MCP processes
* (owning session died) and cap oversized hook logs, across every lane on
* this machine. Machine-wide, not one lane's; pure local process/file
* action, no HTTP round-trip.
@@ -1790,7 +1790,7 @@ function cmdLanesGc(args) {
}
/**
* `agenthub lanes profile init <repo>` — detect a Node.js project and scaffold
* `agenthub workspace profile init <repo>` — detect a Node.js project and scaffold
* `.agenthub/profile/`. Pure filesystem action against the SOURCE repo; does not
* talk to the dashboard server at all.
*/
@@ -1844,9 +1844,9 @@ function cmdLanesProfileInit(args) {
}
/**
* `agenthub lanes profile check [<path>]` — validate a profile without needing a
* `agenthub workspace profile check [<path>]` — validate a profile without needing a
* lane to exist for it yet. Defaults to the current directory, NOT lane-id
* resolution (unlike every other `lanes` subcommand) — this is meant to run
* resolution (unlike every other `workspace` subcommand) — this is meant to run
* against a bare repo right after `profile init`.
*/
async function cmdLanesProfileCheck(args) {
@@ -1883,7 +1883,7 @@ function printLaneFacts(title, facts, fields) {
}
}
/** `agenthub lanes reset|remove|purge` — destructive work only after a fact check. */
/** `agenthub workspace reset|remove|purge` — destructive work only after a fact check. */
async function cmdLanesLifecycle(action, args) {
const id = args.find((arg) => !arg.startsWith("--"));
const fields = LANE_PREFLIGHT_FIELDS[action];
@@ -1894,7 +1894,7 @@ async function cmdLanesLifecycle(action, args) {
return;
}
const preflight = await get(`/api/lanes/${id}/preflight?action=${action}`);
const preflight = await get(`/api/workspaces/${id}/preflight?action=${action}`);
printLaneFacts(`Preflight for ${action} workspace #${id}:`, preflight, fields);
const keepDb = action === "reset" && args.includes("--keep-db");
if (preflight.database) {
@@ -1938,7 +1938,7 @@ async function cmdLanesLifecycle(action, args) {
const body = { confirm: true, expect };
if (args.includes("--force")) body.force = true;
if (keepDb) body.keepDb = true;
const result = await post(`/api/lanes/${id}/${action}`, body, { allowError: true });
const result = await post(`/api/workspaces/${id}/${action}`, body, { allowError: true });
if (result.status) {
const error = result.data?.error || {};
console.error(`✖ ${action} workspace #${id} → ${error.message || `HTTP ${result.status}`}`);
@@ -1961,7 +1961,7 @@ async function cmdLanesLifecycle(action, args) {
}
/**
* `agenthub lanes` — one row per lane: what it is, where it is in its pipeline, and
* `agenthub workspaces` — one row per lane: what it is, where it is in its pipeline, and
* whether the driving session is still breathing.
*/
/**
@@ -1993,7 +1993,7 @@ function detectionLeadsDeclaration(l) {
}
async function cmdLanes() {
const { lanes, counts } = await get("/api/lanes");
const { lanes, counts } = await get("/api/workspaces");
if (!lanes.length) {
console.log(
"no workspaces yet — create one with: agenthub workspace add --cwd <path> --title <text>"
@@ -2040,7 +2040,7 @@ function toolFromEnv() {
*
* Only the FIRST positional counts as an id, and only when it is all digits.
* Scanning the whole argv for a number would swallow flag values —
* `agenthub lanes logs web --tail 4096` would have addressed lane 4096.
* `agenthub workspace logs web --tail 4096` would have addressed lane 4096.
*
* @param {string[]} args - Arguments after the subcommand.
* @returns {Promise<{laneId: string|number, rest: string[]}|null>} null once an error is printed.
@@ -2051,13 +2051,13 @@ async function resolveLaneArg(args) {
return i > -1 ? args[i + 1] : undefined;
};
const explicit = flagValue("lane") || flagValue("workspace");
const explicit = flagValue("workspace");
if (explicit) return { laneId: explicit, rest: args };
if (args.length && /^\d+$/.test(args[0])) return { laneId: args[0], rest: args.slice(1) };
const tool = flagValue("tool") || toolFromEnv();
const cwd = require("path").resolve(flagValue("cwd") || process.cwd());
const { lanes } = await get("/api/lanes");
const { lanes } = await get("/api/workspaces");
// A pane's AGENTHUB_LANE_ID beats cwd matching, but only after it is checked
// against the live lane list AND the directory: a pane outlives the lane it
@@ -2146,7 +2146,7 @@ function printRuntime(runtime) {
}
/**
* `agenthub lanes up|down|runtime|logs|hook` — a lane's own application stack, as
* `agenthub workspace up|down|runtime|logs|hook` — a lane's own application stack, as
* opposed to `start`/`stop`, which drive its Claude run. Two lifecycles, one
* lane id.
*
@@ -2160,7 +2160,7 @@ async function cmdLanesRuntime(sub, args) {
const { laneId, rest: laneArgs } = resolved;
if (sub === "runtime") {
printRuntime(await get(`/api/lanes/${laneId}/runtime`));
printRuntime(await get(`/api/workspaces/${laneId}/runtime`));
return;
}
@@ -2168,7 +2168,7 @@ async function cmdLanesRuntime(sub, args) {
const body = {};
if (laneArgs.includes("--no-build")) body.build = false;
if (laneArgs.includes("--qc")) body.qc = true;
const result = await post(`/api/lanes/${laneId}/up`, body, { allowError: true });
const result = await post(`/api/workspaces/${laneId}/up`, body, { allowError: true });
if (result.status) {
console.error(`✖ up workspace #${laneId} → ${result.data?.error?.message || result.status}`);
process.exitCode = 1;
@@ -2181,7 +2181,7 @@ async function cmdLanesRuntime(sub, args) {
const deadline = Date.now() + 15 * 60 * 1000;
for (;;) {
await new Promise((r) => setTimeout(r, 2000));
const runtime = await get(`/api/lanes/${laneId}/runtime`);
const runtime = await get(`/api/workspaces/${laneId}/runtime`);
if (runtime.healthy) {
printRuntime(runtime);
return;
@@ -2202,7 +2202,7 @@ async function cmdLanesRuntime(sub, args) {
}
if (sub === "down") {
const result = await post(`/api/lanes/${laneId}/down`, {}, { allowError: true });
const result = await post(`/api/workspaces/${laneId}/down`, {}, { allowError: true });
if (result.status) {
console.error(
`✖ down workspace #${laneId} → ${result.data?.error?.message || result.status}`
@@ -2223,7 +2223,7 @@ async function cmdLanesRuntime(sub, args) {
}
const i = laneArgs.indexOf("--tail");
const query = i > -1 && laneArgs[i + 1] ? `?tail=${encodeURIComponent(laneArgs[i + 1])}` : "";
const log = await get(`/api/lanes/${laneId}/logs/${encodeURIComponent(svc)}${query}`);
const log = await get(`/api/workspaces/${laneId}/logs/${encodeURIComponent(svc)}${query}`);
if (!log.available) {
console.log("no logs for this workspace yet.");
return;
@@ -2241,7 +2241,7 @@ async function cmdLanesRuntime(sub, args) {
return;
}
const result = await post(
`/api/lanes/${laneId}/hook/${encodeURIComponent(name)}`,
`/api/workspaces/${laneId}/hook/${encodeURIComponent(name)}`,
{ args: laneArgs.slice(1) },
{ allowError: true }
);
@@ -2263,7 +2263,7 @@ async function cmdLanesRuntime(sub, args) {
: "merge";
const branch = laneArgs.find((arg) => !arg.startsWith("--"));
const result = await post(
`/api/lanes/${laneId}/sync-base`,
`/api/workspaces/${laneId}/sync-base`,
{ mode, branch },
{ allowError: true }
);
@@ -2342,7 +2342,7 @@ async function defaultHolder(argsAfterName) {
const resolved = await resolveLaneArg(argsAfterName);
if (!resolved) return null;
const { lane } = await get(`/api/lanes/${resolved.laneId}`);
const { lane } = await get(`/api/workspaces/${resolved.laneId}`);
return lane.slot ? `lane${lane.slot}` : `lane${lane.id}`;
}
@@ -2474,7 +2474,7 @@ function fmtFeatureRow(f) {
async function cmdFeatureList(args) {
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const { features } = await get(`/api/lanes/${resolved.laneId}/features`);
const { features } = await get(`/api/workspaces/${resolved.laneId}/features`);
if (!features.length) {
console.log("no features activated yet — start one with: agenthub feature activate <slug>");
return;
@@ -2496,7 +2496,7 @@ async function cmdFeatureActivate(args) {
};
const resolved = await resolveLaneArg(args.filter((a) => a !== slug));
if (!resolved) return;
const { lane, feature } = await post(`/api/lanes/${resolved.laneId}/features/activate`, {
const { lane, feature } = await post(`/api/workspaces/${resolved.laneId}/features/activate`, {
slug,
title: flag("title"),
});
@@ -2506,13 +2506,13 @@ async function cmdFeatureActivate(args) {
}
/** `agenthub feature show <slug> [<id>] [--cwd path]` — one feature's saved pipeline. */
/** `agenthub lanes proof-link [<id>] [--cwd path]` — converge the clone-root
/** `agenthub workspace proof-link [<id>] [--cwd path]` — converge the clone-root
* `proof/` onto `.playwright-mcp/proof` (idempotent). Never run automatically
* by anything else in this codebase; a session or hook calls it explicitly. */
async function cmdLanesProofLink(args) {
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const { linked } = await post(`/api/lanes/${resolved.laneId}/proof-link`);
const { linked } = await post(`/api/workspaces/${resolved.laneId}/proof-link`);
console.log(
linked
? `${c.green("✔")} linked proof/ -> .playwright-mcp/proof`
@@ -2520,13 +2520,13 @@ async function cmdLanesProofLink(args) {
);
}
/** `agenthub lanes mcp sync [<id>] [--cwd path]` — relocate the source repo's
/** `agenthub workspace mcp sync [<id>] [--cwd path]` — relocate the source repo's
* already-configured MCP servers into <lane>/.mcp.json and seed Chromium
* profiles. Never run automatically; a session calls it explicitly. */
async function cmdLanesMcpSync(args) {
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const result = await post(`/api/lanes/${resolved.laneId}/mcp/sync`, {}, { allowError: true });
const result = await post(`/api/workspaces/${resolved.laneId}/mcp/sync`, {}, { allowError: true });
if (result.status) {
console.error(`✖ mcp sync → ${result.data?.error?.message || result.status}`);
process.exitCode = 1;
@@ -2538,7 +2538,7 @@ async function cmdLanesMcpSync(args) {
}
}
/** `agenthub lanes integration <name> [<id>]` — check whether a named
/** `agenthub workspace integration <name> [<id>]` — check whether a named
* integration (tracker, dev_qc, ci_wait) is turned on for this lane, per
* its .agenthub/profile/integrations.env. Exit 0 = on, 1 = off. */
async function cmdLanesIntegration(args) {
@@ -2551,13 +2551,13 @@ async function cmdLanesIntegration(args) {
const resolved = await resolveLaneArg(args.filter((a) => a !== name));
if (!resolved) return;
const { enabled } = await get(
`/api/lanes/${resolved.laneId}/integrations/${encodeURIComponent(name)}`
`/api/workspaces/${resolved.laneId}/integrations/${encodeURIComponent(name)}`
);
console.log(enabled ? "on" : "off");
process.exitCode = enabled ? 0 : 1;
}
/** `agenthub lanes ci status <branch-or-sha> [<lane-id>]` — one-shot GitHub
/** `agenthub workspace ci status <branch-or-sha> [<lane-id>]` — one-shot GitHub
* Actions status snapshot for a ref, via GET /:id/ci/status. Never polls;
* the caller (ship-feature-lane Stage 13) owns the wait loop. Exit code
* mirrors the derived state: 0 success, 1 failure or any error, 3 pending. */
@@ -2576,7 +2576,7 @@ async function cmdLanesCi(args) {
const resolved = await resolveLaneArg(args.slice(2));
if (!resolved) return;
const result = await get(
`/api/lanes/${resolved.laneId}/ci/status?ref=${encodeURIComponent(ref)}`,
`/api/workspaces/${resolved.laneId}/ci/status?ref=${encodeURIComponent(ref)}`,
undefined,
{ allowError: true }
);
@@ -2590,7 +2590,7 @@ async function cmdLanesCi(args) {
}
/**
* `agenthub lanes node [<node-id>] [extra…] [<id>] [--print]` — run ONE node of the
* `agenthub workspace node [<node-id>] [extra…] [<id>] [--print]` — run ONE node of the
* lane's pipeline instead of the whole thing.
*
* Every node already carries the one command that drives it (`command`, added
@@ -2599,7 +2599,7 @@ async function cmdLanesCi(args) {
* pasted it into a session by hand. This runs it, in the lane's own cwd:
*
* - a slash command starts `claude` with it as the prompt;
* - anything else (`npm test`, `agenthub lanes hook ci-gate`) is spawned directly.
* - anything else (`npm test`, `agenthub workspace hook ci-gate`) is spawned directly.
*
* With no node id it lists the nodes and their commands, which is also the
* error path's answer. `--print` shows the argv and runs nothing.
@@ -2621,10 +2621,10 @@ async function cmdLanesNode(args) {
const { flags, positional } = parseArgs(args);
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const { lane } = await get(`/api/lanes/${resolved.laneId}`);
const { lane } = await get(`/api/workspaces/${resolved.laneId}`);
// The lane payload's `pipeline_nodes` carries render state but not `aliases`;
// the template does. Merge, so the listing can still mark the current node.
const { pipelines } = await get("/api/lanes/pipelines");
const { pipelines } = await get("/api/workspaces/pipelines");
const stateOf = new Map((lane.pipeline_nodes || []).map((n) => [n.id, n.state]));
const nodes = (pipelines.find((p) => p.id === lane.pipeline)?.nodes || []).map((n) => ({
...n,
@@ -2699,8 +2699,8 @@ async function cmdLanesNode(args) {
}
/**
* `agenthub lanes pipeline [<template-id>] [<id>]` — show or switch which pipeline
* template a lane renders against. `agenthub lanes add --pipeline` could only set
* `agenthub workspace pipeline [<template-id>] [<id>]` — show or switch which pipeline
* template a lane renders against. `agenthub workspace add --pipeline` could only set
* this at creation time, so every lane added from the dashboard's "+ Add lane"
* was stuck on `default` with no way back to a 16-node template.
*/
@@ -2710,15 +2710,15 @@ async function cmdLanesPipeline(args) {
if (!resolved) return;
if (!target) {
const { lane } = await get(`/api/lanes/${resolved.laneId}`);
const { pipelines } = await get("/api/lanes/pipelines");
const { lane } = await get(`/api/workspaces/${resolved.laneId}`);
const { pipelines } = await get("/api/workspaces/pipelines");
console.log(`workspace #${lane.id} → ${lane.pipeline} (${lane.pipeline_nodes.length} nodes)`);
if (lane.pipeline_skill) console.log(`driver: ${lane.pipeline_skill} <requirement>`);
console.log(`available: ${pipelines.map((p) => `${p.id} (${p.nodes.length})`).join(", ")}`);
return;
}
const { lane } = await patch(`/api/lanes/${resolved.laneId}`, { pipeline: target });
const { lane } = await patch(`/api/workspaces/${resolved.laneId}`, { pipeline: target });
console.log(
`${c.green("✔")} workspace #${lane.id} → pipeline ${lane.pipeline} ` +
`(${lane.pipeline_nodes.length} nodes, stage ${lane.stage}, ${lane.progress}%)`
@@ -2746,7 +2746,7 @@ async function cmdLanesPipeline(args) {
async function cmdLanesShell(args) {
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const { lane } = await get(`/api/lanes/${resolved.laneId}`);
const { lane } = await get(`/api/workspaces/${resolved.laneId}`);
const sessionName = `agenthub-lane-${lane.id}`;
const child = spawn("tmux", ["new-session", "-A", "-s", sessionName, "-c", lane.cwd], {
stdio: "inherit",
@@ -2771,7 +2771,7 @@ async function cmdFeatureShow(args) {
const resolved = await resolveLaneArg(args.filter((a) => a !== slug));
if (!resolved) return;
const result = await get(
`/api/lanes/${resolved.laneId}/features/${encodeURIComponent(slug)}`,
`/api/workspaces/${resolved.laneId}/features/${encodeURIComponent(slug)}`,
undefined,
{ allowError: true }
);
@@ -2806,7 +2806,7 @@ async function cmdStage(args) {
if (!resolved) return;
const laneId = resolved.laneId;
const { lane } = await post(`/api/lanes/${laneId}/stage`, {
const { lane } = await post(`/api/workspaces/${laneId}/stage`, {
stage,
status: flag("status"),
evidence: flag("evidence"),
@@ -3101,8 +3101,8 @@ falling back to http://127.0.0.1:4820.
${c.bold("Output:")} colors auto-enable on a TTY and turn off when piped.
Disable with --no-color or NO_COLOR=1; force with FORCE_COLOR=1 / AGENTHUB_COLOR=1.
${c.bold("Old name:")} ${c.bold("agenthub lanes …")} and ${c.bold("--lane")} still work — the same
commands as ${c.bold("agenthub workspace …")} and ${c.bold("--workspace")}.
${c.bold("Old name:")} ${c.bold("agenthub lanes …")} and ${c.bold("--lane")} were removed — use
${c.bold("agenthub workspace …")} and ${c.bold("--workspace")}.
${c.bold("Note:")} agenthub talks to the local dashboard server — API-backed commands
require it to be running (${c.bold("agenthub start")} brings one up in the background).
@@ -3512,8 +3512,8 @@ function replHelp() {
/** Detailed help for one command (or command prefix): every catalog entry
* whose invocation starts with the token. Returns false if none matched. */
function commandHelp(token) {
// `lanes`, `workspace` and `workspaces` are one command (COMMAND_ALIASES),
// so `help lanes` finds the `workspace …` rows and vice versa.
// `workspace` and `workspaces` are one command (COMMAND_ALIASES), so
// `help workspaces` finds the `workspace …` rows and vice versa.
const canon = (words) => {
const [head, ...tail] = words.split(" ");
return [COMMAND_ALIASES[head] || head, ...tail].join(" ");
@@ -3774,23 +3774,22 @@ async function cmdRepl() {
// prompt. Handlers that need the server throw ServerDownError; the caller
// decides whether to fall back to an offline reader or report the refusal.
/** Command names that are another command under the user-facing word.
* `workspace` is the name to use; `lanes` (the handler's own name) and
* `--lane` are the OLD names, still working but deprecated. Removing them is
* step 4 of docs/plans/workspace-rename/README.md — not before the third
* release after the first one that shipped these aliases. */
const COMMAND_ALIASES = { workspace: "lanes", workspaces: "lanes" };
/** Command names that are another command under the user-facing word. */
const COMMAND_ALIASES = { workspaces: "workspace" };
const canonicalArgv = (argv) =>
COMMAND_ALIASES[argv[0]] ? [COMMAND_ALIASES[argv[0]], ...argv.slice(1)] : argv;
/** One stderr line when an old name is typed, so people and agents move over
* before step 4 removes it. stderr only: scripts reading stdout see nothing. */
function warnOldNames(argv) {
const old = [];
if (argv[0] === "lanes") old.push("`agenthub lanes` → `agenthub workspace`");
if (argv.includes("--lane")) old.push("`--lane` → `--workspace`");
if (old.length) {
console.error(c.dim(`note: old name, still works for now — use ${old.join(", ")}`));
/** The old names `agenthub workspace` and `--lane` were removed in step 4 of
* docs/plans/workspace-rename/README.md. They exit 1 naming the new spelling
* — never "Unknown command" or help, so a script written before says why. */
function refuseOldNames(argv) {
if (argv[0] === "lanes") {
console.error(c.red("✖ renamed: use agenthub workspace …"));
process.exit(1);
}
if (argv.includes("--lane")) {
console.error(c.red("✖ renamed: use --workspace <id>"));
process.exit(1);
}
}
@@ -3803,7 +3802,6 @@ const COMMANDS = (() => {
for (const [name] of items) set.add(name.split(" ")[0]);
}
set.add("help");
// The old names stay typeable (and completable) for scripts written before.
for (const [alias, canonical] of Object.entries(COMMAND_ALIASES)) set.add(alias).add(canonical);
return [...set];
})();
@@ -4498,7 +4496,7 @@ async function cmdAccounts(sub, positional, flags = {}) {
* for usage/validation failures. `cmd === "repl"` is handled by the entry
* points, not here, to avoid recursive REPLs. */
async function runCommand(argv) {
warnOldNames(argv);
refuseOldNames(argv);
const [cmd, ...rest] = canonicalArgv(argv);
const { flags, positional } = parseArgs(rest);
switch (cmd) {
@@ -4571,7 +4569,7 @@ async function runCommand(argv) {
return cmdReinstallHooks();
case "update-check":
return cmdUpdateCheck();
case "lanes":
case "workspace":
if (rest[0] === "add") {
return cmdLanesAdd(rest.slice(1));
}