kandown 0.33.6 → 0.34.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.34.0 — 2026-07-24 — "Quick-Wins Collection"
4
+
5
+ - **Added**: **Quick archive button on every card** — task cards in the board and list views now expose a one-click **archive** action next to the existing guarded delete button. Archiving is reversible from the archive view and reuses the existing store `archiveTask` action so files, watchers, and broadcasts stay consistent.
6
+ - **Added**: **Terminal-column bulk actions** — the column whose name matches the configured terminal status now shows a dedicated **Manage tasks** menu with **Archive all** and **Delete all** entries. Both confirm the exact task count and column name before mutating anything; deletions are permanent, archive operations can be undone from the archive view.
7
+ - **Added**: **Robust bulk task actions** — `bulkArchiveTasks(ids)` and `bulkDeleteTasks(ids?)` use `Promise.allSettled` over strict task reads so a single failed file cannot abort the batch. An in-flight guard prevents two destructive runs from racing each other. `nextTaskId` now considers archived ids so new tasks cannot reuse a number that already lives in `tasks/archive/`.
8
+ - **Added**: **Subtask editor in the task drawer and workspace** — both surfaces re-mount the original checklist editor (toggle, edit, enter-to-insert, backspace-to-remove, expand description/report) that was previously removed. `SubtaskEditor` owns the section UI while the drawer/workspace persists edits through the existing `saveDrawer` autosave path.
9
+ - **Fixed**: **Subtask markdown round-trip preserves hand-written prose** — `extractSubtasks` and `injectSubtasks` now preserve unrelated lines that share the `## Subtasks` section, support multi-line `[DESC]`/`[REPORT]` markers, and treat `- [ ]` (empty text) as a valid checklist row so a fresh row can be created without immediately supplying text.
10
+ - **Fixed**: **Subtask editor stays above hooks** — the SubtaskEditor handler is defined before the desktop/media-query guard in both `Drawer` and `TaskWorkspace`, restoring the React rules-of-hooks order and eliminating a mobile-drawer crash.
11
+ - **Fixed**: **Server-mode archive round-trip** — the production daemon's `/api/tasks/:id` routes now read from and write to the `tasks/archive/` directory, accept `POST /api/tasks/:id/archive` and `…/unarchive`, validate task ids, and remove both the active and archived files on `DELETE`. The dev middleware in `vite.config.ts` resolves tasks at the project-root `tasks/` directory (the canonical location since v0.12) and exposes the same archive routes.
12
+ - **Fixed**: **SPA fallback for deep links** — the daemon now serves the single-file app for any non-API path, so refreshing on a deep task URL (e.g. `/2?p=project`) keeps the client routing intact.
13
+ - **Changed**: **File watcher covers the archive directory** — the CLI content-hash watcher also tracks `tasks/archive/*.md`, so archived task updates still trigger board reloads and notifications.
14
+ - **Changed**: **Card action group and a11y** — the card's right-corner actions now live in a flex group that reveals both archive and delete on hover or keyboard focus, with `aria-label` and `aria-haspopup` attributes on the new menu, and a `Card isMountedRef` reset so React StrictMode double-mount doesn't disable the buttons.
15
+ - **Changed**: **i18n strings** — English and French locales gain new keys for archive/delete-all prompts, subtask a11y labels, and progress messages. English remains the source of truth; untranslated keys fall back automatically.
16
+
3
17
  ## 0.33.6 — 2026-07-24 — "Subdirectory Project Discovery"
4
18
 
5
19
  - **Fixed**: **Auto-detect `.kandown/` in sub-directories from parent folder** — when running `kandown` from a parent directory that contains a sub-directory with a `.kandown/` folder, the CLI now recursively searches sub-directories (skipping `node_modules`, hidden dirs, and `tasks/`) to find it automatically. This restores the expected behaviour where `kandown` opens the child project without requiring an explicit `--path` argument.
package/README.md CHANGED
@@ -178,6 +178,7 @@ Interactive runs of `kandown` check npm for updates at most once every 24 hours
178
178
  | Command palette | `⌘K` / `Ctrl+K` for quick actions |
179
179
  | Custom columns | Add, rename, delete columns freely |
180
180
  | Guarded deletion | Double-click to delete — no accidents |
