itsvertical 0.0.9 → 0.0.10
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 +11 -0
- package/cli/dist/index.js +181 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -95,6 +95,16 @@ itsvertical layer status <file> <layer-id> done # Set status to "done"
|
|
|
95
95
|
itsvertical layer status <file> <layer-id> none # Clear status
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
+
### Board History
|
|
99
|
+
|
|
100
|
+
Vertical automatically tracks boards you create and open in `~/.vertical/history.json`.
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
itsvertical history list # List all known boards
|
|
104
|
+
itsvertical history add <file> # Manually add a board to history
|
|
105
|
+
itsvertical history remove <name-or-file> # Remove a board from history
|
|
106
|
+
```
|
|
107
|
+
|
|
98
108
|
### Browser UI
|
|
99
109
|
|
|
100
110
|
`itsvertical open` (or just `itsvertical <file>`) starts a local server and opens the board in your browser. Changes are saved automatically.
|
|
@@ -108,6 +118,7 @@ Each box represents a vertical slice of work.
|
|
|
108
118
|
- **Add tasks** by clicking the input at the bottom of a box
|
|
109
119
|
- **Edit tasks** by clicking on them
|
|
110
120
|
- **Mark tasks done** with the circle checkbox
|
|
121
|
+
- **Add notes** to a task by clicking the sticky note icon (rich text editor with formatting, slash commands, and code blocks)
|
|
111
122
|
- **Drag tasks** between boxes and layers
|
|
112
123
|
- **Split layers** with the scissor tool (click ✂ or press **S**, then click a task to split at that point)
|
|
113
124
|
- **Unsplit layers** by focusing the dashed separator and pressing **Delete**
|
package/cli/dist/index.js
CHANGED
|
@@ -1472,8 +1472,8 @@ var require_react = __commonJS({
|
|
|
1472
1472
|
});
|
|
1473
1473
|
|
|
1474
1474
|
// cli/index.ts
|
|
1475
|
-
import
|
|
1476
|
-
import
|
|
1475
|
+
import fs5 from "fs";
|
|
1476
|
+
import path5 from "path";
|
|
1477
1477
|
import { dirname } from "path";
|
|
1478
1478
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1479
1479
|
import { Command } from "commander";
|
|
@@ -2105,10 +2105,70 @@ function showSummaryTable(state, boxId) {
|
|
|
2105
2105
|
console.log(hLine("\u2514", "\u2534", "\u2518"));
|
|
2106
2106
|
}
|
|
2107
2107
|
|
|
2108
|
-
// cli/
|
|
2108
|
+
// cli/history.ts
|
|
2109
2109
|
import fs2 from "fs";
|
|
2110
|
-
import
|
|
2110
|
+
import os from "os";
|
|
2111
2111
|
import path2 from "path";
|
|
2112
|
+
function getHistoryDir() {
|
|
2113
|
+
return path2.join(os.homedir(), ".vertical");
|
|
2114
|
+
}
|
|
2115
|
+
function getHistoryPath() {
|
|
2116
|
+
return path2.join(getHistoryDir(), "history.json");
|
|
2117
|
+
}
|
|
2118
|
+
function ensureHistoryDir() {
|
|
2119
|
+
const dir = getHistoryDir();
|
|
2120
|
+
if (!fs2.existsSync(dir)) {
|
|
2121
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
function loadHistory() {
|
|
2125
|
+
const historyPath = getHistoryPath();
|
|
2126
|
+
if (!fs2.existsSync(historyPath)) {
|
|
2127
|
+
return { version: 1, boards: [] };
|
|
2128
|
+
}
|
|
2129
|
+
const content = fs2.readFileSync(historyPath, "utf-8");
|
|
2130
|
+
return JSON.parse(content);
|
|
2131
|
+
}
|
|
2132
|
+
function saveHistory(history2) {
|
|
2133
|
+
ensureHistoryDir();
|
|
2134
|
+
fs2.writeFileSync(getHistoryPath(), JSON.stringify(history2, null, 2));
|
|
2135
|
+
}
|
|
2136
|
+
function slugify(name) {
|
|
2137
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2138
|
+
}
|
|
2139
|
+
function recordBoard(name, filePath) {
|
|
2140
|
+
const history2 = loadHistory();
|
|
2141
|
+
const slug = slugify(name);
|
|
2142
|
+
const existing = history2.boards.find((b) => slugify(b.name) === slug);
|
|
2143
|
+
if (existing && existing.filePath !== filePath) {
|
|
2144
|
+
throw new Error(
|
|
2145
|
+
`A board named "${existing.name}" is already tracked at: ${existing.filePath}`
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
if (!existing) {
|
|
2149
|
+
history2.boards.push({ name, filePath });
|
|
2150
|
+
saveHistory(history2);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
function forgetBoard(nameOrPath) {
|
|
2154
|
+
const history2 = loadHistory();
|
|
2155
|
+
const slug = slugify(nameOrPath);
|
|
2156
|
+
const absolutePath = path2.resolve(nameOrPath);
|
|
2157
|
+
const index = history2.boards.findIndex(
|
|
2158
|
+
(b) => slugify(b.name) === slug || b.filePath === absolutePath
|
|
2159
|
+
);
|
|
2160
|
+
if (index === -1) {
|
|
2161
|
+
return false;
|
|
2162
|
+
}
|
|
2163
|
+
history2.boards.splice(index, 1);
|
|
2164
|
+
saveHistory(history2);
|
|
2165
|
+
return true;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// cli/server.ts
|
|
2169
|
+
import fs3 from "fs";
|
|
2170
|
+
import http from "http";
|
|
2171
|
+
import path3 from "path";
|
|
2112
2172
|
import readline from "readline";
|
|
2113
2173
|
import { fileURLToPath } from "url";
|
|
2114
2174
|
import getPort from "get-port";
|
|
@@ -2126,24 +2186,24 @@ var MIME_TYPES = {
|
|
|
2126
2186
|
".woff2": "font/woff2"
|
|
2127
2187
|
};
|
|
2128
2188
|
function getDistPath() {
|
|
2129
|
-
const currentDir =
|
|
2130
|
-
return
|
|
2189
|
+
const currentDir = path3.dirname(fileURLToPath(import.meta.url));
|
|
2190
|
+
return path3.resolve(currentDir, "..", "..", "dist");
|
|
2131
2191
|
}
|
|
2132
2192
|
function serveStaticFile(res, distPath, urlPath) {
|
|
2133
|
-
const filePath =
|
|
2134
|
-
const safePath =
|
|
2193
|
+
const filePath = path3.join(distPath, urlPath);
|
|
2194
|
+
const safePath = path3.resolve(filePath);
|
|
2135
2195
|
if (!safePath.startsWith(distPath)) {
|
|
2136
2196
|
res.writeHead(403);
|
|
2137
2197
|
res.end("Forbidden");
|
|
2138
2198
|
return;
|
|
2139
2199
|
}
|
|
2140
|
-
if (!
|
|
2200
|
+
if (!fs3.existsSync(safePath) || fs3.statSync(safePath).isDirectory()) {
|
|
2141
2201
|
return false;
|
|
2142
2202
|
}
|
|
2143
|
-
const ext =
|
|
2203
|
+
const ext = path3.extname(safePath);
|
|
2144
2204
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2145
2205
|
res.writeHead(200, { "Content-Type": contentType });
|
|
2146
|
-
|
|
2206
|
+
fs3.createReadStream(safePath).pipe(res);
|
|
2147
2207
|
return true;
|
|
2148
2208
|
}
|
|
2149
2209
|
function readRequestBody(req) {
|
|
@@ -2169,10 +2229,10 @@ function confirm(question) {
|
|
|
2169
2229
|
});
|
|
2170
2230
|
}
|
|
2171
2231
|
async function startServer(filePath, options = {}) {
|
|
2172
|
-
const absoluteFilePath =
|
|
2232
|
+
const absoluteFilePath = path3.resolve(filePath);
|
|
2173
2233
|
const distPath = getDistPath();
|
|
2174
2234
|
let browserDirty = false;
|
|
2175
|
-
if (!
|
|
2235
|
+
if (!fs3.existsSync(distPath)) {
|
|
2176
2236
|
console.error(
|
|
2177
2237
|
"Error: dist/ directory not found. Run `pnpm run build` first."
|
|
2178
2238
|
);
|
|
@@ -2181,14 +2241,14 @@ async function startServer(filePath, options = {}) {
|
|
|
2181
2241
|
const server = http.createServer(async (req, res) => {
|
|
2182
2242
|
const url2 = req.url || "/";
|
|
2183
2243
|
if (url2 === "/api/project" && req.method === "GET") {
|
|
2184
|
-
const content =
|
|
2244
|
+
const content = fs3.readFileSync(absoluteFilePath, "utf-8");
|
|
2185
2245
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2186
2246
|
res.end(content);
|
|
2187
2247
|
return;
|
|
2188
2248
|
}
|
|
2189
2249
|
if (url2 === "/api/project" && req.method === "POST") {
|
|
2190
2250
|
const body = await readRequestBody(req);
|
|
2191
|
-
|
|
2251
|
+
fs3.writeFileSync(absoluteFilePath, body);
|
|
2192
2252
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2193
2253
|
res.end('{"ok":true}');
|
|
2194
2254
|
return;
|
|
@@ -2212,10 +2272,10 @@ async function startServer(filePath, options = {}) {
|
|
|
2212
2272
|
}
|
|
2213
2273
|
if (serveStaticFile(res, distPath, url2))
|
|
2214
2274
|
return;
|
|
2215
|
-
const indexPath =
|
|
2216
|
-
if (
|
|
2275
|
+
const indexPath = path3.join(distPath, "index.html");
|
|
2276
|
+
if (fs3.existsSync(indexPath)) {
|
|
2217
2277
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
2218
|
-
|
|
2278
|
+
fs3.createReadStream(indexPath).pipe(res);
|
|
2219
2279
|
return;
|
|
2220
2280
|
}
|
|
2221
2281
|
res.writeHead(404);
|
|
@@ -2224,7 +2284,7 @@ async function startServer(filePath, options = {}) {
|
|
|
2224
2284
|
const port = options.port ?? await getPort();
|
|
2225
2285
|
const url = `http://localhost:${port}`;
|
|
2226
2286
|
const sseClients = /* @__PURE__ */ new Set();
|
|
2227
|
-
|
|
2287
|
+
fs3.watch(absoluteFilePath, () => {
|
|
2228
2288
|
for (const client of sseClients) {
|
|
2229
2289
|
client.write("data: file-changed\n\n");
|
|
2230
2290
|
}
|
|
@@ -2257,11 +2317,11 @@ async function startServer(filePath, options = {}) {
|
|
|
2257
2317
|
|
|
2258
2318
|
// cli/update.ts
|
|
2259
2319
|
import { execFile } from "child_process";
|
|
2260
|
-
import
|
|
2261
|
-
import
|
|
2262
|
-
import
|
|
2263
|
-
var CACHE_DIR =
|
|
2264
|
-
var CACHE_FILE =
|
|
2320
|
+
import fs4 from "fs";
|
|
2321
|
+
import os2 from "os";
|
|
2322
|
+
import path4 from "path";
|
|
2323
|
+
var CACHE_DIR = path4.join(os2.homedir(), ".itsvertical");
|
|
2324
|
+
var CACHE_FILE = path4.join(CACHE_DIR, "update-check.json");
|
|
2265
2325
|
var CHECK_INTERVAL = 36e5;
|
|
2266
2326
|
var FETCH_TIMEOUT = 5e3;
|
|
2267
2327
|
var REGISTRY_URL = "https://registry.npmjs.org/itsvertical/latest";
|
|
@@ -2279,12 +2339,12 @@ function compareSemver(a, b) {
|
|
|
2279
2339
|
return 0;
|
|
2280
2340
|
}
|
|
2281
2341
|
function isPathInside(childPath, parentPath) {
|
|
2282
|
-
const relative =
|
|
2283
|
-
return relative !== "" && !relative.startsWith("..") && !
|
|
2342
|
+
const relative = path4.relative(parentPath, childPath);
|
|
2343
|
+
return relative !== "" && !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
2284
2344
|
}
|
|
2285
2345
|
function resolveScriptPath(scriptPath) {
|
|
2286
2346
|
try {
|
|
2287
|
-
return
|
|
2347
|
+
return fs4.realpathSync(scriptPath);
|
|
2288
2348
|
} catch {
|
|
2289
2349
|
return scriptPath;
|
|
2290
2350
|
}
|
|
@@ -2296,46 +2356,46 @@ function getNpmGlobalPackages() {
|
|
|
2296
2356
|
);
|
|
2297
2357
|
const envValue = envKey ? process.env[envKey] : void 0;
|
|
2298
2358
|
if (envValue) {
|
|
2299
|
-
const prefix2 =
|
|
2300
|
-
return
|
|
2359
|
+
const prefix2 = path4.resolve(envValue);
|
|
2360
|
+
return path4.join(prefix2, isWindows ? "node_modules" : "lib/node_modules");
|
|
2301
2361
|
}
|
|
2302
2362
|
let prefix;
|
|
2303
2363
|
if (isWindows) {
|
|
2304
|
-
prefix = process.env.APPDATA ?
|
|
2364
|
+
prefix = process.env.APPDATA ? path4.join(process.env.APPDATA, "npm") : path4.dirname(process.execPath);
|
|
2305
2365
|
} else if (process.execPath.includes("/Cellar/node")) {
|
|
2306
2366
|
prefix = process.execPath.slice(0, process.execPath.indexOf("/Cellar/node"));
|
|
2307
2367
|
} else {
|
|
2308
|
-
prefix =
|
|
2368
|
+
prefix = path4.dirname(path4.dirname(process.execPath));
|
|
2309
2369
|
}
|
|
2310
|
-
return
|
|
2370
|
+
return path4.join(prefix, isWindows ? "node_modules" : "lib/node_modules");
|
|
2311
2371
|
}
|
|
2312
2372
|
function getPnpmGlobalDir() {
|
|
2313
2373
|
if (process.env.PNPM_HOME)
|
|
2314
2374
|
return process.env.PNPM_HOME;
|
|
2315
2375
|
if (process.env.XDG_DATA_HOME)
|
|
2316
|
-
return
|
|
2376
|
+
return path4.join(process.env.XDG_DATA_HOME, "pnpm");
|
|
2317
2377
|
if (process.platform === "darwin")
|
|
2318
|
-
return
|
|
2378
|
+
return path4.join(os2.homedir(), "Library/pnpm");
|
|
2319
2379
|
if (process.platform !== "win32")
|
|
2320
|
-
return
|
|
2380
|
+
return path4.join(os2.homedir(), ".local/share/pnpm");
|
|
2321
2381
|
if (process.env.LOCALAPPDATA)
|
|
2322
|
-
return
|
|
2323
|
-
return
|
|
2382
|
+
return path4.join(process.env.LOCALAPPDATA, "pnpm");
|
|
2383
|
+
return path4.join(os2.homedir(), ".pnpm");
|
|
2324
2384
|
}
|
|
2325
2385
|
function getYarnGlobalPackages() {
|
|
2326
2386
|
const isWindows = process.platform === "win32";
|
|
2327
2387
|
let dataDir;
|
|
2328
2388
|
if (isWindows) {
|
|
2329
|
-
dataDir = process.env.LOCALAPPDATA ?
|
|
2389
|
+
dataDir = process.env.LOCALAPPDATA ? path4.join(process.env.LOCALAPPDATA, "Yarn/Data") : path4.join(os2.homedir(), ".config/yarn");
|
|
2330
2390
|
} else if (process.env.XDG_DATA_HOME) {
|
|
2331
|
-
dataDir =
|
|
2391
|
+
dataDir = path4.join(process.env.XDG_DATA_HOME, "yarn");
|
|
2332
2392
|
} else {
|
|
2333
|
-
dataDir =
|
|
2393
|
+
dataDir = path4.join(os2.homedir(), ".config/yarn");
|
|
2334
2394
|
}
|
|
2335
|
-
return
|
|
2395
|
+
return path4.join(dataDir, "global/node_modules");
|
|
2336
2396
|
}
|
|
2337
2397
|
function getBunGlobalDir() {
|
|
2338
|
-
return process.env.BUN_INSTALL ||
|
|
2398
|
+
return process.env.BUN_INSTALL || path4.join(os2.homedir(), ".bun");
|
|
2339
2399
|
}
|
|
2340
2400
|
function detectInstallContext(scriptPath) {
|
|
2341
2401
|
const resolved = resolveScriptPath(scriptPath);
|
|
@@ -2343,24 +2403,24 @@ function detectInstallContext(scriptPath) {
|
|
|
2343
2403
|
if (npxMatch) {
|
|
2344
2404
|
return { type: "npx-cache", cachePath: npxMatch[1] };
|
|
2345
2405
|
}
|
|
2346
|
-
const bunDir =
|
|
2406
|
+
const bunDir = path4.resolve(getBunGlobalDir());
|
|
2347
2407
|
if (isPathInside(resolved, bunDir)) {
|
|
2348
|
-
const bunCacheDir =
|
|
2408
|
+
const bunCacheDir = path4.join(bunDir, "install", "cache");
|
|
2349
2409
|
if (isPathInside(resolved, bunCacheDir)) {
|
|
2350
2410
|
return { type: "bunx-cache", cacheDir: bunCacheDir };
|
|
2351
2411
|
}
|
|
2352
2412
|
return { type: "global", packageManager: "bun" };
|
|
2353
2413
|
}
|
|
2354
|
-
const pnpmDir =
|
|
2414
|
+
const pnpmDir = path4.resolve(getPnpmGlobalDir());
|
|
2355
2415
|
if (isPathInside(resolved, pnpmDir)) {
|
|
2356
2416
|
return { type: "global", packageManager: "pnpm" };
|
|
2357
2417
|
}
|
|
2358
|
-
const yarnPackages =
|
|
2418
|
+
const yarnPackages = path4.resolve(getYarnGlobalPackages());
|
|
2359
2419
|
if (isPathInside(resolved, yarnPackages)) {
|
|
2360
2420
|
return { type: "global", packageManager: "yarn" };
|
|
2361
2421
|
}
|
|
2362
2422
|
try {
|
|
2363
|
-
const npmPackages =
|
|
2423
|
+
const npmPackages = fs4.realpathSync(getNpmGlobalPackages());
|
|
2364
2424
|
if (isPathInside(resolved, npmPackages)) {
|
|
2365
2425
|
return { type: "global", packageManager: "npm" };
|
|
2366
2426
|
}
|
|
@@ -2373,15 +2433,15 @@ function detectInstallContext(scriptPath) {
|
|
|
2373
2433
|
}
|
|
2374
2434
|
function readCache() {
|
|
2375
2435
|
try {
|
|
2376
|
-
return JSON.parse(
|
|
2436
|
+
return JSON.parse(fs4.readFileSync(CACHE_FILE, "utf-8"));
|
|
2377
2437
|
} catch {
|
|
2378
2438
|
return null;
|
|
2379
2439
|
}
|
|
2380
2440
|
}
|
|
2381
2441
|
function writeCache(cache) {
|
|
2382
2442
|
try {
|
|
2383
|
-
|
|
2384
|
-
|
|
2443
|
+
fs4.mkdirSync(CACHE_DIR, { recursive: true });
|
|
2444
|
+
fs4.writeFileSync(CACHE_FILE, JSON.stringify(cache));
|
|
2385
2445
|
} catch {
|
|
2386
2446
|
}
|
|
2387
2447
|
}
|
|
@@ -2470,7 +2530,7 @@ function getUpdateCommandString(context) {
|
|
|
2470
2530
|
async function performUpdate(context) {
|
|
2471
2531
|
if (context.type === "npx-cache") {
|
|
2472
2532
|
try {
|
|
2473
|
-
|
|
2533
|
+
fs4.rmSync(context.cachePath, { recursive: true, force: true });
|
|
2474
2534
|
return {
|
|
2475
2535
|
success: true,
|
|
2476
2536
|
message: "Cleared npx cache. Next run will fetch the latest version."
|
|
@@ -2481,9 +2541,9 @@ async function performUpdate(context) {
|
|
|
2481
2541
|
}
|
|
2482
2542
|
if (context.type === "bunx-cache") {
|
|
2483
2543
|
try {
|
|
2484
|
-
for (const entry of
|
|
2544
|
+
for (const entry of fs4.readdirSync(context.cacheDir)) {
|
|
2485
2545
|
if (entry.startsWith("itsvertical@")) {
|
|
2486
|
-
|
|
2546
|
+
fs4.rmSync(path4.join(context.cacheDir, entry), {
|
|
2487
2547
|
recursive: true,
|
|
2488
2548
|
force: true
|
|
2489
2549
|
});
|
|
@@ -2542,8 +2602,8 @@ function getDirname() {
|
|
|
2542
2602
|
throw new Error("Cannot determine directory path in current environment");
|
|
2543
2603
|
}
|
|
2544
2604
|
var packageJson = JSON.parse(
|
|
2545
|
-
|
|
2546
|
-
|
|
2605
|
+
fs5.readFileSync(
|
|
2606
|
+
path5.resolve(getDirname(), "..", "..", "package.json"),
|
|
2547
2607
|
"utf8"
|
|
2548
2608
|
)
|
|
2549
2609
|
);
|
|
@@ -2552,20 +2612,33 @@ program.name("itsvertical").description(
|
|
|
2552
2612
|
"Tickets pile up, scopes get done. Project work isn't linear, it's Vertical.\n\nTip: itsvertical <file.vertical> is a shorthand for itsvertical open <file>."
|
|
2553
2613
|
).version(packageJson.version);
|
|
2554
2614
|
program.command("new").description("Create a new .vertical project file").argument("<path>", "File path for the new .vertical file").argument("<name>", "Project name").option("--json", "Output as JSON").action((fileDest, name, options) => {
|
|
2555
|
-
const filePath =
|
|
2556
|
-
if (
|
|
2615
|
+
const filePath = path5.resolve(fileDest);
|
|
2616
|
+
if (fs5.existsSync(filePath)) {
|
|
2557
2617
|
fail(`File already exists: ${filePath}`, options.json);
|
|
2558
2618
|
}
|
|
2559
|
-
const dir =
|
|
2560
|
-
if (!
|
|
2561
|
-
|
|
2619
|
+
const dir = path5.dirname(filePath);
|
|
2620
|
+
if (!fs5.existsSync(dir)) {
|
|
2621
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
2562
2622
|
}
|
|
2563
2623
|
const state = createBlankProject(name);
|
|
2564
|
-
|
|
2624
|
+
fs5.writeFileSync(filePath, serialize(state));
|
|
2625
|
+
try {
|
|
2626
|
+
recordBoard(name, filePath);
|
|
2627
|
+
} catch (error) {
|
|
2628
|
+
fail(error.message, options.json);
|
|
2629
|
+
}
|
|
2565
2630
|
output(state, Boolean(options.json), `Created: ${filePath}`);
|
|
2566
2631
|
});
|
|
2567
2632
|
program.command("open").description("Open an existing .vertical file in the browser").argument("<file>", "Path to the .vertical file").action(async (file) => {
|
|
2568
2633
|
const filePath = resolveFilePath(file);
|
|
2634
|
+
const state = loadState(filePath);
|
|
2635
|
+
try {
|
|
2636
|
+
recordBoard(state.project.name, filePath);
|
|
2637
|
+
} catch (error) {
|
|
2638
|
+
console.warn(
|
|
2639
|
+
`Warning: could not track board: ${error.message}`
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2569
2642
|
await startServer(filePath);
|
|
2570
2643
|
});
|
|
2571
2644
|
program.command("dev").description("Start dev server (fixed port, no browser open)").argument("<file>", "Path to the .vertical file").action(async (file) => {
|
|
@@ -2597,6 +2670,53 @@ program.command("rename").description("Rename the project").argument("<file>", "
|
|
|
2597
2670
|
const state = applyAction(filePath, { type: "RENAME_PROJECT", name });
|
|
2598
2671
|
output(state, Boolean(options.json), `Project renamed to: ${name}`);
|
|
2599
2672
|
});
|
|
2673
|
+
var history = program.command("history").description("Manage board history");
|
|
2674
|
+
history.command("list").description("List all known boards").option("--json", "Output as JSON").action((options) => {
|
|
2675
|
+
const boardHistory = loadHistory();
|
|
2676
|
+
if (options.json) {
|
|
2677
|
+
const entries = boardHistory.boards.map((b) => ({
|
|
2678
|
+
name: b.name,
|
|
2679
|
+
filePath: b.filePath,
|
|
2680
|
+
exists: fs5.existsSync(b.filePath)
|
|
2681
|
+
}));
|
|
2682
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
2683
|
+
return;
|
|
2684
|
+
}
|
|
2685
|
+
if (boardHistory.boards.length === 0) {
|
|
2686
|
+
console.log("No boards known yet. Create or open a board to get started.");
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
for (const board of boardHistory.boards) {
|
|
2690
|
+
const exists = fs5.existsSync(board.filePath);
|
|
2691
|
+
const marker = exists ? "" : " (missing)";
|
|
2692
|
+
console.log(`${board.name} ${board.filePath}${marker}`);
|
|
2693
|
+
}
|
|
2694
|
+
});
|
|
2695
|
+
history.command("add").description("Add an existing .vertical file to history").argument("<file>", "Path to the .vertical file").option("--json", "Output as JSON").action((file, options) => {
|
|
2696
|
+
const filePath = resolveFilePath(file, options.json);
|
|
2697
|
+
const state = loadState(filePath);
|
|
2698
|
+
try {
|
|
2699
|
+
recordBoard(state.project.name, filePath);
|
|
2700
|
+
} catch (error) {
|
|
2701
|
+
fail(error.message, options.json);
|
|
2702
|
+
}
|
|
2703
|
+
output(
|
|
2704
|
+
state,
|
|
2705
|
+
Boolean(options.json),
|
|
2706
|
+
`Added to history: ${state.project.name} \u2192 ${filePath}`
|
|
2707
|
+
);
|
|
2708
|
+
});
|
|
2709
|
+
history.command("remove").description("Remove a board from history (does not delete the file)").argument("<name-or-file>", "Board name or file path").option("--json", "Output as JSON").action((nameOrFile, options) => {
|
|
2710
|
+
const removed = forgetBoard(nameOrFile);
|
|
2711
|
+
if (!removed) {
|
|
2712
|
+
fail(`Board not found in history: "${nameOrFile}"`, options.json);
|
|
2713
|
+
}
|
|
2714
|
+
if (options.json) {
|
|
2715
|
+
console.log(JSON.stringify({ success: true }));
|
|
2716
|
+
} else {
|
|
2717
|
+
console.log(`Removed from history: ${nameOrFile}`);
|
|
2718
|
+
}
|
|
2719
|
+
});
|
|
2600
2720
|
var task = program.command("task").description("Manage tasks");
|
|
2601
2721
|
task.command("add").description("Add a task to a layer").argument("<file>", "Path to the .vertical file").argument("<layer-id>", "Layer ID to add the task to").argument("<name>", "Task name").option("--json", "Output as JSON").option("--after <task-id>", "Insert after a specific task").action(
|
|
2602
2722
|
(file, layerId, name, options) => {
|