itsvertical 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  **Tickets pile up, scopes get done. Project work isn't linear, it's Vertical.**
4
4
 
5
- Vertical is a file-based project management tool built around the nine-box grid. No accounts, no cloud, no setup. Just a `.vertical` file and your terminal.
5
+ Vertical is a file-based project management tool that organizes work into vertical slices that can each be completed independently. No accounts, no cloud, no setup. Just a `.vertical` file and your terminal.
6
+
7
+ ![Vertical board](app/images/screenshot.png)
6
8
 
7
9
  ```
8
10
  npx itsvertical new my-project.vertical "My Project"
@@ -10,13 +12,13 @@ npx itsvertical new my-project.vertical "My Project"
10
12
 
11
13
  ## Built for AI agents
12
14
 
13
- Vertical is designed to be used through AI coding agents like [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview). The CLI is the primary interface — every entity is addressed by ID, every command accepts `--json` for structured output, and errors are machine-readable. An agent can create a project, break work into scopes, add tasks, and track progress — all through the command line.
15
+ Vertical is designed to be used through AI coding agents. The CLI is the primary interface — every entity is addressed by ID, every command accepts `--json` for structured output, and errors are machine-readable. An agent can create a project, break work into slices, add tasks, and track progress — all through the command line.
14
16
 
15
17
  The browser UI (`itsvertical open`) is there for when you want to see the board visually, drag things around, or get a quick overview.
16
18
 
17
19
  ## How it works
18
20
 
19
- Vertical organizes work into a 3x3 grid of boxes. Each box can hold tasks, and each box can be split into layers (steps). Tasks move between boxes, layers break work into phases, and things get marked done as you go.
21
+ Each slice is a box on the board. Add tasks to a box, and split it into layers when the work has distinct phases design then build, for example. Slices get completed independently. Layers break the work within a slice into steps.
20
22
 
21
23
  Everything is saved to a single `.vertical` file. Version it with git, share it with teammates, or let your agent manage it.
22
24
 
@@ -32,6 +34,16 @@ Or run directly with npx:
32
34
  npx itsvertical new my-project.vertical "My Project"
33
35
  ```
34
36
 
