feat(lanes): add ccam feature list/activate/show CLI (B)

This commit is contained in:
2026-08-04 14:43:48 +07:00
parent 5bc4d03740
commit 5b2f98ab4d
+91 -1
View File
@@ -190,7 +190,7 @@ async function api(method, pathname, body, options = {}) {
} }
return data; return data;
} }
const get = (p) => api("GET", p); const get = (p, b, options) => api("GET", p, undefined, options);
const post = (p, b, options) => api("POST", p, b, options); const post = (p, b, options) => api("POST", p, b, options);
/** /**
@@ -2031,6 +2031,74 @@ async function cmdLockRelease(args) {
* `state.sh N set stage=…`. A skill calls this at each phase boundary so the * `state.sh N set stage=…`. A skill calls this at each phase boundary so the
* dashboard shows a declared stage instead of an inferred one. * dashboard shows a declared stage instead of an inferred one.
*/ */
function fmtFeatureRow(f) {
const marker = f.archived_at ? " " : "▶ ";
return `${marker}${f.slug.padEnd(24)} ${String(f.stage).padEnd(12)} ${f.progress}%${
f.archived_at ? ` (archived ${fmtTime(f.archived_at)})` : ""
}`;
}
/** `ccam feature list [<id>] [--cwd path]` — every feature this lane has activated. */
async function cmdFeatureList(args) {
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const { features } = await get(`/api/lanes/${resolved.laneId}/features`);
if (!features.length) {
console.log("no features activated yet — start one with: ccam feature activate <slug>");
return;
}
for (const f of features) console.log(fmtFeatureRow(f));
}
/** `ccam feature activate <slug> [--title X] [<id>] [--cwd path]`. */
async function cmdFeatureActivate(args) {
const slug = args.find((arg) => !arg.startsWith("--"));
if (!slug) {
console.error("usage: ccam feature activate <slug> [--title text]");
process.exitCode = 1;
return;
}
const flag = (name) => {
const i = args.indexOf(`--${name}`);
return i > -1 ? args[i + 1] : undefined;
};
const resolved = await resolveLaneArg(args.filter((a) => a !== slug));
if (!resolved) return;
const { lane, feature } = await post(`/api/lanes/${resolved.laneId}/features/activate`, {
slug,
title: flag("title"),
});
console.log(
`${c.green("✔")} lane #${lane.id} now on feature "${feature.slug}" (stage: ${feature.stage}, ${feature.progress}%)`
);
}
/** `ccam feature show <slug> [<id>] [--cwd path]` — one feature's saved pipeline. */
async function cmdFeatureShow(args) {
const slug = args.find((arg) => !arg.startsWith("--"));
if (!slug) {
console.error("usage: ccam feature show <slug>");
process.exitCode = 1;
return;
}
const resolved = await resolveLaneArg(args.filter((a) => a !== slug));
if (!resolved) return;
const result = await get(
`/api/lanes/${resolved.laneId}/features/${encodeURIComponent(slug)}`,
undefined,
{ allowError: true }
);
if (result.status) {
console.error(`✖ feature "${slug}" → ${result.data?.error?.message || result.status}`);
process.exitCode = 1;
return;
}
const f = result.feature;
console.log(`${f.slug} ${f.archived_at ? "(archived)" : "(active)"}`);
console.log(` stage: ${f.stage} status: ${f.status} progress: ${f.progress}%`);
for (const node of f.pipeline_nodes) console.log(` ${node.state.padEnd(18)} ${node.label}`);
}
async function cmdStage(args) { async function cmdStage(args) {
const stage = args[0]; const stage = args[0];
if (!stage || stage.startsWith("--")) { if (!stage || stage.startsWith("--")) {
@@ -2188,6 +2256,17 @@ const COMMAND_GROUPS = [
"Cross-lane named lock (serialize builds/e2e across all lanes; holder defaults to the calling lane)", "Cross-lane named lock (serialize builds/e2e across all lanes; holder defaults to the calling lane)",
], ],
["stage <stage> [flags]", "", "Report the current pipeline stage for a lane"], ["stage <stage> [flags]", "", "Report the current pipeline stage for a lane"],
["feature list", "[<id>]", "List every feature this lane has activated, archived or live"],
[
"feature activate",
"<slug> [--title text] [<id>]",
"Switch to a feature by slug, archiving the current one first (echoes the canonicalized slug)",
],
[
"feature show",
"<slug> [<id>]",
"Show one feature's saved pipeline (works on an archived one too)",
],
], ],
], ],
[ [
@@ -3002,6 +3081,17 @@ async function runCommand(argv) {
} }
case "stage": case "stage":
return cmdStage(rest); return cmdStage(rest);
case "feature": {
const sub = rest[0];
if (sub === "list") return cmdFeatureList(rest.slice(1));
if (sub === "activate") return cmdFeatureActivate(rest.slice(1));
if (sub === "show") return cmdFeatureShow(rest.slice(1));
console.error(
"usage: ccam feature list | ccam feature activate <slug> [--title text] | ccam feature show <slug>"
);
process.exitCode = 1;
return;
}
case "open": case "open":
return cmdOpen(); return cmdOpen();
case "version": case "version":