kandown 0.11.1 → 0.12.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/README.md +13 -4
- package/bin/kandown.js +226 -14
- package/bin/tui.js +18 -13
- package/dist/index.html +115 -115
- package/package.json +1 -1
- package/templates/AGENT.md +11 -8
- package/templates/AGENT_KANDOWN.md +15 -8
- package/templates/README.md +24 -20
package/README.md
CHANGED
|
@@ -48,16 +48,25 @@ cd my-project
|
|
|
48
48
|
kandown init
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
This creates
|
|
51
|
+
This creates two folders at your project root:
|
|
52
52
|
|
|
53
53
|
```
|
|
54
|
-
.kandown/
|
|
54
|
+
.kandown/ # config + web UI + agent docs
|
|
55
55
|
├── kandown.html # Single-file web app
|
|
56
56
|
├── kandown.json # Project config (columns, appearance)
|
|
57
|
-
├──
|
|
58
|
-
└──
|
|
57
|
+
├── AGENT.md # AI-agent quick reference
|
|
58
|
+
└── AGENT_KANDOWN.md # Full agent reference
|
|
59
|
+
|
|
60
|
+
tasks/ # Task files (source of truth)
|
|
61
|
+
├── t1.md # One .md file per task
|
|
62
|
+
└── archive/ # Archived tasks live here
|
|
59
63
|
```
|
|
60
64
|
|
|
65
|
+
> **Layout note:** tasks live at the project root in `./tasks/`, not inside
|
|
66
|
+
> `.kandown/`. This keeps config and data cleanly separated. If you're
|
|
67
|
+
> upgrading from an older version with tasks in `.kandown/tasks/`, the CLI
|
|
68
|
+
> will move them automatically the first time you run it — nothing to do.
|
|
69
|
+
|
|
61
70
|
### Launch the board
|
|
62
71
|
|
|
63
72
|
```bash
|
package/bin/kandown.js
CHANGED
|
@@ -19,7 +19,10 @@
|
|
|
19
19
|
* → resolveKandownBin — resolves the global kandown binary path for respawn
|
|
20
20
|
* → semverGt — compares two semver strings
|
|
21
21
|
* → checkForUpdate — non-blocking auto-updater with lock file and graceful fallback
|
|
22
|
-
* →
|
|
22
|
+
* → getProjectRoot — returns the project root (parent of .kandown/)
|
|
23
|
+
* → getTasksDir — returns the project-root tasks/ path
|
|
24
|
+
* → migrateTasksToTopLevel — silently moves legacy .kandown/tasks/ → ./tasks/
|
|
25
|
+
* → cmdInit — installs `.kandown` and top-level `tasks/`
|
|
23
26
|
* → cmdUpdate — refreshes installed kandown.html
|
|
24
27
|
* → injectServerRoot — injects the CLI server root into single-file HTML
|
|
25
28
|
* → createServeServer — creates the local zero-dependency HTTP server
|
|
@@ -57,6 +60,7 @@ import {
|
|
|
57
60
|
statSync,
|
|
58
61
|
unlinkSync,
|
|
59
62
|
renameSync,
|
|
63
|
+
rmdirSync,
|
|
60
64
|
} from 'node:fs';
|
|
61
65
|
import { spawnSync, spawn, execSync } from 'node:child_process';
|
|
62
66
|
|
|
@@ -355,8 +359,8 @@ ${marker}
|
|
|
355
359
|
**IMPORTANT:** Before touching any task files, you MUST read \`${kandownPath}/AGENT_KANDOWN.md\`.
|
|
356
360
|
|
|
357
361
|
This project uses a file-based kanban:
|
|
358
|
-
- **Tasks live in
|
|
359
|
-
- **
|
|
362
|
+
- **Tasks live in \`./tasks/t-xxx.md\`** at the project root — each task file owns its status
|
|
363
|
+
- **Config lives in \`${kandownPath}/kandown.json\`** (columns, appearance, agent settings)
|
|
360
364
|
- **Completion workflow:** set task frontmatter \`status: Done\` + write the completion report
|
|
361
365
|
`;
|
|
362
366
|
|
|
@@ -376,8 +380,8 @@ function createAgentsFileIfMissing(cwd, kandownPath) {
|
|
|
376
380
|
**IMPORTANT:** Before touching any task files, you MUST read \`AGENT_KANDOWN.md\`.
|
|
377
381
|
|
|
378
382
|
This project uses a file-based kandown:
|
|
379
|
-
- **Tasks live in
|
|
380
|
-
- **
|
|
383
|
+
- **Tasks live in \`./tasks/t-xxx.md\`** at the project root — each task file owns its status
|
|
384
|
+
- **Config lives in \`${kandownPath}/kandown.json\`** (columns, appearance, agent settings)
|
|
381
385
|
- **Completion workflow:** set task frontmatter \`status: Done\` + write the completion report
|
|
382
386
|
`;
|
|
383
387
|
writeFileSync(agentsPath, content, 'utf8');
|
|
@@ -396,9 +400,130 @@ function parseArgs(argv) {
|
|
|
396
400
|
return args;
|
|
397
401
|
}
|
|
398
402
|
|
|
403
|
+
/**
|
|
404
|
+
* 📖 Returns the absolute path of the project root (parent of the kandown
|
|
405
|
+
* config dir). Tasks live at the project root in `./tasks/`, not inside
|
|
406
|
+
* `.kandown/tasks/`. Used by every code path that touches task files.
|
|
407
|
+
* @param {string} kandownDir absolute path to the kandown config dir
|
|
408
|
+
* @returns {string} absolute path to the project root
|
|
409
|
+
*/
|
|
410
|
+
function getProjectRoot(kandownDir) {
|
|
411
|
+
return dirname(kandownDir);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* 📖 Returns the absolute path of the tasks directory at the project root.
|
|
416
|
+
* Mirrors the File System Access layout: `./tasks/` is a sibling of `.kandown/`.
|
|
417
|
+
* @param {string} kandownDir absolute path to the kandown config dir
|
|
418
|
+
* @returns {string} absolute path to `./tasks/`
|
|
419
|
+
*/
|
|
420
|
+
function getTasksDir(kandownDir) {
|
|
421
|
+
return join(getProjectRoot(kandownDir), 'tasks');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* 📖 Silently migrates task files from the legacy `.kandown/tasks/` location
|
|
426
|
+
* to the new top-level `./tasks/` location. Idempotent: returns
|
|
427
|
+
* `{ moved, cleanedUp }` describing what was done (or nothing).
|
|
428
|
+
*
|
|
429
|
+
* Rules:
|
|
430
|
+
* - If `./tasks/*.md` already exists, never move anything (avoid clobbering).
|
|
431
|
+
* - If `.kandown/tasks/` doesn't exist or has no .md files, no-op.
|
|
432
|
+
* - Move every `.md` file (including those in `archive/`) to `./tasks/`.
|
|
433
|
+
* - Delete `.kandown/tasks/` only if it's now empty (preserves any non-md
|
|
434
|
+
* files like `.scratch/` notes the user may have stashed there).
|
|
435
|
+
* - Never throw — failures are logged and skipped so a single bad file
|
|
436
|
+
* doesn't block the rest of the migration.
|
|
437
|
+
*
|
|
438
|
+
* @param {string} kandownDir absolute path to the kandown config dir
|
|
439
|
+
* @returns {{ moved: number, cleanedUp: boolean, skipped: boolean }}
|
|
440
|
+
*/
|
|
441
|
+
function migrateTasksToTopLevel(kandownDir) {
|
|
442
|
+
const projectRoot = getProjectRoot(kandownDir);
|
|
443
|
+
const oldDir = join(kandownDir, 'tasks');
|
|
444
|
+
const newDir = getTasksDir(kandownDir);
|
|
445
|
+
const result = { moved: 0, cleanedUp: false, skipped: false };
|
|
446
|
+
|
|
447
|
+
if (!existsSync(oldDir)) return result;
|
|
448
|
+
if (!existsSync(newDir)) mkdirSync(newDir, { recursive: true });
|
|
449
|
+
|
|
450
|
+
// 📖 If the new location already has any .md file, don't touch anything.
|
|
451
|
+
// The user is in a hybrid state and we should not clobber their work.
|
|
452
|
+
const existingNew = existsSync(newDir)
|
|
453
|
+
? readdirSync(newDir).filter(n => n.endsWith('.md'))
|
|
454
|
+
: [];
|
|
455
|
+
if (existingNew.length > 0) {
|
|
456
|
+
result.skipped = true;
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 📖 If the old location has no .md files either, no migration needed.
|
|
461
|
+
const oldMdFiles = readdirSync(oldDir, { withFileTypes: true })
|
|
462
|
+
.filter(e => e.isFile() && e.name.endsWith('.md'));
|
|
463
|
+
if (oldMdFiles.length === 0) {
|
|
464
|
+
// Still try the archive subfolder.
|
|
465
|
+
return migrateArchive(kandownDir, result);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
for (const entry of oldMdFiles) {
|
|
469
|
+
try {
|
|
470
|
+
renameSync(join(oldDir, entry.name), join(newDir, entry.name));
|
|
471
|
+
result.moved += 1;
|
|
472
|
+
} catch (e) {
|
|
473
|
+
warn(`migrate: could not move ${entry.name} (${e.code ?? e.message})`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return migrateArchive(kandownDir, result);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 📖 Helper that moves the legacy `archive/` subfolder to the new location.
|
|
482
|
+
* Called from migrateTasksToTopLevel after the active files have been moved.
|
|
483
|
+
*/
|
|
484
|
+
function migrateArchive(kandownDir, result) {
|
|
485
|
+
const oldArchive = join(kandownDir, 'tasks', 'archive');
|
|
486
|
+
const newArchive = join(getTasksDir(kandownDir), 'archive');
|
|
487
|
+
if (!existsSync(oldArchive)) return cleanupLegacyTasksDir(kandownDir, result);
|
|
488
|
+
|
|
489
|
+
if (!existsSync(newArchive)) mkdirSync(newArchive, { recursive: true });
|
|
490
|
+
for (const entry of readdirSync(oldArchive, { withFileTypes: true })) {
|
|
491
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
492
|
+
try {
|
|
493
|
+
renameSync(join(oldArchive, entry.name), join(newArchive, entry.name));
|
|
494
|
+
result.moved += 1;
|
|
495
|
+
} catch (e) {
|
|
496
|
+
warn(`migrate: could not move archive/${entry.name} (${e.code ?? e.message})`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return cleanupLegacyTasksDir(kandownDir, result);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 📖 Removes the legacy `.kandown/tasks/` directory only if it is empty.
|
|
506
|
+
* Preserves any leftover non-md files (e.g. `.scratch/` notes) so we never
|
|
507
|
+
* delete user data the migration didn't move.
|
|
508
|
+
*/
|
|
509
|
+
function cleanupLegacyTasksDir(kandownDir, result) {
|
|
510
|
+
const oldDir = join(kandownDir, 'tasks');
|
|
511
|
+
if (!existsSync(oldDir)) return result;
|
|
512
|
+
const remaining = readdirSync(oldDir);
|
|
513
|
+
if (remaining.length === 0) {
|
|
514
|
+
try {
|
|
515
|
+
rmdirSync(oldDir);
|
|
516
|
+
result.cleanedUp = true;
|
|
517
|
+
} catch { /* directory not empty or platform limitation — leave it */ }
|
|
518
|
+
}
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
|
|
399
522
|
/**
|
|
400
523
|
* @returns {{ kandownDir: string, alreadyExisted: boolean }} — resolves the
|
|
401
524
|
* kandown directory and auto-inits it if it doesn't exist (no prompt, silent init).
|
|
525
|
+
* Also performs a one-time silent migration of tasks from `.kandown/tasks/` to
|
|
526
|
+
* the project root `./tasks/` on first access of a legacy project.
|
|
402
527
|
*/
|
|
403
528
|
function ensureKandownDir(rawArgs) {
|
|
404
529
|
const args = parseArgs(rawArgs);
|
|
@@ -406,7 +531,18 @@ function ensureKandownDir(rawArgs) {
|
|
|
406
531
|
const explicitPath = rawArgs.includes('--path') || rawArgs.includes('-p');
|
|
407
532
|
const kandownDir = resolve(cwd, args.path);
|
|
408
533
|
|
|
409
|
-
if (existsSync(kandownDir))
|
|
534
|
+
if (existsSync(kandownDir)) {
|
|
535
|
+
// 📖 Silent one-time migration: move any legacy `.kandown/tasks/*.md` to
|
|
536
|
+
// the project-root `./tasks/`. Idempotent — safe to call on every startup.
|
|
537
|
+
const migration = migrateTasksToTopLevel(kandownDir);
|
|
538
|
+
if (migration.moved > 0) {
|
|
539
|
+
success(`Migrated ${migration.moved} task${migration.moved === 1 ? '' : 's'} from .kandown/tasks/ to ./tasks/`);
|
|
540
|
+
if (migration.cleanedUp) info('Removed empty .kandown/tasks/ folder');
|
|
541
|
+
} else if (migration.skipped) {
|
|
542
|
+
info('Both .kandown/tasks/ and ./tasks/ have files — leaving both in place');
|
|
543
|
+
}
|
|
544
|
+
return { kandownDir, alreadyExisted: true };
|
|
545
|
+
}
|
|
410
546
|
|
|
411
547
|
log('');
|
|
412
548
|
info(`No .kandown/ found — auto-installing...`);
|
|
@@ -440,13 +576,16 @@ function doInit(args, cwd, kandownPath, kandownDir) {
|
|
|
440
576
|
success('README.md');
|
|
441
577
|
}
|
|
442
578
|
|
|
579
|
+
// 📖 Tasks live at the project root in `./tasks/`, not inside `.kandown/`.
|
|
580
|
+
// This keeps config separate from data and follows the user-facing convention.
|
|
443
581
|
const tasksSrc = join(templatesDir, 'tasks');
|
|
444
|
-
const tasksDest =
|
|
582
|
+
const tasksDest = getTasksDir(kandownDir);
|
|
445
583
|
if (!existsSync(tasksDest)) {
|
|
584
|
+
mkdirSync(tasksDest, { recursive: true });
|
|
446
585
|
copyRecursive(tasksSrc, tasksDest);
|
|
447
|
-
success('tasks/ (with welcome example)');
|
|
586
|
+
success('./tasks/ (with welcome example)');
|
|
448
587
|
} else {
|
|
449
|
-
info('tasks/ already exists (kept)');
|
|
588
|
+
info('./tasks/ already exists (kept)');
|
|
450
589
|
}
|
|
451
590
|
|
|
452
591
|
if (!existsSync(join(kandownDir, 'kandown.json'))) {
|
|
@@ -512,9 +651,13 @@ function cmdInit(rawArgs) {
|
|
|
512
651
|
log('');
|
|
513
652
|
log(`${c.green}${c.bold}Done.${c.reset}`);
|
|
514
653
|
log('');
|
|
654
|
+
log(` ${c.dim}Layout:${c.reset}`);
|
|
655
|
+
log(` ${c.dim}└─${c.reset} ${c.bold}.kandown/${c.reset} — config, web UI, agent docs`);
|
|
656
|
+
log(` ${c.dim}└─${c.reset} ${c.bold}tasks/${c.reset} — task files (source of truth)`);
|
|
657
|
+
log('');
|
|
515
658
|
log(` ${c.dim}Next steps:${c.reset}`);
|
|
516
659
|
log(` ${c.cyan}1.${c.reset} Open ${c.bold}${kandownPath}/kandown.html${c.reset} in Chrome/Edge/Brave`);
|
|
517
|
-
log(` ${c.cyan}2.${c.reset} Select the ${c.bold}${kandownPath}/${c.reset}
|
|
660
|
+
log(` ${c.cyan}2.${c.reset} Select the ${c.bold}project root${c.reset} (the parent of ${c.bold}${kandownPath}/${c.reset})`);
|
|
518
661
|
log(` ${c.cyan}3.${c.reset} Start creating tasks. Press ${c.cyan}⌘K${c.reset} for the command palette`);
|
|
519
662
|
log('');
|
|
520
663
|
log(` ${c.dim}macOS:${c.reset} open ${kandownPath}/kandown.html`);
|
|
@@ -796,17 +939,21 @@ function putBoard(req, res, kandownDir) {
|
|
|
796
939
|
* first, then the archive subfolder. Returns null when the id exists in
|
|
797
940
|
* neither location. Used by every CRUD handler so archived tasks stay
|
|
798
941
|
* reachable at their real location.
|
|
942
|
+
*
|
|
943
|
+
* Tasks live at the project root in `./tasks/` (sibling of `.kandown/`),
|
|
944
|
+
* not inside `.kandown/tasks/`.
|
|
799
945
|
*/
|
|
800
946
|
function findTaskPath(kandownDir, id) {
|
|
801
|
-
const
|
|
947
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
948
|
+
const inTasks = join(tasksDir, `${id}.md`);
|
|
802
949
|
if (existsSync(inTasks)) return inTasks;
|
|
803
|
-
const inArchive = join(
|
|
950
|
+
const inArchive = join(tasksDir, 'archive', `${id}.md`);
|
|
804
951
|
if (existsSync(inArchive)) return inArchive;
|
|
805
952
|
return null;
|
|
806
953
|
}
|
|
807
954
|
|
|
808
955
|
function getTasks(res, kandownDir) {
|
|
809
|
-
const tasksDir =
|
|
956
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
810
957
|
const archiveDir = join(tasksDir, 'archive');
|
|
811
958
|
const ids = new Set();
|
|
812
959
|
try {
|
|
@@ -853,7 +1000,7 @@ function putTask(req, res, kandownDir, id) {
|
|
|
853
1000
|
return;
|
|
854
1001
|
}
|
|
855
1002
|
readBody(req).then(body => {
|
|
856
|
-
const tasksDir =
|
|
1003
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
857
1004
|
if (!existsSync(tasksDir)) mkdirSync(tasksDir, { recursive: true });
|
|
858
1005
|
// 📖 Write in place: keep an archived task inside archive/ on save so the
|
|
859
1006
|
// file location never drifts from its archived flag.
|
|
@@ -887,6 +1034,66 @@ function deleteTask(res, kandownDir, id) {
|
|
|
887
1034
|
}
|
|
888
1035
|
}
|
|
889
1036
|
|
|
1037
|
+
/**
|
|
1038
|
+
* 📖 Archives a task: writes the (already flag-updated) content into
|
|
1039
|
+
* `tasks/archive/<id>.md` and removes the active `tasks/<id>.md` copy.
|
|
1040
|
+
* The body comes pre-flagged from the web client (which knows frontmatter).
|
|
1041
|
+
*/
|
|
1042
|
+
function archiveTask(req, res, kandownDir, id) {
|
|
1043
|
+
if (!isValidTaskId(id)) {
|
|
1044
|
+
writeText(res, 400, 'Invalid task id');
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
readBody(req).then(body => {
|
|
1048
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
1049
|
+
const archiveDir = join(tasksDir, 'archive');
|
|
1050
|
+
if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
|
|
1051
|
+
writeFileSync(join(archiveDir, `${id}.md`), body, 'utf8');
|
|
1052
|
+
try {
|
|
1053
|
+
unlinkSync(join(tasksDir, `${id}.md`));
|
|
1054
|
+
} catch { /* already absent */ }
|
|
1055
|
+
writeJson(res, 200, { ok: true });
|
|
1056
|
+
}).catch(e => {
|
|
1057
|
+
writeJson(res, 500, { error: `Failed to archive task: ${e.message}` });
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* 📖 Unarchives a task: writes the content back into `tasks/<id>.md` and
|
|
1063
|
+
* removes the archived copy. Mirror of archiveTask.
|
|
1064
|
+
*/
|
|
1065
|
+
function unarchiveTask(req, res, kandownDir, id) {
|
|
1066
|
+
if (!isValidTaskId(id)) {
|
|
1067
|
+
writeText(res, 400, 'Invalid task id');
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
readBody(req).then(body => {
|
|
1071
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
1072
|
+
writeFileSync(join(tasksDir, `${id}.md`), body, 'utf8');
|
|
1073
|
+
try {
|
|
1074
|
+
unlinkSync(join(tasksDir, 'archive', `${id}.md`));
|
|
1075
|
+
} catch { /* already absent */ }
|
|
1076
|
+
writeJson(res, 200, { ok: true });
|
|
1077
|
+
}).catch(e => {
|
|
1078
|
+
writeJson(res, 500, { error: `Failed to unarchive task: ${e.message}` });
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* 📖 REST endpoint to trigger the silent one-time task migration. The web
|
|
1084
|
+
* client (server mode) can call this on startup to perform the same
|
|
1085
|
+
* `.kandown/tasks/` → `./tasks/` move the CLI does on first access.
|
|
1086
|
+
* Safe to call multiple times — idempotent.
|
|
1087
|
+
*/
|
|
1088
|
+
function postMigrateTasks(res, kandownDir) {
|
|
1089
|
+
try {
|
|
1090
|
+
const result = migrateTasksToTopLevel(kandownDir);
|
|
1091
|
+
writeJson(res, 200, { ok: true, ...result });
|
|
1092
|
+
} catch (e) {
|
|
1093
|
+
writeJson(res, 500, { error: `Migration failed: ${e.message}` });
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
890
1097
|
/**
|
|
891
1098
|
* 📖 The single-file Vite bundle can contain literal strings such as
|
|
892
1099
|
* `</head>` from HTML parser libraries. Use the last closing head tag so the
|
|
@@ -941,6 +1148,11 @@ function handleApi(req, res, url, kandownDir) {
|
|
|
941
1148
|
if (req.method === 'POST' && id && parts[2] === 'unarchive') return unarchiveTask(req, res, kandownDir, id);
|
|
942
1149
|
}
|
|
943
1150
|
|
|
1151
|
+
// 📖 Migration endpoint: `POST /api/migrate-tasks` with no id. Idempotent.
|
|
1152
|
+
if (resource === 'migrate-tasks' && req.method === 'POST' && !id) {
|
|
1153
|
+
return postMigrateTasks(res, kandownDir);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
944
1156
|
writeJson(res, 404, { error: 'Not found' });
|
|
945
1157
|
}
|
|
946
1158
|
|
package/bin/tui.js
CHANGED
|
@@ -54536,8 +54536,11 @@ function serializeTaskFile(frontmatter, body) {
|
|
|
54536
54536
|
function getProjectRoot(kandownDir) {
|
|
54537
54537
|
return dirname(kandownDir);
|
|
54538
54538
|
}
|
|
54539
|
+
function getTasksDir(kandownDir) {
|
|
54540
|
+
return join2(getProjectRoot(kandownDir), "tasks");
|
|
54541
|
+
}
|
|
54539
54542
|
function listTaskIds(kandownDir) {
|
|
54540
|
-
const tasksDir =
|
|
54543
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
54541
54544
|
if (!existsSync3(tasksDir)) return [];
|
|
54542
54545
|
return readdirSync(tasksDir).filter((name) => name.endsWith(".md")).map((name) => name.slice(0, -3)).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
|
|
54543
54546
|
}
|
|
@@ -54561,7 +54564,7 @@ function readBoard(kandownDir) {
|
|
|
54561
54564
|
};
|
|
54562
54565
|
}
|
|
54563
54566
|
function readTask(kandownDir, taskId) {
|
|
54564
|
-
const taskPath = join2(kandownDir,
|
|
54567
|
+
const taskPath = join2(getTasksDir(kandownDir), `${taskId}.md`);
|
|
54565
54568
|
if (!existsSync3(taskPath)) {
|
|
54566
54569
|
return {
|
|
54567
54570
|
frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
|
|
@@ -54593,7 +54596,7 @@ function readAgentDoc(kandownDir) {
|
|
|
54593
54596
|
return "";
|
|
54594
54597
|
}
|
|
54595
54598
|
function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
54596
|
-
const taskPath = join2(kandownDir,
|
|
54599
|
+
const taskPath = join2(getTasksDir(kandownDir), `${taskId}.md`);
|
|
54597
54600
|
if (!existsSync3(taskPath)) return false;
|
|
54598
54601
|
const parsed = readTask(kandownDir, taskId);
|
|
54599
54602
|
writeFileSync2(taskPath, serializeTaskFile({
|
|
@@ -56454,9 +56457,12 @@ var FileWatcher = class {
|
|
|
56454
56457
|
* 📖 Start watching task files and kandown.json.
|
|
56455
56458
|
* Uses chokidar for immediate FS event detection, plus a fallback 500ms
|
|
56456
56459
|
* poll to catch any races or network-mounted file changes.
|
|
56460
|
+
*
|
|
56461
|
+
* Tasks live at the project root (`./tasks/`, sibling of `.kandown/`);
|
|
56462
|
+
* kandown.json lives inside `.kandown/`.
|
|
56457
56463
|
*/
|
|
56458
56464
|
start(kandownDir) {
|
|
56459
|
-
const tasksDir =
|
|
56465
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
56460
56466
|
const configPath = join6(kandownDir, "kandown.json");
|
|
56461
56467
|
const existingIds = listTaskIds(kandownDir);
|
|
56462
56468
|
for (const id of existingIds) {
|
|
@@ -56515,7 +56521,7 @@ var FileWatcher = class {
|
|
|
56515
56521
|
// ─── Private ───────────────────────────────────────────────────────────────
|
|
56516
56522
|
handleFsEvent(event, filePath, kandownDir) {
|
|
56517
56523
|
if (this.stopped) return;
|
|
56518
|
-
const tasksDir =
|
|
56524
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
56519
56525
|
const configPath = join6(kandownDir, "kandown.json");
|
|
56520
56526
|
if (filePath === configPath) {
|
|
56521
56527
|
const key = `config:${event}`;
|
|
@@ -56568,7 +56574,7 @@ var FileWatcher = class {
|
|
|
56568
56574
|
/** 📖 Fallback poll — catches changes that chokidar missed (network mounts, exotic FS). */
|
|
56569
56575
|
async pollHashes(kandownDir) {
|
|
56570
56576
|
if (this.stopped) return;
|
|
56571
|
-
const tasksDir =
|
|
56577
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
56572
56578
|
const configPath = join6(kandownDir, "kandown.json");
|
|
56573
56579
|
try {
|
|
56574
56580
|
const newHash = hashFileSync(configPath);
|
|
@@ -57060,23 +57066,22 @@ function CategoryTaskRow({ task, focused, dragging, colWidth }) {
|
|
|
57060
57066
|
const titleClean = category ? task.title.slice(task.title.indexOf(category.key) + category.key.length).trim() : task.title;
|
|
57061
57067
|
const contentWidth = Math.max(4, colWidth - 2);
|
|
57062
57068
|
const titleStr = truncate(titleClean, contentWidth);
|
|
57063
|
-
const bg = dragging ? "yellow" : focused ? "cyan" : "
|
|
57064
|
-
const
|
|
57065
|
-
const catColor = dragging ? "
|
|
57066
|
-
const ttlColor = dragging ? "yellow" : focused ? "white" : "gray";
|
|
57069
|
+
const bg = dragging ? "yellow" : focused ? "cyan" : "#222";
|
|
57070
|
+
const txtColor = dragging ? "black" : focused ? "black" : "white";
|
|
57071
|
+
const catColor = dragging ? "black" : focused ? "black" : "magenta";
|
|
57067
57072
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Box_default, { flexDirection: "column", backgroundColor: bg, children: [
|
|
57068
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color:
|
|
57073
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: txtColor, bold: focused || dragging, children: [
|
|
57069
57074
|
cursor,
|
|
57070
57075
|
" ",
|
|
57071
57076
|
check2,
|
|
57072
57077
|
" ",
|
|
57073
57078
|
idStr
|
|
57074
57079
|
] }),
|
|
57075
|
-
category && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: catColor, bold:
|
|
57080
|
+
category && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: catColor, bold: true, children: [
|
|
57076
57081
|
" ",
|
|
57077
57082
|
category.label
|
|
57078
57083
|
] }),
|
|
57079
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color:
|
|
57084
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(Text, { color: txtColor, children: [
|
|
57080
57085
|
" ",
|
|
57081
57086
|
titleStr
|
|
57082
57087
|
] })
|