37
+ ### Agent skill
38
+
39
+ Install the [Vertical skill](https://skills.sh/seasonedcc/vertical/vertical) to teach your AI agent how to use Vertical:
40
+
41
+ ```
42
+ npx skills add seasonedcc/vertical
43
+ ```
44
+
45
+ This gives agents like Claude Code, Cursor, and GitHub Copilot procedural knowledge of all Vertical commands and workflows.
46
+
35
47
  ## Commands
36
48
 
37
49
  All entities are addressed by ID. Use `itsvertical show` to see IDs. Every command accepts `--json` to output the full board state as JSON (useful for agents).
@@ -44,6 +56,7 @@ itsvertical open <file> # Open in the browser UI
44
56
  itsvertical show <file> # Print the board to the terminal
45
57
  itsvertical show <file> --json # Output the board as JSON
46
58
  itsvertical show <file> --box <slice-id> # Show only a specific box
59
+ itsvertical show <file> --visual # Show the board as a visual 3x3 grid with summary
47
60
  itsvertical rename <file> <name> # Rename the project
48
61
  ```
49
62
 
@@ -82,9 +95,9 @@ itsvertical layer status <file> <layer-id> none # Clear status
82
95
 
83
96
  `itsvertical open` starts a local server and opens the board in your browser. Click **Save** or press **Ctrl+S** / **Cmd+S** to write changes back to the file.
84
97
 
85
- ## The nine-box grid
98
+ ## The board
86
99
 
87
- The board is a 3x3 grid. Each box represents a scope of work.
100
+ Each box represents a vertical slice of work.
88
101
 
89
102
  - **Name boxes** by clicking the title area
90
103
  - **Drag boxes** to rearrange them in the grid
@@ -129,7 +142,7 @@ Source at [github.com/seasonedcc/vertical](https://github.com/seasonedcc/vertica
129
142
 
130
143
  The package has two parts:
131
144
 
132
- - **SPA** (`app/`) — A React app built with Vite. This is the nine-box board UI. Built to `dist/`.
145
+ - **SPA** (`app/`) — A React app built with Vite. The board UI. Built to `dist/`.
133
146
  - **CLI** (`cli/`) — A Node.js CLI built with tsup. Starts a local HTTP server that serves the SPA and provides a read/write API for the `.vertical` file. Built to `cli/dist/`.
134
147
 
135
148
  The CLI and SPA share code: types (`app/state/types.ts`), serialization (`app/file/format.ts`), and project creation (`app/state/initial-state.ts`).
package/cli/dist/index.js CHANGED
@@ -1219,8 +1219,8 @@ var Promise2 = getNative_default(root_default, "Promise");
1219
1219
  var Promise_default = Promise2;
1220
1220
 
1221
1221
  // node_modules/.pnpm/lodash-es@4.17.23/node_modules/lodash-es/_Set.js
1222
- var Set = getNative_default(root_default, "Set");
1223
- var Set_default = Set;
1222
+ var Set2 = getNative_default(root_default, "Set");
1223
+ var Set_default = Set2;
1224
1224
 
1225
1225
  // node_modules/.pnpm/lodash-es@4.17.23/node_modules/lodash-es/_getTag.js
1226
1226
  var mapTag2 = "[object Map]";
@@ -1921,10 +1921,306 @@ function fail(message, json) {
1921
1921
  process.exit(1);
1922
1922
  }
1923
1923
 
1924
+ // cli/board.ts
1925
+ var MIN_COL_WIDTH = 25;
1926
+ var MAX_COL_WIDTH = 40;
1927
+ var PADDING = 2;
1928
+ var ansi = {
1929
+ reset: "\x1B[0m",
1930
+ bold: "\x1B[1m",
1931
+ dim: "\x1B[2m",
1932
+ green: "\x1B[32m",
1933
+ yellow: "\x1B[33m",
1934
+ cyan: "\x1B[36m"
1935
+ };
1936
+ function style(text, ...codes) {
1937
+ return `${codes.join("")}${text}${ansi.reset}`;
1938
+ }
1939
+ var ANSI_PATTERN = new RegExp(`${"\x1B"}\\[[0-9;]*m`, "g");
1940
+ function stripAnsi(text) {
1941
+ return text.replace(ANSI_PATTERN, "");
1942
+ }
1943
+ function visibleLength(text) {
1944
+ return stripAnsi(text).length;
1945
+ }
1946
+ function truncateStyled(text, maxWidth) {
1947
+ if (visibleLength(text) <= maxWidth)
1948
+ return text;
1949
+ let visible = 0;
1950
+ let i = 0;
1951
+ while (i < text.length && visible < maxWidth - 1) {
1952
+ if (text[i] === "\x1B") {
1953
+ const end = text.indexOf("m", i);
1954
+ i = end + 1;
1955
+ continue;
1956
+ }
1957
+ visible++;
1958
+ i++;
1959
+ }
1960
+ return `${text.slice(0, i)}${ansi.reset}\u2026`;
1961
+ }
1962
+ function padRightStyled(text, width) {
1963
+ const padding = Math.max(0, width - visibleLength(text));
1964
+ return text + " ".repeat(padding);
1965
+ }
1966
+ function getSliceLayers(state, slice) {
1967
+ return sortBy_default(
1968
+ state.layers.filter((l) => l.sliceId === slice.id),
1969
+ "sorting"
1970
+ );
1971
+ }
1972
+ function getLayerTasks(state, layer2) {
1973
+ return sortBy_default(
1974
+ state.tasks.filter((t) => t.layerId === layer2.id),
1975
+ "sorting"
1976
+ );
1977
+ }
1978
+ function getSliceTasks(state, slice) {
1979
+ const layers = getSliceLayers(state, slice);
1980
+ return layers.flatMap((l) => getLayerTasks(state, l));
1981
+ }
1982
+ function buildCellLines(state, slice, innerWidth) {
1983
+ const lines = [];
1984
+ const layers = getSliceLayers(state, slice);
1985
+ const allTasks = getSliceTasks(state, slice);
1986
+ const hasMultipleLayers = layers.length > 1;
1987
+ const separator = style("\u2504".repeat(innerWidth), ansi.dim);
1988
+ if (slice.name) {
1989
+ lines.push(style(slice.name, ansi.bold, ansi.cyan));
1990
+ }
1991
+ if (allTasks.length === 0) {
1992
+ lines.push(style("(no tasks)", ansi.dim));
1993
+ return lines;
1994
+ }
1995
+ for (let i = 0; i < layers.length; i++) {
1996
+ const layer2 = layers[i];
1997
+ const tasks = getLayerTasks(state, layer2);
1998
+ if (hasMultipleLayers && i > 0) {
1999
+ lines.push(separator);
2000
+ }
2001
+ if (hasMultipleLayers) {
2002
+ const layerName = layer2.name ?? `Layer ${i + 1}`;
2003
+ if (layer2.status === "done") {
2004
+ lines.push(style(`\u2713 ${layerName} (done)`, ansi.green));
2005
+ } else {
2006
+ lines.push(style(layerName, ansi.bold));
2007
+ }
2008
+ }
2009
+ for (const task2 of tasks) {
2010
+ if (task2.done) {
2011
+ lines.push(` ${style("\u25CF", ansi.green)} ${style(task2.name, ansi.dim)}`);
2012
+ } else {
2013
+ lines.push(` ${style("\u25CB", ansi.yellow)} ${task2.name}`);
2014
+ }
2015
+ }
2016
+ }
2017
+ return lines;
2018
+ }
2019
+ function showBoardGrid(state, boxId) {
2020
+ const sortedSlices = sortBy_default(state.slices, "boxNumber");
2021
+ const filteredSlices = boxId ? sortedSlices.filter((s) => s.id === boxId) : sortedSlices;
2022
+ const colWidth = Math.min(
2023
+ MAX_COL_WIDTH,
2024
+ Math.max(
2025
+ MIN_COL_WIDTH,
2026
+ ...filteredSlices.flatMap((slice) => {
2027
+ const layers = getSliceLayers(state, slice);
2028
+ const tasks = getSliceTasks(state, slice);
2029
+ const widths = [
2030
+ slice.name?.length ?? 0,
2031
+ ...layers.map((l) => (l.name?.length ?? 0) + 10),
2032
+ ...tasks.map((t) => t.name.length + 4)
2033
+ ];
2034
+ return widths;
2035
+ }).map((w) => w + PADDING * 2)
2036
+ )
2037
+ );
2038
+ const innerWidth = colWidth - PADDING * 2;
2039
+ const numCols = boxId ? 1 : 3;
2040
+ const cellContents = /* @__PURE__ */ new Map();
2041
+ for (const slice of filteredSlices) {
2042
+ cellContents.set(slice.id, buildCellLines(state, slice, innerWidth));
2043
+ }
2044
+ const rows = [];
2045
+ if (boxId) {
2046
+ rows.push(filteredSlices);
2047
+ } else {
2048
+ rows.push(
2049
+ filteredSlices.filter((s) => s.boxNumber >= 1 && s.boxNumber <= 3)
2050
+ );
2051
+ rows.push(
2052
+ filteredSlices.filter((s) => s.boxNumber >= 4 && s.boxNumber <= 6)
2053
+ );
2054
+ rows.push(
2055
+ filteredSlices.filter((s) => s.boxNumber >= 7 && s.boxNumber <= 9)
2056
+ );
2057
+ }
2058
+ console.log(style(state.project.name, ansi.bold));
2059
+ console.log();
2060
+ const border = (text) => style(text, ansi.dim);
2061
+ const makeBorder = (left, mid2, right) => {
2062
+ const parts = [];
2063
+ for (let c = 0; c < numCols; c++) {
2064
+ parts.push("\u2500".repeat(colWidth));
2065
+ }
2066
+ return border(left + parts.join(mid2) + right);
2067
+ };
2068
+ const top = makeBorder("\u250C", "\u252C", "\u2510");
2069
+ const mid = makeBorder("\u251C", "\u253C", "\u2524");
2070
+ const bot = makeBorder("\u2514", "\u2534", "\u2518");
2071
+ for (let r = 0; r < rows.length; r++) {
2072
+ const row = rows[r];
2073
+ console.log(r === 0 ? top : mid);
2074
+ const cellLines = row.map((slice) => cellContents.get(slice.id) ?? []);
2075
+ const rowHeight = Math.max(3, ...cellLines.map((l) => l.length + 2));
2076
+ for (let line = 0; line < rowHeight; line++) {
2077
+ let output2 = border("\u2502");
2078
+ for (let c = 0; c < numCols; c++) {
2079
+ const lines = cellLines[c] ?? [];
2080
+ const contentLine = line - 1;
2081
+ let content = "";
2082
+ if (contentLine >= 0 && contentLine < lines.length) {
2083
+ content = truncateStyled(lines[contentLine], innerWidth);
2084
+ }
2085
+ output2 += `${" ".repeat(PADDING) + padRightStyled(content, innerWidth) + " ".repeat(PADDING)}${border("\u2502")}`;
2086
+ }
2087
+ console.log(output2);
2088
+ }
2089
+ }
2090
+ console.log(bot);
2091
+ const totalTasks = state.tasks.length;
2092
+ const doneTasks = state.tasks.filter((t) => t.done).length;
2093
+ const progressColor = doneTasks === totalTasks ? ansi.green : ansi.yellow;
2094
+ console.log();
2095
+ console.log(
2096
+ `${style("Progress:", ansi.bold)} ${style(`${doneTasks}/${totalTasks}`, ansi.bold, progressColor)} tasks done`
2097
+ );
2098
+ }
2099
+ function formatGroupLabel(boxNumbers) {
2100
+ if (boxNumbers.length === 0)
2101
+ return "";
2102
+ if (boxNumbers.length === 1)
2103
+ return `Box ${boxNumbers[0]}`;
2104
+ const ranges = [];
2105
+ let start = boxNumbers[0];
2106
+ let end = boxNumbers[0];
2107
+ for (let i = 1; i < boxNumbers.length; i++) {
2108
+ if (boxNumbers[i] === end + 1) {
2109
+ end = boxNumbers[i];
2110
+ } else {
2111
+ ranges.push(start === end ? `${start}` : `${start}\u2013${end}`);
2112
+ start = boxNumbers[i];
2113
+ end = boxNumbers[i];
2114
+ }
2115
+ }
2116
+ ranges.push(start === end ? `${start}` : `${start}\u2013${end}`);
2117
+ return `Boxes ${ranges.join(", ")}`;
2118
+ }
2119
+ function buildStatusText(state, slice) {
2120
+ const layers = getSliceLayers(state, slice);
2121
+ const tasks = getSliceTasks(state, slice);
2122
+ if (tasks.length === 0)
2123
+ return style("No tasks yet", ansi.dim);
2124
+ const doneTasks = tasks.filter((t) => t.done).length;
2125
+ const totalTasks = tasks.length;
2126
+ if (doneTasks === totalTasks)
2127
+ return style("All done", ansi.green);
2128
+ if (layers.length > 1) {
2129
+ const doneLayerNames = layers.filter((l) => l.status === "done").map((l) => {
2130
+ const idx = layers.indexOf(l);
2131
+ return l.name ?? `Layer ${idx + 1}`;
2132
+ });
2133
+ const openCount = totalTasks - doneTasks;
2134
+ const parts = [];
2135
+ if (doneLayerNames.length > 0) {
2136
+ parts.push(`${doneLayerNames.join(", ")} marked done`);
2137
+ }
2138
+ if (openCount > 0) {
2139
+ parts.push(`${openCount} task${openCount === 1 ? "" : "s"} still open`);
2140
+ }
2141
+ return parts.join(", ");
2142
+ }
2143
+ return `${doneTasks}/${totalTasks} tasks done`;
2144
+ }
2145
+ function showSummaryTable(state, boxId) {
2146
+ const sortedSlices = sortBy_default(state.slices, "boxNumber");
2147
+ const filteredSlices = boxId ? sortedSlices.filter((s) => s.id === boxId) : sortedSlices;
2148
+ const summaryRows = [];
2149
+ const unnamedEmpty = [];
2150
+ for (const slice of filteredSlices) {
2151
+ const tasks = getSliceTasks(state, slice);
2152
+ if (!slice.name && tasks.length === 0) {
2153
+ unnamedEmpty.push(slice.boxNumber);
2154
+ continue;
2155
+ }
2156
+ const doneTasks = tasks.filter((t) => t.done).length;
2157
+ const doneColor = tasks.length > 0 && doneTasks === tasks.length ? ansi.green : ansi.yellow;
2158
+ summaryRows.push({
2159
+ scope: style(slice.name ?? `Box ${slice.boxNumber}`, ansi.bold),
2160
+ done: tasks.length > 0 ? style(`${doneTasks}/${tasks.length}`, doneColor) : style("\u2014", ansi.dim),
2161
+ status: buildStatusText(state, slice)
2162
+ });
2163
+ }
2164
+ if (unnamedEmpty.length > 0) {
2165
+ summaryRows.push({
2166
+ scope: style(formatGroupLabel(unnamedEmpty), ansi.dim),
2167
+ done: style("\u2014", ansi.dim),
2168
+ status: style("Unnamed & empty", ansi.dim)
2169
+ });
2170
+ }
2171
+ if (summaryRows.length === 0)
2172
+ return;
2173
+ const headers = {
2174
+ scope: style("Scope", ansi.bold),
2175
+ done: style("Done", ansi.bold),
2176
+ status: style("Status", ansi.bold)
2177
+ };
2178
+ const scopeWidth = Math.max(
2179
+ visibleLength(headers.scope),
2180
+ ...summaryRows.map((r) => visibleLength(r.scope))
2181
+ );
2182
+ const doneWidth = Math.max(
2183
+ visibleLength(headers.done),
2184
+ ...summaryRows.map((r) => visibleLength(r.done))
2185
+ );
2186
+ const statusWidth = Math.max(
2187
+ visibleLength(headers.status),
2188
+ ...summaryRows.map((r) => visibleLength(r.status))
2189
+ );
2190
+ const pad = (text, width) => ` ${padRightStyled(text, width)} `;
2191
+ const centerPad = (text, width) => {
2192
+ const totalPad = width - visibleLength(text);
2193
+ const left = Math.floor(totalPad / 2);
2194
+ const right = totalPad - left;
2195
+ return ` ${" ".repeat(left)}${text}${" ".repeat(right)} `;
2196
+ };
2197
+ const border = (text) => style(text, ansi.dim);
2198
+ const hLine = (left, mid, right) => border(
2199
+ `${left}${"\u2500".repeat(scopeWidth + 2)}${mid}${"\u2500".repeat(doneWidth + 2)}${mid}${"\u2500".repeat(statusWidth + 2)}${right}`
2200
+ );
2201
+ console.log();
2202
+ console.log(hLine("\u250C", "\u252C", "\u2510"));
2203
+ console.log(
2204
+ `${border("\u2502")}${centerPad(headers.scope, scopeWidth)}${border("\u2502")}${centerPad(headers.done, doneWidth)}${border("\u2502")}${centerPad(headers.status, statusWidth)}${border("\u2502")}`
2205
+ );
2206
+ console.log(hLine("\u251C", "\u253C", "\u2524"));
2207
+ for (let i = 0; i < summaryRows.length; i++) {
2208
+ const row = summaryRows[i];
2209
+ console.log(
2210
+ `${border("\u2502")}${pad(row.scope, scopeWidth)}${border("\u2502")}${pad(row.done, doneWidth)}${border("\u2502")}${pad(row.status, statusWidth)}${border("\u2502")}`
2211
+ );
2212
+ if (i < summaryRows.length - 1) {
2213
+ console.log(hLine("\u251C", "\u253C", "\u2524"));
2214
+ }
2215
+ }
2216
+ console.log(hLine("\u2514", "\u2534", "\u2518"));
2217
+ }
2218
+
1924
2219
  // cli/server.ts
1925
2220
  import fs2 from "fs";
1926
2221
  import http from "http";
1927
2222
  import path2 from "path";
2223
+ import readline from "readline";
1928
2224
  import { fileURLToPath } from "url";
1929
2225
  import getPort from "get-port";
1930
2226
  import open from "open";
@@ -1969,9 +2265,24 @@ function readRequestBody(req) {
1969
2265
  req.on("error", reject);
1970
2266
  });
1971
2267
  }
2268
+ function confirm(question) {
2269
+ const rl = readline.createInterface({
2270
+ input: process.stdin,
2271
+ output: process.stdout
2272
+ });
2273
+ rl.on("error", () => {
2274
+ });
2275
+ return new Promise((resolve) => {
2276
+ rl.question(question, (answer) => {
2277
+ rl.close();
2278
+ resolve(answer.toLowerCase() === "y");
2279
+ });
2280
+ });
2281
+ }
1972
2282
  async function startServer(filePath) {
1973
2283
  const absoluteFilePath = path2.resolve(filePath);
1974
2284
  const distPath = getDistPath();
2285
+ let browserDirty = false;
1975
2286
  if (!fs2.existsSync(distPath)) {
1976
2287
  console.error(
1977
2288
  "Error: dist/ directory not found. Run `pnpm run build` first."
@@ -1993,6 +2304,23 @@ async function startServer(filePath) {
1993
2304
  res.end('{"ok":true}');
1994
2305
  return;
1995
2306
  }
2307
+ if (url2 === "/api/events" && req.method === "GET") {
2308
+ res.writeHead(200, {
2309
+ "Content-Type": "text/event-stream",
2310
+ "Cache-Control": "no-cache",
2311
+ Connection: "keep-alive"
2312
+ });
2313
+ sseClients.add(res);
2314
+ req.on("close", () => sseClients.delete(res));
2315
+ return;
2316
+ }
2317
+ if (url2 === "/api/dirty" && req.method === "POST") {
2318
+ const body = await readRequestBody(req);
2319
+ browserDirty = JSON.parse(body).dirty;
2320
+ res.writeHead(200, { "Content-Type": "application/json" });
2321
+ res.end('{"ok":true}');
2322
+ return;
2323
+ }
1996
2324
  if (serveStaticFile(res, distPath, url2))
1997
2325
  return;
1998
2326
  const indexPath = path2.join(distPath, "index.html");
@@ -2006,6 +2334,12 @@ async function startServer(filePath) {
2006
2334
  });
2007
2335
  const port = await getPort();
2008
2336
  const url = `http://localhost:${port}`;
2337
+ const sseClients = /* @__PURE__ */ new Set();
2338
+ fs2.watch(absoluteFilePath, () => {
2339
+ for (const client of sseClients) {
2340
+ client.write("data: file-changed\n\n");
2341
+ }
2342
+ });
2009
2343
  server.listen(port, () => {
2010
2344
  console.log(`
2011
2345
  Vertical is running at ${url}`);
@@ -2013,6 +2347,22 @@ async function startServer(filePath) {
2013
2347
  console.log(" Press Ctrl+C to stop\n");
2014
2348
  open(url);
2015
2349
  });
2350
+ process.on("SIGINT", async () => {
2351
+ if (browserDirty) {
2352
+ try {
2353
+ const shouldQuit = await confirm(
2354
+ "\n There are unsaved changes in the browser. Quit anyway? (y/N) "
2355
+ );
2356
+ if (!shouldQuit) {
2357
+ console.log(" Resuming...\n");
2358
+ return;
2359
+ }
2360
+ } catch {
2361
+ }
2362
+ }
2363
+ server.close();
2364
+ process.exit(0);
2365
+ });
2016
2366
  }
2017
2367
 
2018
2368
  // cli/index.ts
@@ -2051,21 +2401,26 @@ program.command("open").description("Open an existing .vertical file in the brow
2051
2401
  const filePath = resolveFilePath(file);
2052
2402
  await startServer(filePath);
2053
2403
  });
2054
- program.command("show").description("Print the board to the terminal").argument("<file>", "Path to the .vertical file").option("--json", "Output as JSON").option("--box <slice-id>", "Show only a specific box").action((file, options) => {
2055
- const filePath = resolveFilePath(file, options.json);
2056
- const state = loadState(filePath);
2057
- if (options.box) {
2058
- const slice = state.slices.find((s) => s.id === options.box);
2059
- if (!slice) {
2060
- fail(`Box not found: ${options.box}`, options.json);
2404
+ program.command("show").description("Print the board to the terminal").argument("<file>", "Path to the .vertical file").option("--json", "Output as JSON").option("--box <slice-id>", "Show only a specific box").option("--visual", "Show the board as a visual 3x3 grid with summary").action(
2405
+ (file, options) => {
2406
+ const filePath = resolveFilePath(file, options.json);
2407
+ const state = loadState(filePath);
2408
+ if (options.box) {
2409
+ const slice = state.slices.find((s) => s.id === options.box);
2410
+ if (!slice) {
2411
+ fail(`Box not found: ${options.box}`, options.json);
2412
+ }
2413
+ }
2414
+ if (options.json) {
2415
+ showBoardJson(state);
2416
+ } else if (options.visual) {
2417
+ showBoardGrid(state, options.box);
2418
+ showSummaryTable(state, options.box);
2419
+ } else {
2420
+ showBoard(state, options.box);
2061
2421
  }
2062
2422
  }
2063
- if (options.json) {
2064
- showBoardJson(state);
2065
- } else {
2066
- showBoard(state, options.box);
2067
- }
2068
- });
2423
+ );
2069
2424
  program.command("rename").description("Rename the project").argument("<file>", "Path to the .vertical file").argument("<name>", "New project name").option("--json", "Output as JSON").action((file, name, options) => {
2070
2425
  const filePath = resolveFilePath(file, options.json);
2071
2426
  const state = applyAction(filePath, { type: "RENAME_PROJECT", name });