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 +18 -0
- package/README.md +2 -0
- package/bin/kandown.js +183 -63
- package/bin/tui.js +203 -48
- package/dist/index.html +209 -195
- package/package.json +1 -1
- package/templates/README.md +2 -0
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
|
|
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
|
|
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.
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
-
|
|
566
|
-
|
|
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 =
|
|
595
|
-
if (!
|
|
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 =
|
|
656
|
-
if (!
|
|
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 =
|
|
909
|
-
if (!cliPath) throw new Error(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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
|
|
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
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
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;
|