kandown 0.33.5 → 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,23 @@
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
+
17
+ ## 0.33.6 — 2026-07-24 — "Subdirectory Project Discovery"
18
+
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.
20
+
3
21
  ## 0.33.5 — 2026-07-24 — "Sidebar Task Switch Fix"
4
22
 
5
23
  - **Fixed**: **BlockNote Editor Not Refreshing on Sidebar Task Switch** — the BlockNote markdown editor now properly reloads when switching between tasks via the sidebar explorer. Previously, the editor body kept showing the previous task's content because the `useEffect` only ran once on mount. The fix compares the editor's current markdown (normalized) against the incoming `value` prop and only calls `replaceBlocks` when they differ. This makes the editor reactive to external value changes (task switch) while preserving cursor/focus during the user's own typing sessions.
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
@@ -5,9 +5,9 @@ if (typeof globalThis.require === 'undefined') {
5
5
  }
6
6
 
7
7
  // src/cli/cli.ts
8
- import { existsSync as existsSync8, readFileSync as readFileSync8, copyFileSync as copyFileSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync3 } from "fs";
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.5";
19
+ var KANDOWN_VERSION = "0.34.0";
20
20
 
21
21
  // src/cli/lib/updater.ts
22
22
  import { fileURLToPath } from "url";
