From 5b2f98ab4dbb38d1ad882ac455fe9263ab573557 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Tue, 4 Aug 2026 14:43:48 +0700 Subject: [PATCH] feat(lanes): add ccam feature list/activate/show CLI (B) --- bin/ccam.js | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/bin/ccam.js b/bin/ccam.js index 2611890..9ea982d 100755 --- a/bin/ccam.js +++ b/bin/ccam.js @@ -190,7 +190,7 @@ async function api(method, pathname, body, options = {}) { } 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); /** @@ -2031,6 +2031,74 @@ async function cmdLockRelease(args) { * `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. */ +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 [] [--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 "); + return; + } + for (const f of features) console.log(fmtFeatureRow(f)); +} + +/** `ccam feature activate [--title X] [] [--cwd path]`. */ +async function cmdFeatureActivate(args) { + const slug = args.find((arg) => !arg.startsWith("--")); + if (!slug) { + console.error("usage: ccam feature activate [--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 [] [--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 "); + 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) { const stage = args[0]; 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)", ], ["stage [flags]", "", "Report the current pipeline stage for a lane"], + ["feature list", "[]", "List every feature this lane has activated, archived or live"], + [ + "feature activate", + " [--title text] []", + "Switch to a feature by slug, archiving the current one first (echoes the canonicalized slug)", + ], + [ + "feature show", + " []", + "Show one feature's saved pipeline (works on an archived one too)", + ], ], ], [ @@ -3002,6 +3081,17 @@ async function runCommand(argv) { } case "stage": 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 [--title text] | ccam feature show " + ); + process.exitCode = 1; + return; + } case "open": return cmdOpen(); case "version":