kandown 0.33.2 → 0.33.4
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 +22 -0
- package/bin/kandown.js +1070 -143
- package/bin/tui.js +138 -74
- 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.4";
|
|
20
20
|
|
|
21
21
|
// src/cli/lib/updater.ts
|
|
22
22
|
import { fileURLToPath } from "url";
|
|
@@ -99,6 +99,31 @@ function resolveKandownBin() {
|
|
|
99
99
|
}
|
|
100
100
|
return null;
|
|
101
101
|
}
|
|
102
|
+
async function readInstalledKandownVersion(targetVersion) {
|
|
103
|
+
const localVersion = getCurrentVersion();
|
|
104
|
+
const bin = resolveKandownBin();
|
|
105
|
+
if (!bin) return localVersion;
|
|
106
|
+
return await new Promise((resolveVersion) => {
|
|
107
|
+
const child = spawn(bin, ["--version"], {
|
|
108
|
+
timeout: 5e3,
|
|
109
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
110
|
+
env: { ...process.env, KANDOWN_NO_UPDATE: "1" },
|
|
111
|
+
detached: false
|
|
112
|
+
});
|
|
113
|
+
let stdout = "";
|
|
114
|
+
child.stdout.on("data", (d) => {
|
|
115
|
+
stdout += d;
|
|
116
|
+
});
|
|
117
|
+
child.stderr.on("data", () => {
|
|
118
|
+
});
|
|
119
|
+
child.on("error", () => resolveVersion(localVersion));
|
|
120
|
+
child.on("close", (code) => {
|
|
121
|
+
if (code !== 0) return resolveVersion(localVersion);
|
|
122
|
+
const match = stdout.trim().match(/v?(\d+\.\d+\.\d+(?:-[\w.-]+)?)/);
|
|
123
|
+
resolveVersion(match ? match[1] : localVersion);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
102
127
|
function updateCheckedRecently() {
|
|
103
128
|
try {
|
|
104
129
|
if (!existsSync(UPDATE_CHECK_CACHE)) return false;
|
|
@@ -115,6 +140,10 @@ function rememberUpdateCheck() {
|
|
|
115
140
|
} catch {
|
|
116
141
|
}
|
|
117
142
|
}
|
|
143
|
+
function requestedSemver(packageSpec) {
|
|
144
|
+
const match = packageSpec.match(/@(\d+\.\d+\.\d+(?:-[\w.-]+)?)$/);
|
|
145
|
+
return match ? match[1] : null;
|
|
146
|
+
}
|
|
118
147
|
async function performGlobalPackageUpdate(packageSpec) {
|
|
119
148
|
const cleanEnv = { ...process.env };
|
|
120
149
|
for (const k of Object.keys(cleanEnv)) {
|
|
@@ -122,6 +151,12 @@ async function performGlobalPackageUpdate(packageSpec) {
|
|
|
122
151
|
delete cleanEnv[k];
|
|
123
152
|
}
|
|
124
153
|
}
|
|
154
|
+
const targetVersion = requestedSemver(packageSpec);
|
|
155
|
+
const verifyInstalledVersion = async () => {
|
|
156
|
+
if (!targetVersion) return true;
|
|
157
|
+
const installedVersion = await readInstalledKandownVersion(targetVersion);
|
|
158
|
+
return semverGt(installedVersion, targetVersion) >= 0;
|
|
159
|
+
};
|
|
125
160
|
const tryPkgCmd = (cmd, args) => {
|
|
126
161
|
return new Promise((res) => {
|
|
127
162
|
const child = spawn(cmd, args, {
|
|
@@ -138,17 +173,26 @@ async function performGlobalPackageUpdate(packageSpec) {
|
|
|
138
173
|
child.on("close", (code) => res(code === 0));
|
|
139
174
|
});
|
|
140
175
|
};
|
|
176
|
+
const tryPkgCmdAndVerify = async (cmd, args) => {
|
|
177
|
+
if (!await tryPkgCmd(cmd, args)) return false;
|
|
178
|
+
return verifyInstalledVersion();
|
|
179
|
+
};
|
|
141
180
|
const currentBin = resolveKandownBin() || "";
|
|
181
|
+
const currentBinDir = currentBin ? dirname(currentBin) : null;
|
|
182
|
+
const siblingNpm = currentBinDir ? join(currentBinDir, "npm") : null;
|
|
183
|
+
const siblingPnpm = currentBinDir ? join(currentBinDir, "pnpm") : null;
|
|
142
184
|
const isPnpmInstall = currentBin.includes("pnpm");
|
|
185
|
+
if (siblingPnpm && existsSync(siblingPnpm) && await tryPkgCmdAndVerify(siblingPnpm, ["add", "-g", packageSpec])) return true;
|
|
186
|
+
if (siblingNpm && existsSync(siblingNpm) && await tryPkgCmdAndVerify(siblingNpm, ["install", "-g", packageSpec, "--force"])) return true;
|
|
143
187
|
if (isPnpmInstall) {
|
|
144
|
-
if (await
|
|
145
|
-
if (await
|
|
188
|
+
if (await tryPkgCmdAndVerify("pnpm", ["add", "-g", packageSpec])) return true;
|
|
189
|
+
if (await tryPkgCmdAndVerify("npm", ["install", "-g", packageSpec, "--force"])) return true;
|
|
146
190
|
} else {
|
|
147
|
-
if (await
|
|
148
|
-
if (await
|
|
191
|
+
if (await tryPkgCmdAndVerify("npm", ["install", "-g", packageSpec, "--force"])) return true;
|
|
192
|
+
if (await tryPkgCmdAndVerify("pnpm", ["add", "-g", packageSpec])) return true;
|
|
149
193
|
}
|
|
150
|
-
if (await
|
|
151
|
-
return await
|
|
194
|
+
if (await tryPkgCmdAndVerify("yarn", ["global", "add", packageSpec])) return true;
|
|
195
|
+
return await tryPkgCmdAndVerify("bun", ["add", "-g", packageSpec]);
|
|
152
196
|
}
|
|
153
197
|
async function checkForUpdate(argv = process.argv) {
|
|
154
198
|
if (process.env.KANDOWN_NO_UPDATE === "1") return;
|
|
@@ -165,7 +209,7 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
165
209
|
}
|
|
166
210
|
} catch {
|
|
167
211
|
}
|
|
168
|
-
const latest = await new Promise((
|
|
212
|
+
const latest = await new Promise((resolve3) => {
|
|
169
213
|
const child2 = spawn("npm", ["view", "kandown", "version"], {
|
|
170
214
|
timeout: 6e3,
|
|
171
215
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -178,11 +222,11 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
178
222
|
});
|
|
179
223
|
child2.stderr.on("data", () => {
|
|
180
224
|
});
|
|
181
|
-
child2.on("error", () =>
|
|
225
|
+
child2.on("error", () => resolve3(null));
|
|
182
226
|
child2.on("close", (code) => {
|
|
183
|
-
if (code !== 0) return
|
|
227
|
+
if (code !== 0) return resolve3(null);
|
|
184
228
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
185
|
-
|
|
229
|
+
resolve3(v || null);
|
|
186
230
|
});
|
|
187
231
|
});
|
|
188
232
|
if (!latest) return;
|
|
@@ -547,14 +591,14 @@ function readBoard(kandownDir) {
|
|
|
547
591
|
};
|
|
548
592
|
}
|
|
549
593
|
function readTask(kandownDir, taskId) {
|
|
550
|
-
const
|
|
551
|
-
if (!existsSync3(
|
|
594
|
+
const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
|
|
595
|
+
if (!existsSync3(taskPath2)) {
|
|
552
596
|
return {
|
|
553
597
|
frontmatter: { id: taskId, title: `Task ${taskId}`, status: "Backlog" },
|
|
554
598
|
body: ""
|
|
555
599
|
};
|
|
556
600
|
}
|
|
557
|
-
const content = readFileSync3(
|
|
601
|
+
const content = readFileSync3(taskPath2, "utf8");
|
|
558
602
|
const parsed = parseTaskFile(content);
|
|
559
603
|
return {
|
|
560
604
|
...parsed,
|
|
@@ -608,21 +652,21 @@ ${gitLog}
|
|
|
608
652
|
return sections.filter(Boolean).join("\n\n---\n\n");
|
|
609
653
|
}
|
|
610
654
|
function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
611
|
-
const
|
|
612
|
-
if (!existsSync3(
|
|
655
|
+
const taskPath2 = join3(getTasksDir(kandownDir), `${taskId}.md`);
|
|
656
|
+
if (!existsSync3(taskPath2)) return false;
|
|
613
657
|
try {
|
|
614
|
-
const prevContent = readFileSync3(
|
|
658
|
+
const prevContent = readFileSync3(taskPath2, "utf8");
|
|
615
659
|
const parsed = readTask(kandownDir, taskId);
|
|
616
660
|
const newContent = serializeTaskFile({
|
|
617
661
|
...parsed.frontmatter,
|
|
618
662
|
id: taskId,
|
|
619
663
|
status: targetColumn
|
|
620
664
|
}, parsed.body);
|
|
621
|
-
atomicWriteFileSync(
|
|
665
|
+
atomicWriteFileSync(taskPath2, newContent);
|
|
622
666
|
pushUndo(kandownDir, {
|
|
623
667
|
type: "move",
|
|
624
668
|
taskId,
|
|
625
|
-
path:
|
|
669
|
+
path: taskPath2,
|
|
626
670
|
previousContent: prevContent,
|
|
627
671
|
newContent,
|
|
628
672
|
timestamp: Date.now()
|
|
@@ -652,6 +696,102 @@ function pushUndo(kandownDir, record) {
|
|
|
652
696
|
} catch {
|
|
653
697
|
}
|
|
654
698
|
}
|
|
699
|
+
function createTaskInBoard(kandownDir, rawInput, status) {
|
|
700
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
701
|
+
if (!existsSync3(tasksDir)) mkdirSync2(tasksDir, { recursive: true });
|
|
702
|
+
const ids = listTaskIds(kandownDir);
|
|
703
|
+
let maxN = 0;
|
|
704
|
+
for (const id of ids) {
|
|
705
|
+
const m = id.match(/^t(\d+)$/i);
|
|
706
|
+
if (m) {
|
|
707
|
+
const n = parseInt(m[1], 10);
|
|
708
|
+
if (Number.isFinite(n) && n > maxN) maxN = n;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const newId = `t${maxN + 1}`;
|
|
712
|
+
const config = loadConfig(kandownDir);
|
|
713
|
+
const targetStatus = status || (config.board.columns[0] ?? "Backlog");
|
|
714
|
+
let text = rawInput.trim();
|
|
715
|
+
let priority;
|
|
716
|
+
const tags = [];
|
|
717
|
+
let assignee;
|
|
718
|
+
let due;
|
|
719
|
+
const depends_on = [];
|
|
720
|
+
text = text.replace(/(?:^|\s)p([1-4])(?:\s|$)/i, (_, level) => {
|
|
721
|
+
priority = `P${level}`;
|
|
722
|
+
return " ";
|
|
723
|
+
});
|
|
724
|
+
text = text.replace(/(?:^|\s)#([a-zA-Z0-9_-]+)/g, (_, tag) => {
|
|
725
|
+
tags.push(tag.toLowerCase());
|
|
726
|
+
return " ";
|
|
727
|
+
});
|
|
728
|
+
text = text.replace(/(?:^|\s)@([a-zA-Z0-9_-]+)/g, (_, user) => {
|
|
729
|
+
assignee = user;
|
|
730
|
+
return " ";
|
|
731
|
+
});
|
|
732
|
+
text = text.replace(/(?:^|\s)due:([^\s]+)/i, (_, d) => {
|
|
733
|
+
due = d;
|
|
734
|
+
return " ";
|
|
735
|
+
});
|
|
736
|
+
text = text.replace(/(?:^|\s)\+([a-zA-Z0-9_-]+)/g, (_, depId) => {
|
|
737
|
+
depends_on.push(depId);
|
|
738
|
+
return " ";
|
|
739
|
+
});
|
|
740
|
+
const title = text.replace(/\s+/g, " ").trim() || rawInput;
|
|
741
|
+
const fm = {
|
|
742
|
+
id: newId,
|
|
743
|
+
title,
|
|
744
|
+
status: targetStatus,
|
|
745
|
+
created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
746
|
+
};
|
|
747
|
+
if (priority) fm.priority = priority;
|
|
748
|
+
if (assignee) fm.assignee = assignee;
|
|
749
|
+
if (tags.length > 0) fm.tags = tags;
|
|
750
|
+
if (due) fm.due = due;
|
|
751
|
+
if (depends_on.length > 0) fm.depends_on = depends_on;
|
|
752
|
+
const content = serializeTaskFile(fm, "");
|
|
753
|
+
const taskPath2 = join3(tasksDir, `${newId}.md`);
|
|
754
|
+
atomicWriteFileSync(taskPath2, content);
|
|
755
|
+
pushUndo(kandownDir, {
|
|
756
|
+
type: "create",
|
|
757
|
+
taskId: newId,
|
|
758
|
+
path: taskPath2,
|
|
759
|
+
previousContent: null,
|
|
760
|
+
newContent: content,
|
|
761
|
+
timestamp: Date.now()
|
|
762
|
+
});
|
|
763
|
+
return newId;
|
|
764
|
+
}
|
|
765
|
+
function archiveTaskInBoard(kandownDir, taskId) {
|
|
766
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
767
|
+
const taskPath2 = join3(tasksDir, `${taskId}.md`);
|
|
768
|
+
if (!existsSync3(taskPath2)) return false;
|
|
769
|
+
try {
|
|
770
|
+
const prevContent = readFileSync3(taskPath2, "utf8");
|
|
771
|
+
const archiveDir = join3(tasksDir, "archive");
|
|
772
|
+
if (!existsSync3(archiveDir)) mkdirSync2(archiveDir, { recursive: true });
|
|
773
|
+
const parsed = readTask(kandownDir, taskId);
|
|
774
|
+
const newContent = serializeTaskFile({
|
|
775
|
+
...parsed.frontmatter,
|
|
776
|
+
id: taskId,
|
|
777
|
+
archived: true
|
|
778
|
+
}, parsed.body);
|
|
779
|
+
const destPath = join3(archiveDir, `${taskId}.md`);
|
|
780
|
+
atomicWriteFileSync(destPath, newContent);
|
|
781
|
+
unlinkSync3(taskPath2);
|
|
782
|
+
pushUndo(kandownDir, {
|
|
783
|
+
type: "archive",
|
|
784
|
+
taskId,
|
|
785
|
+
path: destPath,
|
|
786
|
+
previousContent: prevContent,
|
|
787
|
+
newContent,
|
|
788
|
+
timestamp: Date.now()
|
|
789
|
+
});
|
|
790
|
+
return true;
|
|
791
|
+
} catch {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
655
795
|
|
|
656
796
|
// src/cli/lib/daemon.ts
|
|
657
797
|
import { existsSync as existsSync4, readFileSync as readFileSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
@@ -701,9 +841,10 @@ function isProcessAlive(pid) {
|
|
|
701
841
|
}
|
|
702
842
|
function parseRemoteDaemonInfo(value) {
|
|
703
843
|
if (!isRecord(value)) return null;
|
|
704
|
-
const { ok, pid, kandownDir } = value;
|
|
844
|
+
const { ok, pid, kandownDir, version } = value;
|
|
705
845
|
if (ok !== true || typeof pid !== "number" || !Number.isInteger(pid) || typeof kandownDir !== "string") return null;
|
|
706
|
-
|
|
846
|
+
if (version !== null && typeof version !== "string" && version !== void 0) return null;
|
|
847
|
+
return { ok, pid, kandownDir, version: typeof version === "string" ? version : null };
|
|
707
848
|
}
|
|
708
849
|
async function fetchDaemonInfo(port) {
|
|
709
850
|
try {
|
|
@@ -717,16 +858,16 @@ async function fetchDaemonInfo(port) {
|
|
|
717
858
|
}
|
|
718
859
|
}
|
|
719
860
|
function isPortListening(port, timeoutMs = 400) {
|
|
720
|
-
return new Promise((
|
|
861
|
+
return new Promise((resolve3) => {
|
|
721
862
|
const socket = createConnection({ port, host: "127.0.0.1" }, () => {
|
|
722
863
|
socket.destroy();
|
|
723
|
-
|
|
864
|
+
resolve3(true);
|
|
724
865
|
});
|
|
725
|
-
socket.on("error", () =>
|
|
866
|
+
socket.on("error", () => resolve3(false));
|
|
726
867
|
socket.setTimeout(timeoutMs);
|
|
727
868
|
socket.on("timeout", () => {
|
|
728
869
|
socket.destroy();
|
|
729
|
-
|
|
870
|
+
resolve3(false);
|
|
730
871
|
});
|
|
731
872
|
});
|
|
732
873
|
}
|
|
@@ -745,7 +886,7 @@ async function getDaemonStatus(kandownDir) {
|
|
|
745
886
|
removeDaemonMetadata(kandownDir);
|
|
746
887
|
return { running: false, metadata: null };
|
|
747
888
|
}
|
|
748
|
-
return { running: true, metadata };
|
|
889
|
+
return { running: true, metadata: { ...metadata, version: remote.version ?? metadata.version } };
|
|
749
890
|
}
|
|
750
891
|
async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
|
|
751
892
|
const started = Date.now();
|
|
@@ -754,13 +895,16 @@ async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
|
|
|
754
895
|
if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
|
|
755
896
|
return { running: true, metadata };
|
|
756
897
|
}
|
|
757
|
-
await new Promise((
|
|
898
|
+
await new Promise((resolve3) => setTimeout(resolve3, 120));
|
|
758
899
|
}
|
|
759
900
|
return { running: false, metadata: null };
|
|
760
901
|
}
|
|
761
902
|
async function startProjectDaemon(kandownDir, preferredPort) {
|
|
762
903
|
const current = await getDaemonStatus(kandownDir);
|
|
763
|
-
if (current.running)
|
|
904
|
+
if (current.running) {
|
|
905
|
+
if (current.metadata?.version === getCurrentVersion()) return current;
|
|
906
|
+
await stopProjectDaemon(kandownDir);
|
|
907
|
+
}
|
|
764
908
|
const cliPath = process.argv[1];
|
|
765
909
|
if (!cliPath) throw new Error("Cannot locate kandown CLI entrypoint");
|
|
766
910
|
const args = [
|
|
@@ -783,13 +927,54 @@ async function startProjectDaemon(kandownDir, preferredPort) {
|
|
|
783
927
|
child.unref();
|
|
784
928
|
return waitForDaemon(kandownDir);
|
|
785
929
|
}
|
|
930
|
+
async function isOwnedKandownDaemon(pid, port, kandownDir) {
|
|
931
|
+
const remote = await fetchDaemonInfo(port);
|
|
932
|
+
if (remote) return remote.pid === pid && remote.kandownDir === kandownDir;
|
|
933
|
+
try {
|
|
934
|
+
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], {
|
|
935
|
+
encoding: "utf8",
|
|
936
|
+
timeout: 2e3
|
|
937
|
+
}).trim();
|
|
938
|
+
return /kandown/.test(cmd) && cmd.includes(kandownDir);
|
|
939
|
+
} catch {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async function stopProjectDaemon(kandownDir) {
|
|
944
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
945
|
+
if (!metadata) return false;
|
|
946
|
+
const pid = metadata.pid;
|
|
947
|
+
if (!isProcessAlive(pid)) {
|
|
948
|
+
removeDaemonMetadata(kandownDir);
|
|
949
|
+
return false;
|
|
950
|
+
}
|
|
951
|
+
if (!await isOwnedKandownDaemon(pid, metadata.port, kandownDir)) {
|
|
952
|
+
removeDaemonMetadata(kandownDir);
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
try {
|
|
956
|
+
process.kill(pid, "SIGTERM");
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
const started = Date.now();
|
|
960
|
+
while (Date.now() - started < 2500 && isProcessAlive(pid)) {
|
|
961
|
+
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
962
|
+
}
|
|
963
|
+
if (isProcessAlive(pid)) {
|
|
964
|
+
try {
|
|
965
|
+
process.kill(pid, "SIGKILL");
|
|
966
|
+
} catch {
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
removeDaemonMetadata(kandownDir);
|
|
970
|
+
return true;
|
|
971
|
+
}
|
|
786
972
|
|
|
787
973
|
// src/cli/lib/server.ts
|
|
788
974
|
import { createServer } from "http";
|
|
789
975
|
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";
|
|
976
|
+
import { join as join5 } from "path";
|
|
977
|
+
import { spawn as spawn3 } from "child_process";
|
|
793
978
|
var START_PORT_RANGE = 2050;
|
|
794
979
|
var END_PORT_RANGE = 2099;
|
|
795
980
|
var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
|
|
@@ -826,8 +1011,7 @@ function writeText(res, status, text) {
|
|
|
826
1011
|
function syncProjectKandownHtml(kandownDir) {
|
|
827
1012
|
try {
|
|
828
1013
|
const projectHtml = join5(kandownDir, "kandown.html");
|
|
829
|
-
const
|
|
830
|
-
const distHtml = join5(cliRoot, "dist", "index.html");
|
|
1014
|
+
const distHtml = join5(PKG_ROOT, "dist", "index.html");
|
|
831
1015
|
if (!existsSync5(distHtml)) return false;
|
|
832
1016
|
if (!existsSync5(projectHtml)) {
|
|
833
1017
|
copyFileSync(distHtml, projectHtml);
|
|
@@ -843,27 +1027,42 @@ function syncProjectKandownHtml(kandownDir) {
|
|
|
843
1027
|
}
|
|
844
1028
|
return false;
|
|
845
1029
|
}
|
|
846
|
-
function
|
|
1030
|
+
function readDaemonPort(kandownDir) {
|
|
847
1031
|
try {
|
|
848
|
-
const
|
|
849
|
-
|
|
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
|
-
}
|
|
1032
|
+
const raw = JSON.parse(readFileSync5(join5(kandownDir, "daemon.json"), "utf8"));
|
|
1033
|
+
return typeof raw.port === "number" && Number.isInteger(raw.port) ? raw.port : null;
|
|
864
1034
|
} catch {
|
|
1035
|
+
return null;
|
|
865
1036
|
}
|
|
866
1037
|
}
|
|
1038
|
+
function restartDaemonAfterUpdateResponse(res, kandownDir) {
|
|
1039
|
+
const cliPath = process.argv[1];
|
|
1040
|
+
if (!cliPath) return;
|
|
1041
|
+
const args = ["--no-update-check", "daemon", "run", "--path", kandownDir];
|
|
1042
|
+
const port = readDaemonPort(kandownDir);
|
|
1043
|
+
if (port !== null) args.push("--port", String(port));
|
|
1044
|
+
const launcher = `
|
|
1045
|
+
const { spawn } = require('node:child_process');
|
|
1046
|
+
const [nodeBin, cliPath, ...cliArgs] = process.argv.slice(1);
|
|
1047
|
+
setTimeout(() => {
|
|
1048
|
+
const child = spawn(nodeBin, [cliPath, ...cliArgs], {
|
|
1049
|
+
detached: true,
|
|
1050
|
+
stdio: 'ignore',
|
|
1051
|
+
env: { ...process.env, KANDOWN_DAEMON: '1' },
|
|
1052
|
+
});
|
|
1053
|
+
child.unref();
|
|
1054
|
+
}, 350);
|
|
1055
|
+
`;
|
|
1056
|
+
res.on("finish", () => {
|
|
1057
|
+
const child = spawn3(process.execPath, ["-e", launcher, process.execPath, cliPath, ...args], {
|
|
1058
|
+
detached: true,
|
|
1059
|
+
stdio: "ignore",
|
|
1060
|
+
env: { ...process.env, KANDOWN_DAEMON: "1" }
|
|
1061
|
+
});
|
|
1062
|
+
child.unref();
|
|
1063
|
+
setTimeout(() => process.exit(0), 50).unref();
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
867
1066
|
async function handleApi(req, res, url, kandownDir) {
|
|
868
1067
|
const path = url.pathname;
|
|
869
1068
|
const method = req.method || "GET";
|
|
@@ -884,7 +1083,7 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
884
1083
|
}
|
|
885
1084
|
if (path === "/api/update/check" && method === "GET") {
|
|
886
1085
|
const current = getCurrentVersion();
|
|
887
|
-
const latest = await new Promise((
|
|
1086
|
+
const latest = await new Promise((resolve3) => {
|
|
888
1087
|
const child = spawn3("npm", ["view", "kandown", "version"], {
|
|
889
1088
|
timeout: 4e3,
|
|
890
1089
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -897,10 +1096,10 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
897
1096
|
});
|
|
898
1097
|
child.stderr.on("data", () => {
|
|
899
1098
|
});
|
|
900
|
-
child.on("error", () =>
|
|
1099
|
+
child.on("error", () => resolve3(null));
|
|
901
1100
|
child.on("close", (code) => {
|
|
902
|
-
if (code !== 0) return
|
|
903
|
-
|
|
1101
|
+
if (code !== 0) return resolve3(null);
|
|
1102
|
+
resolve3(stdout.trim().replace(/^"|"$/g, "") || null);
|
|
904
1103
|
});
|
|
905
1104
|
});
|
|
906
1105
|
const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
|
|
@@ -912,7 +1111,7 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
912
1111
|
}
|
|
913
1112
|
if (path === "/api/update/apply" && method === "POST") {
|
|
914
1113
|
const current = getCurrentVersion();
|
|
915
|
-
const latest = await new Promise((
|
|
1114
|
+
const latest = await new Promise((resolve3) => {
|
|
916
1115
|
const child = spawn3("npm", ["view", "kandown", "version"], {
|
|
917
1116
|
timeout: 4e3,
|
|
918
1117
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -923,15 +1122,15 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
923
1122
|
child.stdout.on("data", (d) => {
|
|
924
1123
|
stdout += d;
|
|
925
1124
|
});
|
|
926
|
-
child.on("error", () =>
|
|
927
|
-
child.on("close", (code) =>
|
|
1125
|
+
child.on("error", () => resolve3(null));
|
|
1126
|
+
child.on("close", (code) => resolve3(code === 0 ? stdout.trim() : null));
|
|
928
1127
|
});
|
|
929
1128
|
const targetVersion = latest || current;
|
|
930
1129
|
const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
|
|
931
|
-
syncGlobalSymlinks();
|
|
932
1130
|
syncProjectKandownHtml(kandownDir);
|
|
933
1131
|
if (ok) {
|
|
934
|
-
|
|
1132
|
+
restartDaemonAfterUpdateResponse(res, kandownDir);
|
|
1133
|
+
writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully; daemon is restarting" });
|
|
935
1134
|
broadcastSseEvent({ type: "update", version: targetVersion });
|
|
936
1135
|
} else {
|
|
937
1136
|
writeJson(res, 500, { ok: false, message: "Global package installation failed" });
|
|
@@ -1003,10 +1202,10 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
1003
1202
|
if (path.startsWith("/api/tasks/")) {
|
|
1004
1203
|
const taskId = decodeURIComponent(path.slice("/api/tasks/".length));
|
|
1005
1204
|
const tasksDir = getTasksDir(kandownDir);
|
|
1006
|
-
const
|
|
1205
|
+
const taskPath2 = join5(tasksDir, `${taskId}.md`);
|
|
1007
1206
|
if (method === "GET") {
|
|
1008
|
-
if (!existsSync5(
|
|
1009
|
-
return writeText(res, 200, readFileSync5(
|
|
1207
|
+
if (!existsSync5(taskPath2)) return writeText(res, 404, "Task not found");
|
|
1208
|
+
return writeText(res, 200, readFileSync5(taskPath2, "utf8"));
|
|
1010
1209
|
}
|
|
1011
1210
|
if (method === "PUT") {
|
|
1012
1211
|
let body = "";
|
|
@@ -1015,14 +1214,14 @@ async function handleApi(req, res, url, kandownDir) {
|
|
|
1015
1214
|
});
|
|
1016
1215
|
req.on("end", () => {
|
|
1017
1216
|
if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
|
|
1018
|
-
atomicWriteFileSync(
|
|
1217
|
+
atomicWriteFileSync(taskPath2, body);
|
|
1019
1218
|
broadcastSseEvent({ type: "task", id: taskId });
|
|
1020
1219
|
writeJson(res, 200, { ok: true });
|
|
1021
1220
|
});
|
|
1022
1221
|
return;
|
|
1023
1222
|
}
|
|
1024
1223
|
if (method === "DELETE") {
|
|
1025
|
-
if (existsSync5(
|
|
1224
|
+
if (existsSync5(taskPath2)) unlinkSync5(taskPath2);
|
|
1026
1225
|
broadcastSseEvent({ type: "task_delete", id: taskId });
|
|
1027
1226
|
return writeJson(res, 200, { ok: true });
|
|
1028
1227
|
}
|
|
@@ -1041,7 +1240,6 @@ function serveApp(res, kandownDir) {
|
|
|
1041
1240
|
}
|
|
1042
1241
|
function createServeServer(kandownDir) {
|
|
1043
1242
|
syncProjectKandownHtml(kandownDir);
|
|
1044
|
-
syncGlobalSymlinks();
|
|
1045
1243
|
return createServer((req, res) => {
|
|
1046
1244
|
const url = new URL(req.url || "/", "http://localhost");
|
|
1047
1245
|
if (req.method === "OPTIONS") return handleCors(res);
|
|
@@ -1084,6 +1282,281 @@ async function listenOnAvailablePort(kandownDir, preferredPort) {
|
|
|
1084
1282
|
throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
|
|
1085
1283
|
}
|
|
1086
1284
|
|
|
1285
|
+
// src/cli/lib/init.ts
|
|
1286
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1287
|
+
import { join as join6 } from "path";
|
|
1288
|
+
function copyRecursive(src, dest) {
|
|
1289
|
+
const errors = [];
|
|
1290
|
+
try {
|
|
1291
|
+
if (!existsSync6(dest)) mkdirSync4(dest, { recursive: true });
|
|
1292
|
+
const entries = readdirSync2(src);
|
|
1293
|
+
for (const entry of entries) {
|
|
1294
|
+
const srcPath = join6(src, entry);
|
|
1295
|
+
const destPath = join6(dest, entry);
|
|
1296
|
+
try {
|
|
1297
|
+
if (statSync2(srcPath).isDirectory()) {
|
|
1298
|
+
errors.push(...copyRecursive(srcPath, destPath));
|
|
1299
|
+
} else if (!existsSync6(destPath)) {
|
|
1300
|
+
copyFileSync2(srcPath, destPath);
|
|
1301
|
+
}
|
|
1302
|
+
} catch (error) {
|
|
1303
|
+
errors.push(`${entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
errors.push(`${src}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1308
|
+
}
|
|
1309
|
+
return errors;
|
|
1310
|
+
}
|
|
1311
|
+
function syncKandownAgentDoc(kandownDir) {
|
|
1312
|
+
const source = join6(PKG_ROOT, "templates", "AGENT_KANDOWN.md");
|
|
1313
|
+
const target = join6(kandownDir, "AGENT_KANDOWN.md");
|
|
1314
|
+
if (!existsSync6(source)) return false;
|
|
1315
|
+
try {
|
|
1316
|
+
const expected = readFileSync6(source, "utf8");
|
|
1317
|
+
const existing = existsSync6(target) ? readFileSync6(target, "utf8") : null;
|
|
1318
|
+
if (existing === null || !existing.includes("# Kandown")) {
|
|
1319
|
+
atomicWriteFileSync(target, expected.endsWith("\n") ? expected : `${expected}
|
|
1320
|
+
`);
|
|
1321
|
+
return true;
|
|
1322
|
+
}
|
|
1323
|
+
} catch {
|
|
1324
|
+
}
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
function doInit(kandownDir) {
|
|
1328
|
+
try {
|
|
1329
|
+
mkdirSync4(kandownDir, { recursive: true });
|
|
1330
|
+
const htmlSrc = join6(PKG_ROOT, "dist", "index.html");
|
|
1331
|
+
const htmlDest = join6(kandownDir, "kandown.html");
|
|
1332
|
+
if (existsSync6(htmlSrc)) {
|
|
1333
|
+
copyFileSync2(htmlSrc, htmlDest);
|
|
1334
|
+
}
|
|
1335
|
+
syncKandownAgentDoc(kandownDir);
|
|
1336
|
+
const templatesDir = join6(PKG_ROOT, "templates");
|
|
1337
|
+
if (existsSync6(templatesDir)) {
|
|
1338
|
+
if (!existsSync6(join6(kandownDir, "README.md")) && existsSync6(join6(templatesDir, "README.md"))) {
|
|
1339
|
+
copyFileSync2(join6(templatesDir, "README.md"), join6(kandownDir, "README.md"));
|
|
1340
|
+
}
|
|
1341
|
+
if (!existsSync6(join6(kandownDir, "AGENT.md")) && existsSync6(join6(templatesDir, "AGENT.md"))) {
|
|
1342
|
+
copyFileSync2(join6(templatesDir, "AGENT.md"), join6(kandownDir, "AGENT.md"));
|
|
1343
|
+
}
|
|
1344
|
+
const tasksSrc = join6(templatesDir, "tasks");
|
|
1345
|
+
const tasksDest = getTasksDir(kandownDir);
|
|
1346
|
+
if (!existsSync6(tasksDest) && existsSync6(tasksSrc)) {
|
|
1347
|
+
copyRecursive(tasksSrc, tasksDest);
|
|
1348
|
+
}
|
|
1349
|
+
if (!existsSync6(join6(kandownDir, "kandown.json")) && existsSync6(join6(templatesDir, "kandown.json"))) {
|
|
1350
|
+
copyFileSync2(join6(templatesDir, "kandown.json"), join6(kandownDir, "kandown.json"));
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return true;
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
console.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/cli/lib/mcp.ts
|
|
1361
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1362
|
+
import { join as join7 } from "path";
|
|
1363
|
+
function startMcpServer(kandownDir) {
|
|
1364
|
+
process.stdin.setEncoding("utf8");
|
|
1365
|
+
let buffer = "";
|
|
1366
|
+
process.stdin.on("data", (chunk) => {
|
|
1367
|
+
buffer += chunk;
|
|
1368
|
+
const lines = buffer.split("\n");
|
|
1369
|
+
buffer = lines.pop() ?? "";
|
|
1370
|
+
for (const line of lines) {
|
|
1371
|
+
const trimmed = line.trim();
|
|
1372
|
+
if (!trimmed) continue;
|
|
1373
|
+
try {
|
|
1374
|
+
const req = JSON.parse(trimmed);
|
|
1375
|
+
handleJsonRpc(kandownDir, req);
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
sendResponse(null, { error: { code: -32700, message: "Parse error" } });
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
function sendResponse(id, resultOrError) {
|
|
1383
|
+
if (id === void 0) return;
|
|
1384
|
+
const resp = {
|
|
1385
|
+
jsonrpc: "2.0",
|
|
1386
|
+
id,
|
|
1387
|
+
...resultOrError
|
|
1388
|
+
};
|
|
1389
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
1390
|
+
}
|
|
1391
|
+
function handleJsonRpc(kandownDir, req) {
|
|
1392
|
+
const { id, method, params } = req;
|
|
1393
|
+
if (method === "initialize") {
|
|
1394
|
+
sendResponse(id, {
|
|
1395
|
+
result: {
|
|
1396
|
+
protocolVersion: "2024-11-05",
|
|
1397
|
+
capabilities: { tools: {} },
|
|
1398
|
+
serverInfo: { name: "kandown", version: "0.20.0" }
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (method === "notifications/initialized") {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (method === "tools/list") {
|
|
1407
|
+
sendResponse(id, {
|
|
1408
|
+
result: {
|
|
1409
|
+
tools: [
|
|
1410
|
+
{
|
|
1411
|
+
name: "list_tasks",
|
|
1412
|
+
description: "List all tasks on the Kandown board with optional filtering",
|
|
1413
|
+
inputSchema: {
|
|
1414
|
+
type: "object",
|
|
1415
|
+
properties: {
|
|
1416
|
+
status: { type: "string", description: "Filter by column/status name" },
|
|
1417
|
+
assignee: { type: "string", description: "Filter by assignee" },
|
|
1418
|
+
tag: { type: "string", description: "Filter by tag" },
|
|
1419
|
+
priority: { type: "string", description: "Filter by priority (P1, P2, P3, P4)" }
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
{
|
|
1424
|
+
name: "get_task",
|
|
1425
|
+
description: "Get details and full content of a specific task by ID",
|
|
1426
|
+
inputSchema: {
|
|
1427
|
+
type: "object",
|
|
1428
|
+
properties: {
|
|
1429
|
+
id: { type: "string", description: "Task ID (e.g. t1, t42)" }
|
|
1430
|
+
},
|
|
1431
|
+
required: ["id"]
|
|
1432
|
+
}
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
name: "create_task",
|
|
1436
|
+
description: "Create a new task on the Kandown board",
|
|
1437
|
+
inputSchema: {
|
|
1438
|
+
type: "object",
|
|
1439
|
+
properties: {
|
|
1440
|
+
title: { type: "string", description: "Task title (supports inline syntax #tag @user p1 due:date)" },
|
|
1441
|
+
status: { type: "string", description: "Target column name (default: Backlog)" },
|
|
1442
|
+
priority: { type: "string", description: "Priority level (P1, P2, P3, P4)" },
|
|
1443
|
+
assignee: { type: "string", description: "Assignee username" },
|
|
1444
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags" },
|
|
1445
|
+
body: { type: "string", description: "Markdown body content" }
|
|
1446
|
+
},
|
|
1447
|
+
required: ["title"]
|
|
1448
|
+
}
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
name: "move_task",
|
|
1452
|
+
description: "Move a task to a different column or to archived",
|
|
1453
|
+
inputSchema: {
|
|
1454
|
+
type: "object",
|
|
1455
|
+
properties: {
|
|
1456
|
+
id: { type: "string", description: "Task ID" },
|
|
1457
|
+
status: { type: "string", description: 'Target column name or "archived"' }
|
|
1458
|
+
},
|
|
1459
|
+
required: ["id", "status"]
|
|
1460
|
+
}
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
name: "add_report",
|
|
1464
|
+
description: "Append an agent execution report to a task body",
|
|
1465
|
+
inputSchema: {
|
|
1466
|
+
type: "object",
|
|
1467
|
+
properties: {
|
|
1468
|
+
id: { type: "string", description: "Task ID" },
|
|
1469
|
+
report: { type: "string", description: "Markdown report content to append under ## Report" }
|
|
1470
|
+
},
|
|
1471
|
+
required: ["id", "report"]
|
|
1472
|
+
}
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
name: "list_columns",
|
|
1476
|
+
description: "List configured board columns and task counts",
|
|
1477
|
+
inputSchema: { type: "object", properties: {} }
|
|
1478
|
+
}
|
|
1479
|
+
]
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (method === "tools/call") {
|
|
1485
|
+
const { name, arguments: args = {} } = params || {};
|
|
1486
|
+
if (name === "list_tasks") {
|
|
1487
|
+
const board = readBoard(kandownDir);
|
|
1488
|
+
let tasks = [];
|
|
1489
|
+
for (const col of board.columns) {
|
|
1490
|
+
if (args.status && col.name.toLowerCase() !== String(args.status).toLowerCase()) continue;
|
|
1491
|
+
for (const t of col.tasks) {
|
|
1492
|
+
if (args.assignee && t.assignee !== args.assignee) continue;
|
|
1493
|
+
if (args.priority && t.priority !== args.priority) continue;
|
|
1494
|
+
if (args.tag && !t.tags.includes(args.tag)) continue;
|
|
1495
|
+
tasks.push({ ...t, status: col.name });
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] } });
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (name === "get_task") {
|
|
1502
|
+
const task = readTask(kandownDir, args.id);
|
|
1503
|
+
sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(task, null, 2) }] } });
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
if (name === "create_task") {
|
|
1507
|
+
const newId = createTaskInBoard(kandownDir, args.title, args.status);
|
|
1508
|
+
if (args.body || args.priority || args.assignee || args.tags) {
|
|
1509
|
+
const task = readTask(kandownDir, newId);
|
|
1510
|
+
const fm = {
|
|
1511
|
+
...task.frontmatter,
|
|
1512
|
+
...args.priority ? { priority: args.priority } : {},
|
|
1513
|
+
...args.assignee ? { assignee: args.assignee } : {},
|
|
1514
|
+
...args.tags ? { tags: args.tags } : {}
|
|
1515
|
+
};
|
|
1516
|
+
const body = args.body ? (task.body + "\n\n" + args.body).trim() : task.body;
|
|
1517
|
+
const taskPath2 = join7(getTasksDir(kandownDir), `${newId}.md`);
|
|
1518
|
+
atomicWriteFileSync(taskPath2, serializeTaskFile(fm, body));
|
|
1519
|
+
}
|
|
1520
|
+
sendResponse(id, { result: { content: [{ type: "text", text: `Created task ${newId}` }] } });
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (name === "move_task") {
|
|
1524
|
+
const ok = moveTaskToColumn(kandownDir, args.id, args.status);
|
|
1525
|
+
sendResponse(id, { result: { content: [{ type: "text", text: ok ? `Moved ${args.id} to ${args.status}` : `Failed to move ${args.id}` }] } });
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (name === "add_report") {
|
|
1529
|
+
const taskPath2 = join7(getTasksDir(kandownDir), `${args.id}.md`);
|
|
1530
|
+
if (!existsSync7(taskPath2)) {
|
|
1531
|
+
sendResponse(id, { error: { code: -32602, message: `Task ${args.id} not found` } });
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
const task = readTask(kandownDir, args.id);
|
|
1535
|
+
const reportSection = `
|
|
1536
|
+
|
|
1537
|
+
## Report
|
|
1538
|
+
|
|
1539
|
+
${args.report.trim()}`;
|
|
1540
|
+
const newBody = task.body.includes("## Report") ? task.body.replace(/## Report[\s\S]*/, `## Report
|
|
1541
|
+
|
|
1542
|
+
${args.report.trim()}`) : task.body.trim() + reportSection;
|
|
1543
|
+
atomicWriteFileSync(taskPath2, serializeTaskFile(task.frontmatter, newBody));
|
|
1544
|
+
sendResponse(id, { result: { content: [{ type: "text", text: `Appended report to ${args.id}` }] } });
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (name === "list_columns") {
|
|
1548
|
+
const config = loadConfig(kandownDir);
|
|
1549
|
+
const board = readBoard(kandownDir);
|
|
1550
|
+
const cols = board.columns.map((c2) => ({ name: c2.name, count: c2.tasks.length }));
|
|
1551
|
+
sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(cols, null, 2) }] } });
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
sendResponse(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
sendResponse(id, { error: { code: -32601, message: `Method not found: ${method}` } });
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1087
1560
|
// src/cli/cli.ts
|
|
1088
1561
|
var c = {
|
|
1089
1562
|
reset: "\x1B[0m",
|
|
@@ -1099,10 +1572,10 @@ function log(msg) {
|
|
|
1099
1572
|
console.log(msg);
|
|
1100
1573
|
}
|
|
1101
1574
|
function info(msg) {
|
|
1102
|
-
console.
|
|
1575
|
+
console.error(`${c.blue}\u2139${c.reset} ${msg}`);
|
|
1103
1576
|
}
|
|
1104
1577
|
function success(msg) {
|
|
1105
|
-
console.
|
|
1578
|
+
console.error(`${c.green}\u2713${c.reset} ${msg}`);
|
|
1106
1579
|
}
|
|
1107
1580
|
function err(msg) {
|
|
1108
1581
|
console.error(`${c.red}\u2717${c.reset} ${msg}`);
|
|
@@ -1136,20 +1609,74 @@ function parseArgs(args) {
|
|
|
1136
1609
|
}
|
|
1137
1610
|
return { flags, positional, path: typeof flags.path === "string" ? flags.path : ".kandown" };
|
|
1138
1611
|
}
|
|
1612
|
+
var COMMANDS = /* @__PURE__ */ new Set([
|
|
1613
|
+
"init",
|
|
1614
|
+
"update",
|
|
1615
|
+
"upgrade",
|
|
1616
|
+
"doctor",
|
|
1617
|
+
"work",
|
|
1618
|
+
"list",
|
|
1619
|
+
"ls",
|
|
1620
|
+
"show",
|
|
1621
|
+
"move",
|
|
1622
|
+
"help",
|
|
1623
|
+
"daemon",
|
|
1624
|
+
"board",
|
|
1625
|
+
"settings",
|
|
1626
|
+
"tasks",
|
|
1627
|
+
"create",
|
|
1628
|
+
"new",
|
|
1629
|
+
"assign",
|
|
1630
|
+
"commit",
|
|
1631
|
+
"projects",
|
|
1632
|
+
"export",
|
|
1633
|
+
"import",
|
|
1634
|
+
"mcp",
|
|
1635
|
+
"version"
|
|
1636
|
+
]);
|
|
1637
|
+
function splitCommand(args) {
|
|
1638
|
+
const withoutGlobalFlags = args.filter((arg) => arg !== "--no-update-check");
|
|
1639
|
+
const commandIndex = withoutGlobalFlags.findIndex((arg, index) => {
|
|
1640
|
+
if (arg.startsWith("-")) return false;
|
|
1641
|
+
if (COMMANDS.has(arg)) return true;
|
|
1642
|
+
return index === 0;
|
|
1643
|
+
});
|
|
1644
|
+
if (commandIndex === -1) {
|
|
1645
|
+
return { cmd: void 0, rest: withoutGlobalFlags };
|
|
1646
|
+
}
|
|
1647
|
+
return {
|
|
1648
|
+
cmd: withoutGlobalFlags[commandIndex],
|
|
1649
|
+
rest: [...withoutGlobalFlags.slice(0, commandIndex), ...withoutGlobalFlags.slice(commandIndex + 1)]
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
function stripFirstPositional(args, value) {
|
|
1653
|
+
const result = [];
|
|
1654
|
+
let stripped = false;
|
|
1655
|
+
for (const arg of args) {
|
|
1656
|
+
if (!stripped && arg === value) {
|
|
1657
|
+
stripped = true;
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
result.push(arg);
|
|
1661
|
+
}
|
|
1662
|
+
return result;
|
|
1663
|
+
}
|
|
1139
1664
|
function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
|
|
1140
|
-
if (basename(cwd) === ".kandown" ||
|
|
1665
|
+
if (basename(cwd) === ".kandown" || existsSync8(join8(cwd, "kandown.json"))) {
|
|
1141
1666
|
return cwd;
|
|
1142
1667
|
}
|
|
1143
|
-
return
|
|
1668
|
+
return resolve2(cwd, pathArg);
|
|
1144
1669
|
}
|
|
1145
1670
|
function ensureKandownDir(rawArgs) {
|
|
1146
1671
|
const args = parseArgs(rawArgs);
|
|
1147
1672
|
const cwd = process.cwd();
|
|
1148
1673
|
const kandownDir = resolveKandownDir(args.path, cwd);
|
|
1149
|
-
if (!
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1674
|
+
if (!existsSync8(kandownDir)) {
|
|
1675
|
+
info(`No .kandown/ found \u2014 auto-initializing ${c.bold}${kandownDir}${c.reset}`);
|
|
1676
|
+
if (!doInit(kandownDir)) {
|
|
1677
|
+
err(`Could not auto-initialize Kandown at ${c.bold}${kandownDir}${c.reset}`);
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1153
1680
|
}
|
|
1154
1681
|
return { kandownDir, cwd };
|
|
1155
1682
|
}
|
|
@@ -1163,6 +1690,7 @@ ${c.bold}USAGE:${c.reset}
|
|
|
1163
1690
|
|
|
1164
1691
|
${c.bold}COMMANDS:${c.reset}
|
|
1165
1692
|
(none) Start web server, open browser & launch TUI
|
|
1693
|
+
init Initialize Kandown in this project
|
|
1166
1694
|
work Output agent rules + live board digest
|
|
1167
1695
|
list List tasks (alias: ls)
|
|
1168
1696
|
show <id> Display task details
|
|
@@ -1183,13 +1711,25 @@ ${c.bold}OPTIONS:${c.reset}
|
|
|
1183
1711
|
--path <dir> Path to .kandown folder (default: .kandown)
|
|
1184
1712
|
--port <number> Server port (default: 2050)
|
|
1185
1713
|
--no-open Don't open browser automatically
|
|
1714
|
+
--no-update-check Skip the registry update check for this run
|
|
1715
|
+
--version Print CLI version
|
|
1186
1716
|
--help, -h Show help screen
|
|
1187
1717
|
`);
|
|
1188
1718
|
}
|
|
1719
|
+
function cmdInit(rawArgs) {
|
|
1720
|
+
const args = parseArgs(rawArgs);
|
|
1721
|
+
const kandownDir = resolve2(process.cwd(), args.path);
|
|
1722
|
+
const created = doInit(kandownDir);
|
|
1723
|
+
if (!created) {
|
|
1724
|
+
err("Failed to initialize Kandown.");
|
|
1725
|
+
process.exit(1);
|
|
1726
|
+
}
|
|
1727
|
+
success(`Kandown initialized at ${kandownDir}`);
|
|
1728
|
+
}
|
|
1189
1729
|
async function cmdUpdate(rawArgs) {
|
|
1190
1730
|
const current = getCurrentVersion();
|
|
1191
1731
|
log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
|
|
1192
|
-
const latest = await new Promise((
|
|
1732
|
+
const latest = await new Promise((resolve3) => {
|
|
1193
1733
|
const child = spawn4("npm", ["view", "kandown", "version"], {
|
|
1194
1734
|
timeout: 6e3,
|
|
1195
1735
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1202,11 +1742,11 @@ async function cmdUpdate(rawArgs) {
|
|
|
1202
1742
|
});
|
|
1203
1743
|
child.stderr.on("data", () => {
|
|
1204
1744
|
});
|
|
1205
|
-
child.on("error", () =>
|
|
1745
|
+
child.on("error", () => resolve3(null));
|
|
1206
1746
|
child.on("close", (code) => {
|
|
1207
|
-
if (code !== 0) return
|
|
1747
|
+
if (code !== 0) return resolve3(null);
|
|
1208
1748
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
1209
|
-
|
|
1749
|
+
resolve3(v || null);
|
|
1210
1750
|
});
|
|
1211
1751
|
});
|
|
1212
1752
|
if (latest && semverGt(latest, current) > 0) {
|
|
@@ -1222,12 +1762,12 @@ async function cmdUpdate(rawArgs) {
|
|
|
1222
1762
|
}
|
|
1223
1763
|
const args = parseArgs(rawArgs);
|
|
1224
1764
|
const cwd = process.cwd();
|
|
1225
|
-
const kandownDir =
|
|
1226
|
-
const htmlDest =
|
|
1227
|
-
if (
|
|
1228
|
-
const htmlSrc =
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1765
|
+
const kandownDir = resolve2(cwd, args.path);
|
|
1766
|
+
const htmlDest = join8(kandownDir, "kandown.html");
|
|
1767
|
+
if (existsSync8(htmlDest)) {
|
|
1768
|
+
const htmlSrc = resolve2(PKG_ROOT, "dist", "index.html");
|
|
1769
|
+
if (existsSync8(htmlSrc)) {
|
|
1770
|
+
copyFileSync3(htmlSrc, htmlDest);
|
|
1231
1771
|
success(`Refreshed ${args.path}/kandown.html`);
|
|
1232
1772
|
}
|
|
1233
1773
|
}
|
|
@@ -1238,10 +1778,10 @@ async function cmdDoctor(rawArgs) {
|
|
|
1238
1778
|
log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
|
|
1239
1779
|
`);
|
|
1240
1780
|
log(` CLI Version: ${currentVersion}`);
|
|
1241
|
-
const configPath =
|
|
1242
|
-
if (
|
|
1781
|
+
const configPath = join8(kandownDir, "kandown.json");
|
|
1782
|
+
if (existsSync8(configPath)) {
|
|
1243
1783
|
try {
|
|
1244
|
-
JSON.parse(
|
|
1784
|
+
JSON.parse(readFileSync8(configPath, "utf8"));
|
|
1245
1785
|
success("kandown.json valid");
|
|
1246
1786
|
} catch (e) {
|
|
1247
1787
|
err(`kandown.json invalid: ${e.message}`);
|
|
@@ -1275,63 +1815,400 @@ async function cmdWork(rawArgs) {
|
|
|
1275
1815
|
log(`- **${col.name}** (${col.tasks.length}): ${col.tasks.map((t) => `${t.id} ${t.title}`).join(", ") || "empty"}`);
|
|
1276
1816
|
}
|
|
1277
1817
|
}
|
|
1818
|
+
function addMultiFlag(flags, key, value) {
|
|
1819
|
+
const current = flags[key];
|
|
1820
|
+
if (Array.isArray(current)) current.push(value);
|
|
1821
|
+
else if (typeof current === "string") flags[key] = [current, value];
|
|
1822
|
+
else flags[key] = value;
|
|
1823
|
+
}
|
|
1824
|
+
function taskParseArgs(argv) {
|
|
1825
|
+
const flags = {};
|
|
1826
|
+
const positional = [];
|
|
1827
|
+
const aliases = { s: "status", p: "priority", a: "assignee", t: "tag", m: "message" };
|
|
1828
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1829
|
+
const arg = argv[i];
|
|
1830
|
+
if (arg.startsWith("--")) {
|
|
1831
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=", 2);
|
|
1832
|
+
const key = rawKey === "to" ? "to" : rawKey;
|
|
1833
|
+
const next = inlineValue ?? argv[i + 1];
|
|
1834
|
+
if (inlineValue === void 0 && next && !next.startsWith("-")) i++;
|
|
1835
|
+
const value = inlineValue ?? (next && !next.startsWith("-") ? next : true);
|
|
1836
|
+
if (key === "tag") addMultiFlag(flags, key, String(value));
|
|
1837
|
+
else flags[key] = value;
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
if (/^-[spatm]$/.test(arg)) {
|
|
1841
|
+
const key = aliases[arg.slice(1)];
|
|
1842
|
+
const value = argv[i + 1];
|
|
1843
|
+
if (!value || value.startsWith("-")) {
|
|
1844
|
+
flags[key] = true;
|
|
1845
|
+
} else {
|
|
1846
|
+
i++;
|
|
1847
|
+
if (key === "tag") addMultiFlag(flags, key, value);
|
|
1848
|
+
else flags[key] = value;
|
|
1849
|
+
}
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
positional.push(arg);
|
|
1853
|
+
}
|
|
1854
|
+
return { flags, positional };
|
|
1855
|
+
}
|
|
1856
|
+
function stringFlag(flags, ...keys) {
|
|
1857
|
+
for (const key of keys) {
|
|
1858
|
+
const value = flags[key];
|
|
1859
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
1860
|
+
}
|
|
1861
|
+
return null;
|
|
1862
|
+
}
|
|
1863
|
+
function listFlag(flags, key) {
|
|
1864
|
+
const value = flags[key];
|
|
1865
|
+
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
1866
|
+
if (typeof value === "string" && value) return [value];
|
|
1867
|
+
return [];
|
|
1868
|
+
}
|
|
1869
|
+
function resolveStatusArg(kandownDir, status) {
|
|
1870
|
+
const config = loadConfig(kandownDir);
|
|
1871
|
+
return config.board.columns.find((col) => col.toLowerCase() === status.toLowerCase()) ?? null;
|
|
1872
|
+
}
|
|
1873
|
+
function taskPath(kandownDir, id, archived = false) {
|
|
1874
|
+
return archived ? join8(getTasksDir(kandownDir), "archive", `${id}.md`) : join8(getTasksDir(kandownDir), `${id}.md`);
|
|
1875
|
+
}
|
|
1876
|
+
function findTaskPath(kandownDir, id) {
|
|
1877
|
+
const active = taskPath(kandownDir, id);
|
|
1878
|
+
if (existsSync8(active)) return active;
|
|
1879
|
+
const archived = taskPath(kandownDir, id, true);
|
|
1880
|
+
if (existsSync8(archived)) return archived;
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
function nextTaskId(kandownDir) {
|
|
1884
|
+
const ids = new Set(listTaskIds(kandownDir));
|
|
1885
|
+
const archiveDir = join8(getTasksDir(kandownDir), "archive");
|
|
1886
|
+
if (existsSync8(archiveDir)) {
|
|
1887
|
+
for (const file of readdirSync3(archiveDir)) {
|
|
1888
|
+
if (file.endsWith(".md")) ids.add(file.slice(0, -3));
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
let max = 0;
|
|
1892
|
+
for (const id of ids) {
|
|
1893
|
+
const match = id.match(/^t(\d+)$/i);
|
|
1894
|
+
if (match) max = Math.max(max, Number(match[1]));
|
|
1895
|
+
}
|
|
1896
|
+
return `t${max + 1}`;
|
|
1897
|
+
}
|
|
1898
|
+
function readTaskFile(kandownDir, id) {
|
|
1899
|
+
const path = findTaskPath(kandownDir, id);
|
|
1900
|
+
if (!path) return null;
|
|
1901
|
+
const parsed = parseTaskFile(readFileSync8(path, "utf8"));
|
|
1902
|
+
return {
|
|
1903
|
+
path,
|
|
1904
|
+
frontmatter: { ...parsed.frontmatter, id: parsed.frontmatter.id || id },
|
|
1905
|
+
body: parsed.body,
|
|
1906
|
+
archived: path.includes("/archive/")
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1278
1909
|
function cmdList(rawArgs) {
|
|
1279
1910
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1280
|
-
const
|
|
1281
|
-
const
|
|
1282
|
-
const statusFilter =
|
|
1283
|
-
const priorityFilter =
|
|
1284
|
-
|
|
1285
|
-
|
|
1911
|
+
const args = taskParseArgs(rawArgs);
|
|
1912
|
+
const includeArchived = args.flags.archived === true;
|
|
1913
|
+
const statusFilter = stringFlag(args.flags, "status")?.toLowerCase() ?? null;
|
|
1914
|
+
const priorityFilter = stringFlag(args.flags, "priority")?.toUpperCase() ?? null;
|
|
1915
|
+
const assigneeFilter = stringFlag(args.flags, "assignee");
|
|
1916
|
+
const tagFilters = listFlag(args.flags, "tag").map((tag) => tag.toLowerCase());
|
|
1917
|
+
const rows = [];
|
|
1918
|
+
for (const id of listTaskIds(kandownDir)) {
|
|
1919
|
+
const task = readTask(kandownDir, id);
|
|
1920
|
+
rows.push({
|
|
1921
|
+
id,
|
|
1922
|
+
title: task.frontmatter.title || id,
|
|
1923
|
+
status: task.frontmatter.status || "Backlog",
|
|
1924
|
+
priority: task.frontmatter.priority || "",
|
|
1925
|
+
assignee: task.frontmatter.assignee || "",
|
|
1926
|
+
tags: Array.isArray(task.frontmatter.tags) ? task.frontmatter.tags : [],
|
|
1927
|
+
archived: false
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
if (includeArchived) {
|
|
1931
|
+
const archiveDir = join8(getTasksDir(kandownDir), "archive");
|
|
1932
|
+
if (existsSync8(archiveDir)) {
|
|
1933
|
+
for (const file of readdirSync3(archiveDir).filter((name) => name.endsWith(".md"))) {
|
|
1934
|
+
const id = file.slice(0, -3);
|
|
1935
|
+
const parsed = parseTaskFile(readFileSync8(join8(archiveDir, file), "utf8"));
|
|
1936
|
+
rows.push({
|
|
1937
|
+
id,
|
|
1938
|
+
title: parsed.frontmatter.title || id,
|
|
1939
|
+
status: `${parsed.frontmatter.status || "Backlog"} (archived)`,
|
|
1940
|
+
priority: parsed.frontmatter.priority || "",
|
|
1941
|
+
assignee: parsed.frontmatter.assignee || "",
|
|
1942
|
+
tags: Array.isArray(parsed.frontmatter.tags) ? parsed.frontmatter.tags : [],
|
|
1943
|
+
archived: true
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
const filtered = rows.filter((row) => {
|
|
1949
|
+
if (statusFilter && row.status.toLowerCase() !== statusFilter) return false;
|
|
1950
|
+
if (priorityFilter && row.priority.toUpperCase() !== priorityFilter) return false;
|
|
1951
|
+
if (assigneeFilter && row.assignee !== assigneeFilter) return false;
|
|
1952
|
+
if (tagFilters.length > 0 && !tagFilters.every((tag) => row.tags.map((t) => t.toLowerCase()).includes(tag))) return false;
|
|
1953
|
+
return true;
|
|
1954
|
+
});
|
|
1955
|
+
if (args.flags.json === true) {
|
|
1956
|
+
process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
1960
|
+
for (const row of filtered) {
|
|
1961
|
+
const list = byStatus.get(row.status) ?? [];
|
|
1962
|
+
list.push(row);
|
|
1963
|
+
byStatus.set(row.status, list);
|
|
1964
|
+
}
|
|
1965
|
+
for (const [status, tasks] of byStatus) {
|
|
1286
1966
|
log(`
|
|
1287
|
-
${c.bold}${
|
|
1288
|
-
for (const
|
|
1289
|
-
|
|
1290
|
-
|
|
1967
|
+
${c.bold}${status}${c.reset} (${tasks.length})`);
|
|
1968
|
+
for (const task of tasks) {
|
|
1969
|
+
const pri = task.priority || "P2";
|
|
1970
|
+
const assignee = task.assignee ? ` @${task.assignee}` : "";
|
|
1971
|
+
log(` ${c.cyan}${task.id}${c.reset} [${pri}] ${task.title}${assignee}`);
|
|
1291
1972
|
}
|
|
1292
1973
|
}
|
|
1293
1974
|
log("");
|
|
1294
1975
|
}
|
|
1295
1976
|
function cmdShow(rawArgs) {
|
|
1296
1977
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1297
|
-
const
|
|
1978
|
+
const args = taskParseArgs(rawArgs);
|
|
1979
|
+
const id = args.positional[0];
|
|
1298
1980
|
if (!id) {
|
|
1299
1981
|
err("Usage: kandown show <task-id>");
|
|
1300
1982
|
process.exit(1);
|
|
1301
1983
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1984
|
+
const path = findTaskPath(kandownDir, id);
|
|
1985
|
+
if (!path) {
|
|
1986
|
+
err(`Task not found: ${id}`);
|
|
1987
|
+
process.exit(1);
|
|
1988
|
+
}
|
|
1989
|
+
process.stdout.write(readFileSync8(path, "utf8"));
|
|
1990
|
+
}
|
|
1991
|
+
function cmdCreate(rawArgs) {
|
|
1992
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1993
|
+
const args = taskParseArgs(rawArgs);
|
|
1994
|
+
const title = args.positional.join(" ").trim();
|
|
1995
|
+
if (!title) {
|
|
1996
|
+
err('Usage: kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id custom-id] [--json]');
|
|
1997
|
+
process.exit(1);
|
|
1998
|
+
}
|
|
1999
|
+
const id = stringFlag(args.flags, "id") ?? nextTaskId(kandownDir);
|
|
2000
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
2001
|
+
err(`Invalid task id: ${id}`);
|
|
2002
|
+
process.exit(1);
|
|
2003
|
+
}
|
|
2004
|
+
if (findTaskPath(kandownDir, id)) {
|
|
2005
|
+
err(`Task already exists: ${id}`);
|
|
2006
|
+
process.exit(1);
|
|
1310
2007
|
}
|
|
2008
|
+
const config = loadConfig(kandownDir);
|
|
2009
|
+
const rawStatus = stringFlag(args.flags, "to", "status");
|
|
2010
|
+
const status = rawStatus ? resolveStatusArg(kandownDir, rawStatus) : config.board.columns[0] || "Backlog";
|
|
2011
|
+
if (!status) {
|
|
2012
|
+
err(`Unknown status: ${rawStatus}`);
|
|
2013
|
+
process.exit(1);
|
|
2014
|
+
}
|
|
2015
|
+
const fm = {
|
|
2016
|
+
id,
|
|
2017
|
+
title,
|
|
2018
|
+
status,
|
|
2019
|
+
created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
2020
|
+
};
|
|
2021
|
+
const priority = stringFlag(args.flags, "priority")?.toUpperCase();
|
|
2022
|
+
const assignee = stringFlag(args.flags, "assignee");
|
|
2023
|
+
const tags = listFlag(args.flags, "tag");
|
|
2024
|
+
if (priority) fm.priority = priority;
|
|
2025
|
+
if (assignee) fm.assignee = assignee;
|
|
2026
|
+
if (tags.length > 0) fm.tags = tags;
|
|
2027
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
2028
|
+
if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
|
|
2029
|
+
const path = taskPath(kandownDir, id);
|
|
2030
|
+
atomicWriteFileSync(path, serializeTaskFile(fm, ""));
|
|
2031
|
+
process.stderr.write(`${c.green}\u2713${c.reset} Created ${c.bold}${id}${c.reset} \u2192 ${status}
|
|
2032
|
+
`);
|
|
2033
|
+
process.stdout.write(args.flags.json === true ? JSON.stringify(fm, null, 2) + "\n" : `${id}
|
|
2034
|
+
`);
|
|
1311
2035
|
}
|
|
1312
2036
|
function cmdMove(rawArgs) {
|
|
1313
2037
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1314
|
-
const
|
|
1315
|
-
|
|
2038
|
+
const args = taskParseArgs(rawArgs);
|
|
2039
|
+
const id = args.positional[0];
|
|
2040
|
+
const rawStatus = args.positional.slice(1).join(" ") || stringFlag(args.flags, "to", "status");
|
|
2041
|
+
if (!id || !rawStatus) {
|
|
1316
2042
|
err("Usage: kandown move <task-id> <status>");
|
|
1317
2043
|
process.exit(1);
|
|
1318
2044
|
}
|
|
2045
|
+
if (rawStatus.toLowerCase() === "archived") {
|
|
2046
|
+
if (!archiveTaskInBoard(kandownDir, id)) {
|
|
2047
|
+
err(`Archive failed: ${id}`);
|
|
2048
|
+
process.exit(1);
|
|
2049
|
+
}
|
|
2050
|
+
success(`Archived ${id}`);
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
const status = resolveStatusArg(kandownDir, rawStatus);
|
|
2054
|
+
if (!status) {
|
|
2055
|
+
err(`Unknown status: ${rawStatus}`);
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
if (!moveTaskToColumn(kandownDir, id, status)) {
|
|
2059
|
+
err(`Move failed: ${id}`);
|
|
2060
|
+
process.exit(1);
|
|
2061
|
+
}
|
|
2062
|
+
success(`Moved ${id} \u2192 "${status}"`);
|
|
2063
|
+
}
|
|
2064
|
+
function cmdAssign(rawArgs) {
|
|
2065
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2066
|
+
const args = taskParseArgs(rawArgs);
|
|
2067
|
+
const [id, assignee] = args.positional;
|
|
2068
|
+
if (!id) {
|
|
2069
|
+
err("Usage: kandown assign <task-id> [assignee]");
|
|
2070
|
+
process.exit(1);
|
|
2071
|
+
}
|
|
2072
|
+
const task = readTaskFile(kandownDir, id);
|
|
2073
|
+
if (!task) {
|
|
2074
|
+
err(`Task not found: ${id}`);
|
|
2075
|
+
process.exit(1);
|
|
2076
|
+
}
|
|
2077
|
+
const frontmatter = { ...task.frontmatter, id };
|
|
2078
|
+
if (assignee) frontmatter.assignee = assignee;
|
|
2079
|
+
else delete frontmatter.assignee;
|
|
2080
|
+
atomicWriteFileSync(task.path, serializeTaskFile(frontmatter, task.body));
|
|
2081
|
+
success(assignee ? `Assigned ${id} \u2192 ${assignee}` : `Unassigned ${id}`);
|
|
2082
|
+
}
|
|
2083
|
+
function cmdCommit(rawArgs) {
|
|
2084
|
+
ensureKandownDir(rawArgs);
|
|
2085
|
+
const args = taskParseArgs(rawArgs);
|
|
2086
|
+
const message = stringFlag(args.flags, "message") || "tasks: update kandown board";
|
|
2087
|
+
const add = spawnSync("git", ["add", "tasks", ".kandown/kandown.json"], { stdio: "inherit" });
|
|
2088
|
+
if (add.status !== 0) process.exit(add.status ?? 1);
|
|
2089
|
+
const commit = spawnSync("git", ["commit", "-m", message], { stdio: "inherit" });
|
|
2090
|
+
process.exit(commit.status ?? 1);
|
|
2091
|
+
}
|
|
2092
|
+
function printTaskCommandsHelp() {
|
|
2093
|
+
log(`
|
|
2094
|
+
${c.bold}Kandown task commands${c.reset}
|
|
2095
|
+
|
|
2096
|
+
kandown list [-s status] [-p P1] [-a user] [-t tag] [--archived] [--json]
|
|
2097
|
+
kandown show <id>
|
|
2098
|
+
kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id id] [--json]
|
|
2099
|
+
kandown move <id> <status|archived>
|
|
2100
|
+
kandown assign <id> [user]
|
|
2101
|
+
kandown commit [-m "message"]
|
|
2102
|
+
`);
|
|
2103
|
+
}
|
|
2104
|
+
async function launchTui(screen, kandownDir) {
|
|
2105
|
+
if (!process.stdin.isTTY) {
|
|
2106
|
+
info(`TUI skipped because stdin is not interactive. Use ${c.cyan}kandown daemon status${c.reset} to inspect the web daemon.`);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const tuiPath = join8(PKG_ROOT, "bin", "tui.js");
|
|
2110
|
+
if (!existsSync8(tuiPath)) {
|
|
2111
|
+
err(`TUI binary not found at ${tuiPath}`);
|
|
2112
|
+
process.exit(1);
|
|
2113
|
+
}
|
|
2114
|
+
await new Promise((resolveTui) => {
|
|
2115
|
+
const child = spawn4(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
|
|
2116
|
+
child.on("close", (code) => {
|
|
2117
|
+
if (typeof code === "number" && code !== 0) process.exit(code);
|
|
2118
|
+
resolveTui();
|
|
2119
|
+
});
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
async function cmdTui(screen, rawArgs) {
|
|
2123
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2124
|
+
await launchTui(screen, kandownDir);
|
|
2125
|
+
}
|
|
2126
|
+
function cmdExport(rawArgs) {
|
|
2127
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2128
|
+
const board = readBoard(kandownDir);
|
|
2129
|
+
process.stdout.write(JSON.stringify(board, null, 2) + "\n");
|
|
2130
|
+
}
|
|
2131
|
+
function cmdProjects(rawArgs) {
|
|
2132
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2133
|
+
const metadataPath2 = join8(kandownDir, "daemon.json");
|
|
2134
|
+
if (!existsSync8(metadataPath2)) {
|
|
2135
|
+
info("No daemon metadata for this project.");
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
process.stdout.write(readFileSync8(metadataPath2, "utf8").trim() + "\n");
|
|
2139
|
+
}
|
|
2140
|
+
function cmdImport(rawArgs) {
|
|
2141
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2142
|
+
const args = taskParseArgs(rawArgs);
|
|
2143
|
+
const file = args.positional[0];
|
|
2144
|
+
if (!file) {
|
|
2145
|
+
err("Usage: kandown import <file.json> [--overwrite]");
|
|
2146
|
+
process.exit(1);
|
|
2147
|
+
}
|
|
2148
|
+
const importPath = resolve2(process.cwd(), file);
|
|
2149
|
+
if (!existsSync8(importPath)) {
|
|
2150
|
+
err(`Import file not found: ${file}`);
|
|
2151
|
+
process.exit(1);
|
|
2152
|
+
}
|
|
2153
|
+
let raw;
|
|
1319
2154
|
try {
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
2155
|
+
raw = JSON.parse(readFileSync8(importPath, "utf8"));
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
err(`Import file must be JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
2158
|
+
process.exit(1);
|
|
2159
|
+
}
|
|
2160
|
+
const rows = [];
|
|
2161
|
+
if (Array.isArray(raw)) {
|
|
2162
|
+
rows.push(...raw.filter((value) => typeof value === "object" && value !== null));
|
|
2163
|
+
} else if (typeof raw === "object" && raw !== null && Array.isArray(raw.columns)) {
|
|
2164
|
+
for (const column of raw.columns) {
|
|
2165
|
+
if (typeof column !== "object" || column === null) continue;
|
|
2166
|
+
const col = column;
|
|
2167
|
+
if (!Array.isArray(col.tasks)) continue;
|
|
2168
|
+
for (const task of col.tasks) {
|
|
2169
|
+
if (typeof task === "object" && task !== null) rows.push({ ...task, status: String(col.name || "Backlog") });
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
if (rows.length === 0) {
|
|
2174
|
+
err("No tasks found to import. Expected a list JSON array or kandown export object.");
|
|
2175
|
+
process.exit(1);
|
|
2176
|
+
}
|
|
2177
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
2178
|
+
if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
|
|
2179
|
+
let imported = 0;
|
|
2180
|
+
for (const row of rows) {
|
|
2181
|
+
const id = typeof row.id === "string" && /^[a-zA-Z0-9_-]+$/.test(row.id) ? row.id : nextTaskId(kandownDir);
|
|
2182
|
+
const path = taskPath(kandownDir, id);
|
|
2183
|
+
if (existsSync8(path) && args.flags.overwrite !== true) continue;
|
|
2184
|
+
const fm = {
|
|
2185
|
+
id,
|
|
2186
|
+
title: typeof row.title === "string" && row.title ? row.title : id,
|
|
2187
|
+
status: typeof row.status === "string" && row.status ? row.status.replace(/ \(archived\)$/i, "") : "Backlog"
|
|
2188
|
+
};
|
|
2189
|
+
if (typeof row.priority === "string") fm.priority = row.priority;
|
|
2190
|
+
if (typeof row.assignee === "string") fm.assignee = row.assignee;
|
|
2191
|
+
if (Array.isArray(row.tags)) fm.tags = row.tags.map(String);
|
|
2192
|
+
atomicWriteFileSync(path, serializeTaskFile(fm, typeof row.body === "string" ? row.body : ""));
|
|
2193
|
+
imported++;
|
|
1324
2194
|
}
|
|
2195
|
+
success(`Imported ${imported} task${imported === 1 ? "" : "s"}`);
|
|
1325
2196
|
}
|
|
1326
2197
|
async function main() {
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
2198
|
+
const rawArgs = process.argv.slice(2);
|
|
2199
|
+
if (rawArgs.length === 1 && (rawArgs[0] === "--version" || rawArgs[0] === "version")) {
|
|
2200
|
+
log(getCurrentVersion());
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
const { cmd, rest } = splitCommand(rawArgs);
|
|
2204
|
+
const skipUpdate = rawArgs.includes("--no-update-check") || process.env.KANDOWN_NO_UPDATE === "1";
|
|
1331
2205
|
if (!skipUpdate) {
|
|
1332
2206
|
await checkForUpdate(process.argv);
|
|
1333
2207
|
}
|
|
1334
2208
|
switch (cmd) {
|
|
2209
|
+
case "init":
|
|
2210
|
+
cmdInit(rest);
|
|
2211
|
+
break;
|
|
1335
2212
|
case "update":
|
|
1336
2213
|
case "upgrade":
|
|
1337
2214
|
await cmdUpdate(rest);
|
|
@@ -1342,6 +2219,12 @@ async function main() {
|
|
|
1342
2219
|
case "work":
|
|
1343
2220
|
await cmdWork(rest);
|
|
1344
2221
|
break;
|
|
2222
|
+
case "board":
|
|
2223
|
+
await cmdTui("board", rest);
|
|
2224
|
+
break;
|
|
2225
|
+
case "settings":
|
|
2226
|
+
await cmdTui("settings", rest);
|
|
2227
|
+
break;
|
|
1345
2228
|
case "list":
|
|
1346
2229
|
case "ls":
|
|
1347
2230
|
cmdList(rest);
|
|
@@ -1349,21 +2232,52 @@ async function main() {
|
|
|
1349
2232
|
case "show":
|
|
1350
2233
|
cmdShow(rest);
|
|
1351
2234
|
break;
|
|
2235
|
+
case "create":
|
|
2236
|
+
case "new":
|
|
2237
|
+
cmdCreate(rest);
|
|
2238
|
+
break;
|
|
1352
2239
|
case "move":
|
|
1353
2240
|
cmdMove(rest);
|
|
1354
2241
|
break;
|
|
2242
|
+
case "assign":
|
|
2243
|
+
cmdAssign(rest);
|
|
2244
|
+
break;
|
|
2245
|
+
case "commit":
|
|
2246
|
+
cmdCommit(rest);
|
|
2247
|
+
break;
|
|
2248
|
+
case "tasks":
|
|
2249
|
+
printTaskCommandsHelp();
|
|
2250
|
+
break;
|
|
2251
|
+
case "projects":
|
|
2252
|
+
cmdProjects(rest);
|
|
2253
|
+
break;
|
|
2254
|
+
case "export":
|
|
2255
|
+
cmdExport(rest);
|
|
2256
|
+
break;
|
|
2257
|
+
case "import":
|
|
2258
|
+
cmdImport(rest);
|
|
2259
|
+
break;
|
|
2260
|
+
case "mcp": {
|
|
2261
|
+
const { kandownDir } = ensureKandownDir(rest);
|
|
2262
|
+
startMcpServer(kandownDir);
|
|
2263
|
+
break;
|
|
2264
|
+
}
|
|
1355
2265
|
case "help":
|
|
1356
2266
|
case "--help":
|
|
1357
2267
|
case "-h":
|
|
1358
2268
|
help();
|
|
1359
2269
|
break;
|
|
1360
2270
|
case "daemon": {
|
|
1361
|
-
const
|
|
1362
|
-
const
|
|
2271
|
+
const parsedDaemonArgs = parseArgs(rest);
|
|
2272
|
+
const subcommand = parsedDaemonArgs.positional[0] || "status";
|
|
2273
|
+
const daemonArgs = subcommand ? stripFirstPositional(rest, subcommand) : rest;
|
|
2274
|
+
const { kandownDir } = ensureKandownDir(daemonArgs);
|
|
1363
2275
|
if (subcommand === "run") {
|
|
1364
|
-
const
|
|
2276
|
+
const daemonOptions = parseArgs(daemonArgs);
|
|
2277
|
+
const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
|
|
2278
|
+
const { port } = await listenOnAvailablePort(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
|
|
1365
2279
|
const url = `http://localhost:${port}`;
|
|
1366
|
-
const metadataPath2 =
|
|
2280
|
+
const metadataPath2 = join8(kandownDir, "daemon.json");
|
|
1367
2281
|
atomicWriteFileSync(metadataPath2, JSON.stringify({
|
|
1368
2282
|
pid: process.pid,
|
|
1369
2283
|
port,
|
|
@@ -1376,24 +2290,44 @@ async function main() {
|
|
|
1376
2290
|
info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
|
|
1377
2291
|
await new Promise(() => {
|
|
1378
2292
|
});
|
|
1379
|
-
} else if (subcommand === "
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
} else {
|
|
1388
|
-
info("Daemon not running");
|
|
2293
|
+
} else if (subcommand === "start") {
|
|
2294
|
+
const daemonOptions = parseArgs(daemonArgs);
|
|
2295
|
+
const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
|
|
2296
|
+
const status = await startProjectDaemon(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
|
|
2297
|
+
if (status.running && status.metadata) success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2298
|
+
else {
|
|
2299
|
+
err("Daemon failed to start");
|
|
2300
|
+
process.exit(1);
|
|
1389
2301
|
}
|
|
1390
|
-
} else {
|
|
2302
|
+
} else if (subcommand === "restart") {
|
|
2303
|
+
await stopProjectDaemon(kandownDir);
|
|
2304
|
+
const status = await startProjectDaemon(kandownDir);
|
|
2305
|
+
if (status.running && status.metadata) success(`Daemon restarted on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2306
|
+
else {
|
|
2307
|
+
err("Daemon failed to restart");
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
} else if (subcommand === "stop") {
|
|
2311
|
+
const stopped = await stopProjectDaemon(kandownDir);
|
|
2312
|
+
if (stopped) success("Daemon stopped");
|
|
2313
|
+
else info("Daemon not running");
|
|
2314
|
+
} else if (subcommand === "status") {
|
|
1391
2315
|
const status = await getDaemonStatus(kandownDir);
|
|
1392
2316
|
if (status.running && status.metadata) {
|
|
1393
2317
|
success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
1394
2318
|
} else {
|
|
1395
2319
|
info("Daemon not running");
|
|
1396
2320
|
}
|
|
2321
|
+
} else if (subcommand === "refresh-all") {
|
|
2322
|
+
const status = await getDaemonStatus(kandownDir);
|
|
2323
|
+
if (status.running) await stopProjectDaemon(kandownDir);
|
|
2324
|
+
const restarted = await startProjectDaemon(kandownDir);
|
|
2325
|
+
if (restarted.running && restarted.metadata) success(`Refreshed current project daemon on port ${restarted.metadata.port}`);
|
|
2326
|
+
else info("No running daemon refreshed");
|
|
2327
|
+
} else {
|
|
2328
|
+
err(`Unknown daemon command: ${subcommand}`);
|
|
2329
|
+
log(` Use ${c.cyan}kandown daemon start|stop|restart|status|refresh-all${c.reset}`);
|
|
2330
|
+
process.exit(1);
|
|
1397
2331
|
}
|
|
1398
2332
|
break;
|
|
1399
2333
|
}
|
|
@@ -1405,21 +2339,14 @@ async function main() {
|
|
|
1405
2339
|
status = await startProjectDaemon(kandownDir);
|
|
1406
2340
|
}
|
|
1407
2341
|
if (!parsed.flags["no-open"]) {
|
|
1408
|
-
const urlToOpen = status.metadata?.url ||
|
|
2342
|
+
const urlToOpen = status.metadata?.url || join8(kandownDir, "kandown.html");
|
|
1409
2343
|
openBrowser(urlToOpen);
|
|
1410
2344
|
}
|
|
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
|
-
}
|
|
2345
|
+
await launchTui("board", kandownDir);
|
|
1419
2346
|
break;
|
|
1420
2347
|
}
|
|
1421
2348
|
default:
|
|
1422
|
-
if (cmd.startsWith("-")) {
|
|
2349
|
+
if (!cmd || cmd.startsWith("-")) {
|
|
1423
2350
|
help();
|
|
1424
2351
|
break;
|
|
1425
2352
|
}
|