kandown 0.34.0 → 0.34.2
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 +20 -0
- package/bin/kandown.js +1156 -1133
- package/bin/tui.js +130 -116
- package/dist/index.html +171 -162
- package/package.json +1 -1
package/bin/kandown.js
CHANGED
|
@@ -5,9 +5,8 @@ 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 spawn5, spawnSync } from "child_process";
|
|
8
|
+
import { existsSync as existsSync11 } from "fs";
|
|
9
|
+
import { join as join12 } from "path";
|
|
11
10
|
|
|
12
11
|
// src/cli/lib/updater.ts
|
|
13
12
|
import { existsSync, readFileSync, writeFileSync, unlinkSync, statSync, mkdirSync } from "fs";
|
|
@@ -16,7 +15,7 @@ import { spawn, execSync } from "child_process";
|
|
|
16
15
|
import { homedir } from "os";
|
|
17
16
|
|
|
18
17
|
// src/lib/version.ts
|
|
19
|
-
var KANDOWN_VERSION = "0.34.
|
|
18
|
+
var KANDOWN_VERSION = "0.34.2";
|
|
20
19
|
|
|
21
20
|
// src/cli/lib/updater.ts
|
|
22
21
|
import { fileURLToPath } from "url";
|
|
@@ -209,7 +208,7 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
209
208
|
}
|
|
210
209
|
} catch {
|
|
211
210
|
}
|
|
212
|
-
const latest = await new Promise((
|
|
211
|
+
const latest = await new Promise((resolve5) => {
|
|
213
212
|
const child2 = spawn("npm", ["view", "kandown", "version"], {
|
|
214
213
|
timeout: 6e3,
|
|
215
214
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -222,11 +221,11 @@ async function checkForUpdate(argv = process.argv) {
|
|
|
222
221
|
});
|
|
223
222
|
child2.stderr.on("data", () => {
|
|
224
223
|
});
|
|
225
|
-
child2.on("error", () =>
|
|
224
|
+
child2.on("error", () => resolve5(null));
|
|
226
225
|
child2.on("close", (code) => {
|
|
227
|
-
if (code !== 0) return
|
|
226
|
+
if (code !== 0) return resolve5(null);
|
|
228
227
|
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
229
|
-
|
|
228
|
+
resolve5(v || null);
|
|
230
229
|
});
|
|
231
230
|
});
|
|
232
231
|
if (!latest) return;
|
|
@@ -264,15 +263,192 @@ ${now}`, "utf8");
|
|
|
264
263
|
process.exit(0);
|
|
265
264
|
}
|
|
266
265
|
|
|
266
|
+
// src/cli/lib/daemon.ts
|
|
267
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
268
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
269
|
+
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
270
|
+
import { createConnection } from "net";
|
|
271
|
+
function metadataPath(kandownDir) {
|
|
272
|
+
return join2(kandownDir, "daemon.json");
|
|
273
|
+
}
|
|
274
|
+
function isRecord(value) {
|
|
275
|
+
return typeof value === "object" && value !== null;
|
|
276
|
+
}
|
|
277
|
+
function parseMetadata(value) {
|
|
278
|
+
if (!isRecord(value)) return null;
|
|
279
|
+
const { pid, port, url, kandownDir, startedAt, version, token } = value;
|
|
280
|
+
if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
|
|
281
|
+
if (typeof port !== "number" || !Number.isInteger(port)) return null;
|
|
282
|
+
if (typeof url !== "string" || typeof kandownDir !== "string") return null;
|
|
283
|
+
if (typeof startedAt !== "string") return null;
|
|
284
|
+
if (version !== null && typeof version !== "string" && version !== void 0) return null;
|
|
285
|
+
if (token !== null && typeof token !== "string" && token !== void 0) return null;
|
|
286
|
+
return { pid, port, url, kandownDir, startedAt, version: version ?? null, token: typeof token === "string" ? token : null };
|
|
287
|
+
}
|
|
288
|
+
function readDaemonMetadata(kandownDir) {
|
|
289
|
+
const path = metadataPath(kandownDir);
|
|
290
|
+
if (!existsSync2(path)) return null;
|
|
291
|
+
try {
|
|
292
|
+
return parseMetadata(JSON.parse(readFileSync2(path, "utf8")));
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function removeDaemonMetadata(kandownDir) {
|
|
298
|
+
try {
|
|
299
|
+
const path = metadataPath(kandownDir);
|
|
300
|
+
if (existsSync2(path)) unlinkSync2(path);
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function isProcessAlive(pid) {
|
|
305
|
+
try {
|
|
306
|
+
process.kill(pid, 0);
|
|
307
|
+
return true;
|
|
308
|
+
} catch {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function parseRemoteDaemonInfo(value) {
|
|
313
|
+
if (!isRecord(value)) return null;
|
|
314
|
+
const { ok, pid, kandownDir, version } = value;
|
|
315
|
+
if (ok !== true || typeof pid !== "number" || !Number.isInteger(pid) || typeof kandownDir !== "string") return null;
|
|
316
|
+
if (version !== null && typeof version !== "string" && version !== void 0) return null;
|
|
317
|
+
return { ok, pid, kandownDir, version: typeof version === "string" ? version : null };
|
|
318
|
+
}
|
|
319
|
+
async function fetchDaemonInfo(port) {
|
|
320
|
+
try {
|
|
321
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/daemon`, {
|
|
322
|
+
signal: AbortSignal.timeout(700)
|
|
323
|
+
});
|
|
324
|
+
if (!response.ok) return null;
|
|
325
|
+
return parseRemoteDaemonInfo(await response.json());
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function isPortListening(port, timeoutMs = 400) {
|
|
331
|
+
return new Promise((resolve5) => {
|
|
332
|
+
const socket = createConnection({ port, host: "127.0.0.1" }, () => {
|
|
333
|
+
socket.destroy();
|
|
334
|
+
resolve5(true);
|
|
335
|
+
});
|
|
336
|
+
socket.on("error", () => resolve5(false));
|
|
337
|
+
socket.setTimeout(timeoutMs);
|
|
338
|
+
socket.on("timeout", () => {
|
|
339
|
+
socket.destroy();
|
|
340
|
+
resolve5(false);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async function getDaemonStatus(kandownDir) {
|
|
345
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
346
|
+
if (!metadata) return { running: false, metadata: null };
|
|
347
|
+
if (!isProcessAlive(metadata.pid)) {
|
|
348
|
+
removeDaemonMetadata(kandownDir);
|
|
349
|
+
return { running: false, metadata: null };
|
|
350
|
+
}
|
|
351
|
+
const remote = await fetchDaemonInfo(metadata.port);
|
|
352
|
+
if (!remote) {
|
|
353
|
+
return { running: false, metadata: null };
|
|
354
|
+
}
|
|
355
|
+
if (remote.pid !== metadata.pid || remote.kandownDir !== kandownDir) {
|
|
356
|
+
removeDaemonMetadata(kandownDir);
|
|
357
|
+
return { running: false, metadata: null };
|
|
358
|
+
}
|
|
359
|
+
return { running: true, metadata: { ...metadata, version: remote.version ?? metadata.version } };
|
|
360
|
+
}
|
|
361
|
+
async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
|
|
362
|
+
const started = Date.now();
|
|
363
|
+
while (Date.now() - started < timeoutMs) {
|
|
364
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
365
|
+
if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
|
|
366
|
+
return { running: true, metadata };
|
|
367
|
+
}
|
|
368
|
+
await new Promise((resolve5) => setTimeout(resolve5, 120));
|
|
369
|
+
}
|
|
370
|
+
return { running: false, metadata: null };
|
|
371
|
+
}
|
|
372
|
+
async function startProjectDaemon(kandownDir, preferredPort) {
|
|
373
|
+
const current = await getDaemonStatus(kandownDir);
|
|
374
|
+
if (current.running) {
|
|
375
|
+
if (current.metadata?.version === getCurrentVersion()) return current;
|
|
376
|
+
await stopProjectDaemon(kandownDir);
|
|
377
|
+
}
|
|
378
|
+
const cliPath = join2(PKG_ROOT, "bin", "kandown.js");
|
|
379
|
+
if (!existsSync2(cliPath)) throw new Error(`Cannot locate kandown CLI entrypoint at ${cliPath}`);
|
|
380
|
+
const args = [
|
|
381
|
+
cliPath,
|
|
382
|
+
"--no-update-check",
|
|
383
|
+
"daemon",
|
|
384
|
+
"run",
|
|
385
|
+
"--path",
|
|
386
|
+
kandownDir
|
|
387
|
+
];
|
|
388
|
+
if (typeof preferredPort === "number" && Number.isInteger(preferredPort)) {
|
|
389
|
+
args.push("--port", String(preferredPort));
|
|
390
|
+
}
|
|
391
|
+
const child = spawn2(process.execPath, args, {
|
|
392
|
+
cwd: dirname2(kandownDir),
|
|
393
|
+
detached: true,
|
|
394
|
+
stdio: "ignore",
|
|
395
|
+
env: { ...process.env, KANDOWN_DAEMON: "1" }
|
|
396
|
+
});
|
|
397
|
+
child.unref();
|
|
398
|
+
return waitForDaemon(kandownDir);
|
|
399
|
+
}
|
|
400
|
+
async function isOwnedKandownDaemon(pid, port, kandownDir) {
|
|
401
|
+
const remote = await fetchDaemonInfo(port);
|
|
402
|
+
if (remote) return remote.pid === pid && remote.kandownDir === kandownDir;
|
|
403
|
+
try {
|
|
404
|
+
const cmd = execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
405
|
+
encoding: "utf8",
|
|
406
|
+
timeout: 2e3
|
|
407
|
+
}).trim();
|
|
408
|
+
return /kandown/.test(cmd) && cmd.includes(kandownDir);
|
|
409
|
+
} catch {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function stopProjectDaemon(kandownDir) {
|
|
414
|
+
const metadata = readDaemonMetadata(kandownDir);
|
|
415
|
+
if (!metadata) return false;
|
|
416
|
+
const pid = metadata.pid;
|
|
417
|
+
if (!isProcessAlive(pid)) {
|
|
418
|
+
removeDaemonMetadata(kandownDir);
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
if (!await isOwnedKandownDaemon(pid, metadata.port, kandownDir)) {
|
|
422
|
+
removeDaemonMetadata(kandownDir);
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
process.kill(pid, "SIGTERM");
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
const started = Date.now();
|
|
430
|
+
while (Date.now() - started < 2500 && isProcessAlive(pid)) {
|
|
431
|
+
await new Promise((resolve5) => setTimeout(resolve5, 100));
|
|
432
|
+
}
|
|
433
|
+
if (isProcessAlive(pid)) {
|
|
434
|
+
try {
|
|
435
|
+
process.kill(pid, "SIGKILL");
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
removeDaemonMetadata(kandownDir);
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
|
|
267
443
|
// src/cli/lib/board-reader.ts
|
|
268
|
-
import { existsSync as
|
|
269
|
-
import { dirname as
|
|
444
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, mkdirSync as mkdirSync2, unlinkSync as unlinkSync4 } from "fs";
|
|
445
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
270
446
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
271
447
|
import { homedir as homedir2 } from "os";
|
|
272
|
-
import { execFileSync } from "child_process";
|
|
448
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
273
449
|
|
|
274
450
|
// src/cli/lib/atomic-write.ts
|
|
275
|
-
import { renameSync, unlinkSync as
|
|
451
|
+
import { renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
276
452
|
function atomicWriteFileSync(path, content) {
|
|
277
453
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
278
454
|
try {
|
|
@@ -280,7 +456,7 @@ function atomicWriteFileSync(path, content) {
|
|
|
280
456
|
renameSync(tmp, path);
|
|
281
457
|
} catch (e) {
|
|
282
458
|
try {
|
|
283
|
-
|
|
459
|
+
unlinkSync3(tmp);
|
|
284
460
|
} catch {
|
|
285
461
|
}
|
|
286
462
|
throw e;
|
|
@@ -496,8 +672,8 @@ function serializeTaskFile(frontmatter, body) {
|
|
|
496
672
|
}
|
|
497
673
|
|
|
498
674
|
// src/cli/lib/config.ts
|
|
499
|
-
import { readFileSync as
|
|
500
|
-
import { join as
|
|
675
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, readdirSync, statSync as statSync2 } from "fs";
|
|
676
|
+
import { join as join3 } from "path";
|
|
501
677
|
var DEFAULT_CONFIG = {
|
|
502
678
|
ui: { language: "en", theme: "auto", skin: "kandown", font: "inter" },
|
|
503
679
|
agent: { suggestFollowUp: false, maxSuggestions: 3 },
|
|
@@ -526,11 +702,11 @@ var DEFAULT_CONFIG = {
|
|
|
526
702
|
}
|
|
527
703
|
};
|
|
528
704
|
function loadConfig(kandownDir) {
|
|
529
|
-
const configPath =
|
|
530
|
-
if (!
|
|
705
|
+
const configPath = join3(kandownDir, "kandown.json");
|
|
706
|
+
if (!existsSync3(configPath)) return structuredClone(DEFAULT_CONFIG);
|
|
531
707
|
let raw;
|
|
532
708
|
try {
|
|
533
|
-
raw = JSON.parse(
|
|
709
|
+
raw = JSON.parse(readFileSync3(configPath, "utf8"));
|
|
534
710
|
} catch (e) {
|
|
535
711
|
const err2 = e;
|
|
536
712
|
if (err2.code === "ENOENT") return structuredClone(DEFAULT_CONFIG);
|
|
@@ -561,22 +737,22 @@ function loadConfig(kandownDir) {
|
|
|
561
737
|
return merged;
|
|
562
738
|
}
|
|
563
739
|
function saveConfig(kandownDir, config) {
|
|
564
|
-
const configPath =
|
|
740
|
+
const configPath = join3(kandownDir, "kandown.json");
|
|
565
741
|
atomicWriteFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
566
742
|
}
|
|
567
743
|
|
|
568
744
|
// src/cli/lib/board-reader.ts
|
|
569
745
|
function getProjectRoot(kandownDir) {
|
|
570
|
-
return
|
|
746
|
+
return dirname3(kandownDir);
|
|
571
747
|
}
|
|
572
748
|
function getTasksDir(kandownDir) {
|
|
573
|
-
return
|
|
749
|
+
return join4(getProjectRoot(kandownDir), "tasks");
|
|
574
750
|
}
|
|
575
751
|
function listTaskIds(kandownDir) {
|
|
576
752
|
const tasksDir = getTasksDir(kandownDir);
|
|
577
753
|
const ids = /* @__PURE__ */ new Set();
|
|
578
|
-
for (const directory of [tasksDir,
|
|
579
|
-
if (!
|
|
754
|
+
for (const directory of [tasksDir, join4(tasksDir, "archive")]) {
|
|
755
|
+
if (!existsSync4(directory)) continue;
|
|
580
756
|
for (const name of readdirSync2(directory).filter((entry) => entry.endsWith(".md"))) {
|
|
581
757
|
ids.add(name.slice(0, -3));
|
|
582
758
|
}
|
|
@@ -586,10 +762,10 @@ function listTaskIds(kandownDir) {
|
|
|
586
762
|
function findTaskPath(kandownDir, taskId) {
|
|
587
763
|
if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) return null;
|
|
588
764
|
const tasksDir = getTasksDir(kandownDir);
|
|
589
|
-
const activePath =
|
|
590
|
-
if (
|
|
591
|
-
const archivedPath =
|
|
592
|
-
return
|
|
765
|
+
const activePath = join4(tasksDir, `${taskId}.md`);
|
|
766
|
+
if (existsSync4(activePath)) return activePath;
|
|
767
|
+
const archivedPath = join4(tasksDir, "archive", `${taskId}.md`);
|
|
768
|
+
return existsSync4(archivedPath) ? archivedPath : null;
|
|
593
769
|
}
|
|
594
770
|
function readBoard(kandownDir) {
|
|
595
771
|
const config = loadConfig(kandownDir);
|
|
@@ -624,7 +800,7 @@ function readTask(kandownDir, taskId) {
|
|
|
624
800
|
body: ""
|
|
625
801
|
};
|
|
626
802
|
}
|
|
627
|
-
const content =
|
|
803
|
+
const content = readFileSync4(taskPath2, "utf8");
|
|
628
804
|
const parsed = parseTaskFile(content);
|
|
629
805
|
return {
|
|
630
806
|
...parsed,
|
|
@@ -635,37 +811,37 @@ function readTask(kandownDir, taskId) {
|
|
|
635
811
|
}
|
|
636
812
|
};
|
|
637
813
|
}
|
|
638
|
-
var PKG_ROOT2 =
|
|
814
|
+
var PKG_ROOT2 = dirname3(dirname3(fileURLToPath2(import.meta.url)));
|
|
639
815
|
function readAgentDoc(kandownDir) {
|
|
640
816
|
const sections = [];
|
|
641
817
|
try {
|
|
642
|
-
sections.push(
|
|
818
|
+
sections.push(readFileSync4(join4(PKG_ROOT2, "templates", "AGENT_KANDOWN.md"), "utf8").trim());
|
|
643
819
|
} catch (e) {
|
|
644
820
|
console.warn("[kandown] Could not read base agent rules:", e.message);
|
|
645
821
|
}
|
|
646
|
-
const globalPath =
|
|
647
|
-
if (
|
|
822
|
+
const globalPath = join4(homedir2(), ".kandown", "instructions.md");
|
|
823
|
+
if (existsSync4(globalPath)) {
|
|
648
824
|
try {
|
|
649
825
|
sections.push(`## Global instructions
|
|
650
826
|
|
|
651
|
-
${
|
|
827
|
+
${readFileSync4(globalPath, "utf8").trim()}`);
|
|
652
828
|
} catch (e) {
|
|
653
829
|
console.warn(`[kandown] Could not read ${globalPath}:`, e.message);
|
|
654
830
|
}
|
|
655
831
|
}
|
|
656
|
-
const projectPath =
|
|
657
|
-
if (
|
|
832
|
+
const projectPath = join4(kandownDir, "instructions.md");
|
|
833
|
+
if (existsSync4(projectPath)) {
|
|
658
834
|
try {
|
|
659
835
|
sections.push(`## Project-specific instructions
|
|
660
836
|
|
|
661
|
-
${
|
|
837
|
+
${readFileSync4(projectPath, "utf8").trim()}`);
|
|
662
838
|
} catch (e) {
|
|
663
839
|
console.warn(`[kandown] Could not read ${projectPath}:`, e.message);
|
|
664
840
|
}
|
|
665
841
|
}
|
|
666
842
|
try {
|
|
667
843
|
const root = getProjectRoot(kandownDir);
|
|
668
|
-
const gitLog =
|
|
844
|
+
const gitLog = execFileSync2("git", ["log", "-n", "5", "--oneline", "--", "tasks/"], { cwd: root, encoding: "utf8" }).trim();
|
|
669
845
|
if (gitLog) {
|
|
670
846
|
sections.push(`## Recent Task Activity (Git History)
|
|
671
847
|
|
|
@@ -681,7 +857,7 @@ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
|
681
857
|
const taskPath2 = findTaskPath(kandownDir, taskId);
|
|
682
858
|
if (!taskPath2) return false;
|
|
683
859
|
try {
|
|
684
|
-
const prevContent =
|
|
860
|
+
const prevContent = readFileSync4(taskPath2, "utf8");
|
|
685
861
|
const parsed = readTask(kandownDir, taskId);
|
|
686
862
|
const newContent = serializeTaskFile({
|
|
687
863
|
...parsed.frontmatter,
|
|
@@ -705,13 +881,13 @@ function moveTaskToColumn(kandownDir, taskId, targetColumn) {
|
|
|
705
881
|
}
|
|
706
882
|
function pushUndo(kandownDir, record) {
|
|
707
883
|
try {
|
|
708
|
-
const undoDir =
|
|
709
|
-
if (!
|
|
710
|
-
const logPath =
|
|
884
|
+
const undoDir = join4(kandownDir, ".undo");
|
|
885
|
+
if (!existsSync4(undoDir)) mkdirSync2(undoDir, { recursive: true });
|
|
886
|
+
const logPath = join4(undoDir, "log.json");
|
|
711
887
|
let list = [];
|
|
712
|
-
if (
|
|
888
|
+
if (existsSync4(logPath)) {
|
|
713
889
|
try {
|
|
714
|
-
list = JSON.parse(
|
|
890
|
+
list = JSON.parse(readFileSync4(logPath, "utf8"));
|
|
715
891
|
} catch {
|
|
716
892
|
list = [];
|
|
717
893
|
}
|
|
@@ -724,7 +900,7 @@ function pushUndo(kandownDir, record) {
|
|
|
724
900
|
}
|
|
725
901
|
function createTaskInBoard(kandownDir, rawInput, status) {
|
|
726
902
|
const tasksDir = getTasksDir(kandownDir);
|
|
727
|
-
if (!
|
|
903
|
+
if (!existsSync4(tasksDir)) mkdirSync2(tasksDir, { recursive: true });
|
|
728
904
|
const ids = listTaskIds(kandownDir);
|
|
729
905
|
let maxN = 0;
|
|
730
906
|
for (const id of ids) {
|
|
@@ -776,7 +952,7 @@ function createTaskInBoard(kandownDir, rawInput, status) {
|
|
|
776
952
|
if (due) fm.due = due;
|
|
777
953
|
if (depends_on.length > 0) fm.depends_on = depends_on;
|
|
778
954
|
const content = serializeTaskFile(fm, "");
|
|
779
|
-
const taskPath2 =
|
|
955
|
+
const taskPath2 = join4(tasksDir, `${newId}.md`);
|
|
780
956
|
atomicWriteFileSync(taskPath2, content);
|
|
781
957
|
pushUndo(kandownDir, {
|
|
782
958
|
type: "create",
|
|
@@ -790,21 +966,21 @@ function createTaskInBoard(kandownDir, rawInput, status) {
|
|
|
790
966
|
}
|
|
791
967
|
function archiveTaskInBoard(kandownDir, taskId) {
|
|
792
968
|
const tasksDir = getTasksDir(kandownDir);
|
|
793
|
-
const taskPath2 =
|
|
794
|
-
if (!
|
|
969
|
+
const taskPath2 = join4(tasksDir, `${taskId}.md`);
|
|
970
|
+
if (!existsSync4(taskPath2)) return false;
|
|
795
971
|
try {
|
|
796
|
-
const prevContent =
|
|
797
|
-
const archiveDir =
|
|
798
|
-
if (!
|
|
972
|
+
const prevContent = readFileSync4(taskPath2, "utf8");
|
|
973
|
+
const archiveDir = join4(tasksDir, "archive");
|
|
974
|
+
if (!existsSync4(archiveDir)) mkdirSync2(archiveDir, { recursive: true });
|
|
799
975
|
const parsed = readTask(kandownDir, taskId);
|
|
800
976
|
const newContent = serializeTaskFile({
|
|
801
977
|
...parsed.frontmatter,
|
|
802
978
|
id: taskId,
|
|
803
979
|
archived: true
|
|
804
980
|
}, parsed.body);
|
|
805
|
-
const destPath =
|
|
981
|
+
const destPath = join4(archiveDir, `${taskId}.md`);
|
|
806
982
|
atomicWriteFileSync(destPath, newContent);
|
|
807
|
-
|
|
983
|
+
unlinkSync4(taskPath2);
|
|
808
984
|
pushUndo(kandownDir, {
|
|
809
985
|
type: "archive",
|
|
810
986
|
taskId,
|
|
@@ -819,675 +995,48 @@ function archiveTaskInBoard(kandownDir, taskId) {
|
|
|
819
995
|
}
|
|
820
996
|
}
|
|
821
997
|
|
|
822
|
-
// src/cli/lib/
|
|
823
|
-
import { existsSync as
|
|
824
|
-
import {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
844
|
-
function readDaemonMetadata(kandownDir) {
|
|
845
|
-
const path = metadataPath(kandownDir);
|
|
846
|
-
if (!existsSync4(path)) return null;
|
|
847
|
-
try {
|
|
848
|
-
return parseMetadata(JSON.parse(readFileSync4(path, "utf8")));
|
|
849
|
-
} catch {
|
|
850
|
-
return null;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
function removeDaemonMetadata(kandownDir) {
|
|
854
|
-
try {
|
|
855
|
-
const path = metadataPath(kandownDir);
|
|
856
|
-
if (existsSync4(path)) unlinkSync4(path);
|
|
857
|
-
} catch {
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
function isProcessAlive(pid) {
|
|
861
|
-
try {
|
|
862
|
-
process.kill(pid, 0);
|
|
863
|
-
return true;
|
|
864
|
-
} catch {
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
998
|
+
// src/cli/lib/mcp.ts
|
|
999
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1000
|
+
import { join as join5 } from "path";
|
|
1001
|
+
function startMcpServer(kandownDir) {
|
|
1002
|
+
process.stdin.setEncoding("utf8");
|
|
1003
|
+
let buffer = "";
|
|
1004
|
+
process.stdin.on("data", (chunk) => {
|
|
1005
|
+
buffer += chunk;
|
|
1006
|
+
const lines = buffer.split("\n");
|
|
1007
|
+
buffer = lines.pop() ?? "";
|
|
1008
|
+
for (const line of lines) {
|
|
1009
|
+
const trimmed = line.trim();
|
|
1010
|
+
if (!trimmed) continue;
|
|
1011
|
+
try {
|
|
1012
|
+
const req = JSON.parse(trimmed);
|
|
1013
|
+
handleJsonRpc(kandownDir, req);
|
|
1014
|
+
} catch (e) {
|
|
1015
|
+
sendResponse(null, { error: { code: -32700, message: "Parse error" } });
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
867
1019
|
}
|
|
868
|
-
function
|
|
869
|
-
if (
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1020
|
+
function sendResponse(id, resultOrError) {
|
|
1021
|
+
if (id === void 0) return;
|
|
1022
|
+
const resp = {
|
|
1023
|
+
jsonrpc: "2.0",
|
|
1024
|
+
id,
|
|
1025
|
+
...resultOrError
|
|
1026
|
+
};
|
|
1027
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
874
1028
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1029
|
+
function handleJsonRpc(kandownDir, req) {
|
|
1030
|
+
const { id, method, params } = req;
|
|
1031
|
+
if (method === "initialize") {
|
|
1032
|
+
sendResponse(id, {
|
|
1033
|
+
result: {
|
|
1034
|
+
protocolVersion: "2024-11-05",
|
|
1035
|
+
capabilities: { tools: {} },
|
|
1036
|
+
serverInfo: { name: "kandown", version: "0.20.0" }
|
|
1037
|
+
}
|
|
879
1038
|
});
|
|
880
|
-
|
|
881
|
-
return parseRemoteDaemonInfo(await response.json());
|
|
882
|
-
} catch {
|
|
883
|
-
return null;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
function isPortListening(port, timeoutMs = 400) {
|
|
887
|
-
return new Promise((resolve3) => {
|
|
888
|
-
const socket = createConnection({ port, host: "127.0.0.1" }, () => {
|
|
889
|
-
socket.destroy();
|
|
890
|
-
resolve3(true);
|
|
891
|
-
});
|
|
892
|
-
socket.on("error", () => resolve3(false));
|
|
893
|
-
socket.setTimeout(timeoutMs);
|
|
894
|
-
socket.on("timeout", () => {
|
|
895
|
-
socket.destroy();
|
|
896
|
-
resolve3(false);
|
|
897
|
-
});
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
async function getDaemonStatus(kandownDir) {
|
|
901
|
-
const metadata = readDaemonMetadata(kandownDir);
|
|
902
|
-
if (!metadata) return { running: false, metadata: null };
|
|
903
|
-
if (!isProcessAlive(metadata.pid)) {
|
|
904
|
-
removeDaemonMetadata(kandownDir);
|
|
905
|
-
return { running: false, metadata: null };
|
|
906
|
-
}
|
|
907
|
-
const remote = await fetchDaemonInfo(metadata.port);
|
|
908
|
-
if (!remote) {
|
|
909
|
-
return { running: false, metadata: null };
|
|
910
|
-
}
|
|
911
|
-
if (remote.pid !== metadata.pid || remote.kandownDir !== kandownDir) {
|
|
912
|
-
removeDaemonMetadata(kandownDir);
|
|
913
|
-
return { running: false, metadata: null };
|
|
914
|
-
}
|
|
915
|
-
return { running: true, metadata: { ...metadata, version: remote.version ?? metadata.version } };
|
|
916
|
-
}
|
|
917
|
-
async function waitForDaemon(kandownDir, timeoutMs = 8e3) {
|
|
918
|
-
const started = Date.now();
|
|
919
|
-
while (Date.now() - started < timeoutMs) {
|
|
920
|
-
const metadata = readDaemonMetadata(kandownDir);
|
|
921
|
-
if (metadata && isProcessAlive(metadata.pid) && await isPortListening(metadata.port)) {
|
|
922
|
-
return { running: true, metadata };
|
|
923
|
-
}
|
|
924
|
-
await new Promise((resolve3) => setTimeout(resolve3, 120));
|
|
925
|
-
}
|
|
926
|
-
return { running: false, metadata: null };
|
|
927
|
-
}
|
|
928
|
-
async function startProjectDaemon(kandownDir, preferredPort) {
|
|
929
|
-
const current = await getDaemonStatus(kandownDir);
|
|
930
|
-
if (current.running) {
|
|
931
|
-
if (current.metadata?.version === getCurrentVersion()) return current;
|
|
932
|
-
await stopProjectDaemon(kandownDir);
|
|
933
|
-
}
|
|
934
|
-
const cliPath = join4(PKG_ROOT, "bin", "kandown.js");
|
|
935
|
-
if (!existsSync4(cliPath)) throw new Error(`Cannot locate kandown CLI entrypoint at ${cliPath}`);
|
|
936
|
-
const args = [
|
|
937
|
-
cliPath,
|
|
938
|
-
"--no-update-check",
|
|
939
|
-
"daemon",
|
|
940
|
-
"run",
|
|
941
|
-
"--path",
|
|
942
|
-
kandownDir
|
|
943
|
-
];
|
|
944
|
-
if (typeof preferredPort === "number" && Number.isInteger(preferredPort)) {
|
|
945
|
-
args.push("--port", String(preferredPort));
|
|
946
|
-
}
|
|
947
|
-
const child = spawn2(process.execPath, args, {
|
|
948
|
-
cwd: dirname3(kandownDir),
|
|
949
|
-
detached: true,
|
|
950
|
-
stdio: "ignore",
|
|
951
|
-
env: { ...process.env, KANDOWN_DAEMON: "1" }
|
|
952
|
-
});
|
|
953
|
-
child.unref();
|
|
954
|
-
return waitForDaemon(kandownDir);
|
|
955
|
-
}
|
|
956
|
-
async function isOwnedKandownDaemon(pid, port, kandownDir) {
|
|
957
|
-
const remote = await fetchDaemonInfo(port);
|
|
958
|
-
if (remote) return remote.pid === pid && remote.kandownDir === kandownDir;
|
|
959
|
-
try {
|
|
960
|
-
const cmd = execFileSync2("ps", ["-p", String(pid), "-o", "command="], {
|
|
961
|
-
encoding: "utf8",
|
|
962
|
-
timeout: 2e3
|
|
963
|
-
}).trim();
|
|
964
|
-
return /kandown/.test(cmd) && cmd.includes(kandownDir);
|
|
965
|
-
} catch {
|
|
966
|
-
return false;
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
async function stopProjectDaemon(kandownDir) {
|
|
970
|
-
const metadata = readDaemonMetadata(kandownDir);
|
|
971
|
-
if (!metadata) return false;
|
|
972
|
-
const pid = metadata.pid;
|
|
973
|
-
if (!isProcessAlive(pid)) {
|
|
974
|
-
removeDaemonMetadata(kandownDir);
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
if (!await isOwnedKandownDaemon(pid, metadata.port, kandownDir)) {
|
|
978
|
-
removeDaemonMetadata(kandownDir);
|
|
979
|
-
return false;
|
|
980
|
-
}
|
|
981
|
-
try {
|
|
982
|
-
process.kill(pid, "SIGTERM");
|
|
983
|
-
} catch {
|
|
984
|
-
}
|
|
985
|
-
const started = Date.now();
|
|
986
|
-
while (Date.now() - started < 2500 && isProcessAlive(pid)) {
|
|
987
|
-
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
988
|
-
}
|
|
989
|
-
if (isProcessAlive(pid)) {
|
|
990
|
-
try {
|
|
991
|
-
process.kill(pid, "SIGKILL");
|
|
992
|
-
} catch {
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
removeDaemonMetadata(kandownDir);
|
|
996
|
-
return true;
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// src/cli/lib/server.ts
|
|
1000
|
-
import { createServer } from "http";
|
|
1001
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, copyFileSync, unlinkSync as unlinkSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
1002
|
-
import { join as join5 } from "path";
|
|
1003
|
-
import { spawn as spawn3 } from "child_process";
|
|
1004
|
-
var START_PORT_RANGE = 2050;
|
|
1005
|
-
var END_PORT_RANGE = 2099;
|
|
1006
|
-
var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
|
|
1007
|
-
var sseClients = [];
|
|
1008
|
-
var nextClientId = 1;
|
|
1009
|
-
function broadcastSseEvent(data) {
|
|
1010
|
-
const payload = `data: ${JSON.stringify(data)}
|
|
1011
|
-
|
|
1012
|
-
`;
|
|
1013
|
-
sseClients.forEach((c2) => c2.res.write(payload));
|
|
1014
|
-
}
|
|
1015
|
-
function handleCors(res) {
|
|
1016
|
-
res.writeHead(204, {
|
|
1017
|
-
"Access-Control-Allow-Origin": "*",
|
|
1018
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
1019
|
-
"Access-Control-Allow-Headers": "Content-Type, X-Kandown-Token"
|
|
1020
|
-
});
|
|
1021
|
-
res.end();
|
|
1022
|
-
}
|
|
1023
|
-
function writeJson(res, status, data) {
|
|
1024
|
-
res.writeHead(status, {
|
|
1025
|
-
"Content-Type": "application/json",
|
|
1026
|
-
"Access-Control-Allow-Origin": "*"
|
|
1027
|
-
});
|
|
1028
|
-
res.end(JSON.stringify(data));
|
|
1029
|
-
}
|
|
1030
|
-
function writeText(res, status, text) {
|
|
1031
|
-
res.writeHead(status, {
|
|
1032
|
-
"Content-Type": "text/plain; charset=utf-8",
|
|
1033
|
-
"Access-Control-Allow-Origin": "*"
|
|
1034
|
-
});
|
|
1035
|
-
res.end(text);
|
|
1036
|
-
}
|
|
1037
|
-
function readRequestBody(req) {
|
|
1038
|
-
return new Promise((resolveBody, rejectBody) => {
|
|
1039
|
-
let body = "";
|
|
1040
|
-
req.setEncoding("utf8");
|
|
1041
|
-
req.on("data", (chunk) => {
|
|
1042
|
-
body += chunk;
|
|
1043
|
-
});
|
|
1044
|
-
req.on("end", () => resolveBody(body));
|
|
1045
|
-
req.on("error", rejectBody);
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
1048
|
-
function syncProjectKandownHtml(kandownDir) {
|
|
1049
|
-
try {
|
|
1050
|
-
const projectHtml = join5(kandownDir, "kandown.html");
|
|
1051
|
-
const distHtml = join5(PKG_ROOT, "dist", "index.html");
|
|
1052
|
-
if (!existsSync5(distHtml)) return false;
|
|
1053
|
-
if (!existsSync5(projectHtml)) {
|
|
1054
|
-
copyFileSync(distHtml, projectHtml);
|
|
1055
|
-
return true;
|
|
1056
|
-
}
|
|
1057
|
-
const currentContent = readFileSync5(projectHtml, "utf8");
|
|
1058
|
-
const newContent = readFileSync5(distHtml, "utf8");
|
|
1059
|
-
if (currentContent !== newContent) {
|
|
1060
|
-
atomicWriteFileSync(projectHtml, newContent);
|
|
1061
|
-
return true;
|
|
1062
|
-
}
|
|
1063
|
-
} catch {
|
|
1064
|
-
}
|
|
1065
|
-
return false;
|
|
1066
|
-
}
|
|
1067
|
-
function readDaemonPort(kandownDir) {
|
|
1068
|
-
try {
|
|
1069
|
-
const raw = JSON.parse(readFileSync5(join5(kandownDir, "daemon.json"), "utf8"));
|
|
1070
|
-
return typeof raw.port === "number" && Number.isInteger(raw.port) ? raw.port : null;
|
|
1071
|
-
} catch {
|
|
1072
|
-
return null;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
function restartDaemonAfterUpdateResponse(res, kandownDir) {
|
|
1076
|
-
const cliPath = process.argv[1];
|
|
1077
|
-
if (!cliPath) return;
|
|
1078
|
-
const args = ["--no-update-check", "daemon", "run", "--path", kandownDir];
|
|
1079
|
-
const port = readDaemonPort(kandownDir);
|
|
1080
|
-
if (port !== null) args.push("--port", String(port));
|
|
1081
|
-
const launcher = `
|
|
1082
|
-
const { spawn } = require('node:child_process');
|
|
1083
|
-
const [nodeBin, cliPath, ...cliArgs] = process.argv.slice(1);
|
|
1084
|
-
setTimeout(() => {
|
|
1085
|
-
const child = spawn(nodeBin, [cliPath, ...cliArgs], {
|
|
1086
|
-
detached: true,
|
|
1087
|
-
stdio: 'ignore',
|
|
1088
|
-
env: { ...process.env, KANDOWN_DAEMON: '1' },
|
|
1089
|
-
});
|
|
1090
|
-
child.unref();
|
|
1091
|
-
}, 350);
|
|
1092
|
-
`;
|
|
1093
|
-
res.on("finish", () => {
|
|
1094
|
-
const child = spawn3(process.execPath, ["-e", launcher, process.execPath, cliPath, ...args], {
|
|
1095
|
-
detached: true,
|
|
1096
|
-
stdio: "ignore",
|
|
1097
|
-
env: { ...process.env, KANDOWN_DAEMON: "1" }
|
|
1098
|
-
});
|
|
1099
|
-
child.unref();
|
|
1100
|
-
setTimeout(() => process.exit(0), 50).unref();
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
async function handleApi(req, res, url, kandownDir) {
|
|
1104
|
-
const path = url.pathname;
|
|
1105
|
-
const method = req.method || "GET";
|
|
1106
|
-
if (path === "/api/daemon" && method === "GET") {
|
|
1107
|
-
return writeJson(res, 200, {
|
|
1108
|
-
ok: true,
|
|
1109
|
-
pid: process.pid,
|
|
1110
|
-
kandownDir,
|
|
1111
|
-
version: getCurrentVersion(),
|
|
1112
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1113
|
-
agentHook: process.env.KANDOWN_AGENT_HOOK_URL ? { enabled: true, label: process.env.KANDOWN_AGENT_HOOK_LABEL || "Send to Agent" } : null
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
if (path === "/api/version" && method === "GET") {
|
|
1117
|
-
return writeJson(res, 200, {
|
|
1118
|
-
version: getCurrentVersion()
|
|
1119
|
-
});
|
|
1120
|
-
}
|
|
1121
|
-
if (path === "/api/update/check" && method === "GET") {
|
|
1122
|
-
const current = getCurrentVersion();
|
|
1123
|
-
const latest = await new Promise((resolve3) => {
|
|
1124
|
-
const child = spawn3("npm", ["view", "kandown", "version"], {
|
|
1125
|
-
timeout: 4e3,
|
|
1126
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1127
|
-
env: { ...process.env },
|
|
1128
|
-
detached: false
|
|
1129
|
-
});
|
|
1130
|
-
let stdout = "";
|
|
1131
|
-
child.stdout.on("data", (d) => {
|
|
1132
|
-
stdout += d;
|
|
1133
|
-
});
|
|
1134
|
-
child.stderr.on("data", () => {
|
|
1135
|
-
});
|
|
1136
|
-
child.on("error", () => resolve3(null));
|
|
1137
|
-
child.on("close", (code) => {
|
|
1138
|
-
if (code !== 0) return resolve3(null);
|
|
1139
|
-
resolve3(stdout.trim().replace(/^"|"$/g, "") || null);
|
|
1140
|
-
});
|
|
1141
|
-
});
|
|
1142
|
-
const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
|
|
1143
|
-
return writeJson(res, 200, {
|
|
1144
|
-
current,
|
|
1145
|
-
latest: latest || current,
|
|
1146
|
-
updateAvailable
|
|
1147
|
-
});
|
|
1148
|
-
}
|
|
1149
|
-
if (path === "/api/update/apply" && method === "POST") {
|
|
1150
|
-
const current = getCurrentVersion();
|
|
1151
|
-
const latest = await new Promise((resolve3) => {
|
|
1152
|
-
const child = spawn3("npm", ["view", "kandown", "version"], {
|
|
1153
|
-
timeout: 4e3,
|
|
1154
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1155
|
-
env: { ...process.env },
|
|
1156
|
-
detached: false
|
|
1157
|
-
});
|
|
1158
|
-
let stdout = "";
|
|
1159
|
-
child.stdout.on("data", (d) => {
|
|
1160
|
-
stdout += d;
|
|
1161
|
-
});
|
|
1162
|
-
child.on("error", () => resolve3(null));
|
|
1163
|
-
child.on("close", (code) => resolve3(code === 0 ? stdout.trim() : null));
|
|
1164
|
-
});
|
|
1165
|
-
const targetVersion = latest || current;
|
|
1166
|
-
const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
|
|
1167
|
-
syncProjectKandownHtml(kandownDir);
|
|
1168
|
-
if (ok) {
|
|
1169
|
-
restartDaemonAfterUpdateResponse(res, kandownDir);
|
|
1170
|
-
writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully; daemon is restarting" });
|
|
1171
|
-
broadcastSseEvent({ type: "update", version: targetVersion });
|
|
1172
|
-
} else {
|
|
1173
|
-
writeJson(res, 500, { ok: false, message: "Global package installation failed" });
|
|
1174
|
-
}
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
if (path === "/api/events" && method === "GET") {
|
|
1178
|
-
res.writeHead(200, {
|
|
1179
|
-
"Content-Type": "text/event-stream",
|
|
1180
|
-
"Cache-Control": "no-cache",
|
|
1181
|
-
"Connection": "keep-alive",
|
|
1182
|
-
"Access-Control-Allow-Origin": "*"
|
|
1183
|
-
});
|
|
1184
|
-
res.write("retry: 2000\n\n");
|
|
1185
|
-
const id = nextClientId++;
|
|
1186
|
-
sseClients.push({ id, res });
|
|
1187
|
-
req.on("close", () => {
|
|
1188
|
-
sseClients = sseClients.filter((c2) => c2.id !== id);
|
|
1189
|
-
});
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
if (path === "/api/board") {
|
|
1193
|
-
if (method === "GET") {
|
|
1194
|
-
const tasksDir = getTasksDir(kandownDir);
|
|
1195
|
-
const boardPath = join5(tasksDir, "board.md");
|
|
1196
|
-
const text = existsSync5(boardPath) ? readFileSync5(boardPath, "utf8") : "";
|
|
1197
|
-
return writeText(res, 200, text);
|
|
1198
|
-
}
|
|
1199
|
-
if (method === "PUT") {
|
|
1200
|
-
let body = "";
|
|
1201
|
-
req.on("data", (chunk) => {
|
|
1202
|
-
body += chunk;
|
|
1203
|
-
});
|
|
1204
|
-
req.on("end", () => {
|
|
1205
|
-
const tasksDir = getTasksDir(kandownDir);
|
|
1206
|
-
if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
|
|
1207
|
-
atomicWriteFileSync(join5(tasksDir, "board.md"), body);
|
|
1208
|
-
broadcastSseEvent({ type: "board" });
|
|
1209
|
-
writeJson(res, 200, { ok: true });
|
|
1210
|
-
});
|
|
1211
|
-
return;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
if (path === "/api/config") {
|
|
1215
|
-
if (method === "GET") {
|
|
1216
|
-
return writeJson(res, 200, loadConfig(kandownDir));
|
|
1217
|
-
}
|
|
1218
|
-
if (method === "PUT") {
|
|
1219
|
-
let body = "";
|
|
1220
|
-
req.on("data", (chunk) => {
|
|
1221
|
-
body += chunk;
|
|
1222
|
-
});
|
|
1223
|
-
req.on("end", () => {
|
|
1224
|
-
try {
|
|
1225
|
-
const parsed = JSON.parse(body);
|
|
1226
|
-
saveConfig(kandownDir, parsed);
|
|
1227
|
-
broadcastSseEvent({ type: "config" });
|
|
1228
|
-
writeJson(res, 200, { ok: true });
|
|
1229
|
-
} catch (e) {
|
|
1230
|
-
writeJson(res, 400, { error: e.message });
|
|
1231
|
-
}
|
|
1232
|
-
});
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
if (path === "/api/tasks" && method === "GET") {
|
|
1237
|
-
return writeJson(res, 200, listTaskIds(kandownDir));
|
|
1238
|
-
}
|
|
1239
|
-
if (path.startsWith("/api/tasks/")) {
|
|
1240
|
-
const routeParts = path.slice("/api/tasks/".length).split("/").filter(Boolean);
|
|
1241
|
-
let taskId;
|
|
1242
|
-
try {
|
|
1243
|
-
taskId = decodeURIComponent(routeParts[0] ?? "");
|
|
1244
|
-
} catch {
|
|
1245
|
-
return writeText(res, 400, "Invalid task id");
|
|
1246
|
-
}
|
|
1247
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) return writeText(res, 400, "Invalid task id");
|
|
1248
|
-
const tasksDir = getTasksDir(kandownDir);
|
|
1249
|
-
const archiveDir = join5(tasksDir, "archive");
|
|
1250
|
-
const activePath = join5(tasksDir, `${taskId}.md`);
|
|
1251
|
-
const archivedPath = join5(archiveDir, `${taskId}.md`);
|
|
1252
|
-
const action = routeParts[1];
|
|
1253
|
-
if (method === "POST" && (action === "archive" || action === "unarchive")) {
|
|
1254
|
-
if (routeParts.length !== 2) return writeText(res, 400, "Invalid task route");
|
|
1255
|
-
const archiving = action === "archive";
|
|
1256
|
-
const source = archiving ? activePath : archivedPath;
|
|
1257
|
-
const destination = archiving ? archivedPath : activePath;
|
|
1258
|
-
if (!existsSync5(source) && !existsSync5(destination)) {
|
|
1259
|
-
return writeText(res, 404, "Task not found");
|
|
1260
|
-
}
|
|
1261
|
-
try {
|
|
1262
|
-
if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
|
|
1263
|
-
if (!existsSync5(archiveDir)) mkdirSync3(archiveDir, { recursive: true });
|
|
1264
|
-
const body = await readRequestBody(req);
|
|
1265
|
-
atomicWriteFileSync(destination, body);
|
|
1266
|
-
if (source !== destination && existsSync5(source)) unlinkSync5(source);
|
|
1267
|
-
broadcastSseEvent({ type: "task", id: taskId });
|
|
1268
|
-
return writeJson(res, 200, { ok: true });
|
|
1269
|
-
} catch (error) {
|
|
1270
|
-
return writeJson(res, 500, {
|
|
1271
|
-
error: `Failed to ${action} task: ${error instanceof Error ? error.message : String(error)}`
|
|
1272
|
-
});
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
if (routeParts.length !== 1) return writeText(res, 404, "Route not found");
|
|
1276
|
-
if (method === "GET") {
|
|
1277
|
-
const taskPath2 = findTaskPath(kandownDir, taskId);
|
|
1278
|
-
if (!taskPath2) return writeText(res, 404, "Task not found");
|
|
1279
|
-
return writeText(res, 200, readFileSync5(taskPath2, "utf8"));
|
|
1280
|
-
}
|
|
1281
|
-
if (method === "PUT") {
|
|
1282
|
-
try {
|
|
1283
|
-
if (!existsSync5(tasksDir)) mkdirSync3(tasksDir, { recursive: true });
|
|
1284
|
-
const taskPath2 = findTaskPath(kandownDir, taskId) ?? activePath;
|
|
1285
|
-
const body = await readRequestBody(req);
|
|
1286
|
-
atomicWriteFileSync(taskPath2, body);
|
|
1287
|
-
broadcastSseEvent({ type: "task", id: taskId });
|
|
1288
|
-
return writeJson(res, 200, { ok: true });
|
|
1289
|
-
} catch (error) {
|
|
1290
|
-
return writeJson(res, 500, {
|
|
1291
|
-
error: `Failed to write task: ${error instanceof Error ? error.message : String(error)}`
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
if (method === "DELETE") {
|
|
1296
|
-
try {
|
|
1297
|
-
if (existsSync5(activePath)) unlinkSync5(activePath);
|
|
1298
|
-
if (existsSync5(archivedPath)) unlinkSync5(archivedPath);
|
|
1299
|
-
broadcastSseEvent({ type: "task_delete", id: taskId });
|
|
1300
|
-
return writeJson(res, 200, { ok: true });
|
|
1301
|
-
} catch (error) {
|
|
1302
|
-
return writeJson(res, 500, {
|
|
1303
|
-
error: `Failed to delete task: ${error instanceof Error ? error.message : String(error)}`
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
writeJson(res, 404, { error: "Route not found" });
|
|
1309
|
-
}
|
|
1310
|
-
function injectServerRoot(html, kandownDir) {
|
|
1311
|
-
const marker = "</head>";
|
|
1312
|
-
const markerIndex = html.toLowerCase().lastIndexOf(marker);
|
|
1313
|
-
const safeRoot = JSON.stringify(kandownDir).replace(/</g, "\\u003c");
|
|
1314
|
-
const script = `<script>window.__KANDOWN_ROOT__ = ${safeRoot};</script>
|
|
1315
|
-
`;
|
|
1316
|
-
if (markerIndex === -1) return script + html;
|
|
1317
|
-
return html.slice(0, markerIndex) + script + html.slice(markerIndex);
|
|
1318
|
-
}
|
|
1319
|
-
function serveApp(res, kandownDir) {
|
|
1320
|
-
syncProjectKandownHtml(kandownDir);
|
|
1321
|
-
const htmlPath = join5(kandownDir, "kandown.html");
|
|
1322
|
-
if (existsSync5(htmlPath)) {
|
|
1323
|
-
const html = readFileSync5(htmlPath, "utf8");
|
|
1324
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1325
|
-
res.end(injectServerRoot(html, kandownDir));
|
|
1326
|
-
} else {
|
|
1327
|
-
writeText(res, 404, "kandown.html not found");
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
function createServeServer(kandownDir) {
|
|
1331
|
-
syncProjectKandownHtml(kandownDir);
|
|
1332
|
-
return createServer((req, res) => {
|
|
1333
|
-
const url = new URL(req.url || "/", "http://localhost");
|
|
1334
|
-
if (req.method === "OPTIONS") return handleCors(res);
|
|
1335
|
-
if (url.pathname === "/" || url.pathname === "/kandown.html" || !url.pathname.startsWith("/api/")) {
|
|
1336
|
-
return serveApp(res, kandownDir);
|
|
1337
|
-
}
|
|
1338
|
-
if (url.pathname.startsWith("/api/")) {
|
|
1339
|
-
return handleApi(req, res, url, kandownDir);
|
|
1340
|
-
}
|
|
1341
|
-
writeText(res, 404, "Not found");
|
|
1342
|
-
});
|
|
1343
|
-
}
|
|
1344
|
-
function listen(server, port) {
|
|
1345
|
-
return new Promise((resolveListen, rejectListen) => {
|
|
1346
|
-
const onError = (e) => {
|
|
1347
|
-
server.off("listening", onListening);
|
|
1348
|
-
rejectListen(e);
|
|
1349
|
-
};
|
|
1350
|
-
const onListening = () => {
|
|
1351
|
-
server.off("error", onError);
|
|
1352
|
-
resolveListen();
|
|
1353
|
-
};
|
|
1354
|
-
server.once("error", onError);
|
|
1355
|
-
server.once("listening", onListening);
|
|
1356
|
-
server.listen(port, "127.0.0.1");
|
|
1357
|
-
});
|
|
1358
|
-
}
|
|
1359
|
-
async function listenOnAvailablePort(kandownDir, preferredPort) {
|
|
1360
|
-
const startPort = preferredPort ?? START_PORT_RANGE;
|
|
1361
|
-
for (let p = startPort; p <= END_PORT_RANGE; p++) {
|
|
1362
|
-
if (UNSAFE_PORTS.has(p)) continue;
|
|
1363
|
-
const server = createServeServer(kandownDir);
|
|
1364
|
-
try {
|
|
1365
|
-
await listen(server, p);
|
|
1366
|
-
return { server, port: p };
|
|
1367
|
-
} catch (e) {
|
|
1368
|
-
if (e.code !== "EADDRINUSE" && e.code !== "EACCES") throw e;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
// src/cli/lib/init.ts
|
|
1375
|
-
import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync4, copyFileSync as copyFileSync2, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1376
|
-
import { join as join6 } from "path";
|
|
1377
|
-
function copyRecursive(src, dest) {
|
|
1378
|
-
const errors = [];
|
|
1379
|
-
try {
|
|
1380
|
-
if (!existsSync6(dest)) mkdirSync4(dest, { recursive: true });
|
|
1381
|
-
const entries = readdirSync3(src);
|
|
1382
|
-
for (const entry of entries) {
|
|
1383
|
-
const srcPath = join6(src, entry);
|
|
1384
|
-
const destPath = join6(dest, entry);
|
|
1385
|
-
try {
|
|
1386
|
-
if (statSync3(srcPath).isDirectory()) {
|
|
1387
|
-
errors.push(...copyRecursive(srcPath, destPath));
|
|
1388
|
-
} else if (!existsSync6(destPath)) {
|
|
1389
|
-
copyFileSync2(srcPath, destPath);
|
|
1390
|
-
}
|
|
1391
|
-
} catch (error) {
|
|
1392
|
-
errors.push(`${entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
} catch (error) {
|
|
1396
|
-
errors.push(`${src}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1397
|
-
}
|
|
1398
|
-
return errors;
|
|
1399
|
-
}
|
|
1400
|
-
function syncKandownAgentDoc(kandownDir) {
|
|
1401
|
-
const source = join6(PKG_ROOT, "templates", "AGENT_KANDOWN.md");
|
|
1402
|
-
const target = join6(kandownDir, "AGENT_KANDOWN.md");
|
|
1403
|
-
if (!existsSync6(source)) return false;
|
|
1404
|
-
try {
|
|
1405
|
-
const expected = readFileSync6(source, "utf8");
|
|
1406
|
-
const existing = existsSync6(target) ? readFileSync6(target, "utf8") : null;
|
|
1407
|
-
if (existing === null || !existing.includes("# Kandown")) {
|
|
1408
|
-
atomicWriteFileSync(target, expected.endsWith("\n") ? expected : `${expected}
|
|
1409
|
-
`);
|
|
1410
|
-
return true;
|
|
1411
|
-
}
|
|
1412
|
-
} catch {
|
|
1413
|
-
}
|
|
1414
|
-
return false;
|
|
1415
|
-
}
|
|
1416
|
-
function doInit(kandownDir) {
|
|
1417
|
-
try {
|
|
1418
|
-
mkdirSync4(kandownDir, { recursive: true });
|
|
1419
|
-
const htmlSrc = join6(PKG_ROOT, "dist", "index.html");
|
|
1420
|
-
const htmlDest = join6(kandownDir, "kandown.html");
|
|
1421
|
-
if (existsSync6(htmlSrc)) {
|
|
1422
|
-
copyFileSync2(htmlSrc, htmlDest);
|
|
1423
|
-
}
|
|
1424
|
-
syncKandownAgentDoc(kandownDir);
|
|
1425
|
-
const templatesDir = join6(PKG_ROOT, "templates");
|
|
1426
|
-
if (existsSync6(templatesDir)) {
|
|
1427
|
-
if (!existsSync6(join6(kandownDir, "README.md")) && existsSync6(join6(templatesDir, "README.md"))) {
|
|
1428
|
-
copyFileSync2(join6(templatesDir, "README.md"), join6(kandownDir, "README.md"));
|
|
1429
|
-
}
|
|
1430
|
-
if (!existsSync6(join6(kandownDir, "AGENT.md")) && existsSync6(join6(templatesDir, "AGENT.md"))) {
|
|
1431
|
-
copyFileSync2(join6(templatesDir, "AGENT.md"), join6(kandownDir, "AGENT.md"));
|
|
1432
|
-
}
|
|
1433
|
-
const tasksSrc = join6(templatesDir, "tasks");
|
|
1434
|
-
const tasksDest = getTasksDir(kandownDir);
|
|
1435
|
-
if (!existsSync6(tasksDest) && existsSync6(tasksSrc)) {
|
|
1436
|
-
copyRecursive(tasksSrc, tasksDest);
|
|
1437
|
-
}
|
|
1438
|
-
if (!existsSync6(join6(kandownDir, "kandown.json")) && existsSync6(join6(templatesDir, "kandown.json"))) {
|
|
1439
|
-
copyFileSync2(join6(templatesDir, "kandown.json"), join6(kandownDir, "kandown.json"));
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
return true;
|
|
1443
|
-
} catch (error) {
|
|
1444
|
-
console.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1445
|
-
return false;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
// src/cli/lib/mcp.ts
|
|
1450
|
-
import { existsSync as existsSync7 } from "fs";
|
|
1451
|
-
import { join as join7 } from "path";
|
|
1452
|
-
function startMcpServer(kandownDir) {
|
|
1453
|
-
process.stdin.setEncoding("utf8");
|
|
1454
|
-
let buffer = "";
|
|
1455
|
-
process.stdin.on("data", (chunk) => {
|
|
1456
|
-
buffer += chunk;
|
|
1457
|
-
const lines = buffer.split("\n");
|
|
1458
|
-
buffer = lines.pop() ?? "";
|
|
1459
|
-
for (const line of lines) {
|
|
1460
|
-
const trimmed = line.trim();
|
|
1461
|
-
if (!trimmed) continue;
|
|
1462
|
-
try {
|
|
1463
|
-
const req = JSON.parse(trimmed);
|
|
1464
|
-
handleJsonRpc(kandownDir, req);
|
|
1465
|
-
} catch (e) {
|
|
1466
|
-
sendResponse(null, { error: { code: -32700, message: "Parse error" } });
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
function sendResponse(id, resultOrError) {
|
|
1472
|
-
if (id === void 0) return;
|
|
1473
|
-
const resp = {
|
|
1474
|
-
jsonrpc: "2.0",
|
|
1475
|
-
id,
|
|
1476
|
-
...resultOrError
|
|
1477
|
-
};
|
|
1478
|
-
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
1479
|
-
}
|
|
1480
|
-
function handleJsonRpc(kandownDir, req) {
|
|
1481
|
-
const { id, method, params } = req;
|
|
1482
|
-
if (method === "initialize") {
|
|
1483
|
-
sendResponse(id, {
|
|
1484
|
-
result: {
|
|
1485
|
-
protocolVersion: "2024-11-05",
|
|
1486
|
-
capabilities: { tools: {} },
|
|
1487
|
-
serverInfo: { name: "kandown", version: "0.20.0" }
|
|
1488
|
-
}
|
|
1489
|
-
});
|
|
1490
|
-
return;
|
|
1039
|
+
return;
|
|
1491
1040
|
}
|
|
1492
1041
|
if (method === "notifications/initialized") {
|
|
1493
1042
|
return;
|
|
@@ -1603,7 +1152,7 @@ function handleJsonRpc(kandownDir, req) {
|
|
|
1603
1152
|
...args.tags ? { tags: args.tags } : {}
|
|
1604
1153
|
};
|
|
1605
1154
|
const body = args.body ? (task.body + "\n\n" + args.body).trim() : task.body;
|
|
1606
|
-
const taskPath2 =
|
|
1155
|
+
const taskPath2 = join5(getTasksDir(kandownDir), `${newId}.md`);
|
|
1607
1156
|
atomicWriteFileSync(taskPath2, serializeTaskFile(fm, body));
|
|
1608
1157
|
}
|
|
1609
1158
|
sendResponse(id, { result: { content: [{ type: "text", text: `Created task ${newId}` }] } });
|
|
@@ -1615,8 +1164,8 @@ function handleJsonRpc(kandownDir, req) {
|
|
|
1615
1164
|
return;
|
|
1616
1165
|
}
|
|
1617
1166
|
if (name === "add_report") {
|
|
1618
|
-
const taskPath2 =
|
|
1619
|
-
if (!
|
|
1167
|
+
const taskPath2 = join5(getTasksDir(kandownDir), `${args.id}.md`);
|
|
1168
|
+
if (!existsSync5(taskPath2)) {
|
|
1620
1169
|
sendResponse(id, { error: { code: -32602, message: `Task ${args.id} not found` } });
|
|
1621
1170
|
return;
|
|
1622
1171
|
}
|
|
@@ -1640,23 +1189,103 @@ ${args.report.trim()}`) : task.body.trim() + reportSection;
|
|
|
1640
1189
|
sendResponse(id, { result: { content: [{ type: "text", text: JSON.stringify(cols, null, 2) }] } });
|
|
1641
1190
|
return;
|
|
1642
1191
|
}
|
|
1643
|
-
sendResponse(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
1644
|
-
return;
|
|
1192
|
+
sendResponse(id, { error: { code: -32601, message: `Unknown tool: ${name}` } });
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
sendResponse(id, { error: { code: -32601, message: `Method not found: ${method}` } });
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/cli/lib/browser.ts
|
|
1199
|
+
import { spawn as spawn3 } from "child_process";
|
|
1200
|
+
function openBrowser(target) {
|
|
1201
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1202
|
+
try {
|
|
1203
|
+
spawn3(cmd, [target], { detached: true, stdio: "ignore" }).unref();
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/cli/lib/cli-shared.ts
|
|
1209
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
1210
|
+
import { join as join7, resolve as resolve2, basename } from "path";
|
|
1211
|
+
import { spawn as spawn4 } from "child_process";
|
|
1212
|
+
|
|
1213
|
+
// src/cli/lib/init.ts
|
|
1214
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, mkdirSync as mkdirSync3, copyFileSync, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1215
|
+
import { join as join6 } from "path";
|
|
1216
|
+
function copyRecursive(src, dest) {
|
|
1217
|
+
const errors = [];
|
|
1218
|
+
try {
|
|
1219
|
+
if (!existsSync6(dest)) mkdirSync3(dest, { recursive: true });
|
|
1220
|
+
const entries = readdirSync3(src);
|
|
1221
|
+
for (const entry of entries) {
|
|
1222
|
+
const srcPath = join6(src, entry);
|
|
1223
|
+
const destPath = join6(dest, entry);
|
|
1224
|
+
try {
|
|
1225
|
+
if (statSync3(srcPath).isDirectory()) {
|
|
1226
|
+
errors.push(...copyRecursive(srcPath, destPath));
|
|
1227
|
+
} else if (!existsSync6(destPath)) {
|
|
1228
|
+
copyFileSync(srcPath, destPath);
|
|
1229
|
+
}
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
errors.push(`${entry}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
errors.push(`${src}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1645
1236
|
}
|
|
1646
|
-
|
|
1237
|
+
return errors;
|
|
1647
1238
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1239
|
+
function syncKandownAgentDoc(kandownDir) {
|
|
1240
|
+
const source = join6(PKG_ROOT, "templates", "AGENT_KANDOWN.md");
|
|
1241
|
+
const target = join6(kandownDir, "AGENT_KANDOWN.md");
|
|
1242
|
+
if (!existsSync6(source)) return false;
|
|
1653
1243
|
try {
|
|
1654
|
-
|
|
1244
|
+
const expected = readFileSync6(source, "utf8");
|
|
1245
|
+
const existing = existsSync6(target) ? readFileSync6(target, "utf8") : null;
|
|
1246
|
+
if (existing === null || !existing.includes("# Kandown")) {
|
|
1247
|
+
atomicWriteFileSync(target, expected.endsWith("\n") ? expected : `${expected}
|
|
1248
|
+
`);
|
|
1249
|
+
return true;
|
|
1250
|
+
}
|
|
1655
1251
|
} catch {
|
|
1656
1252
|
}
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
function doInit(kandownDir) {
|
|
1256
|
+
try {
|
|
1257
|
+
mkdirSync3(kandownDir, { recursive: true });
|
|
1258
|
+
const htmlSrc = join6(PKG_ROOT, "dist", "index.html");
|
|
1259
|
+
const htmlDest = join6(kandownDir, "kandown.html");
|
|
1260
|
+
if (existsSync6(htmlSrc)) {
|
|
1261
|
+
copyFileSync(htmlSrc, htmlDest);
|
|
1262
|
+
}
|
|
1263
|
+
syncKandownAgentDoc(kandownDir);
|
|
1264
|
+
const templatesDir = join6(PKG_ROOT, "templates");
|
|
1265
|
+
if (existsSync6(templatesDir)) {
|
|
1266
|
+
if (!existsSync6(join6(kandownDir, "README.md")) && existsSync6(join6(templatesDir, "README.md"))) {
|
|
1267
|
+
copyFileSync(join6(templatesDir, "README.md"), join6(kandownDir, "README.md"));
|
|
1268
|
+
}
|
|
1269
|
+
if (!existsSync6(join6(kandownDir, "AGENT.md")) && existsSync6(join6(templatesDir, "AGENT.md"))) {
|
|
1270
|
+
copyFileSync(join6(templatesDir, "AGENT.md"), join6(kandownDir, "AGENT.md"));
|
|
1271
|
+
}
|
|
1272
|
+
const tasksSrc = join6(templatesDir, "tasks");
|
|
1273
|
+
const tasksDest = getTasksDir(kandownDir);
|
|
1274
|
+
if (!existsSync6(tasksDest) && existsSync6(tasksSrc)) {
|
|
1275
|
+
copyRecursive(tasksSrc, tasksDest);
|
|
1276
|
+
}
|
|
1277
|
+
if (!existsSync6(join6(kandownDir, "kandown.json")) && existsSync6(join6(templatesDir, "kandown.json"))) {
|
|
1278
|
+
copyFileSync(join6(templatesDir, "kandown.json"), join6(kandownDir, "kandown.json"));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return true;
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
console.error(`Init failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1657
1286
|
}
|
|
1658
1287
|
|
|
1659
|
-
// src/cli/cli.ts
|
|
1288
|
+
// src/cli/lib/cli-shared.ts
|
|
1660
1289
|
var c = {
|
|
1661
1290
|
reset: "\x1B[0m",
|
|
1662
1291
|
bold: "\x1B[1m",
|
|
@@ -1754,7 +1383,7 @@ function stripFirstPositional(args, value) {
|
|
|
1754
1383
|
return result;
|
|
1755
1384
|
}
|
|
1756
1385
|
function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
|
|
1757
|
-
if (basename(cwd) === ".kandown" ||
|
|
1386
|
+
if (basename(cwd) === ".kandown" || existsSync7(join7(cwd, "kandown.json"))) {
|
|
1758
1387
|
return cwd;
|
|
1759
1388
|
}
|
|
1760
1389
|
if (pathArg !== ".kandown") {
|
|
@@ -1768,7 +1397,7 @@ function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
|
|
|
1768
1397
|
}
|
|
1769
1398
|
for (const name of entries) {
|
|
1770
1399
|
if (name.startsWith(".") || name === "node_modules" || name === "tasks") continue;
|
|
1771
|
-
const subPath =
|
|
1400
|
+
const subPath = join7(cwd, name);
|
|
1772
1401
|
let stat;
|
|
1773
1402
|
try {
|
|
1774
1403
|
stat = statSync4(subPath);
|
|
@@ -1777,7 +1406,7 @@ function resolveKandownDir(pathArg = ".kandown", cwd = process.cwd()) {
|
|
|
1777
1406
|
}
|
|
1778
1407
|
if (!stat.isDirectory()) continue;
|
|
1779
1408
|
const found = resolveKandownDir(pathArg, subPath);
|
|
1780
|
-
if (
|
|
1409
|
+
if (existsSync7(found)) return found;
|
|
1781
1410
|
}
|
|
1782
1411
|
return resolve2(cwd, pathArg);
|
|
1783
1412
|
}
|
|
@@ -1785,7 +1414,7 @@ function ensureKandownDir(rawArgs) {
|
|
|
1785
1414
|
const args = parseArgs(rawArgs);
|
|
1786
1415
|
const cwd = process.cwd();
|
|
1787
1416
|
const kandownDir = resolveKandownDir(args.path, cwd);
|
|
1788
|
-
if (!
|
|
1417
|
+
if (!existsSync7(kandownDir)) {
|
|
1789
1418
|
info(`No .kandown/ found \u2014 auto-initializing ${c.bold}${kandownDir}${c.reset}`);
|
|
1790
1419
|
if (!doInit(kandownDir)) {
|
|
1791
1420
|
err(`Could not auto-initialize Kandown at ${c.bold}${kandownDir}${c.reset}`);
|
|
@@ -1830,105 +1459,6 @@ ${c.bold}OPTIONS:${c.reset}
|
|
|
1830
1459
|
--help, -h Show help screen
|
|
1831
1460
|
`);
|
|
1832
1461
|
}
|
|
1833
|
-
function cmdInit(rawArgs) {
|
|
1834
|
-
const args = parseArgs(rawArgs);
|
|
1835
|
-
const kandownDir = resolve2(process.cwd(), args.path);
|
|
1836
|
-
const created = doInit(kandownDir);
|
|
1837
|
-
if (!created) {
|
|
1838
|
-
err("Failed to initialize Kandown.");
|
|
1839
|
-
process.exit(1);
|
|
1840
|
-
}
|
|
1841
|
-
success(`Kandown initialized at ${kandownDir}`);
|
|
1842
|
-
}
|
|
1843
|
-
async function cmdUpdate(rawArgs) {
|
|
1844
|
-
const current = getCurrentVersion();
|
|
1845
|
-
log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
|
|
1846
|
-
const latest = await new Promise((resolve3) => {
|
|
1847
|
-
const child = spawn5("npm", ["view", "kandown", "version"], {
|
|
1848
|
-
timeout: 6e3,
|
|
1849
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1850
|
-
env: { ...process.env },
|
|
1851
|
-
detached: false
|
|
1852
|
-
});
|
|
1853
|
-
let stdout = "";
|
|
1854
|
-
child.stdout.on("data", (d) => {
|
|
1855
|
-
stdout += d;
|
|
1856
|
-
});
|
|
1857
|
-
child.stderr.on("data", () => {
|
|
1858
|
-
});
|
|
1859
|
-
child.on("error", () => resolve3(null));
|
|
1860
|
-
child.on("close", (code) => {
|
|
1861
|
-
if (code !== 0) return resolve3(null);
|
|
1862
|
-
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
1863
|
-
resolve3(v || null);
|
|
1864
|
-
});
|
|
1865
|
-
});
|
|
1866
|
-
if (latest && semverGt(latest, current) > 0) {
|
|
1867
|
-
info(`Updating kandown package v${current} \u2192 v${latest}\u2026`);
|
|
1868
|
-
const updateOk = await performGlobalPackageUpdate(`kandown@${latest}`);
|
|
1869
|
-
if (updateOk) {
|
|
1870
|
-
success(`Successfully upgraded kandown to v${latest}`);
|
|
1871
|
-
} else {
|
|
1872
|
-
err(`Global CLI update failed \u2014 try: pnpm add -g kandown@latest or npm install -g kandown@latest`);
|
|
1873
|
-
}
|
|
1874
|
-
} else {
|
|
1875
|
-
info(`kandown CLI is already up to date (v${current}).`);
|
|
1876
|
-
}
|
|
1877
|
-
const args = parseArgs(rawArgs);
|
|
1878
|
-
const cwd = process.cwd();
|
|
1879
|
-
const kandownDir = resolve2(cwd, args.path);
|
|
1880
|
-
const htmlDest = join8(kandownDir, "kandown.html");
|
|
1881
|
-
if (existsSync8(htmlDest)) {
|
|
1882
|
-
const htmlSrc = resolve2(PKG_ROOT, "dist", "index.html");
|
|
1883
|
-
if (existsSync8(htmlSrc)) {
|
|
1884
|
-
copyFileSync3(htmlSrc, htmlDest);
|
|
1885
|
-
success(`Refreshed ${args.path}/kandown.html`);
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
async function cmdDoctor(rawArgs) {
|
|
1890
|
-
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1891
|
-
const currentVersion = getCurrentVersion();
|
|
1892
|
-
log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
|
|
1893
|
-
`);
|
|
1894
|
-
log(` CLI Version: ${currentVersion}`);
|
|
1895
|
-
const configPath = join8(kandownDir, "kandown.json");
|
|
1896
|
-
if (existsSync8(configPath)) {
|
|
1897
|
-
try {
|
|
1898
|
-
JSON.parse(readFileSync8(configPath, "utf8"));
|
|
1899
|
-
success("kandown.json valid");
|
|
1900
|
-
} catch (e) {
|
|
1901
|
-
err(`kandown.json invalid: ${e.message}`);
|
|
1902
|
-
}
|
|
1903
|
-
} else {
|
|
1904
|
-
err("Missing kandown.json");
|
|
1905
|
-
}
|
|
1906
|
-
const daemonStatus = await getDaemonStatus(kandownDir);
|
|
1907
|
-
if (daemonStatus.running && daemonStatus.metadata) {
|
|
1908
|
-
success(`Daemon running on port ${daemonStatus.metadata.port} (PID ${daemonStatus.metadata.pid})`);
|
|
1909
|
-
} else {
|
|
1910
|
-
info("Daemon not running");
|
|
1911
|
-
}
|
|
1912
|
-
const taskIds = listTaskIds(kandownDir);
|
|
1913
|
-
success(`Tasks: ${taskIds.length} active task files`);
|
|
1914
|
-
log(`
|
|
1915
|
-
${c.green}\u2713 Everything looks good!${c.reset}
|
|
1916
|
-
`);
|
|
1917
|
-
}
|
|
1918
|
-
async function cmdWork(rawArgs) {
|
|
1919
|
-
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1920
|
-
const doc = readAgentDoc(kandownDir);
|
|
1921
|
-
const board = readBoard(kandownDir);
|
|
1922
|
-
log(doc);
|
|
1923
|
-
log("\n---\n");
|
|
1924
|
-
log(`## Current Board Digest
|
|
1925
|
-
`);
|
|
1926
|
-
const allTasksCount = board.columns.reduce((sum, col) => sum + col.tasks.length, 0);
|
|
1927
|
-
log(`Tasks total: ${allTasksCount}`);
|
|
1928
|
-
for (const col of board.columns) {
|
|
1929
|
-
log(`- **${col.name}** (${col.tasks.length}): ${col.tasks.map((t) => `${t.id} ${t.title}`).join(", ") || "empty"}`);
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
1462
|
function addMultiFlag(flags, key, value) {
|
|
1933
1463
|
const current = flags[key];
|
|
1934
1464
|
if (Array.isArray(current)) current.push(value);
|
|
@@ -1985,19 +1515,19 @@ function resolveStatusArg(kandownDir, status) {
|
|
|
1985
1515
|
return config.board.columns.find((col) => col.toLowerCase() === status.toLowerCase()) ?? null;
|
|
1986
1516
|
}
|
|
1987
1517
|
function taskPath(kandownDir, id, archived = false) {
|
|
1988
|
-
return archived ?
|
|
1518
|
+
return archived ? join7(getTasksDir(kandownDir), "archive", `${id}.md`) : join7(getTasksDir(kandownDir), `${id}.md`);
|
|
1989
1519
|
}
|
|
1990
1520
|
function findTaskPath2(kandownDir, id) {
|
|
1991
1521
|
const active = taskPath(kandownDir, id);
|
|
1992
|
-
if (
|
|
1522
|
+
if (existsSync7(active)) return active;
|
|
1993
1523
|
const archived = taskPath(kandownDir, id, true);
|
|
1994
|
-
if (
|
|
1524
|
+
if (existsSync7(archived)) return archived;
|
|
1995
1525
|
return null;
|
|
1996
1526
|
}
|
|
1997
1527
|
function nextTaskId(kandownDir) {
|
|
1998
1528
|
const ids = new Set(listTaskIds(kandownDir));
|
|
1999
|
-
const archiveDir =
|
|
2000
|
-
if (
|
|
1529
|
+
const archiveDir = join7(getTasksDir(kandownDir), "archive");
|
|
1530
|
+
if (existsSync7(archiveDir)) {
|
|
2001
1531
|
for (const file of readdirSync4(archiveDir)) {
|
|
2002
1532
|
if (file.endsWith(".md")) ids.add(file.slice(0, -3));
|
|
2003
1533
|
}
|
|
@@ -2009,17 +1539,156 @@ function nextTaskId(kandownDir) {
|
|
|
2009
1539
|
}
|
|
2010
1540
|
return `t${max + 1}`;
|
|
2011
1541
|
}
|
|
2012
|
-
function readTaskFile(kandownDir, id) {
|
|
2013
|
-
const path = findTaskPath2(kandownDir, id);
|
|
2014
|
-
if (!path) return null;
|
|
2015
|
-
const parsed = parseTaskFile(
|
|
2016
|
-
return {
|
|
2017
|
-
path,
|
|
2018
|
-
frontmatter: { ...parsed.frontmatter, id: parsed.frontmatter.id || id },
|
|
2019
|
-
body: parsed.body,
|
|
2020
|
-
archived: path.includes("/archive/")
|
|
2021
|
-
};
|
|
1542
|
+
function readTaskFile(kandownDir, id) {
|
|
1543
|
+
const path = findTaskPath2(kandownDir, id);
|
|
1544
|
+
if (!path) return null;
|
|
1545
|
+
const parsed = parseTaskFile(readFileSync7(path, "utf8"));
|
|
1546
|
+
return {
|
|
1547
|
+
path,
|
|
1548
|
+
frontmatter: { ...parsed.frontmatter, id: parsed.frontmatter.id || id },
|
|
1549
|
+
body: parsed.body,
|
|
1550
|
+
archived: path.includes("/archive/")
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
function printTaskCommandsHelp() {
|
|
1554
|
+
log(`
|
|
1555
|
+
${c.bold}Kandown task commands${c.reset}
|
|
1556
|
+
|
|
1557
|
+
kandown list [-s status] [-p P1] [-a user] [-t tag] [--archived] [--json]
|
|
1558
|
+
kandown show <id>
|
|
1559
|
+
kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id id] [--json]
|
|
1560
|
+
kandown move <id> <status|archived>
|
|
1561
|
+
kandown assign <id> [user]
|
|
1562
|
+
kandown commit [-m "message"]
|
|
1563
|
+
`);
|
|
1564
|
+
}
|
|
1565
|
+
async function launchTui(screen, kandownDir) {
|
|
1566
|
+
if (!process.stdin.isTTY) {
|
|
1567
|
+
info(`TUI skipped because stdin is not interactive. Use ${c.cyan}kandown daemon status${c.reset} to inspect the web daemon.`);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const tuiPath = join7(PKG_ROOT, "bin", "tui.js");
|
|
1571
|
+
if (!existsSync7(tuiPath)) {
|
|
1572
|
+
err(`TUI binary not found at ${tuiPath}`);
|
|
1573
|
+
process.exit(1);
|
|
1574
|
+
}
|
|
1575
|
+
await new Promise((resolveTui) => {
|
|
1576
|
+
const child = spawn4(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
|
|
1577
|
+
child.on("close", (code) => {
|
|
1578
|
+
if (typeof code === "number" && code !== 0) process.exit(code);
|
|
1579
|
+
resolveTui();
|
|
1580
|
+
});
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/cli/commands/project.ts
|
|
1585
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, copyFileSync as copyFileSync2 } from "fs";
|
|
1586
|
+
import { join as join8, resolve as resolve3 } from "path";
|
|
1587
|
+
import { spawn as spawn5 } from "child_process";
|
|
1588
|
+
function cmdInit(rawArgs) {
|
|
1589
|
+
const args = parseArgs(rawArgs);
|
|
1590
|
+
const kandownDir = resolve3(process.cwd(), args.path);
|
|
1591
|
+
const created = doInit(kandownDir);
|
|
1592
|
+
if (!created) {
|
|
1593
|
+
err("Failed to initialize Kandown.");
|
|
1594
|
+
process.exit(1);
|
|
1595
|
+
}
|
|
1596
|
+
success(`Kandown initialized at ${kandownDir}`);
|
|
1597
|
+
}
|
|
1598
|
+
async function cmdUpdate(rawArgs) {
|
|
1599
|
+
const current = getCurrentVersion();
|
|
1600
|
+
log(`${c.bold}kandown update${c.reset} ${c.dim}\u2014 v${current}${c.reset}`);
|
|
1601
|
+
const latest = await new Promise((resolve5) => {
|
|
1602
|
+
const child = spawn5("npm", ["view", "kandown", "version"], {
|
|
1603
|
+
timeout: 6e3,
|
|
1604
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1605
|
+
env: { ...process.env },
|
|
1606
|
+
detached: false
|
|
1607
|
+
});
|
|
1608
|
+
let stdout = "";
|
|
1609
|
+
child.stdout.on("data", (d) => {
|
|
1610
|
+
stdout += d;
|
|
1611
|
+
});
|
|
1612
|
+
child.stderr.on("data", () => {
|
|
1613
|
+
});
|
|
1614
|
+
child.on("error", () => resolve5(null));
|
|
1615
|
+
child.on("close", (code) => {
|
|
1616
|
+
if (code !== 0) return resolve5(null);
|
|
1617
|
+
const v = stdout.trim().replace(/^"|"$/g, "");
|
|
1618
|
+
resolve5(v || null);
|
|
1619
|
+
});
|
|
1620
|
+
});
|
|
1621
|
+
if (latest && semverGt(latest, current) > 0) {
|
|
1622
|
+
info(`Updating kandown package v${current} \u2192 v${latest}\u2026`);
|
|
1623
|
+
const updateOk = await performGlobalPackageUpdate(`kandown@${latest}`);
|
|
1624
|
+
if (updateOk) {
|
|
1625
|
+
success(`Successfully upgraded kandown to v${latest}`);
|
|
1626
|
+
} else {
|
|
1627
|
+
err(`Global CLI update failed \u2014 try: pnpm add -g kandown@latest or npm install -g kandown@latest`);
|
|
1628
|
+
}
|
|
1629
|
+
} else {
|
|
1630
|
+
info(`kandown CLI is already up to date (v${current}).`);
|
|
1631
|
+
}
|
|
1632
|
+
const args = parseArgs(rawArgs);
|
|
1633
|
+
const cwd = process.cwd();
|
|
1634
|
+
const kandownDir = resolve3(cwd, args.path);
|
|
1635
|
+
const htmlDest = join8(kandownDir, "kandown.html");
|
|
1636
|
+
if (existsSync8(htmlDest)) {
|
|
1637
|
+
const htmlSrc = resolve3(PKG_ROOT, "dist", "index.html");
|
|
1638
|
+
if (existsSync8(htmlSrc)) {
|
|
1639
|
+
copyFileSync2(htmlSrc, htmlDest);
|
|
1640
|
+
success(`Refreshed ${args.path}/kandown.html`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function cmdDoctor(rawArgs) {
|
|
1645
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1646
|
+
const currentVersion = getCurrentVersion();
|
|
1647
|
+
log(`${c.bold}kandown doctor${c.reset} ${c.dim}\u2014 environment & board diagnostic${c.reset}
|
|
1648
|
+
`);
|
|
1649
|
+
log(` CLI Version: ${currentVersion}`);
|
|
1650
|
+
const configPath = join8(kandownDir, "kandown.json");
|
|
1651
|
+
if (existsSync8(configPath)) {
|
|
1652
|
+
try {
|
|
1653
|
+
JSON.parse(readFileSync8(configPath, "utf8"));
|
|
1654
|
+
success("kandown.json valid");
|
|
1655
|
+
} catch (e) {
|
|
1656
|
+
err(`kandown.json invalid: ${e.message}`);
|
|
1657
|
+
}
|
|
1658
|
+
} else {
|
|
1659
|
+
err("Missing kandown.json");
|
|
1660
|
+
}
|
|
1661
|
+
const daemonStatus = await getDaemonStatus(kandownDir);
|
|
1662
|
+
if (daemonStatus.running && daemonStatus.metadata) {
|
|
1663
|
+
success(`Daemon running on port ${daemonStatus.metadata.port} (PID ${daemonStatus.metadata.pid})`);
|
|
1664
|
+
} else {
|
|
1665
|
+
info("Daemon not running");
|
|
1666
|
+
}
|
|
1667
|
+
const taskIds = listTaskIds(kandownDir);
|
|
1668
|
+
success(`Tasks: ${taskIds.length} active task files`);
|
|
1669
|
+
log(`
|
|
1670
|
+
${c.green}\u2713 Everything looks good!${c.reset}
|
|
1671
|
+
`);
|
|
1672
|
+
}
|
|
1673
|
+
async function cmdWork(rawArgs) {
|
|
1674
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1675
|
+
const doc = readAgentDoc(kandownDir);
|
|
1676
|
+
const board = readBoard(kandownDir);
|
|
1677
|
+
log(doc);
|
|
1678
|
+
log("\n---\n");
|
|
1679
|
+
log(`## Current Board Digest
|
|
1680
|
+
`);
|
|
1681
|
+
const allTasksCount = board.columns.reduce((sum, col) => sum + col.tasks.length, 0);
|
|
1682
|
+
log(`Tasks total: ${allTasksCount}`);
|
|
1683
|
+
for (const col of board.columns) {
|
|
1684
|
+
log(`- **${col.name}** (${col.tasks.length}): ${col.tasks.map((t) => `${t.id} ${t.title}`).join(", ") || "empty"}`);
|
|
1685
|
+
}
|
|
2022
1686
|
}
|
|
1687
|
+
|
|
1688
|
+
// src/cli/commands/tasks.ts
|
|
1689
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync5 } from "fs";
|
|
1690
|
+
import { join as join9, resolve as resolve4 } from "path";
|
|
1691
|
+
import { spawnSync } from "child_process";
|
|
2023
1692
|
function cmdList(rawArgs) {
|
|
2024
1693
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2025
1694
|
const args = taskParseArgs(rawArgs);
|
|
@@ -2042,11 +1711,11 @@ function cmdList(rawArgs) {
|
|
|
2042
1711
|
});
|
|
2043
1712
|
}
|
|
2044
1713
|
if (includeArchived) {
|
|
2045
|
-
const archiveDir =
|
|
2046
|
-
if (
|
|
2047
|
-
for (const file of
|
|
1714
|
+
const archiveDir = join9(getTasksDir(kandownDir), "archive");
|
|
1715
|
+
if (existsSync9(archiveDir)) {
|
|
1716
|
+
for (const file of readdirSync5(archiveDir).filter((name) => name.endsWith(".md"))) {
|
|
2048
1717
|
const id = file.slice(0, -3);
|
|
2049
|
-
const parsed = parseTaskFile(
|
|
1718
|
+
const parsed = parseTaskFile(readFileSync9(join9(archiveDir, file), "utf8"));
|
|
2050
1719
|
rows.push({
|
|
2051
1720
|
id,
|
|
2052
1721
|
title: parsed.frontmatter.title || id,
|
|
@@ -2066,249 +1735,664 @@ function cmdList(rawArgs) {
|
|
|
2066
1735
|
if (tagFilters.length > 0 && !tagFilters.every((tag) => row.tags.map((t) => t.toLowerCase()).includes(tag))) return false;
|
|
2067
1736
|
return true;
|
|
2068
1737
|
});
|
|
2069
|
-
if (args.flags.json === true) {
|
|
2070
|
-
process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
|
|
2071
|
-
return;
|
|
2072
|
-
}
|
|
2073
|
-
const byStatus = /* @__PURE__ */ new Map();
|
|
2074
|
-
for (const row of filtered) {
|
|
2075
|
-
const list = byStatus.get(row.status) ?? [];
|
|
2076
|
-
list.push(row);
|
|
2077
|
-
byStatus.set(row.status, list);
|
|
2078
|
-
}
|
|
2079
|
-
for (const [status, tasks] of byStatus) {
|
|
2080
|
-
log(`
|
|
2081
|
-
${c.bold}${status}${c.reset} (${tasks.length})`);
|
|
2082
|
-
for (const task of tasks) {
|
|
2083
|
-
const pri = task.priority || "P2";
|
|
2084
|
-
const assignee = task.assignee ? ` @${task.assignee}` : "";
|
|
2085
|
-
log(` ${c.cyan}${task.id}${c.reset} [${pri}] ${task.title}${assignee}`);
|
|
1738
|
+
if (args.flags.json === true) {
|
|
1739
|
+
process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
1743
|
+
for (const row of filtered) {
|
|
1744
|
+
const list = byStatus.get(row.status) ?? [];
|
|
1745
|
+
list.push(row);
|
|
1746
|
+
byStatus.set(row.status, list);
|
|
1747
|
+
}
|
|
1748
|
+
for (const [status, tasks] of byStatus) {
|
|
1749
|
+
log(`
|
|
1750
|
+
${c.bold}${status}${c.reset} (${tasks.length})`);
|
|
1751
|
+
for (const task of tasks) {
|
|
1752
|
+
const pri = task.priority || "P2";
|
|
1753
|
+
const assignee = task.assignee ? ` @${task.assignee}` : "";
|
|
1754
|
+
log(` ${c.cyan}${task.id}${c.reset} [${pri}] ${task.title}${assignee}`);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
log("");
|
|
1758
|
+
}
|
|
1759
|
+
function cmdShow(rawArgs) {
|
|
1760
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1761
|
+
const args = taskParseArgs(rawArgs);
|
|
1762
|
+
const id = args.positional[0];
|
|
1763
|
+
if (!id) {
|
|
1764
|
+
err("Usage: kandown show <task-id>");
|
|
1765
|
+
process.exit(1);
|
|
1766
|
+
}
|
|
1767
|
+
const path = findTaskPath2(kandownDir, id);
|
|
1768
|
+
if (!path) {
|
|
1769
|
+
err(`Task not found: ${id}`);
|
|
1770
|
+
process.exit(1);
|
|
1771
|
+
}
|
|
1772
|
+
process.stdout.write(readFileSync9(path, "utf8"));
|
|
1773
|
+
}
|
|
1774
|
+
function cmdCreate(rawArgs) {
|
|
1775
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1776
|
+
const args = taskParseArgs(rawArgs);
|
|
1777
|
+
const title = args.positional.join(" ").trim();
|
|
1778
|
+
if (!title) {
|
|
1779
|
+
err('Usage: kandown create "title" [-p P1] [-a user] [-t tag] [--to status] [--id custom-id] [--json]');
|
|
1780
|
+
process.exit(1);
|
|
1781
|
+
}
|
|
1782
|
+
const id = stringFlag(args.flags, "id") ?? nextTaskId(kandownDir);
|
|
1783
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
1784
|
+
err(`Invalid task id: ${id}`);
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
if (findTaskPath2(kandownDir, id)) {
|
|
1788
|
+
err(`Task already exists: ${id}`);
|
|
1789
|
+
process.exit(1);
|
|
1790
|
+
}
|
|
1791
|
+
const config = loadConfig(kandownDir);
|
|
1792
|
+
const rawStatus = stringFlag(args.flags, "to", "status");
|
|
1793
|
+
const status = rawStatus ? resolveStatusArg(kandownDir, rawStatus) : config.board.columns[0] || "Backlog";
|
|
1794
|
+
if (!status) {
|
|
1795
|
+
err(`Unknown status: ${rawStatus}`);
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
}
|
|
1798
|
+
const fm = {
|
|
1799
|
+
id,
|
|
1800
|
+
title,
|
|
1801
|
+
status,
|
|
1802
|
+
created: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
|
|
1803
|
+
};
|
|
1804
|
+
const priority = stringFlag(args.flags, "priority")?.toUpperCase();
|
|
1805
|
+
const assignee = stringFlag(args.flags, "assignee");
|
|
1806
|
+
const tags = listFlag(args.flags, "tag");
|
|
1807
|
+
if (priority) fm.priority = priority;
|
|
1808
|
+
if (assignee) fm.assignee = assignee;
|
|
1809
|
+
if (tags.length > 0) fm.tags = tags;
|
|
1810
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
1811
|
+
if (!existsSync9(tasksDir)) mkdirSync4(tasksDir, { recursive: true });
|
|
1812
|
+
const path = taskPath(kandownDir, id);
|
|
1813
|
+
atomicWriteFileSync(path, serializeTaskFile(fm, ""));
|
|
1814
|
+
process.stderr.write(`${c.green}\u2713${c.reset} Created ${c.bold}${id}${c.reset} \u2192 ${status}
|
|
1815
|
+
`);
|
|
1816
|
+
process.stdout.write(args.flags.json === true ? JSON.stringify(fm, null, 2) + "\n" : `${id}
|
|
1817
|
+
`);
|
|
1818
|
+
}
|
|
1819
|
+
function cmdMove(rawArgs) {
|
|
1820
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1821
|
+
const args = taskParseArgs(rawArgs);
|
|
1822
|
+
const id = args.positional[0];
|
|
1823
|
+
const rawStatus = args.positional.slice(1).join(" ") || stringFlag(args.flags, "to", "status");
|
|
1824
|
+
if (!id || !rawStatus) {
|
|
1825
|
+
err("Usage: kandown move <task-id> <status>");
|
|
1826
|
+
process.exit(1);
|
|
1827
|
+
}
|
|
1828
|
+
if (rawStatus.toLowerCase() === "archived") {
|
|
1829
|
+
if (!archiveTaskInBoard(kandownDir, id)) {
|
|
1830
|
+
err(`Archive failed: ${id}`);
|
|
1831
|
+
process.exit(1);
|
|
1832
|
+
}
|
|
1833
|
+
success(`Archived ${id}`);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
const status = resolveStatusArg(kandownDir, rawStatus);
|
|
1837
|
+
if (!status) {
|
|
1838
|
+
err(`Unknown status: ${rawStatus}`);
|
|
1839
|
+
process.exit(1);
|
|
1840
|
+
}
|
|
1841
|
+
if (!moveTaskToColumn(kandownDir, id, status)) {
|
|
1842
|
+
err(`Move failed: ${id}`);
|
|
1843
|
+
process.exit(1);
|
|
1844
|
+
}
|
|
1845
|
+
success(`Moved ${id} \u2192 "${status}"`);
|
|
1846
|
+
}
|
|
1847
|
+
function cmdAssign(rawArgs) {
|
|
1848
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1849
|
+
const args = taskParseArgs(rawArgs);
|
|
1850
|
+
const [id, assignee] = args.positional;
|
|
1851
|
+
if (!id) {
|
|
1852
|
+
err("Usage: kandown assign <task-id> [assignee]");
|
|
1853
|
+
process.exit(1);
|
|
1854
|
+
}
|
|
1855
|
+
const task = readTaskFile(kandownDir, id);
|
|
1856
|
+
if (!task) {
|
|
1857
|
+
err(`Task not found: ${id}`);
|
|
1858
|
+
process.exit(1);
|
|
1859
|
+
}
|
|
1860
|
+
const frontmatter = { ...task.frontmatter, id };
|
|
1861
|
+
if (assignee) frontmatter.assignee = assignee;
|
|
1862
|
+
else delete frontmatter.assignee;
|
|
1863
|
+
atomicWriteFileSync(task.path, serializeTaskFile(frontmatter, task.body));
|
|
1864
|
+
success(assignee ? `Assigned ${id} \u2192 ${assignee}` : `Unassigned ${id}`);
|
|
1865
|
+
}
|
|
1866
|
+
function cmdCommit(rawArgs) {
|
|
1867
|
+
ensureKandownDir(rawArgs);
|
|
1868
|
+
const args = taskParseArgs(rawArgs);
|
|
1869
|
+
const message = stringFlag(args.flags, "message") || "tasks: update kandown board";
|
|
1870
|
+
const add = spawnSync("git", ["add", "tasks", ".kandown/kandown.json"], { stdio: "inherit" });
|
|
1871
|
+
if (add.status !== 0) process.exit(add.status ?? 1);
|
|
1872
|
+
const commit = spawnSync("git", ["commit", "-m", message], { stdio: "inherit" });
|
|
1873
|
+
process.exit(commit.status ?? 1);
|
|
1874
|
+
}
|
|
1875
|
+
function cmdExport(rawArgs) {
|
|
1876
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1877
|
+
const board = readBoard(kandownDir);
|
|
1878
|
+
process.stdout.write(JSON.stringify(board, null, 2) + "\n");
|
|
1879
|
+
}
|
|
1880
|
+
function cmdProjects(rawArgs) {
|
|
1881
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1882
|
+
const metadataPath2 = join9(kandownDir, "daemon.json");
|
|
1883
|
+
if (!existsSync9(metadataPath2)) {
|
|
1884
|
+
info("No daemon metadata for this project.");
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
process.stdout.write(readFileSync9(metadataPath2, "utf8").trim() + "\n");
|
|
1888
|
+
}
|
|
1889
|
+
function cmdImport(rawArgs) {
|
|
1890
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1891
|
+
const args = taskParseArgs(rawArgs);
|
|
1892
|
+
const file = args.positional[0];
|
|
1893
|
+
if (!file) {
|
|
1894
|
+
err("Usage: kandown import <file.json> [--overwrite]");
|
|
1895
|
+
process.exit(1);
|
|
1896
|
+
}
|
|
1897
|
+
const importPath = resolve4(process.cwd(), file);
|
|
1898
|
+
if (!existsSync9(importPath)) {
|
|
1899
|
+
err(`Import file not found: ${file}`);
|
|
1900
|
+
process.exit(1);
|
|
1901
|
+
}
|
|
1902
|
+
let raw;
|
|
1903
|
+
try {
|
|
1904
|
+
raw = JSON.parse(readFileSync9(importPath, "utf8"));
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
err(`Import file must be JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
1907
|
+
process.exit(1);
|
|
1908
|
+
}
|
|
1909
|
+
const rows = [];
|
|
1910
|
+
if (Array.isArray(raw)) {
|
|
1911
|
+
rows.push(...raw.filter((value) => typeof value === "object" && value !== null));
|
|
1912
|
+
} else if (typeof raw === "object" && raw !== null && Array.isArray(raw.columns)) {
|
|
1913
|
+
for (const column of raw.columns) {
|
|
1914
|
+
if (typeof column !== "object" || column === null) continue;
|
|
1915
|
+
const col = column;
|
|
1916
|
+
if (!Array.isArray(col.tasks)) continue;
|
|
1917
|
+
for (const task of col.tasks) {
|
|
1918
|
+
if (typeof task === "object" && task !== null) rows.push({ ...task, status: String(col.name || "Backlog") });
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (rows.length === 0) {
|
|
1923
|
+
err("No tasks found to import. Expected a list JSON array or kandown export object.");
|
|
1924
|
+
process.exit(1);
|
|
1925
|
+
}
|
|
1926
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
1927
|
+
if (!existsSync9(tasksDir)) mkdirSync4(tasksDir, { recursive: true });
|
|
1928
|
+
let imported = 0;
|
|
1929
|
+
for (const row of rows) {
|
|
1930
|
+
const id = typeof row.id === "string" && /^[a-zA-Z0-9_-]+$/.test(row.id) ? row.id : nextTaskId(kandownDir);
|
|
1931
|
+
const path = taskPath(kandownDir, id);
|
|
1932
|
+
if (existsSync9(path) && args.flags.overwrite !== true) continue;
|
|
1933
|
+
const fm = {
|
|
1934
|
+
id,
|
|
1935
|
+
title: typeof row.title === "string" && row.title ? row.title : id,
|
|
1936
|
+
status: typeof row.status === "string" && row.status ? row.status.replace(/ \(archived\)$/i, "") : "Backlog"
|
|
1937
|
+
};
|
|
1938
|
+
if (typeof row.priority === "string") fm.priority = row.priority;
|
|
1939
|
+
if (typeof row.assignee === "string") fm.assignee = row.assignee;
|
|
1940
|
+
if (Array.isArray(row.tags)) fm.tags = row.tags.map(String);
|
|
1941
|
+
atomicWriteFileSync(path, serializeTaskFile(fm, typeof row.body === "string" ? row.body : ""));
|
|
1942
|
+
imported++;
|
|
1943
|
+
}
|
|
1944
|
+
success(`Imported ${imported} task${imported === 1 ? "" : "s"}`);
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// src/cli/commands/daemon.ts
|
|
1948
|
+
import { join as join11 } from "path";
|
|
1949
|
+
|
|
1950
|
+
// src/cli/lib/server.ts
|
|
1951
|
+
import { createServer } from "http";
|
|
1952
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10, copyFileSync as copyFileSync3, unlinkSync as unlinkSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
1953
|
+
import { join as join10 } from "path";
|
|
1954
|
+
import { spawn as spawn6 } from "child_process";
|
|
1955
|
+
var START_PORT_RANGE = 2050;
|
|
1956
|
+
var END_PORT_RANGE = 2099;
|
|
1957
|
+
var UNSAFE_PORTS = /* @__PURE__ */ new Set([2049, 4045, 6e3, 6665, 6666, 6667, 6668, 6669, 6697]);
|
|
1958
|
+
var sseClients = [];
|
|
1959
|
+
var nextClientId = 1;
|
|
1960
|
+
function broadcastSseEvent(data) {
|
|
1961
|
+
const payload = `data: ${JSON.stringify(data)}
|
|
1962
|
+
|
|
1963
|
+
`;
|
|
1964
|
+
sseClients.forEach((c2) => c2.res.write(payload));
|
|
1965
|
+
}
|
|
1966
|
+
function handleCors(res) {
|
|
1967
|
+
res.writeHead(204, {
|
|
1968
|
+
"Access-Control-Allow-Origin": "*",
|
|
1969
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
1970
|
+
"Access-Control-Allow-Headers": "Content-Type, X-Kandown-Token"
|
|
1971
|
+
});
|
|
1972
|
+
res.end();
|
|
1973
|
+
}
|
|
1974
|
+
function writeJson(res, status, data) {
|
|
1975
|
+
res.writeHead(status, {
|
|
1976
|
+
"Content-Type": "application/json",
|
|
1977
|
+
"Access-Control-Allow-Origin": "*"
|
|
1978
|
+
});
|
|
1979
|
+
res.end(JSON.stringify(data));
|
|
1980
|
+
}
|
|
1981
|
+
function writeText(res, status, text) {
|
|
1982
|
+
res.writeHead(status, {
|
|
1983
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1984
|
+
"Access-Control-Allow-Origin": "*"
|
|
1985
|
+
});
|
|
1986
|
+
res.end(text);
|
|
1987
|
+
}
|
|
1988
|
+
function readRequestBody(req) {
|
|
1989
|
+
return new Promise((resolveBody, rejectBody) => {
|
|
1990
|
+
let body = "";
|
|
1991
|
+
req.setEncoding("utf8");
|
|
1992
|
+
req.on("data", (chunk) => {
|
|
1993
|
+
body += chunk;
|
|
1994
|
+
});
|
|
1995
|
+
req.on("end", () => resolveBody(body));
|
|
1996
|
+
req.on("error", rejectBody);
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
function syncProjectKandownHtml(kandownDir) {
|
|
2000
|
+
try {
|
|
2001
|
+
const projectHtml = join10(kandownDir, "kandown.html");
|
|
2002
|
+
const distHtml = join10(PKG_ROOT, "dist", "index.html");
|
|
2003
|
+
if (!existsSync10(distHtml)) return false;
|
|
2004
|
+
if (!existsSync10(projectHtml)) {
|
|
2005
|
+
copyFileSync3(distHtml, projectHtml);
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
2008
|
+
const currentContent = readFileSync10(projectHtml, "utf8");
|
|
2009
|
+
const newContent = readFileSync10(distHtml, "utf8");
|
|
2010
|
+
if (currentContent !== newContent) {
|
|
2011
|
+
atomicWriteFileSync(projectHtml, newContent);
|
|
2012
|
+
return true;
|
|
2086
2013
|
}
|
|
2014
|
+
} catch {
|
|
2087
2015
|
}
|
|
2088
|
-
|
|
2016
|
+
return false;
|
|
2089
2017
|
}
|
|
2090
|
-
function
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
process.exit(1);
|
|
2097
|
-
}
|
|
2098
|
-
const path = findTaskPath2(kandownDir, id);
|
|
2099
|
-
if (!path) {
|
|
2100
|
-
err(`Task not found: ${id}`);
|
|
2101
|
-
process.exit(1);
|
|
2018
|
+
function readDaemonPort(kandownDir) {
|
|
2019
|
+
try {
|
|
2020
|
+
const raw = JSON.parse(readFileSync10(join10(kandownDir, "daemon.json"), "utf8"));
|
|
2021
|
+
return typeof raw.port === "number" && Number.isInteger(raw.port) ? raw.port : null;
|
|
2022
|
+
} catch {
|
|
2023
|
+
return null;
|
|
2102
2024
|
}
|
|
2103
|
-
process.stdout.write(readFileSync8(path, "utf8"));
|
|
2104
2025
|
}
|
|
2105
|
-
function
|
|
2106
|
-
const
|
|
2107
|
-
|
|
2108
|
-
const
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2026
|
+
function restartDaemonAfterUpdateResponse(res, kandownDir) {
|
|
2027
|
+
const cliPath = process.argv[1];
|
|
2028
|
+
if (!cliPath) return;
|
|
2029
|
+
const args = ["--no-update-check", "daemon", "run", "--path", kandownDir];
|
|
2030
|
+
const port = readDaemonPort(kandownDir);
|
|
2031
|
+
if (port !== null) args.push("--port", String(port));
|
|
2032
|
+
const launcher = `
|
|
2033
|
+
const { spawn } = require('node:child_process');
|
|
2034
|
+
const [nodeBin, cliPath, ...cliArgs] = process.argv.slice(1);
|
|
2035
|
+
setTimeout(() => {
|
|
2036
|
+
const child = spawn(nodeBin, [cliPath, ...cliArgs], {
|
|
2037
|
+
detached: true,
|
|
2038
|
+
stdio: 'ignore',
|
|
2039
|
+
env: { ...process.env, KANDOWN_DAEMON: '1' },
|
|
2040
|
+
});
|
|
2041
|
+
child.unref();
|
|
2042
|
+
}, 350);
|
|
2043
|
+
`;
|
|
2044
|
+
res.on("finish", () => {
|
|
2045
|
+
const child = spawn6(process.execPath, ["-e", launcher, process.execPath, cliPath, ...args], {
|
|
2046
|
+
detached: true,
|
|
2047
|
+
stdio: "ignore",
|
|
2048
|
+
env: { ...process.env, KANDOWN_DAEMON: "1" }
|
|
2049
|
+
});
|
|
2050
|
+
child.unref();
|
|
2051
|
+
setTimeout(() => process.exit(0), 50).unref();
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
async function handleApi(req, res, url, kandownDir) {
|
|
2055
|
+
const path = url.pathname;
|
|
2056
|
+
const method = req.method || "GET";
|
|
2057
|
+
if (path === "/api/daemon" && method === "GET") {
|
|
2058
|
+
return writeJson(res, 200, {
|
|
2059
|
+
ok: true,
|
|
2060
|
+
pid: process.pid,
|
|
2061
|
+
kandownDir,
|
|
2062
|
+
version: getCurrentVersion(),
|
|
2063
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2064
|
+
agentHook: process.env.KANDOWN_AGENT_HOOK_URL ? { enabled: true, label: process.env.KANDOWN_AGENT_HOOK_LABEL || "Send to Agent" } : null
|
|
2065
|
+
});
|
|
2112
2066
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2067
|
+
if (path === "/api/version" && method === "GET") {
|
|
2068
|
+
return writeJson(res, 200, {
|
|
2069
|
+
version: getCurrentVersion()
|
|
2070
|
+
});
|
|
2117
2071
|
}
|
|
2118
|
-
if (
|
|
2119
|
-
|
|
2120
|
-
|
|
2072
|
+
if (path === "/api/update/check" && method === "GET") {
|
|
2073
|
+
const current = getCurrentVersion();
|
|
2074
|
+
const latest = await new Promise((resolve5) => {
|
|
2075
|
+
const child = spawn6("npm", ["view", "kandown", "version"], {
|
|
2076
|
+
timeout: 4e3,
|
|
2077
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2078
|
+
env: { ...process.env },
|
|
2079
|
+
detached: false
|
|
2080
|
+
});
|
|
2081
|
+
let stdout = "";
|
|
2082
|
+
child.stdout.on("data", (d) => {
|
|
2083
|
+
stdout += d;
|
|
2084
|
+
});
|
|
2085
|
+
child.stderr.on("data", () => {
|
|
2086
|
+
});
|
|
2087
|
+
child.on("error", () => resolve5(null));
|
|
2088
|
+
child.on("close", (code) => {
|
|
2089
|
+
if (code !== 0) return resolve5(null);
|
|
2090
|
+
resolve5(stdout.trim().replace(/^"|"$/g, "") || null);
|
|
2091
|
+
});
|
|
2092
|
+
});
|
|
2093
|
+
const updateAvailable = latest ? semverGt(latest, current) > 0 : false;
|
|
2094
|
+
return writeJson(res, 200, {
|
|
2095
|
+
current,
|
|
2096
|
+
latest: latest || current,
|
|
2097
|
+
updateAvailable
|
|
2098
|
+
});
|
|
2121
2099
|
}
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2100
|
+
if (path === "/api/update/apply" && method === "POST") {
|
|
2101
|
+
const current = getCurrentVersion();
|
|
2102
|
+
const latest = await new Promise((resolve5) => {
|
|
2103
|
+
const child = spawn6("npm", ["view", "kandown", "version"], {
|
|
2104
|
+
timeout: 4e3,
|
|
2105
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2106
|
+
env: { ...process.env },
|
|
2107
|
+
detached: false
|
|
2108
|
+
});
|
|
2109
|
+
let stdout = "";
|
|
2110
|
+
child.stdout.on("data", (d) => {
|
|
2111
|
+
stdout += d;
|
|
2112
|
+
});
|
|
2113
|
+
child.on("error", () => resolve5(null));
|
|
2114
|
+
child.on("close", (code) => resolve5(code === 0 ? stdout.trim() : null));
|
|
2115
|
+
});
|
|
2116
|
+
const targetVersion = latest || current;
|
|
2117
|
+
const ok = await performGlobalPackageUpdate(`kandown@${targetVersion}`);
|
|
2118
|
+
syncProjectKandownHtml(kandownDir);
|
|
2119
|
+
if (ok) {
|
|
2120
|
+
restartDaemonAfterUpdateResponse(res, kandownDir);
|
|
2121
|
+
writeJson(res, 200, { ok: true, version: targetVersion, message: "Update installed successfully; daemon is restarting" });
|
|
2122
|
+
broadcastSseEvent({ type: "update", version: targetVersion });
|
|
2123
|
+
} else {
|
|
2124
|
+
writeJson(res, 500, { ok: false, message: "Global package installation failed" });
|
|
2125
|
+
}
|
|
2126
|
+
return;
|
|
2128
2127
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
const path = taskPath(kandownDir, id);
|
|
2144
|
-
atomicWriteFileSync(path, serializeTaskFile(fm, ""));
|
|
2145
|
-
process.stderr.write(`${c.green}\u2713${c.reset} Created ${c.bold}${id}${c.reset} \u2192 ${status}
|
|
2146
|
-
`);
|
|
2147
|
-
process.stdout.write(args.flags.json === true ? JSON.stringify(fm, null, 2) + "\n" : `${id}
|
|
2148
|
-
`);
|
|
2149
|
-
}
|
|
2150
|
-
function cmdMove(rawArgs) {
|
|
2151
|
-
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2152
|
-
const args = taskParseArgs(rawArgs);
|
|
2153
|
-
const id = args.positional[0];
|
|
2154
|
-
const rawStatus = args.positional.slice(1).join(" ") || stringFlag(args.flags, "to", "status");
|
|
2155
|
-
if (!id || !rawStatus) {
|
|
2156
|
-
err("Usage: kandown move <task-id> <status>");
|
|
2157
|
-
process.exit(1);
|
|
2128
|
+
if (path === "/api/events" && method === "GET") {
|
|
2129
|
+
res.writeHead(200, {
|
|
2130
|
+
"Content-Type": "text/event-stream",
|
|
2131
|
+
"Cache-Control": "no-cache",
|
|
2132
|
+
"Connection": "keep-alive",
|
|
2133
|
+
"Access-Control-Allow-Origin": "*"
|
|
2134
|
+
});
|
|
2135
|
+
res.write("retry: 2000\n\n");
|
|
2136
|
+
const id = nextClientId++;
|
|
2137
|
+
sseClients.push({ id, res });
|
|
2138
|
+
req.on("close", () => {
|
|
2139
|
+
sseClients = sseClients.filter((c2) => c2.id !== id);
|
|
2140
|
+
});
|
|
2141
|
+
return;
|
|
2158
2142
|
}
|
|
2159
|
-
if (
|
|
2160
|
-
if (
|
|
2161
|
-
|
|
2162
|
-
|
|
2143
|
+
if (path === "/api/board") {
|
|
2144
|
+
if (method === "GET") {
|
|
2145
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
2146
|
+
const boardPath = join10(tasksDir, "board.md");
|
|
2147
|
+
const text = existsSync10(boardPath) ? readFileSync10(boardPath, "utf8") : "";
|
|
2148
|
+
return writeText(res, 200, text);
|
|
2149
|
+
}
|
|
2150
|
+
if (method === "PUT") {
|
|
2151
|
+
let body = "";
|
|
2152
|
+
req.on("data", (chunk) => {
|
|
2153
|
+
body += chunk;
|
|
2154
|
+
});
|
|
2155
|
+
req.on("end", () => {
|
|
2156
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
2157
|
+
if (!existsSync10(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
|
|
2158
|
+
atomicWriteFileSync(join10(tasksDir, "board.md"), body);
|
|
2159
|
+
broadcastSseEvent({ type: "board" });
|
|
2160
|
+
writeJson(res, 200, { ok: true });
|
|
2161
|
+
});
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (path === "/api/config") {
|
|
2166
|
+
if (method === "GET") {
|
|
2167
|
+
return writeJson(res, 200, loadConfig(kandownDir));
|
|
2168
|
+
}
|
|
2169
|
+
if (method === "PUT") {
|
|
2170
|
+
let body = "";
|
|
2171
|
+
req.on("data", (chunk) => {
|
|
2172
|
+
body += chunk;
|
|
2173
|
+
});
|
|
2174
|
+
req.on("end", () => {
|
|
2175
|
+
try {
|
|
2176
|
+
const parsed = JSON.parse(body);
|
|
2177
|
+
saveConfig(kandownDir, parsed);
|
|
2178
|
+
broadcastSseEvent({ type: "config" });
|
|
2179
|
+
writeJson(res, 200, { ok: true });
|
|
2180
|
+
} catch (e) {
|
|
2181
|
+
writeJson(res, 400, { error: e.message });
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
return;
|
|
2163
2185
|
}
|
|
2164
|
-
success(`Archived ${id}`);
|
|
2165
|
-
return;
|
|
2166
2186
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
err(`Unknown status: ${rawStatus}`);
|
|
2170
|
-
process.exit(1);
|
|
2187
|
+
if (path === "/api/tasks" && method === "GET") {
|
|
2188
|
+
return writeJson(res, 200, listTaskIds(kandownDir));
|
|
2171
2189
|
}
|
|
2172
|
-
if (
|
|
2173
|
-
|
|
2174
|
-
|
|
2190
|
+
if (path.startsWith("/api/tasks/")) {
|
|
2191
|
+
const routeParts = path.slice("/api/tasks/".length).split("/").filter(Boolean);
|
|
2192
|
+
let taskId;
|
|
2193
|
+
try {
|
|
2194
|
+
taskId = decodeURIComponent(routeParts[0] ?? "");
|
|
2195
|
+
} catch {
|
|
2196
|
+
return writeText(res, 400, "Invalid task id");
|
|
2197
|
+
}
|
|
2198
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(taskId)) return writeText(res, 400, "Invalid task id");
|
|
2199
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
2200
|
+
const archiveDir = join10(tasksDir, "archive");
|
|
2201
|
+
const activePath = join10(tasksDir, `${taskId}.md`);
|
|
2202
|
+
const archivedPath = join10(archiveDir, `${taskId}.md`);
|
|
2203
|
+
const action = routeParts[1];
|
|
2204
|
+
if (method === "POST" && (action === "archive" || action === "unarchive")) {
|
|
2205
|
+
if (routeParts.length !== 2) return writeText(res, 400, "Invalid task route");
|
|
2206
|
+
const archiving = action === "archive";
|
|
2207
|
+
const source = archiving ? activePath : archivedPath;
|
|
2208
|
+
const destination = archiving ? archivedPath : activePath;
|
|
2209
|
+
if (!existsSync10(source) && !existsSync10(destination)) {
|
|
2210
|
+
return writeText(res, 404, "Task not found");
|
|
2211
|
+
}
|
|
2212
|
+
try {
|
|
2213
|
+
if (!existsSync10(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
|
|
2214
|
+
if (!existsSync10(archiveDir)) mkdirSync5(archiveDir, { recursive: true });
|
|
2215
|
+
const body = await readRequestBody(req);
|
|
2216
|
+
atomicWriteFileSync(destination, body);
|
|
2217
|
+
if (source !== destination && existsSync10(source)) unlinkSync5(source);
|
|
2218
|
+
broadcastSseEvent({ type: "task", id: taskId });
|
|
2219
|
+
return writeJson(res, 200, { ok: true });
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
return writeJson(res, 500, {
|
|
2222
|
+
error: `Failed to ${action} task: ${error instanceof Error ? error.message : String(error)}`
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (routeParts.length !== 1) return writeText(res, 404, "Route not found");
|
|
2227
|
+
if (method === "GET") {
|
|
2228
|
+
const taskPath2 = findTaskPath(kandownDir, taskId);
|
|
2229
|
+
if (!taskPath2) return writeText(res, 404, "Task not found");
|
|
2230
|
+
return writeText(res, 200, readFileSync10(taskPath2, "utf8"));
|
|
2231
|
+
}
|
|
2232
|
+
if (method === "PUT") {
|
|
2233
|
+
try {
|
|
2234
|
+
if (!existsSync10(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
|
|
2235
|
+
const taskPath2 = findTaskPath(kandownDir, taskId) ?? activePath;
|
|
2236
|
+
const body = await readRequestBody(req);
|
|
2237
|
+
atomicWriteFileSync(taskPath2, body);
|
|
2238
|
+
broadcastSseEvent({ type: "task", id: taskId });
|
|
2239
|
+
return writeJson(res, 200, { ok: true });
|
|
2240
|
+
} catch (error) {
|
|
2241
|
+
return writeJson(res, 500, {
|
|
2242
|
+
error: `Failed to write task: ${error instanceof Error ? error.message : String(error)}`
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (method === "DELETE") {
|
|
2247
|
+
try {
|
|
2248
|
+
if (existsSync10(activePath)) unlinkSync5(activePath);
|
|
2249
|
+
if (existsSync10(archivedPath)) unlinkSync5(archivedPath);
|
|
2250
|
+
broadcastSseEvent({ type: "task_delete", id: taskId });
|
|
2251
|
+
return writeJson(res, 200, { ok: true });
|
|
2252
|
+
} catch (error) {
|
|
2253
|
+
return writeJson(res, 500, {
|
|
2254
|
+
error: `Failed to delete task: ${error instanceof Error ? error.message : String(error)}`
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2175
2258
|
}
|
|
2176
|
-
|
|
2259
|
+
writeJson(res, 404, { error: "Route not found" });
|
|
2177
2260
|
}
|
|
2178
|
-
function
|
|
2179
|
-
const
|
|
2180
|
-
const
|
|
2181
|
-
const
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2261
|
+
function injectServerRoot(html, kandownDir) {
|
|
2262
|
+
const marker = "</head>";
|
|
2263
|
+
const markerIndex = html.toLowerCase().lastIndexOf(marker);
|
|
2264
|
+
const safeRoot = JSON.stringify(kandownDir).replace(/</g, "\\u003c");
|
|
2265
|
+
const script = `<script>window.__KANDOWN_ROOT__ = ${safeRoot};</script>
|
|
2266
|
+
`;
|
|
2267
|
+
if (markerIndex === -1) return script + html;
|
|
2268
|
+
return html.slice(0, markerIndex) + script + html.slice(markerIndex);
|
|
2269
|
+
}
|
|
2270
|
+
function serveApp(res, kandownDir) {
|
|
2271
|
+
syncProjectKandownHtml(kandownDir);
|
|
2272
|
+
const htmlPath = join10(kandownDir, "kandown.html");
|
|
2273
|
+
if (existsSync10(htmlPath)) {
|
|
2274
|
+
const html = readFileSync10(htmlPath, "utf8");
|
|
2275
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
2276
|
+
res.end(injectServerRoot(html, kandownDir));
|
|
2277
|
+
} else {
|
|
2278
|
+
writeText(res, 404, "kandown.html not found");
|
|
2190
2279
|
}
|
|
2191
|
-
const frontmatter = { ...task.frontmatter, id };
|
|
2192
|
-
if (assignee) frontmatter.assignee = assignee;
|
|
2193
|
-
else delete frontmatter.assignee;
|
|
2194
|
-
atomicWriteFileSync(task.path, serializeTaskFile(frontmatter, task.body));
|
|
2195
|
-
success(assignee ? `Assigned ${id} \u2192 ${assignee}` : `Unassigned ${id}`);
|
|
2196
2280
|
}
|
|
2197
|
-
function
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2281
|
+
function createServeServer(kandownDir) {
|
|
2282
|
+
syncProjectKandownHtml(kandownDir);
|
|
2283
|
+
return createServer((req, res) => {
|
|
2284
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
2285
|
+
if (req.method === "OPTIONS") return handleCors(res);
|
|
2286
|
+
if (url.pathname === "/" || url.pathname === "/kandown.html" || !url.pathname.startsWith("/api/")) {
|
|
2287
|
+
return serveApp(res, kandownDir);
|
|
2288
|
+
}
|
|
2289
|
+
if (url.pathname.startsWith("/api/")) {
|
|
2290
|
+
return handleApi(req, res, url, kandownDir);
|
|
2291
|
+
}
|
|
2292
|
+
writeText(res, 404, "Not found");
|
|
2293
|
+
});
|
|
2205
2294
|
}
|
|
2206
|
-
function
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2295
|
+
function listen(server, port) {
|
|
2296
|
+
return new Promise((resolveListen, rejectListen) => {
|
|
2297
|
+
const onError = (e) => {
|
|
2298
|
+
server.off("listening", onListening);
|
|
2299
|
+
rejectListen(e);
|
|
2300
|
+
};
|
|
2301
|
+
const onListening = () => {
|
|
2302
|
+
server.off("error", onError);
|
|
2303
|
+
resolveListen();
|
|
2304
|
+
};
|
|
2305
|
+
server.once("error", onError);
|
|
2306
|
+
server.once("listening", onListening);
|
|
2307
|
+
server.listen(port, "127.0.0.1");
|
|
2308
|
+
});
|
|
2217
2309
|
}
|
|
2218
|
-
async function
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2310
|
+
async function listenOnAvailablePort(kandownDir, preferredPort) {
|
|
2311
|
+
const startPort = preferredPort ?? START_PORT_RANGE;
|
|
2312
|
+
for (let p = startPort; p <= END_PORT_RANGE; p++) {
|
|
2313
|
+
if (UNSAFE_PORTS.has(p)) continue;
|
|
2314
|
+
const server = createServeServer(kandownDir);
|
|
2315
|
+
try {
|
|
2316
|
+
await listen(server, p);
|
|
2317
|
+
return { server, port: p };
|
|
2318
|
+
} catch (e) {
|
|
2319
|
+
if (e.code !== "EADDRINUSE" && e.code !== "EACCES") throw e;
|
|
2320
|
+
}
|
|
2222
2321
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2322
|
+
throw new Error(`No free port in range ${START_PORT_RANGE}-${END_PORT_RANGE}`);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// src/cli/commands/daemon.ts
|
|
2326
|
+
async function cmdDaemon(rest) {
|
|
2327
|
+
const parsedDaemonArgs = parseArgs(rest);
|
|
2328
|
+
const subcommand = parsedDaemonArgs.positional[0] || "status";
|
|
2329
|
+
const daemonArgs = subcommand ? stripFirstPositional(rest, subcommand) : rest;
|
|
2330
|
+
const { kandownDir } = ensureKandownDir(daemonArgs);
|
|
2331
|
+
if (subcommand === "run") {
|
|
2332
|
+
const daemonOptions = parseArgs(daemonArgs);
|
|
2333
|
+
const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
|
|
2334
|
+
const { port } = await listenOnAvailablePort(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
|
|
2335
|
+
const url = `http://localhost:${port}`;
|
|
2336
|
+
const metadataPath2 = join11(kandownDir, "daemon.json");
|
|
2337
|
+
atomicWriteFileSync(metadataPath2, JSON.stringify({
|
|
2338
|
+
pid: process.pid,
|
|
2339
|
+
port,
|
|
2340
|
+
url,
|
|
2341
|
+
kandownDir,
|
|
2342
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2343
|
+
version: getCurrentVersion(),
|
|
2344
|
+
token: null
|
|
2345
|
+
}, null, 2));
|
|
2346
|
+
info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
|
|
2347
|
+
await new Promise(() => {
|
|
2348
|
+
});
|
|
2349
|
+
} else if (subcommand === "start") {
|
|
2350
|
+
const daemonOptions = parseArgs(daemonArgs);
|
|
2351
|
+
const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
|
|
2352
|
+
const status = await startProjectDaemon(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
|
|
2353
|
+
if (status.running && status.metadata) success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2354
|
+
else {
|
|
2355
|
+
err("Daemon failed to start");
|
|
2356
|
+
process.exit(1);
|
|
2357
|
+
}
|
|
2358
|
+
} else if (subcommand === "restart") {
|
|
2359
|
+
await stopProjectDaemon(kandownDir);
|
|
2360
|
+
const status = await startProjectDaemon(kandownDir);
|
|
2361
|
+
if (status.running && status.metadata) success(`Daemon restarted on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2362
|
+
else {
|
|
2363
|
+
err("Daemon failed to restart");
|
|
2364
|
+
process.exit(1);
|
|
2365
|
+
}
|
|
2366
|
+
} else if (subcommand === "stop") {
|
|
2367
|
+
const stopped = await stopProjectDaemon(kandownDir);
|
|
2368
|
+
if (stopped) success("Daemon stopped");
|
|
2369
|
+
else info("Daemon not running");
|
|
2370
|
+
} else if (subcommand === "status") {
|
|
2371
|
+
const status = await getDaemonStatus(kandownDir);
|
|
2372
|
+
if (status.running && status.metadata) {
|
|
2373
|
+
success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2374
|
+
} else {
|
|
2375
|
+
info("Daemon not running");
|
|
2376
|
+
}
|
|
2377
|
+
} else if (subcommand === "refresh-all") {
|
|
2378
|
+
const status = await getDaemonStatus(kandownDir);
|
|
2379
|
+
if (status.running) await stopProjectDaemon(kandownDir);
|
|
2380
|
+
const restarted = await startProjectDaemon(kandownDir);
|
|
2381
|
+
if (restarted.running && restarted.metadata) success(`Refreshed current project daemon on port ${restarted.metadata.port}`);
|
|
2382
|
+
else info("No running daemon refreshed");
|
|
2383
|
+
} else {
|
|
2384
|
+
err(`Unknown daemon command: ${subcommand}`);
|
|
2385
|
+
log(` Use ${c.cyan}kandown daemon start|stop|restart|status|refresh-all${c.reset}`);
|
|
2226
2386
|
process.exit(1);
|
|
2227
2387
|
}
|
|
2228
|
-
await new Promise((resolveTui) => {
|
|
2229
|
-
const child = spawn5(process.execPath, [tuiPath, screen, kandownDir, getCurrentVersion()], { stdio: "inherit" });
|
|
2230
|
-
child.on("close", (code) => {
|
|
2231
|
-
if (typeof code === "number" && code !== 0) process.exit(code);
|
|
2232
|
-
resolveTui();
|
|
2233
|
-
});
|
|
2234
|
-
});
|
|
2235
2388
|
}
|
|
2389
|
+
|
|
2390
|
+
// src/cli/cli.ts
|
|
2236
2391
|
async function cmdTui(screen, rawArgs) {
|
|
2237
2392
|
const args = parseArgs(rawArgs);
|
|
2238
2393
|
const kandownDir = resolveKandownDir(args.path, process.cwd());
|
|
2239
2394
|
await launchTui(screen, kandownDir);
|
|
2240
2395
|
}
|
|
2241
|
-
function cmdExport(rawArgs) {
|
|
2242
|
-
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2243
|
-
const board = readBoard(kandownDir);
|
|
2244
|
-
process.stdout.write(JSON.stringify(board, null, 2) + "\n");
|
|
2245
|
-
}
|
|
2246
|
-
function cmdProjects(rawArgs) {
|
|
2247
|
-
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2248
|
-
const metadataPath2 = join8(kandownDir, "daemon.json");
|
|
2249
|
-
if (!existsSync8(metadataPath2)) {
|
|
2250
|
-
info("No daemon metadata for this project.");
|
|
2251
|
-
return;
|
|
2252
|
-
}
|
|
2253
|
-
process.stdout.write(readFileSync8(metadataPath2, "utf8").trim() + "\n");
|
|
2254
|
-
}
|
|
2255
|
-
function cmdImport(rawArgs) {
|
|
2256
|
-
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2257
|
-
const args = taskParseArgs(rawArgs);
|
|
2258
|
-
const file = args.positional[0];
|
|
2259
|
-
if (!file) {
|
|
2260
|
-
err("Usage: kandown import <file.json> [--overwrite]");
|
|
2261
|
-
process.exit(1);
|
|
2262
|
-
}
|
|
2263
|
-
const importPath = resolve2(process.cwd(), file);
|
|
2264
|
-
if (!existsSync8(importPath)) {
|
|
2265
|
-
err(`Import file not found: ${file}`);
|
|
2266
|
-
process.exit(1);
|
|
2267
|
-
}
|
|
2268
|
-
let raw;
|
|
2269
|
-
try {
|
|
2270
|
-
raw = JSON.parse(readFileSync8(importPath, "utf8"));
|
|
2271
|
-
} catch (error) {
|
|
2272
|
-
err(`Import file must be JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
2273
|
-
process.exit(1);
|
|
2274
|
-
}
|
|
2275
|
-
const rows = [];
|
|
2276
|
-
if (Array.isArray(raw)) {
|
|
2277
|
-
rows.push(...raw.filter((value) => typeof value === "object" && value !== null));
|
|
2278
|
-
} else if (typeof raw === "object" && raw !== null && Array.isArray(raw.columns)) {
|
|
2279
|
-
for (const column of raw.columns) {
|
|
2280
|
-
if (typeof column !== "object" || column === null) continue;
|
|
2281
|
-
const col = column;
|
|
2282
|
-
if (!Array.isArray(col.tasks)) continue;
|
|
2283
|
-
for (const task of col.tasks) {
|
|
2284
|
-
if (typeof task === "object" && task !== null) rows.push({ ...task, status: String(col.name || "Backlog") });
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
if (rows.length === 0) {
|
|
2289
|
-
err("No tasks found to import. Expected a list JSON array or kandown export object.");
|
|
2290
|
-
process.exit(1);
|
|
2291
|
-
}
|
|
2292
|
-
const tasksDir = getTasksDir(kandownDir);
|
|
2293
|
-
if (!existsSync8(tasksDir)) mkdirSync5(tasksDir, { recursive: true });
|
|
2294
|
-
let imported = 0;
|
|
2295
|
-
for (const row of rows) {
|
|
2296
|
-
const id = typeof row.id === "string" && /^[a-zA-Z0-9_-]+$/.test(row.id) ? row.id : nextTaskId(kandownDir);
|
|
2297
|
-
const path = taskPath(kandownDir, id);
|
|
2298
|
-
if (existsSync8(path) && args.flags.overwrite !== true) continue;
|
|
2299
|
-
const fm = {
|
|
2300
|
-
id,
|
|
2301
|
-
title: typeof row.title === "string" && row.title ? row.title : id,
|
|
2302
|
-
status: typeof row.status === "string" && row.status ? row.status.replace(/ \(archived\)$/i, "") : "Backlog"
|
|
2303
|
-
};
|
|
2304
|
-
if (typeof row.priority === "string") fm.priority = row.priority;
|
|
2305
|
-
if (typeof row.assignee === "string") fm.assignee = row.assignee;
|
|
2306
|
-
if (Array.isArray(row.tags)) fm.tags = row.tags.map(String);
|
|
2307
|
-
atomicWriteFileSync(path, serializeTaskFile(fm, typeof row.body === "string" ? row.body : ""));
|
|
2308
|
-
imported++;
|
|
2309
|
-
}
|
|
2310
|
-
success(`Imported ${imported} task${imported === 1 ? "" : "s"}`);
|
|
2311
|
-
}
|
|
2312
2396
|
async function main() {
|
|
2313
2397
|
const rawArgs = process.argv.slice(2);
|
|
2314
2398
|
if (rawArgs.length === 1 && (rawArgs[0] === "--version" || rawArgs[0] === "version")) {
|
|
@@ -2382,80 +2466,19 @@ async function main() {
|
|
|
2382
2466
|
case "-h":
|
|
2383
2467
|
help();
|
|
2384
2468
|
break;
|
|
2385
|
-
case "daemon":
|
|
2386
|
-
|
|
2387
|
-
const subcommand = parsedDaemonArgs.positional[0] || "status";
|
|
2388
|
-
const daemonArgs = subcommand ? stripFirstPositional(rest, subcommand) : rest;
|
|
2389
|
-
const { kandownDir } = ensureKandownDir(daemonArgs);
|
|
2390
|
-
if (subcommand === "run") {
|
|
2391
|
-
const daemonOptions = parseArgs(daemonArgs);
|
|
2392
|
-
const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
|
|
2393
|
-
const { port } = await listenOnAvailablePort(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
|
|
2394
|
-
const url = `http://localhost:${port}`;
|
|
2395
|
-
const metadataPath2 = join8(kandownDir, "daemon.json");
|
|
2396
|
-
atomicWriteFileSync(metadataPath2, JSON.stringify({
|
|
2397
|
-
pid: process.pid,
|
|
2398
|
-
port,
|
|
2399
|
-
url,
|
|
2400
|
-
kandownDir,
|
|
2401
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2402
|
-
version: getCurrentVersion(),
|
|
2403
|
-
token: null
|
|
2404
|
-
}, null, 2));
|
|
2405
|
-
info(`Kandown daemon running on port ${port} (PID ${process.pid})`);
|
|
2406
|
-
await new Promise(() => {
|
|
2407
|
-
});
|
|
2408
|
-
} else if (subcommand === "start") {
|
|
2409
|
-
const daemonOptions = parseArgs(daemonArgs);
|
|
2410
|
-
const preferredPort = typeof daemonOptions.flags.port === "string" ? Number(daemonOptions.flags.port) : null;
|
|
2411
|
-
const status = await startProjectDaemon(kandownDir, Number.isInteger(preferredPort) ? preferredPort : null);
|
|
2412
|
-
if (status.running && status.metadata) success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2413
|
-
else {
|
|
2414
|
-
err("Daemon failed to start");
|
|
2415
|
-
process.exit(1);
|
|
2416
|
-
}
|
|
2417
|
-
} else if (subcommand === "restart") {
|
|
2418
|
-
await stopProjectDaemon(kandownDir);
|
|
2419
|
-
const status = await startProjectDaemon(kandownDir);
|
|
2420
|
-
if (status.running && status.metadata) success(`Daemon restarted on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2421
|
-
else {
|
|
2422
|
-
err("Daemon failed to restart");
|
|
2423
|
-
process.exit(1);
|
|
2424
|
-
}
|
|
2425
|
-
} else if (subcommand === "stop") {
|
|
2426
|
-
const stopped = await stopProjectDaemon(kandownDir);
|
|
2427
|
-
if (stopped) success("Daemon stopped");
|
|
2428
|
-
else info("Daemon not running");
|
|
2429
|
-
} else if (subcommand === "status") {
|
|
2430
|
-
const status = await getDaemonStatus(kandownDir);
|
|
2431
|
-
if (status.running && status.metadata) {
|
|
2432
|
-
success(`Daemon running on port ${status.metadata.port} (PID ${status.metadata.pid})`);
|
|
2433
|
-
} else {
|
|
2434
|
-
info("Daemon not running");
|
|
2435
|
-
}
|
|
2436
|
-
} else if (subcommand === "refresh-all") {
|
|
2437
|
-
const status = await getDaemonStatus(kandownDir);
|
|
2438
|
-
if (status.running) await stopProjectDaemon(kandownDir);
|
|
2439
|
-
const restarted = await startProjectDaemon(kandownDir);
|
|
2440
|
-
if (restarted.running && restarted.metadata) success(`Refreshed current project daemon on port ${restarted.metadata.port}`);
|
|
2441
|
-
else info("No running daemon refreshed");
|
|
2442
|
-
} else {
|
|
2443
|
-
err(`Unknown daemon command: ${subcommand}`);
|
|
2444
|
-
log(` Use ${c.cyan}kandown daemon start|stop|restart|status|refresh-all${c.reset}`);
|
|
2445
|
-
process.exit(1);
|
|
2446
|
-
}
|
|
2469
|
+
case "daemon":
|
|
2470
|
+
await cmdDaemon(rest);
|
|
2447
2471
|
break;
|
|
2448
|
-
}
|
|
2449
2472
|
case void 0: {
|
|
2450
2473
|
const parsed = parseArgs(rest);
|
|
2451
2474
|
const kandownDir = resolveKandownDir(parsed.path, process.cwd());
|
|
2452
|
-
if (
|
|
2475
|
+
if (existsSync11(kandownDir)) {
|
|
2453
2476
|
let status = await getDaemonStatus(kandownDir);
|
|
2454
2477
|
if (!status.running) {
|
|
2455
2478
|
status = await startProjectDaemon(kandownDir);
|
|
2456
2479
|
}
|
|
2457
2480
|
if (!parsed.flags["no-open"]) {
|
|
2458
|
-
const urlToOpen = status.metadata?.url ||
|
|
2481
|
+
const urlToOpen = status.metadata?.url || join12(kandownDir, "kandown.html");
|
|
2459
2482
|
openBrowser(urlToOpen);
|
|
2460
2483
|
}
|
|
2461
2484
|
} else if (!process.stdin.isTTY) {
|