kandown 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kandown.js +379 -17
- package/bin/tui.js +466 -11
- package/dist/index.html +137 -122
- package/package.json +1 -1
package/bin/kandown.js
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
import { spawn, execSync } from 'node:child_process';
|
|
70
70
|
import { createConnection } from 'node:net';
|
|
71
71
|
import { randomBytes } from 'node:crypto';
|
|
72
|
+
import { watch as watchFs } from 'chokidar';
|
|
72
73
|
|
|
73
74
|
// 📖 Global safety net: a stray exception or unhandled rejection prints a clean
|
|
74
75
|
// one-liner instead of a raw stack trace. The daemon (KANDOWN_DAEMON=1) logs
|
|
@@ -886,6 +887,13 @@ function doInit(args, cwd, kandownPath, kandownDir) {
|
|
|
886
887
|
}
|
|
887
888
|
}
|
|
888
889
|
|
|
890
|
+
const hasGit = existsSync(join(cwd, '.git'));
|
|
891
|
+
if (!hasGit) {
|
|
892
|
+
log('');
|
|
893
|
+
warn(`No git repository detected in ${c.bold}${cwd}${c.reset}.`);
|
|
894
|
+
info(` Tip: Run ${c.cyan}git init${c.reset} and track ${c.bold}./tasks/${c.reset} for git history & portability!`);
|
|
895
|
+
}
|
|
896
|
+
|
|
889
897
|
return true;
|
|
890
898
|
}
|
|
891
899
|
|
|
@@ -1226,26 +1234,54 @@ function depIsResolved(kandownDir, id, terminalLower) {
|
|
|
1226
1234
|
return isArch || (parsed.frontmatter.status || '').toLowerCase() === terminalLower;
|
|
1227
1235
|
}
|
|
1228
1236
|
|
|
1237
|
+
function parseInlineQuickAdd(rawTitle) {
|
|
1238
|
+
let text = (rawTitle || '').trim();
|
|
1239
|
+
let priority;
|
|
1240
|
+
const tags = [];
|
|
1241
|
+
let assignee;
|
|
1242
|
+
let due;
|
|
1243
|
+
const dependsOn = [];
|
|
1244
|
+
|
|
1245
|
+
text = text.replace(/(?:^|\s)p([1-4])(?:\s|$)/i, (_, level) => {
|
|
1246
|
+
priority = `P${level}`;
|
|
1247
|
+
return ' ';
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
text = text.replace(/(?:^|\s)#([a-zA-Z0-9_-]+)/g, (_, tag) => {
|
|
1251
|
+
tags.push(tag.toLowerCase());
|
|
1252
|
+
return ' ';
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
text = text.replace(/(?:^|\s)@([a-zA-Z0-9_-]+)/g, (_, user) => {
|
|
1256
|
+
assignee = user;
|
|
1257
|
+
return ' ';
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
text = text.replace(/(?:^|\s)due:([^\s]+)/i, (_, dateStr) => {
|
|
1261
|
+
due = dateStr;
|
|
1262
|
+
return ' ';
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
text = text.replace(/(?:^|\s)\+([a-zA-Z0-9_-]+)/g, (_, depId) => {
|
|
1266
|
+
dependsOn.push(depId);
|
|
1267
|
+
return ' ';
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
const title = text.replace(/\s+/g, ' ').trim();
|
|
1271
|
+
return { title: title || rawTitle, priority, tags, assignee, due, dependsOn };
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1229
1274
|
function cmdCreate(rawArgs) {
|
|
1230
1275
|
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
1231
1276
|
const args = taskParseArgs(rawArgs);
|
|
1232
|
-
const
|
|
1233
|
-
if (!
|
|
1234
|
-
err('Usage: kandown create "title" [-p priority] [-a assignee] [-t tag] [--to status]');
|
|
1277
|
+
const rawTitle = args.positional[0];
|
|
1278
|
+
if (!rawTitle) {
|
|
1279
|
+
err('Usage: kandown create "<title>" [-p priority] [-a assignee] [-t tag ...] [--to status]');
|
|
1235
1280
|
process.exit(1);
|
|
1236
1281
|
}
|
|
1237
1282
|
const config = readKandownConfig(kandownDir);
|
|
1238
|
-
const
|
|
1239
|
-
const targetStatus = args.flags.to ? resolveStatusArg(config, args.flags.to) : defaultStatus;
|
|
1240
|
-
if (args.flags.to && !targetStatus) {
|
|
1241
|
-
err(`Unknown status: ${args.flags.to}`);
|
|
1242
|
-
process.exit(1);
|
|
1243
|
-
}
|
|
1283
|
+
const targetStatus = resolveStatusArg(config, args.flags.to) || (config && config.board && config.board.columns ? config.board.columns[0] : 'Backlog');
|
|
1244
1284
|
const id = args.flags.id || nextTaskId(kandownDir);
|
|
1245
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
1246
|
-
err(`Invalid task id: ${id}`);
|
|
1247
|
-
process.exit(1);
|
|
1248
|
-
}
|
|
1249
1285
|
const tasksDir = getTasksDir(kandownDir);
|
|
1250
1286
|
if (!existsSync(tasksDir)) mkdirSync(tasksDir, { recursive: true });
|
|
1251
1287
|
const targetPath = join(tasksDir, `${id}.md`);
|
|
@@ -1253,6 +1289,8 @@ function cmdCreate(rawArgs) {
|
|
|
1253
1289
|
err(`Task already exists: ${id}`);
|
|
1254
1290
|
process.exit(1);
|
|
1255
1291
|
}
|
|
1292
|
+
const parsedInline = parseInlineQuickAdd(rawTitle);
|
|
1293
|
+
const title = parsedInline.title;
|
|
1256
1294
|
const fm = {
|
|
1257
1295
|
id,
|
|
1258
1296
|
title,
|
|
@@ -1883,11 +1921,37 @@ function handleCors(res) {
|
|
|
1883
1921
|
* daemon token; otherwise answers 401 and returns false.
|
|
1884
1922
|
*/
|
|
1885
1923
|
function requireToken(req, res) {
|
|
1886
|
-
|
|
1924
|
+
const tokenHeader = req.headers['x-kandown-token'];
|
|
1925
|
+
const requestUrl = new URL(req.url || '/', 'http://localhost');
|
|
1926
|
+
const tokenQuery = requestUrl.searchParams.get('token');
|
|
1927
|
+
if (tokenHeader === DAEMON_TOKEN || tokenQuery === DAEMON_TOKEN) return true;
|
|
1887
1928
|
writeJson(res, 401, { error: 'missing or invalid X-Kandown-Token' });
|
|
1888
1929
|
return false;
|
|
1889
1930
|
}
|
|
1890
1931
|
|
|
1932
|
+
const sseClients = new Set();
|
|
1933
|
+
|
|
1934
|
+
function broadcastSseEvent(eventData) {
|
|
1935
|
+
const payload = `data: ${JSON.stringify(eventData)}\n\n`;
|
|
1936
|
+
for (const client of sseClients) {
|
|
1937
|
+
try {
|
|
1938
|
+
client.write(payload);
|
|
1939
|
+
} catch {
|
|
1940
|
+
sseClients.delete(client);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
setInterval(() => {
|
|
1946
|
+
for (const client of sseClients) {
|
|
1947
|
+
try {
|
|
1948
|
+
client.write(':\n\n');
|
|
1949
|
+
} catch {
|
|
1950
|
+
sseClients.delete(client);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}, 15000);
|
|
1954
|
+
|
|
1891
1955
|
function writeJson(res, status, body) {
|
|
1892
1956
|
const headers = { ...apiHeaders(), 'Content-Type': 'application/json' };
|
|
1893
1957
|
res.writeHead(status, headers);
|
|
@@ -2309,6 +2373,44 @@ function handleApi(req, res, url, kandownDir) {
|
|
|
2309
2373
|
}
|
|
2310
2374
|
}
|
|
2311
2375
|
|
|
2376
|
+
if (resource === 'events') {
|
|
2377
|
+
if (req.method === 'GET') {
|
|
2378
|
+
res.writeHead(200, {
|
|
2379
|
+
'Content-Type': 'text/event-stream',
|
|
2380
|
+
'Cache-Control': 'no-cache',
|
|
2381
|
+
'Connection': 'keep-alive',
|
|
2382
|
+
'Access-Control-Allow-Origin': '*',
|
|
2383
|
+
});
|
|
2384
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
2385
|
+
sseClients.add(res);
|
|
2386
|
+
req.on('close', () => {
|
|
2387
|
+
sseClients.delete(res);
|
|
2388
|
+
});
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
if (resource === 'git' && parts[1] === 'history') {
|
|
2394
|
+
const taskId = url.searchParams.get('id');
|
|
2395
|
+
if (!taskId) return writeJson(res, 400, { error: 'Missing task id' });
|
|
2396
|
+
try {
|
|
2397
|
+
const taskPath = findTaskPath(kandownDir, taskId);
|
|
2398
|
+
if (!taskPath) return writeJson(res, 404, { error: 'Task not found' });
|
|
2399
|
+
const relativePath = relative(getProjectRoot(kandownDir), taskPath);
|
|
2400
|
+
const output = execFileSync('git', ['log', '-n', '10', '--follow', '--format=%h|%an|%ar|%s', '--', relativePath], {
|
|
2401
|
+
cwd: getProjectRoot(kandownDir),
|
|
2402
|
+
encoding: 'utf8',
|
|
2403
|
+
}).trim();
|
|
2404
|
+
const commits = output ? output.split('\n').map(line => {
|
|
2405
|
+
const [hash, author, date, message] = line.split('|');
|
|
2406
|
+
return { hash, author, date, message };
|
|
2407
|
+
}) : [];
|
|
2408
|
+
return writeJson(res, 200, { commits });
|
|
2409
|
+
} catch {
|
|
2410
|
+
return writeJson(res, 200, { commits: [] });
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2312
2414
|
if (resource === 'config') {
|
|
2313
2415
|
if (req.method === 'GET') return getConfig(res, kandownDir);
|
|
2314
2416
|
if (req.method === 'PUT') return putConfig(req, res, kandownDir);
|
|
@@ -2367,6 +2469,21 @@ function serveApp(res, kandownDir) {
|
|
|
2367
2469
|
* follow-up REST task, keeping this refactor limited to server bootstrapping.
|
|
2368
2470
|
*/
|
|
2369
2471
|
function createServeServer(kandownDir) {
|
|
2472
|
+
try {
|
|
2473
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
2474
|
+
const configPath = join(kandownDir, 'kandown.json');
|
|
2475
|
+
const watcher = watchFs([join(tasksDir, '*.md'), configPath], {
|
|
2476
|
+
ignoreInitial: true,
|
|
2477
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
|
|
2478
|
+
});
|
|
2479
|
+
watcher.on('all', (_event, filePath) => {
|
|
2480
|
+
const taskId = filePath.replace(/\\/g, '/').split('/').pop()?.replace(/\.md$/, '') || '';
|
|
2481
|
+
broadcastSseEvent({ type: 'change', taskId });
|
|
2482
|
+
});
|
|
2483
|
+
} catch (e) {
|
|
2484
|
+
console.error('[daemon] File watcher init warning:', e.message);
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2370
2487
|
return createServer((req, res) => {
|
|
2371
2488
|
const requestUrl = new URL(req.url || '/', 'http://localhost');
|
|
2372
2489
|
if (req.method === 'OPTIONS') return handleCors(res);
|
|
@@ -2690,21 +2807,266 @@ const skipUpdate = process.argv.slice(2).includes('--no-update-check');
|
|
|
2690
2807
|
// children, status probes) must never pay a network round-trip nor risk a
|
|
2691
2808
|
// mid-pipeline respawn. Inside checkForUpdate there are further guards: 24h
|
|
2692
2809
|
// throttle, TTY-only, and the KANDOWN_NO_UPDATE=1 opt-out.
|
|
2810
|
+
async function cmdDoctor(rawArgs) {
|
|
2811
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2812
|
+
const fix = rawArgs.includes('--fix');
|
|
2813
|
+
log(`${c.bold}kandown doctor${c.reset} ${c.dim}— environment & board diagnostic${c.reset}\n`);
|
|
2814
|
+
|
|
2815
|
+
let errors = 0;
|
|
2816
|
+
let warnings = 0;
|
|
2817
|
+
|
|
2818
|
+
const cliVer = getCurrentVersion();
|
|
2819
|
+
log(` ${c.cyan}CLI Version:${c.reset} ${cliVer}`);
|
|
2820
|
+
|
|
2821
|
+
const configPath = join(kandownDir, 'kandown.json');
|
|
2822
|
+
if (existsSync(configPath)) {
|
|
2823
|
+
try {
|
|
2824
|
+
JSON.parse(readFileSync(configPath, 'utf8'));
|
|
2825
|
+
log(` ${c.green}✓${c.reset} kandown.json valid`);
|
|
2826
|
+
} catch (e) {
|
|
2827
|
+
log(` ${c.red}✗${c.reset} kandown.json invalid JSON: ${e.message}`);
|
|
2828
|
+
errors++;
|
|
2829
|
+
}
|
|
2830
|
+
} else {
|
|
2831
|
+
log(` ${c.red}✗${c.reset} kandown.json missing`);
|
|
2832
|
+
errors++;
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
const daemon = readDaemonMetadata(kandownDir);
|
|
2836
|
+
if (daemon) {
|
|
2837
|
+
const alive = isProcessAlive(daemon.pid);
|
|
2838
|
+
if (alive) {
|
|
2839
|
+
log(` ${c.green}✓${c.reset} Daemon running on port ${daemon.port} (PID ${daemon.pid})`);
|
|
2840
|
+
} else {
|
|
2841
|
+
log(` ${c.yellow}⚠${c.reset} Daemon metadata stale (PID ${daemon.pid} dead)`);
|
|
2842
|
+
warnings++;
|
|
2843
|
+
if (fix) {
|
|
2844
|
+
removeDaemonMetadata(kandownDir);
|
|
2845
|
+
log(` ${c.green}└─ Fixed: removed stale daemon.json${c.reset}`);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
} else {
|
|
2849
|
+
log(` ${c.dim}ℹ Daemon not running${c.reset}`);
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
const tasksDir = getTasksDir(kandownDir);
|
|
2853
|
+
if (existsSync(tasksDir)) {
|
|
2854
|
+
const activeFiles = readdirSync(tasksDir).filter(f => f.endsWith('.md'));
|
|
2855
|
+
const archiveDir = join(tasksDir, 'archive');
|
|
2856
|
+
const archiveFiles = existsSync(archiveDir) ? readdirSync(archiveDir).filter(f => f.endsWith('.md')) : [];
|
|
2857
|
+
|
|
2858
|
+
log(` ${c.cyan}Tasks:${c.reset} ${activeFiles.length} active, ${archiveFiles.length} archived`);
|
|
2859
|
+
|
|
2860
|
+
const activeSet = new Set(activeFiles);
|
|
2861
|
+
const duplicates = archiveFiles.filter(f => activeSet.has(f));
|
|
2862
|
+
if (duplicates.length > 0) {
|
|
2863
|
+
log(` ${c.red}✗${c.reset} Found ${duplicates.length} duplicate file(s) in tasks/ and archive/: ${duplicates.join(', ')}`);
|
|
2864
|
+
errors++;
|
|
2865
|
+
if (fix) {
|
|
2866
|
+
for (const dup of duplicates) {
|
|
2867
|
+
unlinkSync(join(archiveDir, dup));
|
|
2868
|
+
}
|
|
2869
|
+
log(` ${c.green}└─ Fixed: removed duplicate archived files${c.reset}`);
|
|
2870
|
+
}
|
|
2871
|
+
} else {
|
|
2872
|
+
log(` ${c.green}✓${c.reset} No duplicate task files`);
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
let invalidFm = 0;
|
|
2876
|
+
for (const f of activeFiles) {
|
|
2877
|
+
try {
|
|
2878
|
+
const content = readFileSync(join(tasksDir, f), 'utf8');
|
|
2879
|
+
parseFrontmatter(content);
|
|
2880
|
+
} catch {
|
|
2881
|
+
invalidFm++;
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
if (invalidFm > 0) {
|
|
2885
|
+
log(` ${c.yellow}⚠${c.reset} ${invalidFm} task file(s) have invalid frontmatter formatting`);
|
|
2886
|
+
warnings++;
|
|
2887
|
+
} else {
|
|
2888
|
+
log(` ${c.green}✓${c.reset} Task frontmatters valid`);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
log('');
|
|
2893
|
+
if (errors === 0 && warnings === 0) {
|
|
2894
|
+
success('Everything looks good!');
|
|
2895
|
+
} else {
|
|
2896
|
+
info(`Doctor summary: ${errors} error(s), ${warnings} warning(s). ${fix ? '' : 'Run with --fix to resolve automatically.'}`);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
function cmdUndo(rawArgs) {
|
|
2901
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2902
|
+
const logPath = join(kandownDir, '.undo', 'log.json');
|
|
2903
|
+
if (!existsSync(logPath)) {
|
|
2904
|
+
info('No actions to undo');
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
try {
|
|
2908
|
+
const list = JSON.parse(readFileSync(logPath, 'utf8'));
|
|
2909
|
+
if (!list || list.length === 0) {
|
|
2910
|
+
info('No actions to undo');
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
const record = list.shift();
|
|
2914
|
+
writeFileSync(logPath, JSON.stringify(list, null, 2), 'utf8');
|
|
2915
|
+
if (record.previousContent === null) {
|
|
2916
|
+
if (existsSync(record.path)) unlinkSync(record.path);
|
|
2917
|
+
} else {
|
|
2918
|
+
const dir = dirname(record.path);
|
|
2919
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
2920
|
+
writeFileSync(record.path, record.previousContent, 'utf8');
|
|
2921
|
+
if (record.newContent !== null && record.path.includes('/archive/')) {
|
|
2922
|
+
const activePath = record.path.replace('/archive/', '/');
|
|
2923
|
+
if (existsSync(activePath)) unlinkSync(activePath);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
success(`Undid last action (${record.type} ${record.taskId})`);
|
|
2927
|
+
} catch (e) {
|
|
2928
|
+
err(`Undo failed: ${e.message}`);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
async function cmdProjects(rawArgs) {
|
|
2933
|
+
const isJson = rawArgs.includes('--json');
|
|
2934
|
+
const running = [];
|
|
2935
|
+
|
|
2936
|
+
for (let port = START_PORT_RANGE; port <= END_PORT_RANGE; port++) {
|
|
2937
|
+
if (isBrowserUnsafePort(port)) continue;
|
|
2938
|
+
try {
|
|
2939
|
+
const controller = new AbortController();
|
|
2940
|
+
const timeout = setTimeout(() => controller.abort(), 200);
|
|
2941
|
+
const res = await fetch(`http://127.0.0.1:${port}/api/daemon`, { signal: controller.signal });
|
|
2942
|
+
clearTimeout(timeout);
|
|
2943
|
+
if (res.ok) {
|
|
2944
|
+
const data = await res.json();
|
|
2945
|
+
if (data && data.ok) {
|
|
2946
|
+
running.push({ port, pid: data.pid, kandownDir: data.kandownDir, startedAt: data.startedAt, version: data.version });
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
} catch {}
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
if (isJson) {
|
|
2953
|
+
out(JSON.stringify(running, null, 2));
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
if (running.length === 0) {
|
|
2958
|
+
info('No active kandown web daemons running on this machine');
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
log(`${c.bold}Active Kandown Daemons (${running.length})${c.reset}\n`);
|
|
2963
|
+
for (const d of running) {
|
|
2964
|
+
log(` ${c.green}●${c.reset} Port ${c.cyan}${d.port}${c.reset} PID ${d.pid} ${c.dim}${d.kandownDir}${c.reset}`);
|
|
2965
|
+
}
|
|
2966
|
+
function cmdExport(rawArgs) {
|
|
2967
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2968
|
+
const isCsv = rawArgs.includes('--csv');
|
|
2969
|
+
const board = readBoard(kandownDir);
|
|
2970
|
+
|
|
2971
|
+
if (isCsv) {
|
|
2972
|
+
let csv = 'id,title,status,priority,assignee,tags,created\n';
|
|
2973
|
+
for (const col of board.columns) {
|
|
2974
|
+
for (const t of col.tasks) {
|
|
2975
|
+
const task = readTask(kandownDir, t.id);
|
|
2976
|
+
const tags = (t.tags || []).join(';');
|
|
2977
|
+
csv += `"${t.id}","${t.title.replace(/"/g, '""')}","${col.name}","${t.priority || ''}","${t.assignee || ''}","${tags}","${task.frontmatter.created || ''}"\n`;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
out(csv);
|
|
2981
|
+
} else {
|
|
2982
|
+
const data = [];
|
|
2983
|
+
for (const col of board.columns) {
|
|
2984
|
+
for (const t of col.tasks) {
|
|
2985
|
+
const task = readTask(kandownDir, t.id);
|
|
2986
|
+
data.push({ ...task, status: col.name });
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
out(JSON.stringify(data, null, 2));
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
function cmdImport(rawArgs) {
|
|
2994
|
+
const { kandownDir } = ensureKandownDir(rawArgs);
|
|
2995
|
+
const fileIdx = rawArgs.findIndex(a => a.endsWith('.json') || a.endsWith('.md'));
|
|
2996
|
+
if (fileIdx === -1 || !existsSync(rawArgs[fileIdx])) {
|
|
2997
|
+
err('Usage: kandown import <file.json | file.md>');
|
|
2998
|
+
process.exit(1);
|
|
2999
|
+
}
|
|
3000
|
+
const filePath = rawArgs[fileIdx];
|
|
3001
|
+
const content = readFileSync(filePath, 'utf8');
|
|
3002
|
+
let count = 0;
|
|
3003
|
+
|
|
3004
|
+
if (filePath.endsWith('.json')) {
|
|
3005
|
+
try {
|
|
3006
|
+
const parsed = JSON.parse(content);
|
|
3007
|
+
const cards = Array.isArray(parsed) ? parsed : (parsed.cards || []);
|
|
3008
|
+
for (const c of cards) {
|
|
3009
|
+
const title = c.name || c.title || 'Imported Task';
|
|
3010
|
+
createTaskInBoard(kandownDir, title, c.status || 'Backlog');
|
|
3011
|
+
count++;
|
|
3012
|
+
}
|
|
3013
|
+
} catch (e) {
|
|
3014
|
+
err(`Import failed: ${e.message}`);
|
|
3015
|
+
process.exit(1);
|
|
3016
|
+
}
|
|
3017
|
+
} else {
|
|
3018
|
+
const lines = content.split('\n');
|
|
3019
|
+
for (const line of lines) {
|
|
3020
|
+
const m = line.match(/^#{1,3}\s+(.+)$/);
|
|
3021
|
+
if (m) {
|
|
3022
|
+
createTaskInBoard(kandownDir, m[1].trim(), 'Backlog');
|
|
3023
|
+
count++;
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
success(`Imported ${count} tasks into board`);
|
|
3029
|
+
}
|
|
3030
|
+
|
|
2693
3031
|
const SCRIPTED_COMMANDS = new Set([
|
|
2694
3032
|
'list', 'ls', 'show', 'create', 'new', 'move', 'assign', 'commit', 'tasks', 'work', 'daemon',
|
|
3033
|
+
'doctor', 'undo', 'projects', 'export', 'import',
|
|
2695
3034
|
]);
|
|
2696
3035
|
if (!skipUpdate && !SCRIPTED_COMMANDS.has(cmd)) await checkForUpdate(process.argv);
|
|
2697
3036
|
|
|
2698
|
-
// 📖 Catches breaking-change notices for upgrades that bypassed the
|
|
2699
|
-
// auto-updater above (manual `npm install -g kandown`, pnpm/yarn/bun, a
|
|
2700
|
-
// respawned child, etc). See checkVersionSeenNotices' doc comment.
|
|
2701
3037
|
if (!SCRIPTED_COMMANDS.has(cmd)) checkVersionSeenNotices();
|
|
2702
3038
|
|
|
2703
3039
|
switch (cmd) {
|
|
3040
|
+
case 'export':
|
|
3041
|
+
cmdExport(rest);
|
|
3042
|
+
break;
|
|
3043
|
+
|
|
3044
|
+
case 'import':
|
|
3045
|
+
cmdImport(rest);
|
|
3046
|
+
break;
|
|
2704
3047
|
case 'init':
|
|
2705
3048
|
cmdInit(rest);
|
|
2706
3049
|
break;
|
|
2707
3050
|
|
|
3051
|
+
case 'doctor':
|
|
3052
|
+
await cmdDoctor(rest);
|
|
3053
|
+
break;
|
|
3054
|
+
|
|
3055
|
+
case 'undo':
|
|
3056
|
+
cmdUndo(rest);
|
|
3057
|
+
break;
|
|
3058
|
+
|
|
3059
|
+
case 'mcp': {
|
|
3060
|
+
const { kandownDir } = ensureKandownDir(rest);
|
|
3061
|
+
const { startMcpServer } = await import('../src/cli/lib/mcp.js');
|
|
3062
|
+
startMcpServer(kandownDir);
|
|
3063
|
+
break;
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
case 'projects':
|
|
3067
|
+
await cmdProjects(rest);
|
|
3068
|
+
break;
|
|
3069
|
+
|
|
2708
3070
|
case 'board':
|
|
2709
3071
|
// 📖 kandown board — open the interactive kanban board TUI only
|
|
2710
3072
|
await cmdTui('board', rest);
|