kandown 0.8.0 → 0.9.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 +15 -2
- package/dist/index.html +375 -1904
- 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
|
@@ -54438,7 +54438,7 @@ function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
|
|
|
54438
54438
|
const configured = columnNames.map((name) => ({ name, tasks: [] }));
|
|
54439
54439
|
for (const column of configured) columnsByName.set(column.name.toLowerCase(), column);
|
|
54440
54440
|
const unknownColumns = [];
|
|
54441
|
-
const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).sort((a, b) => {
|
|
54441
|
+
const sortedTasks = [...tasks].filter((task) => Boolean(task.frontmatter.id)).filter((task) => !isArchived(task)).sort((a, b) => {
|
|
54442
54442
|
const byOrder = taskOrder(a) - taskOrder(b);
|
|
54443
54443
|
if (byOrder !== 0) return byOrder;
|
|
54444
54444
|
return a.frontmatter.id.localeCompare(b.frontmatter.id, void 0, { numeric: true });
|
|
@@ -54455,6 +54455,9 @@ function buildColumnsFromTasks(tasks, configuredColumns = DEFAULT_COLUMNS) {
|
|
|
54455
54455
|
}
|
|
54456
54456
|
return [...unknownColumns, ...configured];
|
|
54457
54457
|
}
|
|
54458
|
+
function isArchived(task) {
|
|
54459
|
+
return String(task.frontmatter.archived) === "true";
|
|
54460
|
+
}
|
|
54458
54461
|
function extractSubtasks(body) {
|
|
54459
54462
|
const subtasks = [];
|
|
54460
54463
|
if (!body || typeof body !== "string") return { subtasks, bodyWithoutSubtasks: body ?? "" };
|
|
@@ -54488,6 +54491,16 @@ function extractSubtasks(body) {
|
|
|
54488
54491
|
subtasks[subtasks.length - 1].report = reportMatch[1];
|
|
54489
54492
|
continue;
|
|
54490
54493
|
}
|
|
54494
|
+
const legacyDescMatch = line.match(/^\s+description:\s*(.+)$/);
|
|
54495
|
+
if (legacyDescMatch && inSubtaskSection && subtasks.length > 0) {
|
|
54496
|
+
subtasks[subtasks.length - 1].description = legacyDescMatch[1].trim();
|
|
54497
|
+
continue;
|
|
54498
|
+
}
|
|
54499
|
+
const legacyReportMatch = line.match(/^\s+report:\s*(.+)$/);
|
|
54500
|
+
if (legacyReportMatch && inSubtaskSection && subtasks.length > 0) {
|
|
54501
|
+
subtasks[subtasks.length - 1].report = legacyReportMatch[1].trim();
|
|
54502
|
+
continue;
|
|
54503
|
+
}
|
|
54491
54504
|
kept.push(line);
|
|
54492
54505
|
}
|
|
54493
54506
|
return { subtasks, bodyWithoutSubtasks: kept.join("\n") };
|
|
@@ -54504,7 +54517,7 @@ function serializeTaskFile(frontmatter, body) {
|
|
|
54504
54517
|
lines.push(`${k}: [${v.join(", ")}]`);
|
|
54505
54518
|
} else if (typeof v === "string" && v.includes("\n")) {
|
|
54506
54519
|
lines.push(`${k}: |`);
|
|
54507
|
-
lines.push(...v.split("\n").map((line) => ` ${line}`));
|
|
54520
|
+
lines.push(...v.split("\n").map((line) => line === "" ? "" : ` ${line}`));
|
|
54508
54521
|
} else if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
54509
54522
|
lines.push(`${k}: ${v}`);
|
|
54510
54523
|
}
|