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 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
@@ -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
  }