kandown 0.33.2 → 0.33.3
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/CHANGELOG.md +14 -0
- package/bin/kandown.js +983 -136
- package/bin/tui.js +16 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
package/bin/kandown.js
CHANGED
|
@@ -5,9 +5,9 @@ if (typeof globalThis.require === 'undefined') {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
// src/cli/cli.ts
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import { join as
|
|
10
|
-
import { spawn as spawn4 } from "child_process";
|
|
8
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, copyFileSync as copyFileSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync3 } from "fs";
|
|
9
|
+
import { join as join8, resolve as resolve2, basename } from "path";
|
|
10
|
+
import { spawn as spawn4, spawnSync } from "child_process";
|
|
11
11
|
|
|
12
12
|
// src/cli/lib/updater.ts
|
|
13
13
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync } from "fs";
|
|
@@ -16,7 +16,7 @@ import { spawn, execSync } from "child_process";
|
|
|
16
16
|
import { homedir } from "os";
|
|
17
17
|
|
|
18
18
|
// src/lib/version.ts
|
|
19
|
-
var KANDOWN_VERSION = "0.33.
|
|
19
|
+
var KANDOWN_VERSION = "0.33.3";
|
|
20
20
|
|
|
21
21
|
// src/cli/lib/updater.ts
|
|
22
22
|
import { fileURLToPath } from "url";
|
|
@@ -139,7 +139,12 @@ async function performGlobalPackageUpdate(packageSpec) {
|
|
|
139
139
|
});
|
|
140
140
|
};
|
|
141
141
|
const currentBin = resolveKandownBin() || "";
|
|
142
|
+
const currentBinDir = currentBin ? dirname(currentBin) : null;
|
|
143
|
+
const siblingNpm = currentBinDir ? join(currentBinDir, "npm") : null;
|
|
144
|
+
const siblingPnpm = currentBinDir ? join(currentBinDir, "pnpm") : null;
|
|
142
145
|
const isPnpmInstall = currentBin.includes("pnpm");
|
|
146
|
+
if (siblingPnpm && existsSync(siblingPnpm) && await tryPkgCmd(siblingPnpm, ["add", "-g", packageSpec])) return true;
|
|
147
|
+
if (siblingNpm && existsSync(siblingNpm) && await tryPkgCmd(siblingNpm, ["install", "-g", packageSpec, "--force"])) return true;
|
|
143
148
|
if (isPnpmInstall) {
|
|
144
149
|
if (await tryPkgCmd("pnpm", ["add", "-g", packageSpec])) return true;
|
|
145
150
|
if (await tryPkgCmd("npm", ["install", "-g", packageSpec, "--force"])) return true;
|
|
@@ -165,7 +170,7 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
165
170
|
}
|
|
166
171
|
} catch {
|
|
167
172
|
}
|
|
168
|
-
const latest = await new Promise((
|
|
173
|
+
const latest = await new Promise((resolve3) => {
|
|
169
174
|
const child2 = spawn("npm", ["view", "kandown", "version"], {
|
|
170
175
|
timeout: 6e3,
|
|
171
176
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -178,11 +183,11 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
178
183
|
});
|
|
179
184
|
child2.stderr.on("data", () => {
|
|
180
185
|
});
|
|
181
|
-
child2.on("error", () =>
|
|
186
|
+
child2.on("error", () => resolve3(null));
|
|
182
187
|
child2.on("close", (code) => {
|
|
183
|
-
if (code !== 0) return
|
|
188
|
+
if (code !== 0) return resolve3(null);
|
|
184
189
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
185
|
-
|
|
190
|
+
resolve3(v || null);
|
|
186
191
|
});
|
|
187
192
|
});
|
|
188
193
|
if (!latest) return;
|
|
@@ -547,14 +552,14 @@ function readBoard(kandownDir) {
|
|
|
547
552
|
};
|
|
548
553
|
}
|
|
549
554
|
function readTask(kandownDir, taskId) {
|
|
550
|
-
const
|
|
551
|
-
if (!existsSync3(
|
|
555
|
+
const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
|
|
556
|
+
if (!existsSync3(taskPath2)) {
|
|
552
557
|
return {
|
|
553
558
|
frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
|
|
554
559
|
body: ""
|
|
555
560
|
};
|
|
556
561
|
}
|
|
557
|
-
const content = readFileSync3(
|
|
562
|
+
const content = readFileSync3(taskPath2, "utf8");
|
|
558
563
|
const parsed = parseTaskFile(content);
|
|
559
564
|
return {
|
|
560
565
|
...parsed,
|
|
@@ -608,21 +613,21 @@ ${gitLog}
|
|
|
608
613
|
return sections.filter(Boolean).join("\n\n---\n\n");
|
|
609
614
|
}
|
|
610
615
|
function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
611
|
-
const
|
|
612
|
-
if (!existsSync3(
|
|
616
|
+
const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
|
|
617
|
+
if (!existsSync3(taskPath2)) return false;
|
|
613
618
|
try {
|
|
614
|
-
const prevContent = readFileSync3(
|
|
619
|
+
const prevContent = readFileSync3(taskPath2, "utf8");
|
|
615
620
|
const parsed = readTask(kandownDir, taskId);
|
|
616
621
|
const newContent = serializeTaskFile({
|
|
617
622
|
...parsed.frontmatter,
|
|
618
623
|
id: taskId,
|
|
619
624
|
status: targetColumn
|
|
620
625
|
}, parsed.body);
|
|
621
|
-
atomicWriteFileSync(
|
|
626
|
+
atomicWriteFileSync(taskPath2, newContent);
|
|
622
627
|
pushUndo(kandownDir, {
|
|
623
628
|
type: "move",
|
|
624
629
|
taskId,
|
|
625
|
-
path:
|
|
630
|
+
path: taskPath2,
|
|
626
631
|
previousContent: prevContent,
|
|
627
632
|
newContent,
|
|
628
633
|
timestamp: Date.now()
|
|
@@ -652,6 +657,102 @@ function pushUndo(kandownDir, record) {
|
|
|
652
657
|
} catch {
|
|
653
658
|
}
|
|
654
659
|
}
|
|
660
|
+
function createTaskInBoard(kandownDir, rawInput, status) {
|
|
661
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
662
|
+
if (!existsSync3(tasksDir)) mkdirSync2(tasksDir, { recursive: true });
|
|
663
|
+
const ids = listTaskIds(kandownDir);
|
|
664
|
+
let maxN = 0;
|
|
665
|
+
for (const id of ids) {
|
|
666
|
+
const m = id.match(/^t(\d+)$/i);
|
|
667
|
+
if (m) {
|
|
668
|
+
const n = parseInt(m[1], 10);
|
|
669
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
const newId = `t${maxN + 1}`;
|
|
673
|
+
const config = loadConfig(kandownDir);
|
|
674
|
+
const targetStatus = status || (config.board.columns[0] ?? "Backlog");
|
|
675
|
+
let text = rawInput.trim();
|
|
676
|
+
let priority;
|
|
677
|
+
const tags = [];
|
|
678
|
+
let assignee;
|
|
679
|
+
let due;
|
|
680
|
+
const depends_on = [];
|
|
681
|
+
text = text.replace(/(?:^|\s)p([1-4])(?:\s|$)/i, (_, level) => {
|
|
682
|
+
priority = `P${level}`;
|
|
683
|
+
return " ";
|
|
684
|
+
});
|
|
685
|
+
text = text.replace(/(?:^|\s)#([a-zA-Z0-9_-]+)/g, (_, tag) => {
|
|
686
|
+
tags.push(tag.toLowerCase());
|
|
687
|
+
return " ";
|
|
688
|
+
});
|
|
689
|
+
text = text.replace(/(?:^|\s)@([a-zA-Z0-9_-]+)/g, (_, user) => {
|
|
690
|
+
assignee = user;
|
|
691
|
+
return " ";
|
|
692
|
+
});
|
|
693
|
+
text = text.replace(/(?:^|\s)due:([^\s]+)/i, (_, d) => {
|
|
694
|
+
due = d;
|
|
695
|
+
return " ";
|
|
696
|
+
});
|
|
697
|
+
text = text.replace(/(?:^|\s)\+([a-zA-Z0-9_-]+)/g, (_, depId) => {
|
|
698
|
+
depends_on.push(depId);
|
|
699
|
+
return " ";
|
|
700
|
+
});
|
|
701
|
+
const title = text.replace(/\s+/g, " ").trim() || rawInput;
|
|
702
|
+
const fm = {
|
|
703
|
+
id: newId,
|
|
704
|
+
title,
|
|
705
|
+
status: targetStatus,
|
|
706
|
+
created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
707
|
+
};
|
|
708
|
+
if (priority) fm.priority = priority;
|
|
709
|
+
if (assignee) fm.assignee = assignee;
|
|
710
|
+
if (tags.length > 0) fm.tags = tags;
|
|
711
|
+
if (due) fm.due = due;
|
|
712
|
+
if (depends_on.length > 0) fm.depends_on = depends_on;
|
|
713
|
+
const content = serializeTaskFile(fm, "");
|
|
714
|
+
const taskPath2 = join3(tasksDir, `${newId}.md`);
|
|
715
|
+
atomicWriteFileSync(taskPath2, content);
|
|
716
|
+
pushUndo(kandownDir, {
|
|
717
|
+
type: "create",
|
|
718
|
+
taskId: newId,
|
|
719
|
+
path: taskPath2,
|
|
720
|
+
previousContent: null,
|
|
721
|
+
newContent: content,
|
|
722
|
+
timestamp: Date.now()
|
|
723
|
+
});
|
|
724
|
+
return newId;
|
|
725
|
+
}
|
|
726
|
+
function archiveTaskInBoard(kandownDir, taskId) {
|
|
727
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
728
|
+
const taskPath2 = join3(tasksDir, `${taskId}.md`);
|
|
729
|
+
if (!existsSync3(taskPath2)) return false;
|
|
730
|
+
try {
|
|
731
|
+
const prevContent = readFileSync3(taskPath2, "utf8");
|
|
732
|
+
const archiveDir = join3(tasksDir, "archive");
|
|
733
|
+
if (!existsSync3(archiveDir)) mkdirSync2(archiveDir, { recursive: true });
|
|
734
|
+
const parsed = readTask(kandownDir, taskId);
|
|
735
|
+
const newContent = serializeTaskFile({
|
|
736
|
+
...parsed.frontmatter,
|
|
737
|
+
id: taskId,
|
|
738
|
+
archived: true
|
|
739
|
+
}, parsed.body);
|
|
740
|
+
const destPath = join3(archiveDir, `${taskId}.md`);
|
|
741
|
+
atomicWriteFileSync(destPath, newContent);
|
|
742
|
+
unlinkSync3(taskPath2);
|
|
743
|
+
pushUndo(kandownDir, {
|
|
744
|
+
type: "archive",
|
|
745
|
+
taskId,
|
|
746
|
+
path: destPath,
|
|
747
|
+
previousContent: prevContent,
|
|
748
|
+
newContent,
|
|
749
|
+
timestamp: Date.now()
|
|
750
|
+
});
|
|
751
|
+
return true;
|
|
752
|
+
} catch {
|
|
753
|
+
return false;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
655
756
|
|
|
656
757
|
// src/cli/lib/daemon.ts
|
|
657
758
|
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
@@ -717,16 +818,16 @@ async function fetchDaemonInfo(port) {
|
|
|
717
818
|
}
|
|
718
819
|
}
|
|
719
820
|
function isPortListening(port, timeoutMs = 400) {
|
|
720
|
-
return new Promise((
|
|
821
|
+
return new Promise((resolve3) => {
|
|
721
822
|
const socket = createConnection({ port, host: "127.0.0.1" }, () => {
|
|
722
823
|
socket.destroy();
|
|
723
|
-
|
|
824
|
+
resolve3(true);
|
|
724
825
|
});
|
|
725
|
-
socket.on("error", () =>
|
|
826
|
+
socket.on("error", () => resolve3(false));
|
|
726
827
|
socket.setTimeout(timeoutMs);
|
|
727
828
|
socket.on("timeout", () => {
|
|
728
829
|
socket.destroy();
|
|
729
|
-
|
|
830
|
+
resolve3(false);
|
|
730
831
|
});
|
|
731
832
|
});
|
|
732
833
|
}
|
|
@@ -754,7 +855,7 @@ async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
|
|
|
754
855
|
if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
|
|
755
856
|
return { running: true, metadata };
|
|
756
857
|
}
|
|
757
|
-
await new Promise((
|
|
858
|
+
await new Promise((resolve3) => setTimeout(resolve3, 120));
|
|
758
859
|
}
|
|
759
860
|
return { running: false, metadata: null };
|
|
760
861
|
}
|
|
@@ -783,13 +884,54 @@ async function startProjectDaemon(kandownDir, preferredPort) {
|
|
|
783
884
|
child.unref();
|
|
784
885
|
return waitForDaemon(kandownDir);
|
|
785
886
|
}
|
|
887
|
+
async function isOwnedKandownDaemon(pid, port, kandownDir) {
|
|
888
|
+
const remote = await fetchDaemonInfo(port);
|
|
889
|
+
if (remote) return remote.pid === pid && remote.kandownDir === kandownDir;
|
|
890
|
+
try {
|
|
891
|
+
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], {
|
|
892
|
+
encoding: "utf8",
|
|
893
|
+
timeout: 2e3
|
|
894
|
+
}).trim();
|
|
895
|
+
return /kandown/.test(cmd) && cmd.includes(kandownDir);
|
|
896
|
+
} catch {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
async function stopProjectDaemon(kandownDir) {
|
|
901
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
902
|
+
if (!metadata) return false;
|
|
903
|
+
const pid = metadata.pid;
|
|
904
|
+
if (!isProcessAlive(pid)) {
|
|
905
|
+
removeDaemonMetadata(kandownDir);
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
if (!await isOwnedKandownDaemon(pid, metadata.port, kandownDir)) {
|
|
909
|
+
removeDaemonMetadata(kandownDir);
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
try {
|
|
913
|
+
process.kill(pid, "SIGTERM");
|
|
914
|
+
} catch {
|
|
915
|
+
}
|
|
916
|
+
const started = Date.now();
|
|
917
|
+
while (Date.now() - started < 2500 && isProcessAlive(pid)) {
|
|
918
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
919
|
+
}
|
|
920
|
+
if (isProcessAlive(pid)) {
|
|
921
|
+
try {
|
|
922
|
+
process.kill(pid, "SIGKILL");
|
|
923
|
+
} catch {
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
removeDaemonMetadata(kandownDir);
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
786
929
|
|
|
787
930
|
// src/cli/lib/server.ts
|
|
788
931
|
import { createServer } from "http";
|
|
789
932
|
import { existsSync as existsSync5, readFileSync as readFileSync5, copyFileSync, unlinkSync as unlinkSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
790
|
-
import { join as join5
|
|
791
|
-
import {
|
|
792
|
-
import { homedir as homedir3 } from "os";
|
|
933
|
+
import { join as join5 } from "path";
|
|
934
|
+
import { spawn as spawn3 } from "child_process";
|
|
793
935
|
var START_PORT_RANGE = 2050;
|
|
794
936
|
var END_PORT_RANGE = 2099;
|
|
795
937
|
var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
|
|
@@ -826,8 +968,7 @@ function writeText(res, status, text) {
|
|
|
826
968
|
function syncProjectKandownHtml(kandownDir) {
|
|
827
969
|
try {
|
|
828
970
|
const projectHtml = join5(kandownDir, "kandown.html");
|
|
829
|
-
const
|
|
830
|
-
const distHtml = join5(cliRoot, "dist", "index.html");
|
|
971
|
+
const distHtml = join5(PKG_ROOT, "dist", "index.html");
|
|
831
972
|
if (!existsSync5(distHtml)) return false;
|
|
832
973
|
if (!existsSync5(projectHtml)) {
|
|
833
974
|
copyFileSync(distHtml, projectHtml);
|
|
@@ -843,27 +984,6 @@ function syncProjectKandownHtml(kandownDir) {
|
|
|
843
984
|
}
|
|
844
985
|
return false;
|
|
845
986
|
}
|
|
846
|
-
function syncGlobalSymlinks() {
|
|
847
|
-
try {
|
|
848
|
-
const home = homedir3();
|
|
849
|
-
const candidatePaths = [
|
|
850
|
-
join5(home, ".local", "bin", "kandown"),
|
|
851
|
-
join5(home, "Library", "pnpm", "bin", "kandown"),
|
|
852
|
-
join5(home, ".nvm", "versions", "node", "v25.2.1", "bin", "kandown")
|
|
853
|
-
];
|
|
854
|
-
const currentBin = resolve2(import.meta.url ? new URL("../../bin/kandown.js", import.meta.url).pathname : process.cwd());
|
|
855
|
-
for (const targetPath of candidatePaths) {
|
|
856
|
-
if (existsSync5(dirname4(targetPath))) {
|
|
857
|
-
try {
|
|
858
|
-
if (existsSync5(targetPath)) unlinkSync5(targetPath);
|
|
859
|
-
execSync2(`ln -sf "${currentBin}" "${targetPath}" 2>/dev/null || true`);
|
|
860
|
-
} catch {
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
} catch {
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
987
|
async function handleApi(req, res, url, kandownDir) {
|
|
868
988
|
const path = url.pathname;
|
|
869
989
|
const method = req.method || "GET";
|
|
@@ -884,7 +1004,7 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
884
1004
|
}
|
|
885
1005
|
if (path === "/api/update/check" && method === "GET") {
|
|
886
1006
|
const current = getCurrentVersion();
|
|
887
|
-
const latest = await new Promise((
|
|
1007
|
+
const latest = await new Promise((resolve3) => {
|
|
888
1008
|
const child = spawn3("npm", ["view", "kandown", "version"], {
|
|
889
1009
|
timeout: 4e3,
|
|
890
1010
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -897,10 +1017,10 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
897
1017
|
});
|
|
898
1018
|
child.stderr.on("data", () => {
|
|
899
1019
|
});
|
|
900
|
-
child.on("error", () =>
|
|
1020
|
+
child.on("error", () => resolve3(null));
|
|
901
1021
|
child.on("close", (code) => {
|
|
902
|
-
if (code !== 0) return
|
|
903
|
-
|
|
1022
|
+
if (code !== 0) return resolve3(null);
|
|
1023
|
+
resolve3(stdout.trim().replace(/^"|"$/g, "") || null);
|
|
904
1024
|
});
|
|
905
1025
|
});
|
|
906
1026
|
const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
|
|
@@ -912,7 +1032,7 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
912
1032
|
}
|
|
913
1033
|
if (path === "/api/update/apply" && method === "POST") {
|
|
914
1034
|
const current = getCurrentVersion();
|
|
915
|
-
const latest = await new Promise((
|
|
1035
|
+
const latest = await new Promise((resolve3) => {
|
|
916
1036
|
const child = spawn3("npm", ["view", "kandown", "version"], {
|
|
917
1037
|
timeout: 4e3,
|
|
918
1038
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -923,12 +1043,11 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
923
1043
|
child.stdout.on("data", (d) => {
|
|
924
1044
|
stdout += d;
|
|
925
1045
|
});
|
|
926
|
-
child.on("error", () =>
|
|
927
|
-
child.on("close", (code) =>
|
|
1046
|
+
child.on("error", () => resolve3(null));
|
|
1047
|
+
child.on("close", (code) => resolve3(code === 0 ? stdout.trim() : null));
|
|
928
1048
|
});
|
|
929
1049
|
const targetVersion = latest || current;
|
|
930
1050
|
const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
|
|
931
|
-
syncGlobalSymlinks();
|
|
932
1051
|
syncProjectKandownHtml(kandownDir);
|
|
933
1052
|
if (ok) {
|
|
934
1053
|
writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully" });
|
|
@@ -1003,10 +1122,10 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
1003
1122
|
if (path.startsWith("/api/tasks/")) {
|
|
1004
1123
|
const taskId = decodeURIComponent(path.slice("/api/tasks/".length));
|
|
1005
1124
|
const tasksDir = getTasksDir(kandownDir);
|
|
1006
|
-
const
|
|
1125
|
+
const taskPath2 = join5(tasksDir, `${taskId}.md`);
|
|
1007
1126
|
if (method === "GET") {
|
|
1008
|
-
if (!existsSync5(
|
|
1009
|
-
return writeText(res, 200, readFileSync5(
|
|
1127
|
+
if (!existsSync5(taskPath2)) return writeText(res, 404, "Task not found");
|
|
1128
|
+
return writeText(res, 200, readFileSync5(taskPath2, "utf8"));
|
|
1010
1129
|
}
|
|
1011
1130
|
if (method === "PUT") {
|
|
1012
1131
|
let body = "";
|
|
@@ -1015,14 +1134,14 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
1015
1134
|
});
|
|
1016
1135
|
req.on("end", () => {
|
|
1017
1136
|
if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
|
|
1018
|
-
atomicWriteFileSync(
|
|
1137
|
+
atomicWriteFileSync(taskPath2, body);
|
|
1019
1138
|
broadcastSseEvent({ type: "task", id: taskId });
|
|
1020
1139
|
writeJson(res, 200, { ok: true });
|
|
1021
1140
|
});
|
|
1022
1141
|
return;
|
|
1023
1142
|
}
|
|
1024
1143
|
if (method === "DELETE") {
|
|
1025
|
-
if (existsSync5(
|
|
1144
|
+
if (existsSync5(taskPath2)) unlinkSync5(taskPath2);
|
|
1026
1145
|
broadcastSseEvent({ type: "task_delete", id: taskId });
|
|
1027
1146
|
return writeJson(res, 200, { ok: true });
|
|
1028
1147
|
}
|
|
@@ -1041,7 +1160,6 @@ function serveApp(res, kandownDir) {
|
|
|
1041
1160
|
}
|
|
1042
1161
|
function createServeServer(kandownDir) {
|
|
1043
1162
|
syncProjectKandownHtml(kandownDir);
|
|
1044
|
-
syncGlobalSymlinks();
|
|
1045
1163
|
return createServer((req, res) => {
|
|
1046
1164
|
const url = new URL(req.url || "/", "http://localhost");
|
|
1047
1165
|
if (req.method === "OPTIONS") return handleCors(res);
|
|
@@ -1084,6 +1202,281 @@ async function listenOnAvailablePort(kandownDir, preferredPort) {
|
|
|
1084
1202
|
throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
|
|
1085
1203
|
}
|
|
1086
1204
|
|
|
1205
|
+
// src/cli/lib/init.ts
|
|
1206
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1207
|
+
import { join as join6 } from "path";
|
|
1208
|
+
function copyRecursive(src, dest) {
|
|
1209
|
+
const errors = [];
|
|
1210
|
+
try {
|
|
1211
|
+
if (!existsSync6(dest)) mkdirSync4(dest, { recursive: true });
|
|
1212
|
+
const entries = readdirSync2(src);
|
|
1213
|
+
for (const entry of entries) {
|
|
1214
|
+
const srcPath = join6(src, entry);
|
|
1215
|
+
const destPath = join6(dest, entry);
|
|
1216
|
+
try {
|
|
1217
|
+
if (statSync2(srcPath).isDirectory()) {
|
|
1218
|
+
errors.push(...copyRecursive(srcPath, destPath));
|
|
1219
|
+
} else if (!existsSync6(destPath)) {
|
|
1220
|
+
copyFileSync2(srcPath, destPath);
|
|
1221
|
+
}
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
errors.push(`${entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
errors.push(`${src}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1228
|
+
}
|
|
1229
|
+
return errors;
|
|
1230
|
+
}
|
|
1231
|
+
function syncKandownAgentDoc(kandownDir) {
|
|
1232
|
+
const source = join6(PKG_ROOT, "templates", "AGENT_KANDOWN.md");
|
|
1233
|
+
const target = join6(kandownDir, "AGENT_KANDOWN.md");
|
|
1234
|
+
if (!existsSync6(source)) return false;
|
|
1235
|
+
try {
|
|
1236
|
+
const expected = readFileSync6(source, "utf8");
|
|
1237
|
+
const existing = existsSync6(target) ? readFileSync6(target, "utf8") : null;
|
|
1238
|
+
if (existing === null || !existing.includes("# Kandown")) {
|
|
1239
|
+
atomicWriteFileSync(target, expected.endsWith("\n") ? expected : `${expected}
|
|
1240
|
+
`);
|
|
1241
|
+
return true;
|
|
1242
|
+
}
|
|
1243
|
+
} catch {
|
|
1244
|
+
}
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
function doInit(kandownDir) {
|
|
1248
|
+
try {
|
|
1249
|
+
mkdirSync4(kandownDir, { recursive: true });
|
|
1250
|
+
const htmlSrc = join6(PKG_ROOT, "dist", "index.html");
|
|
1251
|
+
const htmlDest = join6(kandownDir, "kandown.html");
|
|
1252
|
+
if (existsSync6(htmlSrc)) {
|
|
1253
|
+
copyFileSync2(htmlSrc, htmlDest);
|
|
1254
|
+
}
|
|
1255
|
+
syncKandownAgentDoc(kandownDir);
|
|
1256
|
+
const templatesDir = join6(PKG_ROOT, "templates");
|
|
1257
|
+
if (existsSync6(templatesDir)) {
|
|
1258
|
+
if (!existsSync6(join6(kandownDir, "README.md")) && existsSync6(join6(templatesDir, "README.md"))) {
|
|
1259
|
+
copyFileSync2(join6(templatesDir, "README.md"), join6(kandownDir, "README.md"));
|
|
1260
|
+
}
|
|
1261
|
+
if (!existsSync6(join6(kandownDir, "AGENT.md")) && existsSync6(join6(templatesDir, "AGENT.md"))) {
|
|
1262
|
+
copyFileSync2(join6(templatesDir, "AGENT.md"), join6(kandownDir, "AGENT.md"));
|
|
1263
|
+
}
|
|
1264
|
+
const tasksSrc = join6(templatesDir, "tasks");
|
|
1265
|
+
const tasksDest = getTasksDir(kandownDir);
|
|
1266
|
+
if (!existsSync6(tasksDest) && existsSync6(tasksSrc)) {
|
|
1267
|
+
copyRecursive(tasksSrc, tasksDest);
|
|
1268
|
+
}
|
|
1269
|
+
if (!existsSync6(join6(kandownDir, "kandown.json")) && existsSync6(join6(templatesDir, "kandown.json"))) {
|
|
1270
|
+
copyFileSync2(join6(templatesDir, "kandown.json"), join6(kandownDir, "kandown.json"));
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return true;
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
console.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/cli/lib/mcp.ts
|
|
1281
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1282
|
+
import { join as join7 } from "path";
|
|
1283
|
+
function startMcpServer(kandownDir) {
|
|
1284
|
+
process.stdin.setEncoding("utf8");
|
|
1285
|
+
let buffer = "";
|
|
1286
|
+
process.stdin.on("data", (chunk) => {
|
|
1287
|
+
buffer += chunk;
|
|
1288
|
+
const lines = buffer.split("\n");
|
|
1289
|
+
buffer = lines.pop() ?? "";
|
|
1290
|
+
for (const line of lines) {
|
|
1291
|
+
const trimmed = line.trim();
|
|
1292
|
+
if (!trimmed) continue;
|
|
1293
|
+
try {
|
|
1294
|
+
const req = JSON.parse(trimmed);
|
|
1295
|
+
handleJsonRpc(kandownDir, req);
|
|
1296
|
+
} catch (e) {
|
|
1297
|
+
sendResponse(null, { error: { code: -32700, message: "Parse error" } });
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
function sendResponse(id, resultOrError) {
|
|
1303
|
+
if (id === void 0) return;
|
|
1304
|
+
const resp = {
|
|
1305
|
+
jsonrpc: "2.0",
|
|
1306
|
+
id,
|
|
1307
|
+
...resultOrError
|
|
1308
|
+
};
|
|
1309
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
1310
|
+
}
|
|
1311
|
+
function handleJsonRpc(kandownDir, req) {
|
|
1312
|
+
const { id, method, params } = req;
|
|
1313
|
+
if (method === "initialize") {
|
|
1314
|
+
sendResponse(id, {
|
|
1315
|
+
result: {
|
|
1316
|
+
protocolVersion: "2024-11-05",
|
|
1317
|
+
capabilities: { tools: {} },
|
|
1318
|
+
serverInfo: { name: "kandown", version: "0.20.0" }
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (method === "notifications/initialized") {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if (method === "tools/list") {
|
|
1327
|
+
sendResponse(id, {
|
|
1328
|
+
result: {
|
|
1329
|
+
tools: [
|
|
1330
|
+
{
|
|
1331
|
+
name: "list_tasks",
|
|
1332
|
+
description: "List all tasks on the Kandown board with optional filtering",
|
|
1333
|
+
inputSchema: {
|
|
1334
|
+
type: "object",
|
|
1335
|
+
properties: {
|
|
1336
|
+
status: { type: "string", description: "Filter by column/status name" },
|
|
1337
|
+
assignee: { type: "string", description: "Filter by assignee" },
|
|
1338
|
+
tag: { type: "string", description: "Filter by tag" },
|
|
1339
|
+
priority: { type: "string", description: "Filter by priority (P1, P2, P3, P4)" }
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
name: "get_task",
|
|
1345
|
+
description: "Get details and full content of a specific task by ID",
|
|
1346
|
+
inputSchema: {
|
|
1347
|
+
type: "object",
|
|
1348
|
+
properties: {
|
|
1349
|
+
id: { type: "string", description: "Task ID (e.g. t1, t42)" }
|
|
1350
|
+
},
|
|
1351
|
+
required: ["id"]
|
|
1352
|
+
}
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: "create_task",
|
|
1356
|
+
description: "Create a new task on the Kandown board",
|
|
1357
|
+
inputSchema: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
properties: {
|
|
1360
|
+
title: { type: "string", description: "Task title (supports inline syntax #tag @user p1 due:date)" },
|
|
1361
|
+
status: { type: "string", description: "Target column name (default: Backlog)" },
|
|
1362
|
+
priority: { type: "string", description: "Priority level (P1, P2, P3, P4)" },
|
|
1363
|
+
assignee: { type: "string", description: "Assignee username" },
|
|
1364
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags" },
|
|
1365
|
+
body: { type: "string", description: "Markdown body content" }
|
|
1366
|
+
},
|
|
1367
|
+
required: ["title"]
|
|
1368
|
+
}
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
name: "move_task",
|
|
1372
|
+
description: "Move a task to a different column or to archived",
|
|
1373
|
+
inputSchema: {
|
|
1374
|
+
type: "object",
|
|
1375
|
+
properties: {
|
|
1376
|
+
id: { type: "string", description: "Task ID" },
|
|
1377
|
+
status: { type: "string", description: 'Target column name or "archived"' }
|
|
1378
|
+
},
|
|
1379
|
+
required: ["id", "status"]
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
name: "add_report",
|
|
1384
|
+
description: "Append an agent execution report to a task body",
|
|
1385
|
+
inputSchema: {
|
|
1386
|
+
type: "object",
|
|
1387
|
+
properties: {
|
|
1388
|
+
id: { type: "string", description: "Task ID" },
|
|
1389
|
+
report: { type: "string", description: "Markdown report content to append under ## Report" }
|
|
1390
|
+
},
|
|
1391
|
+
required: ["id", "report"]
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
{
|
|
1395
|
+
name: "list_columns",
|
|
1396
|
+
description: "List configured board columns and task counts",
|
|
1397
|
+
inputSchema: { type: "object", properties: {} }
|
|
1398
|
+
}
|
|
1399
|
+
]
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (method === "tools/call") {
|
|
1405
|
+
const { name, arguments: args = {} } = params || {};
|
|
1406
|
+
if (name === "list_tasks") {
|
|
1407
|
+
const board = readBoard(kandownDir);
|
|
1408
|
+
let tasks = [];
|
|
1409
|
+
for (const col of board.columns) {
|
|
1410
|
+
if (args.status && col.name.toLowerCase() !== String(args.status).toLowerCase()) continue;
|
|
1411
|
+
for (const t of col.tasks) {
|
|
1412
|
+
if (args.assignee && t.assignee !== args.assignee) continue;
|
|
1413
|
+
if (args.priority && t.priority !== args.priority) continue;
|
|
1414
|
+
if (args.tag && !t.tags.includes(args.tag)) continue;
|
|
1415
|
+
tasks.push({ ...t, status: col.name });
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] } });
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
if (name === "get_task") {
|
|
1422
|
+
const task = readTask(kandownDir, args.id);
|
|
1423
|
+
sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] } });
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (name === "create_task") {
|
|
1427
|
+
const newId = createTaskInBoard(kandownDir, args.title, args.status);
|
|
1428
|
+
if (args.body || args.priority || args.assignee || args.tags) {
|
|
1429
|
+
const task = readTask(kandownDir, newId);
|
|
1430
|
+
const fm = {
|
|
1431
|
+
...task.frontmatter,
|
|
1432
|
+
...args.priority ? { priority: args.priority } : {},
|
|
1433
|
+
...args.assignee ? { assignee: args.assignee } : {},
|
|
1434
|
+
...args.tags ? { tags: args.tags } : {}
|
|
1435
|
+
};
|
|
1436
|
+
const body = args.body ? (task.body + "\n\n" + args.body).trim() : task.body;
|
|
1437
|
+
const taskPath2 = join7(getTasksDir(kandownDir), `${newId}.md`);
|
|
1438
|
+
atomicWriteFileSync(taskPath2, serializeTaskFile(fm, body));
|
|
1439
|
+
}
|
|
1440
|
+
sendResponse(id, { result: { content: [{ type: "text", text: `Created task ${newId}` }] } });
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (name === "move_task") {
|
|
1444
|
+
const ok = moveTaskToColumn(kandownDir, args.id, args.status);
|
|
1445
|
+
sendResponse(id, { result: { content: [{ type: "text", text: ok ? `Moved ${args.id} to ${args.status}` : `Failed to move ${args.id}` }] } });
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (name === "add_report") {
|
|
1449
|
+
const taskPath2 = join7(getTasksDir(kandownDir), `${args.id}.md`);
|
|
1450
|
+
if (!existsSync7(taskPath2)) {
|
|
1451
|
+
sendResponse(id, { error: { code: -32602, message: `Task ${args.id} not found` } });
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const task = readTask(kandownDir, args.id);
|
|
1455
|
+
const reportSection = `
|
|
1456
|
+
|
|
1457
|
+
## Report
|
|
1458
|
+
|
|
1459
|
+
${args.report.trim()}`;
|
|
1460
|
+
const newBody = task.body.includes("## Report") ? task.body.replace(/## Report[\s\S]*/, `## Report
|
|
1461
|
+
|
|
1462
|
+
${args.report.trim()}`) : task.body.trim() + reportSection;
|
|
1463
|
+
atomicWriteFileSync(taskPath2, serializeTaskFile(task.frontmatter, newBody));
|
|
1464
|
+
sendResponse(id, { result: { content: [{ type: "text", text: `Appended report to ${args.id}` }] } });
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (name === "list_columns") {
|
|
1468
|
+
const config = loadConfig(kandownDir);
|
|
1469
|
+
const board = readBoard(kandownDir);
|
|
1470
|
+
const cols = board.columns.map((c2) => ({ name: c2.name, count: c2.tasks.length }));
|
|
1471
|
+
sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(cols, null, 2) }] } });
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
sendResponse(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
sendResponse(id, { error: { code: -32601, message: `Method not found: ${method}` } });
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1087
1480
|
// src/cli/cli.ts
|
|
1088
1481
|
var c = {
|
|
1089
1482
|
reset: "\x1B[0m",
|
|
@@ -1099,10 +1492,10 @@ function log(msg) {
|
|
|
1099
1492
|
console.log(msg);
|
|
1100
1493
|
}
|
|
1101
1494
|
function info(msg) {
|
|
1102
|
-
console.
|
|
1495
|
+
console.error(`${c.blue}\u2139${c.reset} ${msg}`);
|
|
1103
1496
|
}
|
|
1104
1497
|
function success(msg) {
|
|
1105
|
-
console.
|
|
1498
|
+
console.error(`${c.green}\u2713${c.reset} ${msg}`);
|
|
1106
1499
|
}
|
|
1107
1500
|
function err(msg) {
|
|
1108
1501
|
console.error(`${c.red}\u2717${c.reset} ${msg}`);
|
|
@@ -1136,20 +1529,74 @@ function parseArgs(args) {
|
|
|
1136
1529
|
}
|
|
1137
1530
|
return { flags, positional, path: typeof flags.path === "string" ? flags.path : ".kandown" };
|
|
1138
1531
|
}
|
|
1532
|
+
var COMMANDS = /* @__PURE__ */ new Set([
|
|
1533
|
+
"init",
|
|
1534
|
+
"update",
|
|
1535
|
+
"upgrade",
|
|
1536
|
+
"doctor",
|
|
1537
|
+
"work",
|
|
1538
|
+
"list",
|
|
1539
|
+
"ls",
|
|
1540
|
+
"show",
|
|
1541
|
+
"move",
|
|
1542
|
+
"help",
|
|
1543
|
+
"daemon",
|
|
1544
|
+
"board",
|
|
1545
|
+
"settings",
|
|
1546
|
+
"tasks",
|
|
1547
|
+
"create",
|
|
1548
|
+
"new",
|
|
1549
|
+
"assign",
|
|
1550
|
+
"commit",
|
|
1551
|
+
"projects",
|
|
1552
|
+
"export",
|
|
1553
|
+
"import",
|
|
1554
|
+
"mcp",
|
|
1555
|
+
"version"
|
|
1556
|
+
]);
|
|
1557
|
+
function splitCommand(args) {
|
|
1558
|
+
const withoutGlobalFlags = args.filter((arg) => arg !== "--no-update-check");
|
|
1559
|
+
const commandIndex = withoutGlobalFlags.findIndex((arg, index) => {
|
|
1560
|
+
if (arg.startsWith("-")) return false;
|
|
1561
|
+
if (COMMANDS.has(arg)) return true;
|
|
1562
|
+
return index === 0;
|
|
1563
|
+
});
|
|
1564
|
+
if (commandIndex === -1) {
|
|
1565
|
+
return { cmd: void 0, rest: withoutGlobalFlags };
|
|
1566
|
+
}
|
|
1567
|
+
return {
|
|
1568
|
+
cmd: withoutGlobalFlags[commandIndex],
|
|
1569
|
+
rest: [...withoutGlobalFlags.slice(0, commandIndex), ...withoutGlobalFlags.slice(commandIndex + 1)]
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function stripFirstPositional(args, value) {
|
|
1573
|
+
const result = [];
|
|
1574
|
+
let stripped = false;
|
|
1575
|
+
for (const arg of args) {
|
|
1576
|
+
if (!stripped && arg === value) {
|
|
1577
|
+
stripped = true;
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
result.push(arg);
|
|
1581
|
+
}
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1139
1584
|
function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
|
|
1140
|
-
if (basename(cwd) === ".kandown" ||
|
|
1585
|
+
if (basename(cwd) === ".kandown" || existsSync8(join8(cwd, "kandown.json"))) {
|
|
1141
1586
|
return cwd;
|
|
1142
1587
|
}
|
|
1143
|
-
return
|
|
1588
|
+
return resolve2(cwd, pathArg);
|
|
1144
1589
|
}
|
|
1145
1590
|
function ensureKandownDir(rawArgs) {
|
|
1146
1591
|
const args = parseArgs(rawArgs);
|
|
1147
1592
|
const cwd = process.cwd();
|
|
1148
1593
|
const kandownDir = resolveKandownDir(args.path, cwd);
|
|
1149
|
-
if (!
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1594
|
+
if (!existsSync8(kandownDir)) {
|
|
1595
|
+
info(`No .kandown/ found \u2014 auto-initializing ${c.bold}${kandownDir}${c.reset}`);
|
|
1596
|
+
if (!doInit(kandownDir)) {
|
|
1597
|
+
err(`Could not auto-initialize Kandown at ${c.bold}${kandownDir}${c.reset}`);
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1153
1600
|
}
|
|
1154
1601
|
return { kandownDir, cwd };
|
|
1155
1602
|
}
|
|
@@ -1163,6 +1610,7 @@ ${c.bold}USAGE:${c.reset}
|
|
|
1163
1610
|
|
|
1164
1611
|
${c.bold}COMMANDS:${c.reset}
|
|
1165
1612
|
(none) Start web server, open browser & launch TUI
|
|
1613
|
+
init Initialize Kandown in this project
|
|
1166
1614
|
work Output agent rules + live board digest
|
|
1167
1615
|
list List tasks (alias: ls)
|
|
1168
1616
|
show <id> Display task details
|
|
@@ -1183,13 +1631,25 @@ ${c.bold}OPTIONS:${c.reset}
|
|
|
1183
1631
|
--path <dir> Path to .kandown folder (default: .kandown)
|
|
1184
1632
|
--port <number> Server port (default: 2050)
|
|
1185
1633
|
--no-open Don't open browser automatically
|
|
1634
|
+
--no-update-check Skip the registry update check for this run
|
|
1635
|
+
--version Print CLI version
|
|
1186
1636
|
--help, -h Show help screen
|
|
1187
1637
|
`);
|
|
1188
1638
|
}
|
|
1639
|
+
function cmdInit(rawArgs) {
|
|
1640
|
+
const args = parseArgs(rawArgs);
|
|
1641
|
+
const kandownDir = resolve2(process.cwd(), args.path);
|
|
1642
|
+
const created = doInit(kandownDir);
|
|
1643
|
+
if (!created) {
|
|
1644
|
+
err("Failed to initialize Kandown.");
|
|
1645
|
+
process.exit(1);
|
|
1646
|
+
}
|
|
1647
|
+
success(`Kandown initialized at ${kandownDir}`);
|
|
1648
|
+
}
|
|
1189
1649
|
async function cmdUpdate(rawArgs) {
|
|
1190
1650
|
const current = getCurrentVersion();
|
|
1191
1651
|
log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
|
|
1192
|
-
const latest = await new Promise((
|
|
1652
|
+
const latest = await new Promise((resolve3) => {
|
|
1193
1653
|
const child = spawn4("npm", ["view", "kandown", "version"], {
|
|
1194
1654
|
timeout: 6e3,
|
|
1195
1655
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1202,11 +1662,11 @@ async function cmdUpdate(rawArgs) {
|
|
|
1202
1662
|
});
|
|
1203
1663
|
child.stderr.on("data", () => {
|
|
1204
1664
|
});
|
|
1205
|
-
child.on("error", () =>
|
|
1665
|
+
child.on("error", () => resolve3(null));
|
|
1206
1666
|
child.on("close", (code) => {
|
|
1207
|
-
if (code !== 0) return
|
|
1667
|
+
if (code !== 0) return resolve3(null);
|
|
1208
1668
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
1209
|
-
|
|
1669
|
+
resolve3(v || null);
|
|
1210
1670
|
});
|
|
1211
1671
|
});
|
|
1212
1672
|
if (latest && semverGt(latest, current) > 0) {
|
|
@@ -1222,12 +1682,12 @@ async function cmdUpdate(rawArgs) {
|
|
|
1222
1682
|
}
|
|
1223
1683
|
const args = parseArgs(rawArgs);
|
|
1224
1684
|
const cwd = process.cwd();
|
|
1225
|
-
const kandownDir =
|
|
1226
|
-
const htmlDest =
|
|
1227
|
-
if (
|
|
1228
|
-
const htmlSrc =
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1685
|
+
const kandownDir = resolve2(cwd, args.path);
|
|
1686
|
+
const htmlDest = join8(kandownDir, "kandown.html");
|
|
1687
|
+
if (existsSync8(htmlDest)) {
|
|
1688
|
+
const htmlSrc = resolve2(PKG_ROOT, "dist", "index.html");
|
|
1689
|
+
if (existsSync8(htmlSrc)) {
|
|
1690
|
+
copyFileSync3(htmlSrc, htmlDest);
|
|
1231
1691
|
success(`Refreshed ${args.path}/kandown.html`);
|
|
1232
1692
|
}
|
|
1233
1693
|
}
|
|
@@ -1238,10 +1698,10 @@ async function cmdDoctor(rawArgs) {
|
|
|
1238
1698
|
log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
|
|
1239
1699
|
`);
|
|
1240
1700
|
log(` CLI Version: ${currentVersion}`);
|
|
1241
|
-
const configPath =
|
|
1242
|
-
if (
|
|
1701
|
+
const configPath = join8(kandownDir, "kandown.json");
|
|
1702
|
+
if (existsSync8(configPath)) {
|
|
1243
1703
|
try {
|
|
1244
|
-
JSON.parse(
|
|
1704
|
+
JSON.parse(readFileSync8(configPath, "utf8"));
|
|
1245
1705
|
success("kandown.json valid");
|
|
1246
1706
|
} catch (e) {
|
|
1247
1707
|
err(`kandown.json invalid: ${e.message}`);
|
|
@@ -1275,63 +1735,400 @@ async function cmdWork(rawArgs) {
|
|
|
1275
1735
|
log(`- **${col.name}** (${col.tasks.length}): ${col.tasks.map((t) => `${t.id} ${t.title}`).join(", ") || "empty"}`);
|
|
1276
1736
|
}
|
|
1277
1737
|
}
|
|
1738
|
+
function addMultiFlag(flags, key, value) {
|
|
1739
|
+
const current = flags[key];
|
|
1740
|
+
if (Array.isArray(current)) current.push(value);
|
|
1741
|
+
else if (typeof current === "string") flags[key] = [current, value];
|
|
1742
|
+
else flags[key] = value;
|
|
1743
|
+
}
|
|
1744
|
+
function taskParseArgs(argv) {
|
|
1745
|
+
const flags = {};
|
|
1746
|
+
const positional = [];
|
|
1747
|
+
const aliases = { s: "status", p: "priority", a: "assignee", t: "tag", m: "message" };
|
|
1748
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1749
|
+
const arg = argv[i];
|
|
1750
|
+
if (arg.startsWith("--")) {
|
|
1751
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
|
|
1752
|
+
const key = rawKey === "to" ? "to" : rawKey;
|
|
1753
|
+
const next = inlineValue ?? argv[i + 1];
|
|
1754
|
+
if (inlineValue === void 0 && next && !next.startsWith("-")) i++;
|
|
1755
|
+
const value = inlineValue ?? (next && !next.startsWith("-") ? next : true);
|
|
1756
|
+
if (key === "tag") addMultiFlag(flags, key, String(value));
|
|
1757
|
+
else flags[key] = value;
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
if (/^-[spatm]$/.test(arg)) {
|
|
1761
|
+
const key = aliases[arg.slice(1)];
|
|
1762
|
+
const value = argv[i + 1];
|
|
1763
|
+
if (!value || value.startsWith("-")) {
|
|
1764
|
+
flags[key] = true;
|
|
1765
|
+
} else {
|
|
1766
|
+
i++;
|
|
1767
|
+
if (key === "tag") addMultiFlag(flags, key, value);
|
|
1768
|
+
else flags[key] = value;
|
|
1769
|
+
}
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
positional.push(arg);
|
|
1773
|
+
}
|
|
1774
|
+
return { flags, positional };
|
|
1775
|
+
}
|
|
1776
|
+
function stringFlag(flags, ...keys) {
|
|
1777
|
+
for (const key of keys) {
|
|
1778
|
+
const value = flags[key];
|
|
1779
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1780
|
+
}
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
function listFlag(flags, key) {
|
|
1784
|
+
const value = flags[key];
|
|
1785
|
+
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
1786
|
+
if (typeof value === "string" && value) return [value];
|
|
1787
|
+
return [];
|
|
1788
|
+
}
|
|
1789
|
+
function resolveStatusArg(kandownDir, status) {
|
|
1790
|
+
const config = loadConfig(kandownDir);
|
|
1791
|
+
return config.board.columns.find((col) => col.toLowerCase() === status.toLowerCase()) ?? null;
|
|
1792
|
+
}
|
|
1793
|
+
function taskPath(kandownDir, id, archived = false) {
|
|
1794
|
+
return archived ? join8(getTasksDir(kandownDir), "archive", `${id}.md`) : join8(getTasksDir(kandownDir), `${id}.md`);
|
|
1795
|
+
}
|
|
1796
|
+
function findTaskPath(kandownDir, id) {
|
|
1797
|
+
const active = taskPath(kandownDir, id);
|
|
1798
|
+
if (existsSync8(active)) return active;
|
|
1799
|
+
const archived = taskPath(kandownDir, id, true);
|
|
1800
|
+
if (existsSync8(archived)) return archived;
|
|
1801
|
+
return null;
|
|
1802
|
+
}
|
|
1803
|
+
function nextTaskId(kandownDir) {
|
|
1804
|
+
const ids = new Set(listTaskIds(kandownDir));
|
|
1805
|
+
const archiveDir = join8(getTasksDir(kandownDir), "archive");
|
|
1806
|
+
if (existsSync8(archiveDir)) {
|
|
1807
|
+
for (const file of readdirSync3(archiveDir)) {
|
|
1808
|
+
if (file.endsWith(".md")) ids.add(file.slice(0, -3));
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
let max = 0;
|
|
1812
|
+
for (const id of ids) {
|
|
1813
|
+
const match = id.match(/^t(\d+)$/i);
|
|
1814
|
+
if (match) max = Math.max(max, Number(match[1]));
|
|
1815
|
+
}
|
|
1816
|
+
return `t${max + 1}`;
|
|
1817
|
+
}
|
|
1818
|
+
function readTaskFile(kandownDir, id) {
|
|
1819
|
+
const path = findTaskPath(kandownDir, id);
|
|
1820
|
+
if (!path) return null;
|
|
1821
|
+
const parsed = parseTaskFile(readFileSync8(path, "utf8"));
|
|
1822
|
+
return {
|
|
1823
|
+
path,
|
|
1824
|
+
frontmatter: { ...parsed.frontmatter, id: parsed.frontmatter.id || id },
|
|
1825
|
+
body: parsed.body,
|
|
1826
|
+
archived: path.includes("/archive/")
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1278
1829
|
function cmdList(rawArgs) {
|
|
1279
1830
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1280
|
-
const
|
|
1281
|
-
const
|
|
1282
|
-
const statusFilter =
|
|
1283
|
-
const priorityFilter =
|
|
1284
|
-
|
|
1285
|
-
|
|
1831
|
+
const args = taskParseArgs(rawArgs);
|
|
1832
|
+
const includeArchived = args.flags.archived === true;
|
|
1833
|
+
const statusFilter = stringFlag(args.flags, "status")?.toLowerCase() ?? null;
|
|
1834
|
+
const priorityFilter = stringFlag(args.flags, "priority")?.toUpperCase() ?? null;
|
|
1835
|
+
const assigneeFilter = stringFlag(args.flags, "assignee");
|
|
1836
|
+
const tagFilters = listFlag(args.flags, "tag").map((tag) => tag.toLowerCase());
|
|
1837
|
+
const rows = [];
|
|
1838
|
+
for (const id of listTaskIds(kandownDir)) {
|
|
1839
|
+
const task = readTask(kandownDir, id);
|
|
1840
|
+
rows.push({
|
|
1841
|
+
id,
|
|
1842
|
+
title: task.frontmatter.title || id,
|
|
1843
|
+
status: task.frontmatter.status || "Backlog",
|
|
1844
|
+
priority: task.frontmatter.priority || "",
|
|
1845
|
+
assignee: task.frontmatter.assignee || "",
|
|
1846
|
+
tags: Array.isArray(task.frontmatter.tags) ? task.frontmatter.tags : [],
|
|
1847
|
+
archived: false
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
if (includeArchived) {
|
|
1851
|
+
const archiveDir = join8(getTasksDir(kandownDir), "archive");
|
|
1852
|
+
if (existsSync8(archiveDir)) {
|
|
1853
|
+
for (const file of readdirSync3(archiveDir).filter((name) => name.endsWith(".md"))) {
|
|
1854
|
+
const id = file.slice(0, -3);
|
|
1855
|
+
const parsed = parseTaskFile(readFileSync8(join8(archiveDir, file), "utf8"));
|
|
1856
|
+
rows.push({
|
|
1857
|
+
id,
|
|
1858
|
+
title: parsed.frontmatter.title || id,
|
|
1859
|
+
status: `${parsed.frontmatter.status || "Backlog"} (archived)`,
|
|
1860
|
+
priority: parsed.frontmatter.priority || "",
|
|
1861
|
+
assignee: parsed.frontmatter.assignee || "",
|
|
1862
|
+
tags: Array.isArray(parsed.frontmatter.tags) ? parsed.frontmatter.tags : [],
|
|
1863
|
+
archived: true
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
const filtered = rows.filter((row) => {
|
|
1869
|
+
if (statusFilter && row.status.toLowerCase() !== statusFilter) return false;
|
|
1870
|
+
if (priorityFilter && row.priority.toUpperCase() !== priorityFilter) return false;
|
|
1871
|
+
if (assigneeFilter && row.assignee !== assigneeFilter) return false;
|
|
1872
|
+
if (tagFilters.length > 0 && !tagFilters.every((tag) => row.tags.map((t) => t.toLowerCase()).includes(tag))) return false;
|
|
1873
|
+
return true;
|
|
1874
|
+
});
|
|
1875
|
+
if (args.flags.json === true) {
|
|
1876
|
+
process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
1880
|
+
for (const row of filtered) {
|
|
1881
|
+
const list = byStatus.get(row.status) ?? [];
|
|
1882
|
+
list.push(row);
|
|
1883
|
+
byStatus.set(row.status, list);
|
|
1884
|
+
}
|
|
1885
|
+
for (const [status, tasks] of byStatus) {
|
|
1286
1886
|
log(`
|
|
1287
|
-
${c.bold}${
|
|
1288
|
-
for (const
|
|
1289
|
-
|
|
1290
|
-
|
|
1887
|
+
${c.bold}${status}${c.reset} (${tasks.length})`);
|
|
1888
|
+
for (const task of tasks) {
|
|
1889
|
+
const pri = task.priority || "P2";
|
|
1890
|
+
const assignee = task.assignee ? ` @${task.assignee}` : "";
|
|
1891
|
+
log(` ${c.cyan}${task.id}${c.reset} [${pri}] ${task.title}${assignee}`);
|
|
1291
1892
|
}
|
|
1292
1893
|
}
|
|
1293
1894
|
log("");
|
|
1294
1895
|
}
|
|
1295
1896
|
function cmdShow(rawArgs) {
|
|
1296
1897
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1297
|
-
const
|
|
1898
|
+
const args = taskParseArgs(rawArgs);
|
|
1899
|
+
const id = args.positional[0];
|
|
1298
1900
|
if (!id) {
|
|
1299
1901
|
err("Usage: kandown show <task-id>");
|
|
1300
1902
|
process.exit(1);
|
|
1301
1903
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
`);
|
|
1307
|
-
log(task.body);
|
|
1308
|
-
} catch (e) {
|
|
1309
|
-
err(`Could not read task ${id}: ${e.message}`);
|
|
1904
|
+
const path = findTaskPath(kandownDir, id);
|
|
1905
|
+
if (!path) {
|
|
1906
|
+
err(`Task not found: ${id}`);
|
|
1907
|
+
process.exit(1);
|
|
1310
1908
|
}
|
|
1909
|
+
process.stdout.write(readFileSync8(path, "utf8"));
|
|
1910
|
+
}
|
|
1911
|
+
function cmdCreate(rawArgs) {
|
|
1912
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1913
|
+
const args = taskParseArgs(rawArgs);
|
|
1914
|
+
const title = args.positional.join(" ").trim();
|
|
1915
|
+
if (!title) {
|
|
1916
|
+
err('Usage: kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id custom-id] [--json]');
|
|
1917
|
+
process.exit(1);
|
|
1918
|
+
}
|
|
1919
|
+
const id = stringFlag(args.flags, "id") ?? nextTaskId(kandownDir);
|
|
1920
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
1921
|
+
err(`Invalid task id: ${id}`);
|
|
1922
|
+
process.exit(1);
|
|
1923
|
+
}
|
|
1924
|
+
if (findTaskPath(kandownDir, id)) {
|
|
1925
|
+
err(`Task already exists: ${id}`);
|
|
1926
|
+
process.exit(1);
|
|
1927
|
+
}
|
|
1928
|
+
const config = loadConfig(kandownDir);
|
|
1929
|
+
const rawStatus = stringFlag(args.flags, "to", "status");
|
|
1930
|
+
const status = rawStatus ? resolveStatusArg(kandownDir, rawStatus) : config.board.columns[0] || "Backlog";
|
|
1931
|
+
if (!status) {
|
|
1932
|
+
err(`Unknown status: ${rawStatus}`);
|
|
1933
|
+
process.exit(1);
|
|
1934
|
+
}
|
|
1935
|
+
const fm = {
|
|
1936
|
+
id,
|
|
1937
|
+
title,
|
|
1938
|
+
status,
|
|
1939
|
+
created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1940
|
+
};
|
|
1941
|
+
const priority = stringFlag(args.flags, "priority")?.toUpperCase();
|
|
1942
|
+
const assignee = stringFlag(args.flags, "assignee");
|
|
1943
|
+
const tags = listFlag(args.flags, "tag");
|
|
1944
|
+
if (priority) fm.priority = priority;
|
|
1945
|
+
if (assignee) fm.assignee = assignee;
|
|
1946
|
+
if (tags.length > 0) fm.tags = tags;
|
|
1947
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
1948
|
+
if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
|
|
1949
|
+
const path = taskPath(kandownDir, id);
|
|
1950
|
+
atomicWriteFileSync(path, serializeTaskFile(fm, ""));
|
|
1951
|
+
process.stderr.write(`${c.green}\u2713${c.reset} Created ${c.bold}${id}${c.reset} \u2192 ${status}
|
|
1952
|
+
`);
|
|
1953
|
+
process.stdout.write(args.flags.json === true ? JSON.stringify(fm, null, 2) + "\n" : `${id}
|
|
1954
|
+
`);
|
|
1311
1955
|
}
|
|
1312
1956
|
function cmdMove(rawArgs) {
|
|
1313
1957
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1958
|
+
const args = taskParseArgs(rawArgs);
|
|
1959
|
+
const id = args.positional[0];
|
|
1960
|
+
const rawStatus = args.positional.slice(1).join(" ") || stringFlag(args.flags, "to", "status");
|
|
1961
|
+
if (!id || !rawStatus) {
|
|
1316
1962
|
err("Usage: kandown move <task-id> <status>");
|
|
1317
1963
|
process.exit(1);
|
|
1318
1964
|
}
|
|
1965
|
+
if (rawStatus.toLowerCase() === "archived") {
|
|
1966
|
+
if (!archiveTaskInBoard(kandownDir, id)) {
|
|
1967
|
+
err(`Archive failed: ${id}`);
|
|
1968
|
+
process.exit(1);
|
|
1969
|
+
}
|
|
1970
|
+
success(`Archived ${id}`);
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const status = resolveStatusArg(kandownDir, rawStatus);
|
|
1974
|
+
if (!status) {
|
|
1975
|
+
err(`Unknown status: ${rawStatus}`);
|
|
1976
|
+
process.exit(1);
|
|
1977
|
+
}
|
|
1978
|
+
if (!moveTaskToColumn(kandownDir, id, status)) {
|
|
1979
|
+
err(`Move failed: ${id}`);
|
|
1980
|
+
process.exit(1);
|
|
1981
|
+
}
|
|
1982
|
+
success(`Moved ${id} \u2192 "${status}"`);
|
|
1983
|
+
}
|
|
1984
|
+
function cmdAssign(rawArgs) {
|
|
1985
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1986
|
+
const args = taskParseArgs(rawArgs);
|
|
1987
|
+
const [id, assignee] = args.positional;
|
|
1988
|
+
if (!id) {
|
|
1989
|
+
err("Usage: kandown assign <task-id> [assignee]");
|
|
1990
|
+
process.exit(1);
|
|
1991
|
+
}
|
|
1992
|
+
const task = readTaskFile(kandownDir, id);
|
|
1993
|
+
if (!task) {
|
|
1994
|
+
err(`Task not found: ${id}`);
|
|
1995
|
+
process.exit(1);
|
|
1996
|
+
}
|
|
1997
|
+
const frontmatter = { ...task.frontmatter, id };
|
|
1998
|
+
if (assignee) frontmatter.assignee = assignee;
|
|
1999
|
+
else delete frontmatter.assignee;
|
|
2000
|
+
atomicWriteFileSync(task.path, serializeTaskFile(frontmatter, task.body));
|
|
2001
|
+
success(assignee ? `Assigned ${id} \u2192 ${assignee}` : `Unassigned ${id}`);
|
|
2002
|
+
}
|
|
2003
|
+
function cmdCommit(rawArgs) {
|
|
2004
|
+
ensureKandownDir(rawArgs);
|
|
2005
|
+
const args = taskParseArgs(rawArgs);
|
|
2006
|
+
const message = stringFlag(args.flags, "message") || "tasks: update kandown board";
|
|
2007
|
+
const add = spawnSync("git", ["add", "tasks", ".kandown/kandown.json"], { stdio: "inherit" });
|
|
2008
|
+
if (add.status !== 0) process.exit(add.status ?? 1);
|
|
2009
|
+
const commit = spawnSync("git", ["commit", "-m", message], { stdio: "inherit" });
|
|
2010
|
+
process.exit(commit.status ?? 1);
|
|
2011
|
+
}
|
|
2012
|
+
function printTaskCommandsHelp() {
|
|
2013
|
+
log(`
|
|
2014
|
+
${c.bold}Kandown task commands${c.reset}
|
|
2015
|
+
|
|
2016
|
+
kandown list [-s status] [-p P1] [-a user] [-t tag] [--archived] [--json]
|
|
2017
|
+
kandown show <id>
|
|
2018
|
+
kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id id] [--json]
|
|
2019
|
+
kandown move <id> <status|archived>
|
|
2020
|
+
kandown assign <id> [user]
|
|
2021
|
+
kandown commit [-m "message"]
|
|
2022
|
+
`);
|
|
2023
|
+
}
|
|
2024
|
+
async function launchTui(screen, kandownDir) {
|
|
2025
|
+
if (!process.stdin.isTTY) {
|
|
2026
|
+
info(`TUI skipped because stdin is not interactive. Use ${c.cyan}kandown daemon status${c.reset} to inspect the web daemon.`);
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
const tuiPath = join8(PKG_ROOT, "bin", "tui.js");
|
|
2030
|
+
if (!existsSync8(tuiPath)) {
|
|
2031
|
+
err(`TUI binary not found at ${tuiPath}`);
|
|
2032
|
+
process.exit(1);
|
|
2033
|
+
}
|
|
2034
|
+
await new Promise((resolveTui) => {
|
|
2035
|
+
const child = spawn4(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
|
|
2036
|
+
child.on("close", (code) => {
|
|
2037
|
+
if (typeof code === "number" && code !== 0) process.exit(code);
|
|
2038
|
+
resolveTui();
|
|
2039
|
+
});
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
async function cmdTui(screen, rawArgs) {
|
|
2043
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2044
|
+
await launchTui(screen, kandownDir);
|
|
2045
|
+
}
|
|
2046
|
+
function cmdExport(rawArgs) {
|
|
2047
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2048
|
+
const board = readBoard(kandownDir);
|
|
2049
|
+
process.stdout.write(JSON.stringify(board, null, 2) + "\n");
|
|
2050
|
+
}
|
|
2051
|
+
function cmdProjects(rawArgs) {
|
|
2052
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2053
|
+
const metadataPath2 = join8(kandownDir, "daemon.json");
|
|
2054
|
+
if (!existsSync8(metadataPath2)) {
|
|
2055
|
+
info("No daemon metadata for this project.");
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
process.stdout.write(readFileSync8(metadataPath2, "utf8").trim() + "\n");
|
|
2059
|
+
}
|
|
2060
|
+
function cmdImport(rawArgs) {
|
|
2061
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2062
|
+
const args = taskParseArgs(rawArgs);
|
|
2063
|
+
const file = args.positional[0];
|
|
2064
|
+
if (!file) {
|
|
2065
|
+
err("Usage: kandown import <file.json> [--overwrite]");
|
|
2066
|
+
process.exit(1);
|
|
2067
|
+
}
|
|
2068
|
+
const importPath = resolve2(process.cwd(), file);
|
|
2069
|
+
if (!existsSync8(importPath)) {
|
|
2070
|
+
err(`Import file not found: ${file}`);
|
|
2071
|
+
process.exit(1);
|
|
2072
|
+
}
|
|
2073
|
+
let raw;
|
|
1319
2074
|
try {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
2075
|
+
raw = JSON.parse(readFileSync8(importPath, "utf8"));
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
err(`Import file must be JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
2078
|
+
process.exit(1);
|
|
2079
|
+
}
|
|
2080
|
+
const rows = [];
|
|
2081
|
+
if (Array.isArray(raw)) {
|
|
2082
|
+
rows.push(...raw.filter((value) => typeof value === "object" && value !== null));
|
|
2083
|
+
} else if (typeof raw === "object" && raw !== null && Array.isArray(raw.columns)) {
|
|
2084
|
+
for (const column of raw.columns) {
|
|
2085
|
+
if (typeof column !== "object" || column === null) continue;
|
|
2086
|
+
const col = column;
|
|
2087
|
+
if (!Array.isArray(col.tasks)) continue;
|
|
2088
|
+
for (const task of col.tasks) {
|
|
2089
|
+
if (typeof task === "object" && task !== null) rows.push({ ...task, status: String(col.name || "Backlog") });
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
if (rows.length === 0) {
|
|
2094
|
+
err("No tasks found to import. Expected a list JSON array or kandown export object.");
|
|
2095
|
+
process.exit(1);
|
|
2096
|
+
}
|
|
2097
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
2098
|
+
if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
|
|
2099
|
+
let imported = 0;
|
|
2100
|
+
for (const row of rows) {
|
|
2101
|
+
const id = typeof row.id === "string" && /^[a-zA-Z0-9_-]+$/.test(row.id) ? row.id : nextTaskId(kandownDir);
|
|
2102
|
+
const path = taskPath(kandownDir, id);
|
|
2103
|
+
if (existsSync8(path) && args.flags.overwrite !== true) continue;
|
|
2104
|
+
const fm = {
|
|
2105
|
+
id,
|
|
2106
|
+
title: typeof row.title === "string" && row.title ? row.title : id,
|
|
2107
|
+
status: typeof row.status === "string" && row.status ? row.status.replace(/ \(archived\)$/i, "") : "Backlog"
|
|
2108
|
+
};
|
|
2109
|
+
if (typeof row.priority === "string") fm.priority = row.priority;
|
|
2110
|
+
if (typeof row.assignee === "string") fm.assignee = row.assignee;
|
|
2111
|
+
if (Array.isArray(row.tags)) fm.tags = row.tags.map(String);
|
|
2112
|
+
atomicWriteFileSync(path, serializeTaskFile(fm, typeof row.body === "string" ? row.body : ""));
|
|
2113
|
+
imported++;
|
|
1324
2114
|
}
|
|
2115
|
+
success(`Imported ${imported} task${imported === 1 ? "" : "s"}`);
|
|
1325
2116
|
}
|
|
1326
2117
|
async function main() {
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
2118
|
+
const rawArgs = process.argv.slice(2);
|
|
2119
|
+
if (rawArgs.length === 1 && (rawArgs[0] === "--version" || rawArgs[0] === "version")) {
|
|
2120
|
+
log(getCurrentVersion());
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
const { cmd, rest } = splitCommand(rawArgs);
|
|
2124
|
+
const skipUpdate = rawArgs.includes("--no-update-check") || process.env.KANDOWN_NO_UPDATE === "1";
|
|
1331
2125
|
if (!skipUpdate) {
|
|
1332
2126
|
await checkForUpdate(process.argv);
|
|
1333
2127
|
}
|
|
1334
2128
|
switch (cmd) {
|
|
2129
|
+
case "init":
|
|
2130
|
+
cmdInit(rest);
|
|
2131
|
+
break;
|
|
1335
2132
|
case "update":
|
|
1336
2133
|
case "upgrade":
|
|
1337
2134
|
await cmdUpdate(rest);
|
|
@@ -1342,6 +2139,12 @@ async function main() {
|
|
|
1342
2139
|
case "work":
|
|
1343
2140
|
await cmdWork(rest);
|
|
1344
2141
|
break;
|
|
2142
|
+
case "board":
|
|
2143
|
+
await cmdTui("board", rest);
|
|
2144
|
+
break;
|
|
2145
|
+
case "settings":
|
|
2146
|
+
await cmdTui("settings", rest);
|
|
2147
|
+
break;
|
|
1345
2148
|
case "list":
|
|
1346
2149
|
case "ls":
|
|
1347
2150
|
cmdList(rest);
|
|
@@ -1349,21 +2152,52 @@ async function main() {
|
|
|
1349
2152
|
case "show":
|
|
1350
2153
|
cmdShow(rest);
|
|
1351
2154
|
break;
|
|
2155
|
+
case "create":
|
|
2156
|
+
case "new":
|
|
2157
|
+
cmdCreate(rest);
|
|
2158
|
+
break;
|
|
1352
2159
|
case "move":
|
|
1353
2160
|
cmdMove(rest);
|
|
1354
2161
|
break;
|
|
2162
|
+
case "assign":
|
|
2163
|
+
cmdAssign(rest);
|
|
2164
|
+
break;
|
|
2165
|
+
case "commit":
|
|
2166
|
+
cmdCommit(rest);
|
|
2167
|
+
break;
|
|
2168
|
+
case "tasks":
|
|
2169
|
+
printTaskCommandsHelp();
|
|
2170
|
+
break;
|
|
2171
|
+
case "projects":
|
|
2172
|
+
cmdProjects(rest);
|
|
2173
|
+
break;
|
|
2174
|
+
case "export":
|
|
2175
|
+
cmdExport(rest);
|
|
2176
|
+
break;
|
|
2177
|
+
case "import":
|
|
2178
|
+
cmdImport(rest);
|
|
2179
|
+
break;
|
|
2180
|
+
case "mcp": {
|
|
2181
|
+
const { kandownDir } = ensureKandownDir(rest);
|
|
2182
|
+
startMcpServer(kandownDir);
|
|
2183
|
+
break;
|
|
2184
|
+
}
|
|
1355
2185
|
case "help":
|
|
1356
2186
|
case "--help":
|
|
1357
2187
|
case "-h":
|
|
1358
2188
|
help();
|
|
1359
2189
|
break;
|
|
1360
2190
|
case "daemon": {
|
|
1361
|
-
const
|
|
1362
|
-
const
|
|
2191
|
+
const parsedDaemonArgs = parseArgs(rest);
|
|
2192
|
+
const subcommand = parsedDaemonArgs.positional[0] || "status";
|
|
2193
|
+
const daemonArgs = subcommand ? stripFirstPositional(rest, subcommand) : rest;
|
|
2194
|
+
const { kandownDir } = ensureKandownDir(daemonArgs);
|
|
1363
2195
|
if (subcommand === "run") {
|
|
1364
|
-
const
|
|
2196
|
+
const daemonOptions = parseArgs(daemonArgs);
|
|
2197
|
+
const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
|
|
2198
|
+
const { port } = await listenOnAvailablePort(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
|
|
1365
2199
|
const url = `http://localhost:${port}`;
|
|
1366
|
-
const metadataPath2 =
|
|
2200
|
+
const metadataPath2 = join8(kandownDir, "daemon.json");
|
|
1367
2201
|
atomicWriteFileSync(metadataPath2, JSON.stringify({
|
|
1368
2202
|
pid: process.pid,
|
|
1369
2203
|
port,
|
|
@@ -1376,24 +2210,44 @@ async function main() {
|
|
|
1376
2210
|
info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
|
|
1377
2211
|
await new Promise(() => {
|
|
1378
2212
|
});
|
|
1379
|
-
} else if (subcommand === "
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
} else {
|
|
1388
|
-
info("Daemon not running");
|
|
2213
|
+
} else if (subcommand === "start") {
|
|
2214
|
+
const daemonOptions = parseArgs(daemonArgs);
|
|
2215
|
+
const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
|
|
2216
|
+
const status = await startProjectDaemon(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
|
|
2217
|
+
if (status.running && status.metadata) success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2218
|
+
else {
|
|
2219
|
+
err("Daemon failed to start");
|
|
2220
|
+
process.exit(1);
|
|
1389
2221
|
}
|
|
1390
|
-
} else {
|
|
2222
|
+
} else if (subcommand === "restart") {
|
|
2223
|
+
await stopProjectDaemon(kandownDir);
|
|
2224
|
+
const status = await startProjectDaemon(kandownDir);
|
|
2225
|
+
if (status.running && status.metadata) success(`Daemon restarted on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2226
|
+
else {
|
|
2227
|
+
err("Daemon failed to restart");
|
|
2228
|
+
process.exit(1);
|
|
2229
|
+
}
|
|
2230
|
+
} else if (subcommand === "stop") {
|
|
2231
|
+
const stopped = await stopProjectDaemon(kandownDir);
|
|
2232
|
+
if (stopped) success("Daemon stopped");
|
|
2233
|
+
else info("Daemon not running");
|
|
2234
|
+
} else if (subcommand === "status") {
|
|
1391
2235
|
const status = await getDaemonStatus(kandownDir);
|
|
1392
2236
|
if (status.running && status.metadata) {
|
|
1393
2237
|
success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
1394
2238
|
} else {
|
|
1395
2239
|
info("Daemon not running");
|
|
1396
2240
|
}
|
|
2241
|
+
} else if (subcommand === "refresh-all") {
|
|
2242
|
+
const status = await getDaemonStatus(kandownDir);
|
|
2243
|
+
if (status.running) await stopProjectDaemon(kandownDir);
|
|
2244
|
+
const restarted = await startProjectDaemon(kandownDir);
|
|
2245
|
+
if (restarted.running && restarted.metadata) success(`Refreshed current project daemon on port ${restarted.metadata.port}`);
|
|
2246
|
+
else info("No running daemon refreshed");
|
|
2247
|
+
} else {
|
|
2248
|
+
err(`Unknown daemon command: ${subcommand}`);
|
|
2249
|
+
log(` Use ${c.cyan}kandown daemon start|stop|restart|status|refresh-all${c.reset}`);
|
|
2250
|
+
process.exit(1);
|
|
1397
2251
|
}
|
|
1398
2252
|
break;
|
|
1399
2253
|
}
|
|
@@ -1405,21 +2259,14 @@ async function main() {
|
|
|
1405
2259
|
status = await startProjectDaemon(kandownDir);
|
|
1406
2260
|
}
|
|
1407
2261
|
if (!parsed.flags["no-open"]) {
|
|
1408
|
-
const urlToOpen = status.metadata?.url ||
|
|
2262
|
+
const urlToOpen = status.metadata?.url || join8(kandownDir, "kandown.html");
|
|
1409
2263
|
openBrowser(urlToOpen);
|
|
1410
2264
|
}
|
|
1411
|
-
|
|
1412
|
-
if (existsSync6(tuiPath)) {
|
|
1413
|
-
const child = spawn4(process.execPath, [tuiPath, ...rest], { stdio: "inherit" });
|
|
1414
|
-
child.on("close", (code) => process.exit(code || 0));
|
|
1415
|
-
} else {
|
|
1416
|
-
err(`TUI binary not found at ${tuiPath}`);
|
|
1417
|
-
process.exit(1);
|
|
1418
|
-
}
|
|
2265
|
+
await launchTui("board", kandownDir);
|
|
1419
2266
|
break;
|
|
1420
2267
|
}
|
|
1421
2268
|
default:
|
|
1422
|
-
if (cmd.startsWith("-")) {
|
|
2269
|
+
if (!cmd || cmd.startsWith("-")) {
|
|
1423
2270
|
help();
|
|
1424
2271
|
break;
|
|
1425
2272
|
}
|