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 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
- if (!existsSync(tasksDir)) {
796
- writeJson(res, 200, []);
797
- return;
798
- }
810
+ const archiveDir = join(tasksDir, 'archive');
811
+ const ids = new Set();
799
812
  try {
800
- const files = readdirSync(tasksDir).filter(f => f.endsWith('.md'));
801
- const ids = files.map(f => f.replace(/\.md$/, ''));
802
- writeJson(res, 200, ids);
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 = join(kandownDir, 'tasks', `${id}.md`);
814
- if (!existsSync(taskPath)) {
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
- const taskPath = join(tasksDir, `${id}.md`);
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 = join(kandownDir, 'tasks', `${id}.md`);
848
- if (!existsSync(taskPath)) {
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
  }