itsvertical 0.0.2 → 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
|
|
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
|
+

|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -54,6 +56,7 @@ itsvertical open <file> # Open in the browser UI
|
|
|
54
56
|
itsvertical show <file> # Print the board to the terminal
|
|
55
57
|
itsvertical show <file> --json # Output the board as JSON
|
|
56
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
|
|
57
60
|
itsvertical rename <file> <name> # Rename the project
|
|
58
61
|
```
|
|
59
62
|
|
|
@@ -92,9 +95,9 @@ itsvertical layer status <file> <layer-id> none # Clear status
|
|
|
92
95
|
|
|
93
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.
|
|
94
97
|
|
|
95
|
-
## The
|
|
98
|
+
## The board
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
Each box represents a vertical slice of work.
|
|
98
101
|
|
|
99
102
|
- **Name boxes** by clicking the title area
|
|
100
103
|
- **Drag boxes** to rearrange them in the grid
|
|
@@ -139,7 +142,7 @@ Source at [github.com/seasonedcc/vertical](https://github.com/seasonedcc/vertica
|
|
|
139
142
|
|
|
140
143
|
The package has two parts:
|
|
141
144
|
|
|
142
|
-
- **SPA** (`app/`) — A React app built with Vite.
|
|
145
|
+
- **SPA** (`app/`) — A React app built with Vite. The board UI. Built to `dist/`.
|
|
143
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/`.
|
|
144
147
|
|
|
145
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
|
@@ -1921,6 +1921,301 @@ 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";
|
|
@@ -2106,21 +2401,26 @@ program.command("open").description("Open an existing .vertical file in the brow
|
|
|
2106
2401
|
const filePath = resolveFilePath(file);
|
|
2107
2402
|
await startServer(filePath);
|
|
2108
2403
|
});
|
|
2109
|
-
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").
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
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);
|
|
2116
2421
|
}
|
|
2117
2422
|
}
|
|
2118
|
-
|
|
2119
|
-
showBoardJson(state);
|
|
2120
|
-
} else {
|
|
2121
|
-
showBoard(state, options.box);
|
|
2122
|
-
}
|
|
2123
|
-
});
|
|
2423
|
+
);
|
|
2124
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) => {
|
|
2125
2425
|
const filePath = resolveFilePath(file, options.json);
|
|
2126
2426
|
const state = applyAction(filePath, { type: "RENAME_PROJECT", name });
|