@@ -265,7 +265,7 @@ ${now}`, "utf8");
265
265
  }
266
266
 
267
267
  // src/cli/lib/board-reader.ts
268
- import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync3 } from "fs";
268
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, unlinkSync as unlinkSync3 } from "fs";
269
269
  import { dirname as dirname2, join as join3 } from "path";
270
270
  import { fileURLToPath as fileURLToPath2 } from "url";
271
271
  import { homedir as homedir2 } from "os";
@@ -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);
@@ -484,7 +496,7 @@ function serializeTaskFile(frontmatter, body) {
484
496
  }
485
497
 
486
498
  // src/cli/lib/config.ts
487
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
499
+ import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync, statSync as statSync2 } from "fs";
488
500
  import { join as join2 } from "path";
489
501
  var DEFAULT_CONFIG = {
490
502
  ui: { language: "en", theme: "auto", skin: "kandown", font: "inter" },
@@ -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 readdirSync(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/")) {
@@ -1283,18 +1372,18 @@ async function listenOnAvailablePort(kandownDir, preferredPort) {
1283
1372
  }
1284
1373
 
1285
1374
  // src/cli/lib/init.ts
1286
- import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1375
+ import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
1287
1376
  import { join as join6 } from "path";
1288
1377
  function copyRecursive(src, dest) {
1289
1378
  const errors = [];
1290
1379
  try {
1291
1380
  if (!existsSync6(dest)) mkdirSync4(dest, { recursive: true });
1292
- const entries = readdirSync2(src);
1381
+ const entries = readdirSync3(src);
1293
1382
  for (const entry of entries) {
1294
1383
  const srcPath = join6(src, entry);
1295
1384
  const destPath = join6(dest, entry);
1296
1385
  try {
1297
- if (statSync2(srcPath).isDirectory()) {
1386
+ if (statSync3(srcPath).isDirectory()) {
1298
1387
  errors.push(...copyRecursive(srcPath, destPath));
1299
1388
  } else if (!existsSync6(destPath)) {
1300
1389
  copyFileSync2(srcPath, destPath);
@@ -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 = [];
@@ -1665,6 +1757,28 @@ function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
1665
1757
  if (basename(cwd) === ".kandown" || existsSync8(join8(cwd, "kandown.json"))) {
1666
1758
  return cwd;
1667
1759
  }
1760
+ if (pathArg !== ".kandown") {
1761
+ return resolve2(cwd, pathArg);
1762
+ }
1763
+ let entries;
1764
+ try {
1765
+ entries = readdirSync4(cwd);
1766
+ } catch {
1767
+ return resolve2(cwd, pathArg);
1768
+ }
1769
+ for (const name of entries) {
1770
+ if (name.startsWith(".") || name === "node_modules" || name === "tasks") continue;
1771
+ const subPath = join8(cwd, name);
1772
+ let stat;
1773
+ try {
1774
+ stat = statSync4(subPath);
1775
+ } catch {
1776
+ continue;
1777
+ }
1778
+ if (!stat.isDirectory()) continue;
1779
+ const found = resolveKandownDir(pathArg, subPath);
1780
+ if (existsSync8(found)) return found;
1781
+ }
1668
1782
  return resolve2(cwd, pathArg);
1669
1783
  }
1670
1784
  function ensureKandownDir(rawArgs) {
@@ -1730,7 +1844,7 @@ async function cmdUpdate(rawArgs) {
1730
1844
  const current = getCurrentVersion();
1731
1845
  log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
1732
1846
  const latest = await new Promise((resolve3) => {
1733
- const child = spawn4("npm", ["view", "kandown", "version"], {
1847
+ const child = spawn5("npm", ["view", "kandown", "version"], {
1734
1848
  timeout: 6e3,
1735
1849
  stdio: ["pipe", "pipe", "pipe"],
1736
1850
  env: { ...process.env },
@@ -1873,7 +1987,7 @@ function resolveStatusArg(kandownDir, status) {
1873
1987
  function taskPath(kandownDir, id, archived = false) {
1874
1988
  return archived ? join8(getTasksDir(kandownDir), "archive", `${id}.md`) : join8(getTasksDir(kandownDir), `${id}.md`);
1875
1989
  }
1876
- function findTaskPath(kandownDir, id) {
1990
+ function findTaskPath2(kandownDir, id) {
1877
1991
  const active = taskPath(kandownDir, id);
1878
1992
  if (existsSync8(active)) return active;
1879
1993
  const archived = taskPath(kandownDir, id, true);
@@ -1884,7 +1998,7 @@ function nextTaskId(kandownDir) {
1884
1998
  const ids = new Set(listTaskIds(kandownDir));
1885
1999
  const archiveDir = join8(getTasksDir(kandownDir), "archive");
1886
2000
  if (existsSync8(archiveDir)) {
1887
- for (const file of readdirSync3(archiveDir)) {
2001
+ for (const file of readdirSync4(archiveDir)) {
1888
2002
  if (file.endsWith(".md")) ids.add(file.slice(0, -3));
1889
2003
  }
1890
2004
  }
@@ -1896,7 +2010,7 @@ function nextTaskId(kandownDir) {
1896
2010
  return `t${max + 1}`;
1897
2011
  }
1898
2012
  function readTaskFile(kandownDir, id) {
1899
- const path = findTaskPath(kandownDir, id);
2013
+ const path = findTaskPath2(kandownDir, id);
1900
2014
  if (!path) return null;
1901
2015
  const parsed = parseTaskFile(readFileSync8(path, "utf8"));
1902
2016
  return {
@@ -1930,7 +2044,7 @@ function cmdList(rawArgs) {
1930
2044
  if (includeArchived) {
1931
2045
  const archiveDir = join8(getTasksDir(kandownDir), "archive");
1932
2046
  if (existsSync8(archiveDir)) {
1933
- for (const file of readdirSync3(archiveDir).filter((name) => name.endsWith(".md"))) {
2047
+ for (const file of readdirSync4(archiveDir).filter((name) => name.endsWith(".md"))) {
1934
2048
  const id = file.slice(0, -3);
1935
2049
  const parsed = parseTaskFile(readFileSync8(join8(archiveDir, file), "utf8"));
1936
2050
  rows.push({
@@ -1981,7 +2095,7 @@ function cmdShow(rawArgs) {
1981
2095
  err("Usage: kandown show <task-id>");
1982
2096
  process.exit(1);
1983
2097
  }
1984
- const path = findTaskPath(kandownDir, id);
2098
+ const path = findTaskPath2(kandownDir, id);
1985
2099
  if (!path) {
1986
2100
  err(`Task not found: ${id}`);
1987
2101
  process.exit(1);
@@ -2001,7 +2115,7 @@ function cmdCreate(rawArgs) {
2001
2115
  err(`Invalid task id: ${id}`);
2002
2116
  process.exit(1);
2003
2117
  }
2004
- if (findTaskPath(kandownDir, id)) {
2118
+ if (findTaskPath2(kandownDir, id)) {
2005
2119
  err(`Task already exists: ${id}`);
2006
2120
  process.exit(1);
2007
2121
  }
@@ -2112,7 +2226,7 @@ async function launchTui(screen, kandownDir) {
2112
2226
  process.exit(1);
2113
2227
  }
2114
2228
  await new Promise((resolveTui) => {
2115
- const child = spawn4(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
2229
+ const child = spawn5(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
2116
2230
  child.on("close", (code) => {
2117
2231
  if (typeof code === "number" && code !== 0) process.exit(code);
2118
2232
  resolveTui();
@@ -2120,7 +2234,8 @@ async function launchTui(screen, kandownDir) {
2120
2234
  });
2121
2235
  }
2122
2236
  async function cmdTui(screen, rawArgs) {
2123
- const { kandownDir } = ensureKandownDir(rawArgs);
2237
+ const args = parseArgs(rawArgs);
2238
+ const kandownDir = resolveKandownDir(args.path, process.cwd());
2124
2239
  await launchTui(screen, kandownDir);
2125
2240
  }
2126
2241
  function cmdExport(rawArgs) {
@@ -2333,14 +2448,19 @@ async function main() {
2333
2448
  }
2334
2449
  case void 0: {
2335
2450
  const parsed = parseArgs(rest);
2336
- const { kandownDir } = ensureKandownDir(rest);
2337
- let status = await getDaemonStatus(kandownDir);
2338
- if (!status.running) {
2339
- status = await startProjectDaemon(kandownDir);
2340
- }
2341
- if (!parsed.flags["no-open"]) {
2342
- const urlToOpen = status.metadata?.url || join8(kandownDir, "kandown.html");
2343
- 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);
2344
2464
  }
2345
2465
  await launchTui("board", kandownDir);
2346
2466
  break;