kandown 0.8.0 → 0.10.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/bin/kandown.js +46 -12
- package/bin/tui.js +18 -3
- package/dist/index.html +379 -1908
- package/package.json +1 -3
package/bin/kandown.js
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
readdirSync,
|
|
57
57
|
statSync,
|
|
58
58
|
unlinkSync,
|
|
59
|
+
renameSync,
|
|
59
60
|
} from 'node:fs';
|
|
60
61
|
import { spawnSync, spawn, execSync } from 'node:child_process';
|
|
61
62
|
|
|
@@ -790,16 +791,39 @@ function putBoard(req, res, kandownDir) {
|
|
|
790
791
|
});
|
|
791
792
|
}
|
|
792
793
|
|
|
794
|
+
/**
|
|
795
|
+
* 📖 Resolves the on-disk path of a task id, searching the active tasks dir
|
|
796
|
+
* first, then the archive subfolder. Returns null when the id exists in
|
|
797
|
+
* neither location. Used by every CRUD handler so archived tasks stay
|
|
798
|
+
* reachable at their real location.
|
|
799
|
+
*/
|
|
800
|
+
function findTaskPath(kandownDir, id) {
|
|
801
|
+
const inTasks = join(kandownDir, 'tasks', `${id}.md`);
|
|
802
|
+
if (existsSync(inTasks)) return inTasks;
|
|
803
|
+
const inArchive = join(kandownDir, 'tasks', 'archive', `${id}.md`);
|
|
804
|
+
if (existsSync(inArchive)) return inArchive;
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
|
|
793
808
|
function getTasks(res, kandownDir) {
|
|
794
809
|
const tasksDir = join(kandownDir, 'tasks');
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
810
|
+
const archiveDir = join(tasksDir, 'archive');
|
|
811
|
+
const ids = new Set();
|
|
799
812
|
try {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
813
|
+
if (existsSync(tasksDir)) {
|
|
814
|
+
for (const f of readdirSync(tasksDir).filter(f => f.endsWith('.md'))) {
|
|
815
|
+
ids.add(f.replace(/\.md$/, ''));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
// 📖 Also surface archived tasks so the UI can list them in the archive
|
|
819
|
+
// view. The archived flag in frontmatter (not the folder) is the source of
|
|
820
|
+
// truth for hiding them from the active board.
|
|
821
|
+
if (existsSync(archiveDir)) {
|
|
822
|
+
for (const f of readdirSync(archiveDir).filter(f => f.endsWith('.md'))) {
|
|
823
|
+
ids.add(f.replace(/\.md$/, ''));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
writeJson(res, 200, [...ids].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })));
|
|
803
827
|
} catch (e) {
|
|
804
828
|
writeJson(res, 500, { error: `Failed to list tasks: ${e.message}` });
|
|
805
829
|
}
|
|
@@ -810,8 +834,8 @@ function getTask(res, kandownDir, id) {
|
|
|
810
834
|
writeText(res, 400, 'Invalid task id');
|
|
811
835
|
return;
|
|
812
836
|
}
|
|
813
|
-
const taskPath =
|
|
814
|
-
if (!
|
|
837
|
+
const taskPath = findTaskPath(kandownDir, id);
|
|
838
|
+
if (!taskPath) {
|
|
815
839
|
writeText(res, 404, 'Task not found');
|
|
816
840
|
return;
|
|
817
841
|
}
|
|
@@ -831,7 +855,13 @@ function putTask(req, res, kandownDir, id) {
|
|
|
831
855
|
readBody(req).then(body => {
|
|
832
856
|
const tasksDir = join(kandownDir, 'tasks');
|
|
833
857
|
if (!existsSync(tasksDir)) mkdirSync(tasksDir, { recursive: true });
|
|
834
|
-
|
|
858
|
+
// 📖 Write in place: keep an archived task inside archive/ on save so the
|
|
859
|
+
// file location never drifts from its archived flag.
|
|
860
|
+
const existing = findTaskPath(kandownDir, id);
|
|
861
|
+
const archiveDir = join(tasksDir, 'archive');
|
|
862
|
+
const inArchive = existing && existing.startsWith(archiveDir);
|
|
863
|
+
const targetDir = inArchive ? archiveDir : tasksDir;
|
|
864
|
+
const taskPath = join(targetDir, `${id}.md`);
|
|
835
865
|
writeFileSync(taskPath, body, 'utf8');
|
|
836
866
|
writeJson(res, 200, { ok: true });
|
|
837
867
|
}).catch(e => {
|
|
@@ -844,8 +874,8 @@ function deleteTask(res, kandownDir, id) {
|
|
|
844
874
|
writeText(res, 400, 'Invalid task id');
|
|
845
875
|
return;
|
|
846
876
|
}
|
|
847
|
-
const taskPath =
|
|
848
|
-
if (!
|
|
877
|
+
const taskPath = findTaskPath(kandownDir, id);
|
|
878
|
+
if (!taskPath) {
|
|
849
879
|
writeJson(res, 404, { error: 'Task not found' });
|
|
850
880
|
return;
|
|
851
881
|
}
|
|
@@ -905,6 +935,10 @@ function handleApi(req, res, url, kandownDir) {
|
|
|
905
935
|
if (req.method === 'GET' && id) return getTask(res, kandownDir, id);
|
|
906
936
|
if (req.method === 'PUT' && id) return putTask(req, res, kandownDir, id);
|
|
907
937
|
if (req.method === 'DELETE' && id) return deleteTask(res, kandownDir, id);
|
|
938
|
+
// parts[2] is the sub-resource: 'archive' or 'unarchive'. The body carries
|
|
939
|
+
// the full task file content with the archived flag already toggled.
|
|
940
|
+
if (req.method === 'POST' && id && parts[2] === 'archive') return archiveTask(req, res, kandownDir, id);
|
|
941
|
+
if (req.method === 'POST' && id && parts[2] === 'unarchive') return unarchiveTask(req, res, kandownDir, id);
|
|
908
942
|
}
|
|
909
943
|
|
|
910
944
|
writeJson(res, 404, { error: 'Not found' });
|
package/bin/tui.js
CHANGED
|
@@ -54421,6 +54421,7 @@ function taskToBoardTask(task) {
|
|
|
54421
54421
|
const total = subtasks.length;
|
|
54422
54422
|
const status = normalizeStatus(frontmatter.status);
|
|
54423
54423
|
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
|
|
54424
|
+
const { id: _id, title: _title, status: _status, order: _order, created: _created, archived: _archived, report: _report, ...metadata } = frontmatter;
|
|
54424
54425
|
return {
|
|
54425
54426
|
id: frontmatter.id || "",
|
|
54426
54427
|
title: frontmatter.title || frontmatter.id || "Untitled task",
|
|
@@ -54429,7 +54430,8 @@ function taskToBoardTask(task) {
|
|
|
54429
54430
|
assignee: typeof frontmatter.assignee === "string" && frontmatter.assignee ? frontmatter.assignee : null,
|
|
54430
54431
|
priority: normalizePriority(frontmatter.priority),
|
|
54431
54432
|
ownerType: normalizeOwnerType(frontmatter.ownerType),
|
|
54432
|
-
progress: total > 0 ? { done, total } : null
|
|
54433
|
+
progress: total > 0 ? { done, total } : null,
|
|
54434
|
+
frontmatter: metadata
|
|
54433
54435
|
};
|
|
54434
54436
|
}
|
|
54435
54437
|
function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
|
|
@@ -54438,7 +54440,7 @@ function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
|
|
|
54438
54440
|
const configured = columnNames.map((name) => ({ name, tasks: [] }));
|
|
54439
54441
|
for (const column of configured) columnsByName.set(column.name.toLowerCase(), column);
|
|
54440
54442
|
const unknownColumns = [];
|
|
54441
|
-
const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).sort((a, b) => {
|
|
54443
|
+
const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).filter((task) => !isArchived(task)).sort((a, b) => {
|
|
54442
54444
|
const byOrder = taskOrder(a) - taskOrder(b);
|
|
54443
54445
|
if (byOrder !== 0) return byOrder;
|
|
54444
54446
|
return a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true });
|
|
@@ -54455,6 +54457,9 @@ function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
|
|
|
54455
54457
|
}
|
|
54456
54458
|
return [...unknownColumns, ...configured];
|
|
54457
54459
|
}
|
|
54460
|
+
function isArchived(task) {
|
|
54461
|
+
return String(task.frontmatter.archived) === "true";
|
|
54462
|
+
}
|
|
54458
54463
|
function extractSubtasks(body) {
|
|
54459
54464
|
const subtasks = [];
|
|
54460
54465
|
if (!body || typeof body !== "string") return { subtasks, bodyWithoutSubtasks: body ?? "" };
|
|
@@ -54488,6 +54493,16 @@ function extractSubtasks(body) {
|
|
|
54488
54493
|
subtasks[subtasks.length - 1].report = reportMatch[1];
|
|
54489
54494
|
continue;
|
|
54490
54495
|
}
|
|
54496
|
+
const legacyDescMatch = line.match(/^\s+description:\s*(.+)$/);
|
|
54497
|
+
if (legacyDescMatch && inSubtaskSection && subtasks.length > 0) {
|
|
54498
|
+
subtasks[subtasks.length - 1].description = legacyDescMatch[1].trim();
|
|
54499
|
+
continue;
|
|
54500
|
+
}
|
|
54501
|
+
const legacyReportMatch = line.match(/^\s+report:\s*(.+)$/);
|
|
54502
|
+
if (legacyReportMatch && inSubtaskSection && subtasks.length > 0) {
|
|
54503
|
+
subtasks[subtasks.length - 1].report = legacyReportMatch[1].trim();
|
|
54504
|
+
continue;
|
|
54505
|
+
}
|
|
54491
54506
|
kept.push(line);
|
|
54492
54507
|
}
|
|
54493
54508
|
return { subtasks, bodyWithoutSubtasks: kept.join("\n") };
|
|
@@ -54504,7 +54519,7 @@ function serializeTaskFile(frontmatter, body) {
|
|
|
54504
54519
|
lines.push(`${k}: [${v.join(", ")}]`);
|
|
54505
54520
|
} else if (typeof v === "string" && v.includes("\n")) {
|
|
54506
54521
|
lines.push(`${k}: |`);
|
|
54507
|
-
lines.push(...v.split("\n").map((line) => ` ${line}`));
|
|
54522
|
+
lines.push(...v.split("\n").map((line) => line === "" ? "" : ` ${line}`));
|
|
54508
54523
|
} else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
54509
54524
|
lines.push(`${k}: ${v}`);
|
|
54510
54525
|
}
|