181
+ | Terminal bulk actions | Archive or permanently delete every task in the terminal column with confirmation |
181
182
 
182
183
  ### Task Management
183
184
 
@@ -185,6 +186,7 @@ Interactive runs of `kandown` check npm for updates at most once every 24 hours
185
186
  |---|---|
186
187
  | Rich task drawer | WYSIWYG markdown editor for title, metadata, subtasks, body |
187
188
  | Subtasks | Full checklist with progress tracking on cards |
189
+ | Subtask editor | Add, reorder-by-keyboard, check off, remove, and edit per-step descriptions and reports |
188
190
  | Metadata fields | Priority, assignee, tags, due date, owner type |
189
191
  | Owner filtering | Filter human vs AI-agent tasks separately |
190
192
  | External-change detection | Warns when files are modified outside the app |
package/bin/kandown.js CHANGED
@@ -7,7 +7,7 @@ if (typeof globalThis.require === 'undefined') {
7
7
  // src/cli/cli.ts
8
8
  import { existsSync as existsSync8, readFileSync as readFileSync8, copyFileSync as copyFileSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
9
9
  import { join as join8, resolve as resolve2, basename } from "path";
10
- import { spawn as spawn4, spawnSync } from "child_process";
10
+ import { spawn as spawn5, spawnSync } from "child_process";
11
11
 
12
12
  // src/cli/lib/updater.ts
13
13
  import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync } from "fs";
@@ -16,7 +16,7 @@ import { spawn, execSync } from "child_process";
16
16
  import { homedir } from "os";
17
17
 
18
18
  // src/lib/version.ts
19
- var KANDOWN_VERSION = "0.33.6";
19
+ var KANDOWN_VERSION = "0.34.0";
20
20
 
21
21
  // src/cli/lib/updater.ts
22
22
  import { fileURLToPath } from "url";
@@ -428,30 +428,42 @@ function extractSubtasks(body) {
428
428
  kept.push(line);
429
429
  continue;
430
430
  }
431
- const m = line.match(/^\s*-\s+\[([ xX])\]\s+(.+)$/);
431
+ const m = line.match(/^\s*-\s+\[([ xX])\]\s*(.*)$/);
432
432
  if (m && inSubtaskSection) {
433
433
  const text = m[2]?.trim() ?? "";
434
434
  subtasks.push({ done: (m[1]?.toLowerCase() ?? "") === "x", text });
435
435
  continue;
436
436
  }
437
- const descMatch = line.match(/^\s*\[DESC\]\s*(.*)$/);
437
+ const descMatch = line.match(/^\s*\[DESC\]\s?(.*)$/);
438
438
  if (descMatch && subtasks.length > 0) {
439
- subtasks[subtasks.length - 1].description = descMatch[1];
439
+ const subtask = subtasks[subtasks.length - 1];
440
+ const nextLine = descMatch[1] ?? "";
441
+ subtask.description = subtask.description === void 0 ? nextLine : `${subtask.description}
442
+ ${nextLine}`;
440
443
  continue;
441
444
  }
442
- const reportMatch = line.match(/^\s*\[REPORT\]\s*(.*)$/);
445
+ const reportMatch = line.match(/^\s*\[REPORT\]\s?(.*)$/);
443
446
  if (reportMatch && subtasks.length > 0) {
444
- subtasks[subtasks.length - 1].report = reportMatch[1];
447
+ const subtask = subtasks[subtasks.length - 1];
448
+ const nextLine = reportMatch[1] ?? "";
449
+ subtask.report = subtask.report === void 0 ? nextLine : `${subtask.report}
450
+ ${nextLine}`;
445
451
  continue;
446
452
  }
447
453
  const legacyDescMatch = line.match(/^\s+description:\s*(.+)$/);
448
454
  if (legacyDescMatch && inSubtaskSection && subtasks.length > 0) {
449
- subtasks[subtasks.length - 1].description = legacyDescMatch[1].trim();
455
+ const subtask = subtasks[subtasks.length - 1];
456
+ const nextLine = legacyDescMatch[1].trim();
457
+ subtask.description = subtask.description ? `${subtask.description}
458
+ ${nextLine}` : nextLine;
450
459
  continue;
451
460
  }
452
461
  const legacyReportMatch = line.match(/^\s+report:\s*(.+)$/);
453
462
  if (legacyReportMatch && inSubtaskSection && subtasks.length > 0) {
454
- subtasks[subtasks.length - 1].report = legacyReportMatch[1].trim();
463
+ const subtask = subtasks[subtasks.length - 1];
464
+ const nextLine = legacyReportMatch[1].trim();
465
+ subtask.report = subtask.report ? `${subtask.report}
466
+ ${nextLine}` : nextLine;
455
467
  continue;
456
468
  }
457
469
  kept.push(line);
@@ -562,8 +574,22 @@ function getTasksDir(kandownDir) {
562
574
  }
563
575
  function listTaskIds(kandownDir) {
564
576
  const tasksDir = getTasksDir(kandownDir);
565
- if (!existsSync3(tasksDir)) return [];
566
- return readdirSync2(tasksDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
577
+ const ids = /* @__PURE__ */ new Set();
578
+ for (const directory of [tasksDir, join3(tasksDir, "archive")]) {
579
+ if (!existsSync3(directory)) continue;
580
+ for (const name of readdirSync2(directory).filter((entry) => entry.endsWith(".md"))) {
581
+ ids.add(name.slice(0, -3));
582
+ }
583
+ }
584
+ return [...ids].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
585
+ }
586
+ function findTaskPath(kandownDir, taskId) {
587
+ if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) return null;
588
+ const tasksDir = getTasksDir(kandownDir);
589
+ const activePath = join3(tasksDir, `${taskId}.md`);
590
+ if (existsSync3(activePath)) return activePath;
591
+ const archivedPath = join3(tasksDir, "archive", `${taskId}.md`);
592
+ return existsSync3(archivedPath) ? archivedPath : null;
567
593
  }
568
594
  function readBoard(kandownDir) {
569
595
  const config = loadConfig(kandownDir);
@@ -591,8 +617,8 @@ function readBoard(kandownDir) {
591
617
  };
592
618
  }
593
619
  function readTask(kandownDir, taskId) {
594
- const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
595
- if (!existsSync3(taskPath2)) {
620
+ const taskPath2 = findTaskPath(kandownDir, taskId);
621
+ if (!taskPath2) {
596
622
  return {
597
623
  frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
598
624
  body: ""
@@ -652,8 +678,8 @@ ${gitLog}
652
678
  return sections.filter(Boolean).join("\n\n---\n\n");
653
679
  }
654
680
  function moveTaskToColumn(kandownDir, taskId, targetColumn) {
655
- const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
656
- if (!existsSync3(taskPath2)) return false;
681
+ const taskPath2 = findTaskPath(kandownDir, taskId);
682
+ if (!taskPath2) return false;
657
683
  try {
658
684
  const prevContent = readFileSync3(taskPath2, "utf8");
659
685
  const parsed = readTask(kandownDir, taskId);
@@ -905,8 +931,8 @@ async function startProjectDaemon(kandownDir, preferredPort) {
905
931
  if (current.metadata?.version === getCurrentVersion()) return current;
906
932
  await stopProjectDaemon(kandownDir);
907
933
  }
908
- const cliPath = process.argv[1];
909
- if (!cliPath) throw new Error("Cannot locate kandown CLI entrypoint");
934
+ const cliPath = join4(PKG_ROOT, "bin", "kandown.js");
935
+ if (!existsSync4(cliPath)) throw new Error(`Cannot locate kandown CLI entrypoint at ${cliPath}`);
910
936
  const args = [
911
937
  cliPath,
912
938
  "--no-update-check",
@@ -1008,6 +1034,17 @@ function writeText(res, status, text) {
1008
1034
  });
1009
1035
  res.end(text);
1010
1036
  }
1037
+ function readRequestBody(req) {
1038
+ return new Promise((resolveBody, rejectBody) => {
1039
+ let body = "";
1040
+ req.setEncoding("utf8");
1041
+ req.on("data", (chunk) => {
1042
+ body += chunk;
1043
+ });
1044
+ req.on("end", () => resolveBody(body));
1045
+ req.on("error", rejectBody);
1046
+ });
1047
+ }
1011
1048
  function syncProjectKandownHtml(kandownDir) {
1012
1049
  try {
1013
1050
  const projectHtml = join5(kandownDir, "kandown.html");
@@ -1200,40 +1237,92 @@ async function handleApi(req, res, url, kandownDir) {
1200
1237
  return writeJson(res, 200, listTaskIds(kandownDir));
1201
1238
  }
1202
1239
  if (path.startsWith("/api/tasks/")) {
1203
- const taskId = decodeURIComponent(path.slice("/api/tasks/".length));
1240
+ const routeParts = path.slice("/api/tasks/".length).split("/").filter(Boolean);
1241
+ let taskId;
1242
+ try {
1243
+ taskId = decodeURIComponent(routeParts[0] ?? "");
1244
+ } catch {
1245
+ return writeText(res, 400, "Invalid task id");
1246
+ }
1247
+ if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) return writeText(res, 400, "Invalid task id");
1204
1248
  const tasksDir = getTasksDir(kandownDir);
1205
- const taskPath2 = join5(tasksDir, `${taskId}.md`);
1249
+ const archiveDir = join5(tasksDir, "archive");
1250
+ const activePath = join5(tasksDir, `${taskId}.md`);
1251
+ const archivedPath = join5(archiveDir, `${taskId}.md`);
1252
+ const action = routeParts[1];
1253
+ if (method === "POST" && (action === "archive" || action === "unarchive")) {
1254
+ if (routeParts.length !== 2) return writeText(res, 400, "Invalid task route");
1255
+ const archiving = action === "archive";
1256
+ const source = archiving ? activePath : archivedPath;
1257
+ const destination = archiving ? archivedPath : activePath;
1258
+ if (!existsSync5(source) && !existsSync5(destination)) {
1259
+ return writeText(res, 404, "Task not found");
1260
+ }
1261
+ try {
1262
+ if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1263
+ if (!existsSync5(archiveDir)) mkdirSync3(archiveDir, { recursive: true });
1264
+ const body = await readRequestBody(req);
1265
+ atomicWriteFileSync(destination, body);
1266
+ if (source !== destination && existsSync5(source)) unlinkSync5(source);
1267
+ broadcastSseEvent({ type: "task", id: taskId });
1268
+ return writeJson(res, 200, { ok: true });
1269
+ } catch (error) {
1270
+ return writeJson(res, 500, {
1271
+ error: `Failed to ${action} task: ${error instanceof Error ? error.message : String(error)}`
1272
+ });
1273
+ }
1274
+ }
1275
+ if (routeParts.length !== 1) return writeText(res, 404, "Route not found");
1206
1276
  if (method === "GET") {
1207
- if (!existsSync5(taskPath2)) return writeText(res, 404, "Task not found");
1277
+ const taskPath2 = findTaskPath(kandownDir, taskId);
1278
+ if (!taskPath2) return writeText(res, 404, "Task not found");
1208
1279
  return writeText(res, 200, readFileSync5(taskPath2, "utf8"));
1209
1280
  }
1210
1281
  if (method === "PUT") {
1211
- let body = "";
1212
- req.on("data", (chunk) => {
1213
- body += chunk;
1214
- });
1215
- req.on("end", () => {
1282
+ try {
1216
1283
  if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
1284
+ const taskPath2 = findTaskPath(kandownDir, taskId) ?? activePath;
1285
+ const body = await readRequestBody(req);
1217
1286
  atomicWriteFileSync(taskPath2, body);
1218
1287
  broadcastSseEvent({ type: "task", id: taskId });
1219
- writeJson(res, 200, { ok: true });
1220
- });
1221
- return;
1288
+ return writeJson(res, 200, { ok: true });
1289
+ } catch (error) {
1290
+ return writeJson(res, 500, {
1291
+ error: `Failed to write task: ${error instanceof Error ? error.message : String(error)}`
1292
+ });
1293
+ }
1222
1294
  }
1223
1295
  if (method === "DELETE") {
1224
- if (existsSync5(taskPath2)) unlinkSync5(taskPath2);
1225
- broadcastSseEvent({ type: "task_delete", id: taskId });
1226
- return writeJson(res, 200, { ok: true });
1296
+ try {
1297
+ if (existsSync5(activePath)) unlinkSync5(activePath);
1298
+ if (existsSync5(archivedPath)) unlinkSync5(archivedPath);
1299
+ broadcastSseEvent({ type: "task_delete", id: taskId });
1300
+ return writeJson(res, 200, { ok: true });
1301
+ } catch (error) {
1302
+ return writeJson(res, 500, {
1303
+ error: `Failed to delete task: ${error instanceof Error ? error.message : String(error)}`
1304
+ });
1305
+ }
1227
1306
  }
1228
1307
  }
1229
1308
  writeJson(res, 404, { error: "Route not found" });
1230
1309
  }
1310
+ function injectServerRoot(html, kandownDir) {
1311
+ const marker = "</head>";
1312
+ const markerIndex = html.toLowerCase().lastIndexOf(marker);
1313
+ const safeRoot = JSON.stringify(kandownDir).replace(/</g, "\\u003c");
1314
+ const script = `<script>window.__KANDOWN_ROOT__ = ${safeRoot};</script>
1315
+ `;
1316
+ if (markerIndex === -1) return script + html;
1317
+ return html.slice(0, markerIndex) + script + html.slice(markerIndex);
1318
+ }
1231
1319
  function serveApp(res, kandownDir) {
1232
1320
  syncProjectKandownHtml(kandownDir);
1233
1321
  const htmlPath = join5(kandownDir, "kandown.html");
1234
1322
  if (existsSync5(htmlPath)) {
1323
+ const html = readFileSync5(htmlPath, "utf8");
1235
1324
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1236
- res.end(readFileSync5(htmlPath, "utf8"));
1325
+ res.end(injectServerRoot(html, kandownDir));
1237
1326
  } else {
1238
1327
  writeText(res, 404, "kandown.html not found");
1239
1328
  }
@@ -1243,7 +1332,7 @@ function createServeServer(kandownDir) {
1243
1332
  return createServer((req, res) => {
1244
1333
  const url = new URL(req.url || "/", "http://localhost");
1245
1334
  if (req.method === "OPTIONS") return handleCors(res);
1246
- if (url.pathname === "/" || url.pathname === "/kandown.html") {
1335
+ if (url.pathname === "/" || url.pathname === "/kandown.html" || !url.pathname.startsWith("/api/")) {
1247
1336
  return serveApp(res, kandownDir);
1248
1337
  }
1249
1338
  if (url.pathname.startsWith("/api/")) {
@@ -1557,6 +1646,16 @@ ${args.report.trim()}`) : task.body.trim() + reportSection;
1557
1646
  sendResponse(id, { error: { code: -32601, message: `Method not found: ${method}` } });
1558
1647
  }
1559
1648
 
1649
+ // src/cli/lib/browser.ts
1650
+ import { spawn as spawn4 } from "child_process";
1651
+ function openBrowser(target) {
1652
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1653
+ try {
1654
+ spawn4(cmd, [target], { detached: true, stdio: "ignore" }).unref();
1655
+ } catch {
1656
+ }
1657
+ }
1658
+
1560
1659
  // src/cli/cli.ts
1561
1660
  var c = {
1562
1661
  reset: "\x1B[0m",
@@ -1580,13 +1679,6 @@ function success(msg) {
1580
1679
  function err(msg) {
1581
1680
  console.error(`${c.red}\u2717${c.reset} ${msg}`);
1582
1681
  }
1583
- function openBrowser(target) {
1584
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1585
- try {
1586
- spawn4(cmd, [target], { detached: true, stdio: "ignore" }).unref();
1587
- } catch {
1588
- }
1589
- }
1590
1682
  function parseArgs(args) {
1591
1683
  const flags = {};
1592
1684
  const positional = [];
@@ -1752,7 +1844,7 @@ async function cmdUpdate(rawArgs) {
1752
1844
  const current = getCurrentVersion();
1753
1845
  log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
1754
1846
  const latest = await new Promise((resolve3) => {
1755
- const child = spawn4("npm", ["view", "kandown", "version"], {
1847
+ const child = spawn5("npm", ["view", "kandown", "version"], {
1756
1848
  timeout: 6e3,
1757
1849
  stdio: ["pipe", "pipe", "pipe"],
1758
1850
  env: { ...process.env },
@@ -1895,7 +1987,7 @@ function resolveStatusArg(kandownDir, status) {
1895
1987
  function taskPath(kandownDir, id, archived = false) {
1896
1988
  return archived ? join8(getTasksDir(kandownDir), "archive", `${id}.md`) : join8(getTasksDir(kandownDir), `${id}.md`);
1897
1989
  }
1898
- function findTaskPath(kandownDir, id) {
1990
+ function findTaskPath2(kandownDir, id) {
1899
1991
  const active = taskPath(kandownDir, id);
1900
1992
  if (existsSync8(active)) return active;
1901
1993
  const archived = taskPath(kandownDir, id, true);
@@ -1918,7 +2010,7 @@ function nextTaskId(kandownDir) {
1918
2010
  return `t${max + 1}`;
1919
2011
  }
1920
2012
  function readTaskFile(kandownDir, id) {
1921
- const path = findTaskPath(kandownDir, id);
2013
+ const path = findTaskPath2(kandownDir, id);
1922
2014
  if (!path) return null;
1923
2015
  const parsed = parseTaskFile(readFileSync8(path, "utf8"));
1924
2016
  return {
@@ -2003,7 +2095,7 @@ function cmdShow(rawArgs) {
2003
2095
  err("Usage: kandown show <task-id>");
2004
2096
  process.exit(1);
2005
2097
  }
2006
- const path = findTaskPath(kandownDir, id);
2098
+ const path = findTaskPath2(kandownDir, id);
2007
2099
  if (!path) {
2008
2100
  err(`Task not found: ${id}`);
2009
2101
  process.exit(1);
@@ -2023,7 +2115,7 @@ function cmdCreate(rawArgs) {
2023
2115
  err(`Invalid task id: ${id}`);
2024
2116
  process.exit(1);
2025
2117
  }
2026
- if (findTaskPath(kandownDir, id)) {
2118
+ if (findTaskPath2(kandownDir, id)) {
2027
2119
  err(`Task already exists: ${id}`);
2028
2120
  process.exit(1);
2029
2121
  }
@@ -2134,7 +2226,7 @@ async function launchTui(screen, kandownDir) {
2134
2226
  process.exit(1);
2135
2227
  }
2136
2228
  await new Promise((resolveTui) => {
2137
- const child = spawn4(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
2229
+ const child = spawn5(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
2138
2230
  child.on("close", (code) => {
2139
2231
  if (typeof code === "number" && code !== 0) process.exit(code);
2140
2232
  resolveTui();
@@ -2142,7 +2234,8 @@ async function launchTui(screen, kandownDir) {
2142
2234
  });
2143
2235
  }
2144
2236
  async function cmdTui(screen, rawArgs) {
2145
- const { kandownDir } = ensureKandownDir(rawArgs);
2237
+ const args = parseArgs(rawArgs);
2238
+ const kandownDir = resolveKandownDir(args.path, process.cwd());
2146
2239
  await launchTui(screen, kandownDir);
2147
2240
  }
2148
2241
  function cmdExport(rawArgs) {
@@ -2355,14 +2448,19 @@ async function main() {
2355
2448
  }
2356
2449
  case void 0: {
2357
2450
  const parsed = parseArgs(rest);
2358
- const { kandownDir } = ensureKandownDir(rest);
2359
- let status = await getDaemonStatus(kandownDir);
2360
- if (!status.running) {
2361
- status = await startProjectDaemon(kandownDir);
2362
- }
2363
- if (!parsed.flags["no-open"]) {
2364
- const urlToOpen = status.metadata?.url || join8(kandownDir, "kandown.html");
2365
- openBrowser(urlToOpen);
2451
+ const kandownDir = resolveKandownDir(parsed.path, process.cwd());
2452
+ if (existsSync8(kandownDir)) {
2453
+ let status = await getDaemonStatus(kandownDir);
2454
+ if (!status.running) {
2455
+ status = await startProjectDaemon(kandownDir);
2456
+ }
2457
+ if (!parsed.flags["no-open"]) {
2458
+ const urlToOpen = status.metadata?.url || join8(kandownDir, "kandown.html");
2459
+ openBrowser(urlToOpen);
2460
+ }
2461
+ } else if (!process.stdin.isTTY) {
2462
+ err(`No kandown project found at ${c.bold}${kandownDir}${c.reset} \u2014 run ${c.cyan}kandown init${c.reset} first.`);
2463
+ process.exit(1);
2366
2464
  }
2367
2465
  await launchTui("board", kandownDir);
2368
2466
  break;